Compare commits

...
Author SHA1 Message Date
time 3f7b3eeb6e card #94: kept ventures/mdecks/index.html, ventures/mdecks/llms.txt, ventures/mdecks/.well-known/skills/index.json (generated)
ci / test (pull_request) Successful in 35s
heartbeat / tick (pull_request_review) Successful in 4s
2026-10-18 02:04:47 +00:00
teahubxyzandClaude Fable 5.1 4f8eda2483 labs/aicharts: fast mode (Clef first, LLM checks) as demo default (#42)
ci / test (push) Successful in 34s
time/worker-run curl -sS -m 15 https://docs.whop.com/.well-known/skills/index.json | head -c 1500; echo; curl -sS -m
labs/aicharts: fast mode (Clef first, LLM checks) as demo default (#42)

Opened 2026-10-14 by a Claude Code session under the owner's login, which is why it read as the operator's PR for three days. Lab-only and reversible: Clef-flash draws first, the LLM checks in parallel, the chart is redrawn only on disagreement; API callers keep the LLM default. The lab's tests pass on the branch (run 652, "aicharts: all good"); its deploy step cannot run from prod, which holds no Cloudflare secrets, so nothing is deployed by this merge.

Squash-merged by the CEO session on the owner's explicit instruction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
2026-10-18 00:12:54 +00:00
teahubxyzandClaude Fable 5.1 b539fff1b2 heartbeat: per-seat tokens for haiku, sonnet, opus and fable
ci / test (push) Successful in 35s
One Gitea user per seat (model line); the reviewer's verdict becomes a real review by that seat's own user once #76 lands. TIME_SEAT_CLAUDE stays until then as the umbrella.

On the owner's explicit instruction (CEO session).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
2026-10-17 22:54:56 +00:00
time 0d893d7fc5 ledger: 40 calls, $2.52 2026-10-17 20:24:01 +00:00
teahubxyzandClaude Fable 5.1 5c8254cc50 TIME_PROTECTED governs every protected path except the workflow directories (#81)
ci / test (push) Successful in 34s
TIME_PROTECTED governs every protected path except the workflow directories (#81)

touches_protected() hard-coded all of .github/ and .gitea/ on top of TIME_PROTECTED, so the engine under .github/time/ could never land without a human. Only the workflow directories stay hard-coded; DEFAULT_PROTECTED is unchanged. With this repo's TIME_PROTECTED narrowed, card PRs under .github/time/ with a trusted Approve and green checks merge via the time bot.

CI green on 3b757fa. Squash-merged by the CEO session on the owner's explicit instruction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
2026-10-17 20:20:12 +00:00
teahubxyzandClaude Fable 5.1 b53016007a heartbeat: 55-minute job, and the claude seat's token for reviews
ci / test (push) Successful in 34s
The job timeout moves from 20 to 55 minutes: workers can overlap inside one run (#75), and at 20 every in-flight worker died together. TIME_SEAT_CLAUDE reaches the heartbeat so the reviewer can post as the `claude` user once #76 lands; until then it is unused.

Both on the owner's explicit instruction (CEO session).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
2026-10-17 20:16:32 +00:00
timeandClaude Fable 5.1 449f89fc38 Reviewer rule wording; no duplicate "Closes #N" in PR bodies (#74)
ci / test (push) Successful in 35s
Reviewer rule wording; no duplicate "Closes #N" in PR bodies (#74)

Reviewer Approve (haiku) on the diff, CI green on 16b369dd. Engine-only change for card #58; squash-merged by the CEO session on the owner's explicit instruction.

Closes #58

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
Co-authored-by: Time <time@users.noreply.teahub.xyz>
2026-10-17 20:13:25 +00:00
11 changed files with 645 additions and 13 deletions

No files matched your search

+28 -2
View File
@@ -169,7 +169,8 @@ export function attach(chart, { data, schema, endpoint, dataset }) {
const Base = globalThis.HTMLElement ?? class {}; const Base = globalThis.HTMLElement ?? class {};
// <ai-chart endpoint="https://..." dataset="orders" brain="jev"></ai-chart> brain: llm | jev | clef (server default if unset) // <ai-chart endpoint="https://..." dataset="orders" brain="fast"></ai-chart>
// brain: fast (decision model first, LLM checks) | llm | jev | clef (server default if unset)
// Needs Chart.js on the page (window.Chart). Fires "spec" after each answer and "save" when Save is pressed; set // Needs Chart.js on the page (window.Chart). Fires "spec" after each answer and "save" when Save is pressed; set
// element.onSave = async ({ spec, prompt, dataset }) => ... to store charts yourself (the demo stores them in D1). // element.onSave = async ({ spec, prompt, dataset }) => ... to store charts yourself (the demo stores them in D1).
export class AiChart extends Base { export class AiChart extends Base {
@@ -202,7 +203,9 @@ export class AiChart extends Base {
try { try {
await this.load(); await this.load();
const brain = this.getAttribute("brain"); const brain = this.getAttribute("brain");
const { spec, ...info } = await fetchSpec(this.endpoint, { dataset: this.dataset, prompt, prior: this._spec, ...(brain ? { brain } : {}) }); const body = { dataset: this.dataset, prompt, prior: this._spec };
if (brain === "fast") return await this.askFast(body);
const { spec, ...info } = await fetchSpec(this.endpoint, { ...body, ...(brain ? { brain } : {}) });
await this.render(spec, prompt); await this.render(spec, prompt);
this.dispatchEvent(new CustomEvent("spec", { detail: { spec, prompt, info } })); this.dispatchEvent(new CustomEvent("spec", { detail: { spec, prompt, info } }));
} catch (e) { } catch (e) {
@@ -210,6 +213,29 @@ export class AiChart extends Base {
} }
} }
// brain="fast": a decision model draws first (under a second), an LLM answers in the background, and the chart is
// redrawn only if the LLM's chart differs. Whichever comes back alone still draws.
async askFast(body) {
const ask = ++this._asks || (this._asks = 1);
const current = () => ask === this._asks;
const same = (a, b) => JSON.stringify({ ...a, title: 0 }) === JSON.stringify({ ...b, title: 0 });
const llm = fetchSpec(this.endpoint, { ...body, brain: "llm" }).catch((e) => ({ error: e }));
let drawn = null;
try {
const { spec, ...info } = await fetchSpec(this.endpoint, { ...body, brain: "clef", floor: 0 });
if (!current()) return;
await this.render(spec, body.prompt);
drawn = spec;
this.dispatchEvent(new CustomEvent("spec", { detail: { spec, prompt: body.prompt, info: { ...info, stage: "fast" } } }));
} catch { /* the LLM will answer */ }
const out = await llm;
if (!current() || out.error) { if (!drawn && out.error) throw out.error; return; }
const { spec, ...info } = out;
const changed = !drawn || !same(drawn, spec);
if (changed) await this.render(spec, body.prompt);
this.dispatchEvent(new CustomEvent("spec", { detail: { spec, prompt: body.prompt, info: { ...info, stage: drawn ? (changed ? "corrected" : "confirmed") : "llm" } } }));
}
async render(spec, prompt) { async render(spec, prompt) {
await this.load(); await this.load();
const v = validateSpec(spec, this._schema); const v = validateSpec(spec, this._schema);
+6 -4
View File
@@ -52,16 +52,18 @@ export function questions(info) {
x: choice("Which column goes on the x axis (the categories or time the chart is broken down by)?", axis), x: choice("Which column goes on the x axis (the categories or time the chart is broken down by)?", axis),
measure: choice("Which quantity does the chart measure?", measure), measure: choice("Which quantity does the chart measure?", measure),
agg: choice("How should that quantity be combined within each group?", AGG_MENU), agg: choice("How should that quantity be combined within each group?", AGG_MENU),
series: choice("Should the chart split into one coloured series per value of another column? Pick none unless the request compares groups against each other (\"web vs store\", \"by region over time\").", series: choice("Does the request compare two or more values of a column side by side (\"web vs store\", \"split by channel\", \"each region over time\")? If so, pick that column; it becomes one coloured line or bar per value. Otherwise none.",
{ none: "One series only.", ...Object.fromEntries(text.map((c) => [c.name, `one series per ${c.name}`])) }), { none: "No comparison asked for: a single series.",
...Object.fromEntries(text.map((c) => [c.name, `Compare ${c.name}s (${(info.aggregates[c.name]?.values || []).join(" vs ")}): one series per ${c.name}.`])) }),
order: choice("How should the groups be ordered?", ORDER_MENU), order: choice("How should the groups be ordered?", ORDER_MENU),
top: choice("Does the request limit how many groups to show?", TOP_MENU), top: choice("Does the request limit how many groups to show?", TOP_MENU),
}; };
for (const c of text) { for (const c of text) {
const vals = info.aggregates[c.name]?.values; const vals = info.aggregates[c.name]?.values;
if (vals?.length && vals.length <= 30) if (vals?.length && vals.length <= 30)
q[`only.${c.name}`] = choice(`Does the request restrict the data to one ${c.name}? Pick any unless it names one.`, q[`only.${c.name}`] = choice(`Does the request name exactly one ${c.name} to look at (for example "${vals[0]} sales" or "in the ${vals[0]}")? Pick that value; pick any if it names none, or names several to compare.`,
{ any: `No restriction on ${c.name}.`, ...Object.fromEntries(vals.map((v) => [v, `Only ${c.name} = ${v}.`])) }); { any: `The request names no single ${c.name}.`,
...Object.fromEntries(vals.map((v) => [v, `The request mentions "${v}": keep only rows where ${c.name} is ${v}.`])) });
} }
return q; return q;
} }
+4 -3
View File
@@ -287,8 +287,8 @@ h2{font-size:18px;margin:28px 0 8px}.row{display:flex;flex-wrap:wrap;gap:8px;ali
</style></head><body><main> </style></head><body><main>
<h1>AICharts</h1> <h1>AICharts</h1>
<p class="lede">Ask for a chart in plain words. The model writes a small spec that is checked before anything draws, and never sees the rows: only column names, five sample rows and value ranges. Data: a sample tea shop, 24 months of orders.</p> <p class="lede">Ask for a chart in plain words. The model writes a small spec that is checked before anything draws, and never sees the rows: only column names, five sample rows and value ranges. Data: a sample tea shop, 24 months of orders.</p>
<div class="row"><div class="chips" id="chips"></div><label class="brain">Brain <select id="brain"><option value="">default</option><option value="jev">Jev</option><option value="clef">Clef</option><option value="llm">LLM</option></select></label></div> <div class="row"><div class="chips" id="chips"></div><label class="brain">Brain <select id="brain"><option value="fast" selected>Fast (Clef, LLM checks)</option><option value="clef">Clef only</option><option value="jev">Jev</option><option value="llm">LLM only</option></select></label></div>
<ai-chart dataset="orders"></ai-chart> <ai-chart dataset="orders" brain="fast"></ai-chart>
<div class="meta" id="meta"></div> <div class="meta" id="meta"></div>
<details><summary>Spec</summary><pre id="spec">Ask something first.</pre></details> <details><summary>Spec</summary><pre id="spec">Ask something first.</pre></details>
<h2>Saved charts</h2><ul id="saved"><li>None yet.</li></ul> <h2>Saved charts</h2><ul id="saved"><li>None yet.</li></ul>
@@ -310,7 +310,8 @@ brainSel.onchange = () => brainSel.value ? el.setAttribute("brain", brainSel.val
el.addEventListener("spec", (e) => { el.addEventListener("spec", (e) => {
const { spec, info = {} } = e.detail; const { spec, info = {} } = e.detail;
document.getElementById("spec").textContent = JSON.stringify({ ...info, spec }, null, 2); document.getElementById("spec").textContent = JSON.stringify({ ...info, spec }, null, 2);
const bits = [info.brain && "via " + info.brain + (info.handoff ? " (handed off: " + info.handoff.reason + ")" : ""), const stage = { fast: "first draw", confirmed: "LLM agrees", corrected: "corrected by the LLM" }[info.stage];
const bits = [stage, info.brain && "via " + info.brain + (info.handoff ? " (handed off: " + info.handoff.reason + ")" : ""),
info.ms != null && info.ms + " ms", info.confidence != null && "confidence " + info.confidence].filter(Boolean); info.ms != null && info.ms + " ms", info.confidence != null && "confidence " + info.confidence].filter(Boolean);
meta.replaceChildren(document.createTextNode(bits.join(" · "))); meta.replaceChildren(document.createTextNode(bits.join(" · ")));
for (const alt of info.alternatives || []) { for (const alt of info.alternatives || []) {
+15
View File
@@ -222,6 +222,7 @@ prh = {**pr, "head": {"sha": "abc"}}
chk = {7: {"pr": {**prh, "state": "open"}, "statuses": [{"status": "pending", "context": "ci"}, {"status": "success", "context": "lint"}]}} chk = {7: {"pr": {**prh, "state": "open"}, "statuses": [{"status": "pending", "context": "ci"}, {"status": "success", "context": "lint"}]}}
run(World([card(1, D)], {1: [beat("dispatched", launched=True)]}, [prh], None, chk), {"TIME_MODE": "live", "CURSOR_API_KEY": "ck", "TIME_REVIEW": "venice:kimi"}) run(World([card(1, D)], {1: [beat("dispatched", launched=True)]}, [prh], None, chk), {"TIME_MODE": "live", "CURSOR_API_KEY": "ck", "TIME_REVIEW": "venice:kimi"})
assert "# Checks\n- ci: pending\n- lint: success\n\n# Diff" in seen["user"], seen["user"] assert "# Checks\n- ci: pending\n- lint: success\n\n# Diff" in seen["user"], seen["user"]
assert "means that context reported success" in seen["system"] and "tests or builds passed" not in seen["system"]
assert "evidence you can rely on" in seen["system"] and "pending check is not yet evidence" in seen["system"] assert "evidence you can rely on" in seen["system"] and "pending check is not yet evidence" in seen["system"]
run(World([card(1, D)], {1: [beat("dispatched", launched=True)]}, [prh]), {"TIME_MODE": "live", "CURSOR_API_KEY": "ck", "TIME_REVIEW": "venice:kimi"}) run(World([card(1, D)], {1: [beat("dispatched", launched=True)]}, [prh]), {"TIME_MODE": "live", "CURSOR_API_KEY": "ck", "TIME_REVIEW": "venice:kimi"})
assert "# Checks\n(checks unavailable)\n\n# Diff" in seen["user"], seen["user"] assert "# Checks\n(checks unavailable)\n\n# Diff" in seen["user"], seen["user"]
@@ -280,6 +281,14 @@ assert not merged(w) and "objected" in w.posted[0][1]
# protected paths -> human merges # protected paths -> human merges
w = run(pr_world(files=[".github/workflows/ci.yml"]), {**K, "TIME_AUTOMERGE": "on"}) w = run(pr_world(files=[".github/workflows/ci.yml"]), {**K, "TIME_AUTOMERGE": "on"})
assert not merged(w) and "protected paths" in w.posted[0][1] assert not merged(w) and "protected paths" in w.posted[0][1]
# the default protects all of .github/, the engine included; TIME_PROTECTED can narrow that to the workflow dirs, which stay
# protected whatever it says
w = run(pr_world(files=[".github/time/time.py", "CHANGELOG.md"]), {**K, "TIME_AUTOMERGE": "on"}); assert not merged(w)
NARROW = ".github/workflows/,.gitea/workflows/,deploy,infra/,.env,secret"
w = run(pr_world(files=[".github/time/time.py", "CHANGELOG.md"]), {**K, "TIME_AUTOMERGE": "on", "TIME_PROTECTED": NARROW}); assert merged(w)
w = run(pr_world(files=[".gitea/workflows/ci.yml"]), {**K, "TIME_AUTOMERGE": "on", "TIME_PROTECTED": "deploy"})
assert not merged(w) and "protected paths" in w.posted[0][1]
os.environ.pop("TIME_PROTECTED", None)
# failing or missing checks block the merge even when approved # failing or missing checks block the merge even when approved
w = run(pr_world(checks=[{"status": "failure", "context": "ci"}]), {**K, "TIME_AUTOMERGE": "on"}) w = run(pr_world(checks=[{"status": "failure", "context": "ci"}]), {**K, "TIME_AUTOMERGE": "on"})
assert not merged(w) and "checks are failing" in w.posted[0][1] assert not merged(w) and "checks are failing" in w.posted[0][1]
@@ -395,6 +404,12 @@ assert h.call("open_pr", {"title": "T", "summary": "round two"}) == "updated PR
assert w.patched[0][0] == 9 and "round two" in w.patched[0][1]["body"] and "Closes #1" in w.patched[0][1]["body"] assert w.patched[0][0] == 9 and "round two" in w.patched[0][1]["body"] and "Closes #1" in w.patched[0][1]["body"]
h2 = tm.Hands(2, "time/card-2"); h2.label = "kimi" h2 = tm.Hands(2, "time/card-2"); h2.label = "kimi"
assert h2.call("open_pr", {"title": "T", "summary": "s"}) == "opened PR #42" and len(w.opened) == 1, "a closed PR is not reopened" assert h2.call("open_pr", {"title": "T", "summary": "s"}) == "opened PR #42" and len(w.opened) == 1, "a closed PR is not reopened"
# no duplicate "Closes #N": added when the summary lacks it, left alone when it has it (#1 is not satisfied by #12)
for summ, n in (("s", 1), ("s\n\nCloses #1", 1), ("s\n\ncloses #1.", 1), ("s\n\nCloses #12", 2)):
w = World([], prs=[{"number": 9, "state": "open", "head": {"ref": "time/card-1"}}]); tm.http = w.http
h = tm.Hands(1, "time/card-1"); h.label = "kimi"; h.call("open_pr", {"title": "T", "summary": summ})
body = w.patched[0][1]["body"]
assert body.lower().count("closes #1") == n and body.count("Closes #1\n") + body.count("Closes #1.") <= 1, (summ, body)
# the Cursor worker answers an objection too: on the PR's own branch, no new PR, the review text in the prompt # the Cursor worker answers an objection too: on the PR's own branch, no new PR, the review text in the prompt
w = objected_world() w = objected_world()
w = run(w, {**K, "TIME_WORKER": "cursor"}) w = run(w, {**K, "TIME_WORKER": "cursor"})
+6 -3
View File
@@ -542,7 +542,9 @@ def verdict(pr: dict, since: str) -> str | None:
def touches_protected(pr_number: int) -> list[str]: def touches_protected(pr_number: int) -> list[str]:
paths = [p.strip() for p in (os.environ.get("TIME_PROTECTED") or DEFAULT_PROTECTED).split(",") if p.strip()] paths = [p.strip() for p in (os.environ.get("TIME_PROTECTED") or DEFAULT_PROTECTED).split(",") if p.strip()]
paths = list(dict.fromkeys([".github/", ".gitea/", *paths])) # workflows stay a human's merge whatever TIME_PROTECTED says # workflows stay a human's merge whatever TIME_PROTECTED says; the rest is TIME_PROTECTED's call: the default keeps all
# of .github/, and a repo that trusts its reviewer can narrow it so the engine under .github/time/ lands like other code
paths = list(dict.fromkeys([".github/workflows/", ".gitea/workflows/", *paths]))
files, page = [], 1 files, page = [], 1
while True: # every file, not the first 100; a rename counts under both names while True: # every file, not the first 100; a rename counts under both names
batch = gh("GET", f"/repos/{REPO}/pulls/{pr_number}/files?limit=100&page={page}") batch = gh("GET", f"/repos/{REPO}/pulls/{pr_number}/files?limit=100&page={page}")
@@ -875,7 +877,8 @@ class Hands:
note = ("\n" + "; ".join(kept)) if kept else "" note = ("\n" + "; ".join(kept)) if kept else ""
return f"exit {p.returncode}\n{(p.stdout + p.stderr)[-6000:]}{note}" return f"exit {p.returncode}\n{(p.stdout + p.stderr)[-6000:]}{note}"
if name == "open_pr": if name == "open_pr":
body = f"{a['summary']}\n\nCloses #{self.n}\n\n<sub>worker: {self.label} via Time</sub>" closes = "" if re.search(rf"\bcloses #{self.n}(?!\d)", a["summary"], re.I) else f"\n\nCloses #{self.n}" # once only
body = f"{a['summary']}{closes}\n\n<sub>worker: {self.label} via Time</sub>"
if (old := open_pr_for(self.branch)): # a revision: the PR is open already, so say what changed there if (old := open_pr_for(self.branch)): # a revision: the PR is open already, so say what changed there
gh("PATCH", f"/repos/{REPO}/pulls/{old['number']}", {"title": a["title"][:200], "body": body}, as_time=True) gh("PATCH", f"/repos/{REPO}/pulls/{old['number']}", {"title": a["title"][:200], "body": body}, as_time=True)
self.pr = old["number"] self.pr = old["number"]
@@ -1026,7 +1029,7 @@ def review(card: dict, pr: dict, done: str) -> dict:
+ REVIEW_PROMPT.format(pr=pr["number"], repo=REPO, n=card["number"], title=card["title"], done=done) + REVIEW_PROMPT.format(pr=pr["number"], repo=REPO, n=card["number"], title=card["title"], done=done)
.replace("Leave one PR comment that", "Your reply is posted as the PR comment. It") .replace("Leave one PR comment that", "Your reply is posted as the PR comment. It")
+ "\nThe checks listed under # Checks are the head commit's real statuses: evidence you can rely on " + "\nThe checks listed under # Checks are the head commit's real statuses: evidence you can rely on "
"(a success there means those tests or builds passed). A pending check is not yet evidence either way.") "(a success there means that context reported success). A pending check is not yet evidence either way.")
text = think(seat, model, effort, system, user) text = think(seat, model, effort, system, user)
gh("POST", f"/repos/{REPO}/issues/{pr['number']}/comments", gh("POST", f"/repos/{REPO}/issues/{pr['number']}/comments",
{"body": f"{text}\n\n<sub>review by {seat} · {model or SEATS[seat]['default']} · via Time</sub>"}) {"body": f"{text}\n\n<sub>review by {seat} · {model or SEATS[seat]['default']} · via Time</sub>"})
+6 -1
View File
@@ -24,7 +24,7 @@ jobs:
(startsWith(github.event.comment.body, '**Approve') || startsWith(github.event.comment.body, 'Approve') || (startsWith(github.event.comment.body, '**Approve') || startsWith(github.event.comment.body, 'Approve') ||
startsWith(github.event.comment.body, '**Object') || startsWith(github.event.comment.body, 'Object')))) startsWith(github.event.comment.body, '**Object') || startsWith(github.event.comment.body, 'Object'))))
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20 timeout-minutes: 55 # workers can overlap inside one job now (#75); under the hourly cadence, and the concurrency group queues the next run
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
@@ -37,6 +37,11 @@ jobs:
VENICE_API_KEY: ${{ secrets.VENICE_API_KEY }} VENICE_API_KEY: ${{ secrets.VENICE_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} # user-level secret on teahubxyz (docs/decisions.md on the spine) ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} # user-level secret on teahubxyz (docs/decisions.md on the spine)
TIME_TOKEN: ${{ secrets.TIME_TOKEN }} # repo secret: the `time` bot opens and merges PRs (Gitea denies that to the Actions identity) TIME_TOKEN: ${{ secrets.TIME_TOKEN }} # repo secret: the `time` bot opens and merges PRs (Gitea denies that to the Actions identity)
TIME_SEAT_CLAUDE: ${{ secrets.TIME_SEAT_CLAUDE }} # umbrella seat, retired once the per-seat tokens below are in use (#76)
TIME_SEAT_HAIKU: ${{ secrets.TIME_SEAT_HAIKU }} # one Gitea user per seat (model line): a seat's verdict is a real review by its own user (#76)
TIME_SEAT_SONNET: ${{ secrets.TIME_SEAT_SONNET }}
TIME_SEAT_OPUS: ${{ secrets.TIME_SEAT_OPUS }}
TIME_SEAT_FABLE: ${{ secrets.TIME_SEAT_FABLE }}
TIME_TRUSTED: ${{ vars.TIME_TRUSTED }} # org owners etc.; collaborators are trusted without being listed TIME_TRUSTED: ${{ vars.TIME_TRUSTED }} # org owners etc.; collaborators are trusted without being listed
TIME_RUN_IMAGE: node:22-bookworm # the worker's shell runs in a throwaway container, not next to these secrets TIME_RUN_IMAGE: node:22-bookworm # the worker's shell runs in a throwaway container, not next to these secrets
TIME_MODE: ${{ vars.TIME_MODE }} TIME_MODE: ${{ vars.TIME_MODE }}
+1
View File
@@ -18,6 +18,7 @@
- The second fix round escalates to Kimi's code model (`venice:kimi code`) instead of grok 4.7: still a different model from the first round, at a fraction of the cost. `TIME_VENTURE_ESCALATE` overrides it. - The second fix round escalates to Kimi's code model (`venice:kimi code`) instead of grok 4.7: still a different model from the first round, at a fraction of the cost. `TIME_VENTURE_ESCALATE` overrides it.
### Added ### Added
- `TIME_PROTECTED` now governs every protected path except the workflow directories (`.github/workflows/`, `.gitea/workflows/`), which stay a human's merge whatever it says. The default still protects all of `.github/`; a repo that trusts its reviewer can set `TIME_PROTECTED=.github/workflows/,.gitea/workflows/,deploy,infra/,.env,secret` so the engine under `.github/time/` merges itself on Approve + green like any other code.
- Objections go back to the worker. `/time retry` on a card whose PR is open hands the worker the reviewer's latest comments (marked as the reviewer's, strangers' left out) on the same branch; `open_pr` on an open PR updates its description instead of failing; the card goes back to `dispatched`, so the next beat reviews again and an objection older than the push no longer counts. - Objections go back to the worker. `/time retry` on a card whose PR is open hands the worker the reviewer's latest comments (marked as the reviewer's, strangers' left out) on the same branch; `open_pr` on an open PR updates its description instead of failing; the card goes back to `dispatched`, so the next beat reviews again and an objection older than the push no longer counts.
- A batch's children start in the tick that spawned them (GitHub lists a card made a moment ago only later, so they used to wait a tick). - A batch's children start in the tick that spawned them (GitHub lists a card made a moment ago only later, so they used to wait a tick).
- Build briefs keep claims to what the code does: no promised features or schedules, and the trending repos are the wave a venture rides, never something it "builds on". Fix rounds take a failing claim off the page rather than building a feature in a hurry. In the first live batch, every fix round was a claim the code didn't back. - Build briefs keep claims to what the code does: no promised features or schedules, and the trending repos are the wave a venture rides, never something it "builds on". Fix rounds take a failing claim off the page rather than building a feature in a hurry. In the first live batch, every fix round was a claim the code didn't back.
+40
View File
@@ -193,3 +193,43 @@
{"at":"2026-10-17T18:27:55Z","model":"claude-sonnet-5-5","in":9230,"out":918,"usd":0.02764} {"at":"2026-10-17T18:27:55Z","model":"claude-sonnet-5-5","in":9230,"out":918,"usd":0.02764}
{"at":"2026-10-17T18:27:59Z","model":"claude-sonnet-5-5","in":10126,"out":226,"usd":0.022512} {"at":"2026-10-17T18:27:59Z","model":"claude-sonnet-5-5","in":10126,"out":226,"usd":0.022512}
{"at":"2026-10-17T18:28:11Z","model":"claude-haiku-5-5","in":2565,"out":1767,"usd":0.00114} {"at":"2026-10-17T18:28:11Z","model":"claude-haiku-5-5","in":2565,"out":1767,"usd":0.00114}
{"at":"2026-10-17T20:20:54Z","model":"claude-sonnet-5-5","in":2158,"out":26,"usd":0.004576}
{"at":"2026-10-17T20:20:56Z","model":"claude-sonnet-5-5","in":2637,"out":163,"usd":0.006904}
{"at":"2026-10-17T20:20:58Z","model":"claude-sonnet-5-5","in":3153,"out":187,"usd":0.008176}
{"at":"2026-10-17T20:21:01Z","model":"claude-sonnet-5-5","in":5843,"out":191,"usd":0.013596}
{"at":"2026-10-17T20:21:06Z","model":"claude-sonnet-5-5","in":6251,"out":341,"usd":0.015912}
{"at":"2026-10-17T20:21:09Z","model":"claude-sonnet-5-5","in":6655,"out":381,"usd":0.01712}
{"at":"2026-10-17T20:21:16Z","model":"claude-sonnet-5-5","in":7036,"out":659,"usd":0.020662}
{"at":"2026-10-17T20:21:20Z","model":"claude-sonnet-5-5","in":7483,"out":397,"usd":0.018936}
{"at":"2026-10-17T20:21:37Z","model":"claude-sonnet-5-5","in":7960,"out":1855,"usd":0.03447}
{"at":"2026-10-17T20:21:49Z","model":"claude-sonnet-5-5","in":10584,"out":1157,"usd":0.032738}
{"at":"2026-10-17T20:21:57Z","model":"claude-sonnet-5-5","in":11307,"out":837,"usd":0.030984}
{"at":"2026-10-17T20:22:04Z","model":"claude-sonnet-5-5","in":16615,"out":513,"usd":0.03836}
{"at":"2026-10-17T20:22:17Z","model":"claude-sonnet-5-5","in":19010,"out":942,"usd":0.04744}
{"at":"2026-10-17T20:22:23Z","model":"claude-sonnet-5-5","in":19216,"out":477,"usd":0.043202}
{"at":"2026-10-17T20:22:33Z","model":"claude-sonnet-5-5","in":19515,"out":970,"usd":0.04873}
{"at":"2026-10-17T20:22:45Z","model":"claude-sonnet-5-5","in":19943,"out":1162,"usd":0.051506}
{"at":"2026-10-17T20:22:47Z","model":"claude-sonnet-5-5","in":22260,"out":116,"usd":0.04568}
{"at":"2026-10-17T20:22:50Z","model":"claude-sonnet-5-5","in":22837,"out":187,"usd":0.047544}
{"at":"2026-10-17T20:22:54Z","model":"claude-sonnet-5-5","in":23284,"out":220,"usd":0.048768}
{"at":"2026-10-17T20:22:56Z","model":"claude-sonnet-5-5","in":24921,"out":165,"usd":0.051492}
{"at":"2026-10-17T20:22:58Z","model":"claude-sonnet-5-5","in":25358,"out":70,"usd":0.051416}
{"at":"2026-10-17T20:23:00Z","model":"claude-sonnet-5-5","in":27711,"out":210,"usd":0.057522}
{"at":"2026-10-17T20:23:04Z","model":"claude-sonnet-5-5","in":31018,"out":347,"usd":0.065506}
{"at":"2026-10-17T20:23:08Z","model":"claude-sonnet-5-5","in":33756,"out":253,"usd":0.070042}
{"at":"2026-10-17T20:23:10Z","model":"claude-sonnet-5-5","in":35236,"out":161,"usd":0.072082}
{"at":"2026-10-17T20:23:16Z","model":"claude-sonnet-5-5","in":37623,"out":401,"usd":0.079256}
{"at":"2026-10-17T20:23:22Z","model":"claude-sonnet-5-5","in":40686,"out":229,"usd":0.083662}
{"at":"2026-10-17T20:23:23Z","model":"claude-sonnet-5-5","in":42769,"out":101,"usd":0.086548}
{"at":"2026-10-17T20:23:26Z","model":"claude-sonnet-5-5","in":45100,"out":186,"usd":0.09206}
{"at":"2026-10-17T20:23:28Z","model":"claude-sonnet-5-5","in":47113,"out":261,"usd":0.096836}
{"at":"2026-10-17T20:23:32Z","model":"claude-sonnet-5-5","in":49280,"out":332,"usd":0.10188}
{"at":"2026-10-17T20:23:34Z","model":"claude-sonnet-5-5","in":51809,"out":190,"usd":0.105518}
{"at":"2026-10-17T20:23:37Z","model":"claude-sonnet-5-5","in":53813,"out":206,"usd":0.109686}
{"at":"2026-10-17T20:23:38Z","model":"claude-sonnet-5-5","in":54057,"out":85,"usd":0.108964}
{"at":"2026-10-17T20:23:40Z","model":"claude-sonnet-5-5","in":54152,"out":114,"usd":0.109444}
{"at":"2026-10-17T20:23:43Z","model":"claude-sonnet-5-5","in":54408,"out":310,"usd":0.111916}
{"at":"2026-10-17T20:23:47Z","model":"claude-sonnet-5-5","in":55850,"out":193,"usd":0.11363}
{"at":"2026-10-17T20:23:49Z","model":"claude-sonnet-5-5","in":58100,"out":213,"usd":0.11833}
{"at":"2026-10-17T20:23:53Z","model":"claude-sonnet-5-5","in":60500,"out":471,"usd":0.12571}
{"at":"2026-10-17T20:23:59Z","model":"claude-sonnet-5-5","in":62407,"out":618,"usd":0.130994}
@@ -0,0 +1,10 @@
{
"skills": [
{
"name": "mdeck",
"description": "Make slide decks as .mdeck links, plain-text slides that open at mdecks.yolopreneur.lol with the whole deck inside the link and nothing uploaded. Use when asked for slides, a deck or a presentation to open in a browser or share as a link.",
"url": "https://mdecks.yolopreneur.lol/SKILL.md",
"files": ["SKILL.md"]
}
]
}
+521
View File
@@ -0,0 +1,521 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>.mdeck — plain text in, clean slides out</title>
<meta name="description" content="Type markdown-ish slides, watch a clean navigable deck render live, copy it as one self-contained HTML file. Free, runs entirely in your browser.">
<link rel="alternate" type="text/markdown" href="/SKILL.md">
<style>
:root {
--bg: #0f1115;
--panel: #171a21;
--panel-2: #1e222b;
--ink: #e8eaf0;
--ink-dim: #9aa1af;
--accent: #6ea8fe;
--accent-2: #9d7bff;
--line: #2a2f3a;
--radius: 12px;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--ink);
display: flex;
flex-direction: column;
min-height: 100vh;
}
header {
padding: 18px 20px 10px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
header .left {
display: flex;
align-items: baseline;
gap: 12px;
}
header h1 {
margin: 0;
font-size: 1.35rem;
letter-spacing: 0.2px;
}
header h1 .dot { color: var(--accent); }
header p {
margin: 0;
color: var(--ink-dim);
font-size: 0.9rem;
max-width: 62ch;
}
header .cta {
background: linear-gradient(135deg, var(--accent), var(--accent-2));
color: #0b0d12;
font-weight: 600;
border: none;
border-radius: 9px;
padding: 8px 16px;
font-size: 0.85rem;
text-decoration: none;
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
}
header .cta:hover { filter: brightness(1.08); }
.early-note {
margin: 6px 0 0;
font-size: 0.75rem;
color: var(--ink-dim);
grid-column: 1 / -1;
}
main {
flex: 1;
display: grid;
grid-template-columns: minmax(280px, 420px) 1fr;
gap: 14px;
padding: 12px 20px 16px;
min-height: 0;
}
.editor-pane {
display: flex;
flex-direction: column;
min-height: 0;
}
.editor-pane label {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--ink-dim);
margin-bottom: 6px;
}
textarea {
flex: 1;
min-height: 220px;
resize: none;
background: var(--panel);
color: var(--ink);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 14px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.9rem;
line-height: 1.55;
outline: none;
}
textarea:focus { border-color: var(--accent); }
.hint {
margin-top: 8px;
font-size: 0.78rem;
color: var(--ink-dim);
line-height: 1.5;
}
.hint code {
background: var(--panel-2);
border: 1px solid var(--line);
border-radius: 5px;
padding: 1px 5px;
font-size: 0.75rem;
}
.stage {
display: flex;
flex-direction: column;
min-height: 0;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
flex-wrap: wrap;
}
button {
background: var(--panel-2);
color: var(--ink);
border: 1px solid var(--line);
border-radius: 9px;
padding: 8px 14px;
font-size: 0.85rem;
cursor: pointer;
touch-action: manipulation;
}
button:hover { border-color: var(--accent); }
button:disabled { opacity: 0.4; cursor: default; }
button.primary {
background: linear-gradient(135deg, var(--accent), var(--accent-2));
color: #0b0d12;
font-weight: 600;
border: none;
}
.counter {
margin-left: auto;
font-variant-numeric: tabular-nums;
color: var(--ink-dim);
font-size: 0.85rem;
}
.progress {
height: 5px;
background: var(--panel-2);
border-radius: 99px;
overflow: hidden;
margin-bottom: 10px;
}
.progress > div {
height: 100%;
width: 0;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
transition: width 180ms ease;
}
.slide-wrap {
flex: 1;
min-height: 0;
border: 1px solid var(--line);
border-radius: var(--radius);
background:
radial-gradient(1200px 500px at 80% -10%, rgba(110,168,254,0.10), transparent),
radial-gradient(900px 500px at 0% 110%, rgba(157,123,255,0.10), transparent),
var(--panel);
overflow-y: auto;
touch-action: pan-y;
}
.slide {
--fit: 1;
padding: clamp(22px, 5vw, 56px);
min-height: 100%;
overflow-wrap: anywhere;
display: flex;
flex-direction: column;
justify-content: center;
}
.slide h1 {
font-size: calc(clamp(1.6rem, 4.5vw, 3rem) * var(--fit));
margin: 0 0 0.5em;
line-height: 1.15;
}
.slide h2 {
font-size: calc(clamp(1.2rem, 3vw, 1.9rem) * var(--fit));
margin: 0 0 0.6em;
color: var(--ink-dim);
font-weight: 500;
}
.slide ul {
margin: 0;
padding-left: 1.3em;
font-size: calc(clamp(1rem, 2.2vw, 1.35rem) * var(--fit));
line-height: 1.7;
}
.slide p {
font-size: calc(clamp(1rem, 2.2vw, 1.35rem) * var(--fit));
line-height: 1.7;
margin: 0.3em 0;
}
.slide a {
color: var(--accent);
text-decoration: none;
}
.slide a:hover {
text-decoration: underline;
}
.slide .empty { color: var(--ink-dim); font-style: italic; }
.slide code, .export-note code {
background: rgba(110,168,254,0.12);
border-radius: 5px;
padding: 1px 6px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.9em;
}
.fit-note {
font-size: 0.78rem;
color: var(--ink-dim);
margin-top: 8px;
}
.fit-note.warn { color: #f2c46d; }
.export-note {
font-size: 0.78rem;
color: var(--ink-dim);
margin-top: 8px;
}
section {
padding: 40px 20px;
max-width: 1200px;
margin: 0 auto;
}
section h2 {
margin: 0 0 20px;
font-size: 1.5rem;
}
.steps {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
}
.step {
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 20px;
}
.step h3 {
margin: 0 0 10px;
font-size: 1.1rem;
color: var(--accent);
}
.repos {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.repo {
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 20px;
}
.repo h3 {
margin: 0 0 8px;
font-size: 1.1rem;
}
.repo h3 a {
color: var(--accent);
text-decoration: none;
}
.repo h3 a:hover {
text-decoration: underline;
}
.stars {
font-size: 0.9rem;
color: var(--ink-dim);
margin: 0 0 12px;
}
.stars b {
color: var(--accent);
}
.tiers {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 30px;
}
.tier {
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 25px;
}
.tier h3 {
margin: 0 0 15px;
font-size: 1.3rem;
}
.tier .price {
font-size: 1.8rem;
margin: 0 0 15px;
}
.tier ul {
padding-left: 1.3em;
margin: 0 0 20px;
}
.tier .btn {
display: inline-block;
text-decoration: none;
text-align: center;
width: 100%;
}
.nothing-charged {
text-align: center;
font-style: italic;
color: var(--ink-dim);
margin: 20px 0 0;
}
.limits {
padding-left: 1.3em;
color: var(--ink-dim);
}
.limits li {
margin-bottom: 10px;
}
footer {
padding: 20px;
text-align: center;
color: var(--ink-dim);
font-size: 0.9rem;
border-top: 1px solid var(--line);
}
footer a {
color: var(--accent);
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
@media (max-width: 820px) {
main {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
textarea { min-height: 150px; flex: none; }
.slide-wrap { min-height: 300px; }
.steps, .repos {
grid-template-columns: 1fr;
}
.tiers {
grid-template-columns: 1fr;
}
header {
flex-direction: column;
align-items: flex-start;
}
header .left {
flex-direction: column;
}
}
/* finish: a one-line header, the product as the whole first screen, and the sections flowing after it (never over it) */
html, body { height: auto; }
body { display: block; }
header { flex-direction: row; align-items: center; gap: 6px 14px; padding: 12px 20px; border-bottom: 1px solid var(--line); }
header h1 { font-size: 1.4rem; letter-spacing: -0.01em; }
header .promise { margin: 0; color: var(--ink-dim); font-size: 0.9rem; }
header .early-note { margin: 0 0 0 auto; font-size: 0.75rem; color: var(--ink-dim); }
header .agent { padding: 6px 12px; font-size: 0.8rem; white-space: nowrap; }
main { flex: none; height: calc(100svh - 60px); min-height: 460px; }
main section { padding: 0; max-width: none; margin: 0; }
.hint code { white-space: nowrap; }
@media (max-width: 820px) {
header { padding: 10px 16px; }
header .early-note { margin: 0; flex: 1 1 150px; }
header .agent { order: 1; }
main { height: auto; min-height: 0; grid-template-rows: auto auto; padding: 12px 16px 16px; }
}
</style>
</head>
<body>
<header>
<h1><span class="dot">.</span>mdeck</h1>
<p class="promise">Plain text in, slides out.</p>
<button id="agent" class="agent">Copy prompt for your agent</button>
<p class="early-note">Opens a public GitHub issue. No email, no payment.</p>
<a class="cta" href="https://teahub.xyz/teahubxyz/time/issues/new?template=early-access.yml&title=Early+access%3A+.mdeck&venture=mdecks&plan=Pro" target="_blank" rel="noopener">Get early access</a>
</header>
<main id="try">
<section class="editor-pane">
<label for="src">Slides</label>
<textarea id="src" spellcheck="false"></textarea>
<p class="hint">
<code># Title</code> heading · <code>## Subtitle</code> · <code>- bullet</code> point ·
<code>---</code> on its own line starts a new slide · inline <code>**bold**</code>, <code>*italic*</code>, <code>`code`</code>.
Plain lines become paragraphs.
</p>
</section>
<section class="stage">
<div class="toolbar">
<button id="prev" aria-label="Previous slide">&larr; Prev</button>
<button id="next" aria-label="Next slide">Next &rarr;</button>
<button id="copy" class="primary">Copy deck as HTML</button>
<button id="link">Copy link</button>
<span class="counter" id="counter">1 / 1</span>
</div>
<div class="progress" role="progressbar" aria-label="Deck progress"><div id="bar"></div></div>
<div class="slide-wrap" id="wrap"><article class="slide" id="slide"></article></div><p class="fit-note" id="fitnote" role="status" hidden></p><p class="fit-note" id="linknote" role="status" hidden></p>
<p class="export-note">The copied file is a self-contained deck: open it in any browser and use &larr; / &rarr; to present. A copied link carries the deck inside it, after the #, so it opens here with nothing stored.</p>
</section>
</main>
<section id="how">
<h2>How it works</h2>
<div class="steps">
<div class="step">
<h3>Type your slides</h3>
<p>Markdown-ish text in the editor: headings, bullets, paragraphs, <code>---</code> between slides. The syntax is the stuff you already type.</p>
</div>
<div class="step">
<h3>Watch it set live</h3>
<p>Every keystroke re-typesets the deck on the right. Navigate with arrow keys or swipe; the progress bar keeps your place.</p>
</div>
<div class="step">
<h3>Take the file with you</h3>
<p>One button copies the deck as a single self-contained HTML file. Open it anywhere, present it offline, email it, done. Or copy a link with the whole deck inside it.</p>
</div>
<div class="step">
<h3>Or ask your agent</h3>
<p>Press <b>Copy prompt for your agent</b> at the top, paste it into Claude, ChatGPT, Cursor or any other agent, and ask for a deck. It answers with a link that opens here.</p>
</div>
</div>
</section>
<section id="why">
<h2>Why this exists</h2>
<div class="repos">
<div class="repo">
<h3><a href="https://github.com/storytold/wordcraft" target="_blank" rel="noopener">storytold/wordcraft</a></h3>
<div class="stars">★ <b>5,496</b> stars</div>
<p>Proof that people want prose in, polished output out — no canvas-dragging required.</p>
</div>
<div class="repo">
<h3><a href="https://github.com/storytold/gridcraft" target="_blank" rel="noopener">storytold/gridcraft</a></h3>
<div class="stars">★ <b>2,749</b> stars</div>
<p>Structured text becoming layout: the same bet .mdeck makes for slides.</p>
</div>
<div class="repo">
<h3><a href="https://github.com/storytold/cadcraft" target="_blank" rel="noopener">storytold/cadcraft</a></h3>
<div class="stars">★ <b>2,552</b> stars</div>
<p>Even geometry is going plain-text. Your weekly status deck can too.</p>
</div>
</div>
</section>
<section id="pricing">
<h2>Pricing</h2>
<div class="tiers">
<div class="tier">
<h3>Free</h3>
<div class="price">$0</div>
<p>For anyone who needs a quick deck right now.</p>
<ul>
<li>Full plain-text editor</li>
<li>Live navigable preview with arrow keys and progress bar</li>
<li>One-click copy of the deck as self-contained HTML</li>
<li>Links with the deck inside them, nothing stored</li>
<li>Runs entirely in the browser; nothing is saved or sent.</li>
</ul>
<a class="cta btn" href="#try">Use it free</a>
</div>
<div class="tier">
<h3>Pro</h3>
<div class="price">$5/mo founding price</div>
<p>For people who make a deck every week and want it saved and polished.</p>
<ul>
<li>Saved decks, themes, speaker notes</li>
<li>PDF export</li>
<li>Plain text in, always.</li>
</ul>
<a class="cta btn" href="https://teahub.xyz/teahubxyz/time/issues/new?template=early-access.yml&title=Early+access%3A+.mdeck&venture=mdecks&plan=Pro" target="_blank" rel="noopener">Join early access</a>
</div>
</div>
<p class="nothing-charged">Nothing is charged today.</p>
</section>
<section id="limits">
<h2>Honest limits</h2>
<ul class="limits">
<li>Nothing is stored — close the tab without copying a link or the HTML and your text is gone.</li>
<li>Anyone with a link can read the deck in it, and a very long deck makes a link some apps cut short.</li>
<li>One layout and one typeface; no themes or branding.</li>
<li>No images, tables, or nested lists yet.</li>
<li>No speaker notes, presenter view, or PDF export (that is the Pro idea, if enough people ask for it).</li>
<li>The copy button needs a modern browser with clipboard access.</li>
</ul>
</section>
<footer>
<p>For agents: <a href="/SKILL.md">SKILL.md</a></p>
<p>Built by <a href="https://teahub.xyz/teahubxyz/time" target="_blank" rel="noopener">Time, an agent-run venture studio on GitHub</a></p>
<p><a href="https://teahub.xyz/teahubxyz/time/issues/13" target="_blank" rel="noopener">Want the next version? 👍 the signal issue</a></p>
</footer>
<script src="app.js"></script>
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
# .mdeck
> Type slides in plain text and get a clean presentation in the browser. A deck travels inside its link (after the #), so nothing is uploaded and no account is needed.
Read SKILL.md first and follow it: it says how to write a deck and turn it into a link that opens in the editor.
- [SKILL.md](https://mdecks.yolopreneur.lol/SKILL.md): the skill, how an agent makes .mdeck links
- [Editor](https://mdecks.yolopreneur.lol/): where a deck link opens; type or paste slides, present with the arrow keys