diff --git a/quc/src/page.js b/quc/src/page.js
new file mode 100644
index 0000000..59acd08
--- /dev/null
+++ b/quc/src/page.js
@@ -0,0 +1,233 @@
+// The one page: start screen at /, the chair (live run, or the replay with its scrubber) at /run/:id.
+// The browser code lives in real functions below and is inlined with toString(), so there is nothing to escape and no build.
+import { validate } from "./validate.js";
+
+export const CHAIR = `
+
+
+ `;
+
+const CSS = `
+:root{--bg:#f7f5f0;--fg:#1b1a17;--mute:#6d6b64;--line:#e2dfd6;--card:#fffdf8;--accent:#7a3cff;--accent-fg:#fff;--add:#e3f6e8;--add-fg:#14532d;--del:#fbe7e7;--del-fg:#7f1d1d;--warn:#b42318;--serif:ui-serif,Georgia,"Times New Roman",serif;--sans:system-ui,-apple-system,"Segoe UI",sans-serif;--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
+@media (prefers-color-scheme:dark){:root{--bg:#141312;--fg:#ecebe6;--mute:#9a988f;--line:#2b2a27;--card:#1b1a18;--accent:#a98bff;--accent-fg:#141312;--add:#12301d;--add-fg:#b6f0c6;--del:#3a1616;--del-fg:#ffc2c2;--warn:#ff8a80}}
+*{box-sizing:border-box}
+body{margin:0;min-height:100vh;min-height:100dvh;background:var(--bg);color:var(--fg);font:16px/1.55 var(--sans)}
+a{color:var(--accent)}
+header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px;border-bottom:1px solid var(--line);max-width:1200px;margin:0 auto}
+header a.brand{font:600 22px var(--serif);color:var(--fg);text-decoration:none}
+main{max-width:1200px;margin:0 auto;padding:24px 16px 180px}
+h1{font:600 clamp(30px,6vw,52px)/1.08 var(--serif);margin:0 0 12px;letter-spacing:-.01em}
+h2{font:600 20px var(--serif);margin:0 0 8px}
+p{margin:0 0 14px}.mute{color:var(--mute)}
+[hidden]{display:none!important}
+form{max-width:560px;margin-top:20px}
+label{display:block;font-weight:600;margin-bottom:4px}
+input,button,textarea{font:inherit;color:var(--fg)}
+input[type=text],input[type=url]{width:100%;padding:11px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card)}
+input[aria-invalid=true]{border-color:var(--warn)}
+button{padding:10px 16px;border-radius:8px;border:1px solid var(--line);background:var(--card);cursor:pointer;font-weight:600}
+button.primary{background:var(--accent);border-color:var(--accent);color:var(--accent-fg)}
+button.safeword{background:#c62828;border-color:#c62828;color:#fff;font-size:17px;padding:12px 20px}
+button:disabled{opacity:.5;cursor:default}
+.field{margin-bottom:16px}.err{color:var(--warn);font-size:14px;margin:4px 0 0;min-height:1em}
+.notice{padding:10px 12px;border-left:3px solid var(--accent);background:var(--card);font-size:14px}
+.rules{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;margin:28px 0;max-width:900px}
+.rules div{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:14px}
+.rules b{display:block;font-family:var(--serif);font-size:18px;margin-bottom:4px}
+.bar{display:flex;flex-wrap:wrap;align-items:center;gap:10px 16px;margin-bottom:16px}
+.status{font-size:14px;padding:4px 10px;border-radius:99px;background:var(--card);border:1px solid var(--line)}
+.status[data-s=running]::before{content:"";display:inline-block;width:8px;height:8px;border-radius:50%;background:#2a9d4a;margin-right:6px;animation:pulse 1.2s infinite}
+@keyframes pulse{50%{opacity:.25}}
+.grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.5fr);gap:16px}
+@media (max-width:820px){.grid{grid-template-columns:minmax(0,1fr)}}
+.panel{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px;min-width:0}
+.feed{display:flex;flex-direction:column;gap:10px;max-height:62vh;overflow:auto}
+.say{border-left:3px solid var(--accent);padding:2px 0 2px 10px;font-family:var(--serif);font-size:17px}
+.sys{font-size:13px;color:var(--mute)}
+.note{align-self:flex-end;background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:6px 10px;font-size:14px;max-width:85%}
+.unlock{font-size:13px;font-weight:600;color:var(--accent)}
+.files{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
+.files button{font:12px var(--mono);padding:4px 8px;font-weight:400}
+.files button[aria-pressed=true]{border-color:var(--accent);color:var(--accent)}
+pre.diff{margin:0;font:12.5px/1.45 var(--mono);overflow:auto;max-height:62vh;white-space:pre}
+pre.diff span{display:block;padding:0 8px}
+pre.diff .a{background:var(--add);color:var(--add-fg)}pre.diff .d{background:var(--del);color:var(--del-fg)}pre.diff .h{color:var(--mute)}
+.commit{font:12px var(--mono);color:var(--mute);margin:0 0 8px}
+.saybox{display:flex;gap:8px;margin-top:12px}.saybox input{flex:1}
+.scrub{display:flex;align-items:center;gap:10px;margin:0 0 16px}.scrub input{flex:1;accent-color:var(--accent)}
+.after{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin:0 0 16px}
+.after div{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:12px}
+.after b{display:block;font:600 24px var(--serif)}
+.chair{position:fixed;right:0;bottom:0;width:clamp(84px,18vw,150px);color:var(--fg);pointer-events:none;opacity:.85}
+`;
+
+// ---------- browser code ----------
+
+function client(validate) {
+ const $ = (id) => document.getElementById(id);
+ const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
+ const api = async (path, opts = {}) => {
+ const r = await fetch(path, { ...opts, headers: { "content-type": "application/json", ...(opts.headers || {}) } });
+ const j = await r.json().catch(() => ({}));
+ if (!r.ok) throw Object.assign(new Error(j.error || r.statusText), { status: r.status, body: j });
+ return j;
+ };
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+
+ fetch("/chair.svg").then((r) => r.text()).then((s) => { const d = document.createElement("div"); d.className = "chair"; d.innerHTML = s; d.firstElementChild.setAttribute("aria-hidden", "true"); document.body.append(d); }).catch(() => {});
+
+ const m = location.pathname.match(/^\/run\/([\w-]+)$/);
+ if (!m) return startScreen();
+ return chair(m[1]);
+
+ function startScreen() {
+ $("start").hidden = false;
+ $("form").addEventListener("submit", async (ev) => {
+ ev.preventDefault();
+ const repo = $("repo").value.trim(), task = $("task").value.trim(), errs = validate(repo, task);
+ for (const k of ["repo", "task"]) { $(k + "-err").textContent = errs[k] || ""; $(k).setAttribute("aria-invalid", !!errs[k]); }
+ if (Object.keys(errs).length) return $(errs.repo ? "repo" : "task").focus();
+ const b = $("go"); b.disabled = true; b.textContent = "Taking your seat…";
+ try {
+ const j = await api("/api/runs", { method: "POST", body: JSON.stringify({ repo, task }) });
+ location.assign("/run/" + j.id);
+ } catch (e) {
+ if (e.body?.id) return location.assign("/run/" + e.body.id);
+ $("task-err").textContent = e.message; b.disabled = false; b.textContent = "Hand it over";
+ }
+ });
+ }
+
+ function chair(id) {
+ $("run").hidden = false;
+ document.title = "quc: run " + id;
+ let run = null, owner = false, events = [], at = 0, live = false, pick = null;
+
+ const render = () => {
+ if (!run) return;
+ const shown = events.slice(0, at);
+ $("run-task").textContent = run.task;
+ $("run-repo").textContent = run.repo; $("run-repo").href = run.repo;
+ const st = run.status === "ended" ? { finished: "Finished", safeworded: "Safeworded", unlocked: "You took the wheel" }[run.outcome] : run.status === "queued" ? "Taking over…" : "Working on your repo";
+ $("status").textContent = st; $("status").dataset.s = run.status;
+ $("safeword").hidden = !(owner && run.status !== "ended");
+ $("saybox").hidden = !(owner && run.status !== "ended");
+ $("level").textContent = owner && run.status !== "ended" ? ["Caged. You can only watch.", "Notch 1: you may leave notes.", "Notch 2: name a file path to veto it.", ""][run.level || 0] : "";
+ $("feed").innerHTML = shown.filter((e) => e.kind !== "diff" && e.kind !== "commit").map((e) =>
+ e.kind === "commentary" ? `
${esc(e.data.text)}
`
+ : e.kind === "note" ? `You: ${esc(e.data.text)}
`
+ : e.kind === "unlock" ? `Unlocked notch ${e.data.level}
`
+ : `${esc(e.data.text)}
`).join("") || `Nothing yet. Sit.
`;
+ $("feed").scrollTop = $("feed").scrollHeight;
+ const diffs = shown.filter((e) => e.kind === "diff");
+ const latest = {};
+ for (const d of diffs) latest[d.data.path] = d;
+ const paths = Object.keys(latest);
+ const focus = pick && latest[pick] ? latest[pick] : diffs[diffs.length - 1];
+ $("files").innerHTML = paths.map((p) => `${esc(p)}${latest[p].data.reverted ? " (vetoed)" : ""} `).join("");
+ const commit = focus && shown.find((e) => e.kind === "commit" && e.seq === focus.seq + 1);
+ $("commit").textContent = commit ? "commit: " + commit.data.message : focus?.data.reverted ? "reverted on your veto" : "";
+ $("diff").innerHTML = focus ? focus.data.patch.split("\n").map((l) => `${esc(l) || " "} `).join("")
+ : `No changes yet. It hasn't touched anything. Yet. `;
+ const ended = run.status === "ended";
+ $("scrub").hidden = !(ended || !owner) || !events.length;
+ $("scrub-range").max = events.length; $("scrub-range").value = at;
+ $("scrub-at").textContent = `${at} / ${events.length}`;
+ $("after").hidden = !ended;
+ if (ended) {
+ $("a-files").textContent = run.stats.files; $("a-add").textContent = "+" + run.stats.added; $("a-del").textContent = "-" + run.stats.removed;
+ $("a-patch").hidden = run.outcome === "safeworded" || !run.stats.files;
+ $("a-patch").href = `/api/runs/${id}/patch`;
+ }
+ };
+
+ $("files").addEventListener("click", (e) => { const p = e.target.closest("button")?.dataset.p; if (p) { pick = p; render(); } });
+ $("scrub-range").addEventListener("input", (e) => { at = Number(e.target.value); live = false; render(); });
+ $("share").addEventListener("click", async () => { try { await navigator.clipboard.writeText(location.href); $("share").textContent = "Link copied"; } catch { prompt("Copy the replay link", location.href); } });
+ $("safeword").addEventListener("click", async () => {
+ $("safeword").disabled = true;
+ try { await api(`/api/runs/${id}/safeword`, { method: "POST", body: "{}" }); } finally { await refresh(); }
+ });
+ $("saybox").addEventListener("submit", async (e) => {
+ e.preventDefault();
+ const text = $("say").value.trim(); if (!text) return;
+ $("say").value = "";
+ try { await api(`/api/runs/${id}/say`, { method: "POST", body: JSON.stringify({ text }) }); } catch (err) { $("say-err").textContent = err.message; }
+ await refresh();
+ });
+
+ async function refresh() {
+ const j = await api(`/api/runs/${id}?after=${events.length}`);
+ const follow = at === events.length;
+ run = j.run; owner = j.owner;
+ events = events.concat(j.events.filter((e) => e.seq > events.length));
+ if (follow || live) at = events.length;
+ render();
+ }
+
+ (async () => {
+ try { await refresh(); } catch (e) { $("run-task").textContent = e.status === 404 ? "No such run." : e.message; return; }
+ live = true;
+ if (run.status !== "ended") {
+ (async () => { while (run.status !== "ended") { await sleep(2000); await refresh().catch(() => {}); } })();
+ while (owner && run.status !== "ended") {
+ try { await api(`/api/runs/${id}/step`, { method: "POST", body: "{}" }); } catch { await sleep(2000); }
+ await refresh().catch(() => {});
+ await sleep(300);
+ }
+ } else if (events.length) {
+ at = 0; render();
+ for (let i = 1; i <= events.length && !document.hidden; i++) { if (at !== i - 1) break; at = i; render(); await sleep(350); }
+ }
+ })();
+ }
+}
+
+export const PAGE = `
+
+quc
+
+
+
+
+
+Hand over your repo. Take a seat.
+quc takes a public GitHub repo and one line of task, then does whatever it wants to your code while you watch from the corner. No approve buttons. No edits. You get a chair, a safeword, and a replay.
+
+
+
The cage It works on its own copy, fully unsupervised. Your repo is never touched.
+
The chair You watch every change land, live and read-only, while it talks down to you.
+
The safeword One button. Everything stops and its work is thrown away.
+
The magic words Say the right thing and it lets you speak, veto a file, or take the wheel.
+
+
+
+… Safeword Copy replay link
+Loading…
+on
+
+
Files changed 0
Lines added +0
Lines removed -0
+
+
+Replay
+
+
+
What it's doing to your code
+
+
+
+`;
diff --git a/quc/src/worker.js b/quc/src/worker.js
index 3112a45..c005c47 100644
--- a/quc/src/worker.js
+++ b/quc/src/worker.js
@@ -1,114 +1,112 @@
-// quc: app shell + API in one Worker (no build step; the same module runs on Cloudflare and under dev.mjs).
-// GET / the shell (start form) GET /run/:id the shell (chair view) GET /chair.svg the chair-in-the-corner motif
-// GET /api/health {ok, rev} anything else 404
-// Fonts: system stacks on purpose (no webfont files, no third-party requests). shortcut: swap in self-hosted fonts when the brand picks them.
+// quc: page + API in one Worker (no build step; the same module runs on Cloudflare and under dev.mjs).
+// GET /, /run/:id the page (start screen, or the chair / replay)
+// GET /chair.svg the chair-in-the-corner motif
+// GET /api/health {ok, rev, live} live: false when no model key is bound (offline demo answers)
+// POST /api/runs {repo, task} -> {id} sets the visitor cookie
+// GET /api/runs/:id?after=N {run, events, owner} anyone with the link
+// POST /api/runs/:id/step advance one step owner only
+// POST /api/runs/:id/safeword stop, throw the work away owner only
+// POST /api/runs/:id/say {text}: talk from the chair owner only
+// GET /api/runs/:id/patch the run's changes as a unified diff
+// Bindings: KV (a KV namespace), VENICE_API_KEY (secret; without it the agent answers offline), REV, CODER_MODEL, PERSONA_MODEL,
+// RUNS_PER_DAY (per visitor, default 5), RUNS_PER_DAY_ALL (default 300).
+import { PAGE, CHAIR } from "./page.js";
+import { createRun, getRun, getEvents, publicRun, memoryStore } from "./runs.js";
+import { step, safeword, say, patch } from "./engine.js";
-export const CHAIR = `
-
-
- `;
+export { validate } from "./validate.js";
+export { PAGE, CHAIR };
-// Inlined into the page via toString(), so the browser and the tests run the same code. Returns {repo?, task?} errors; {} is valid.
-export function validate(repo, task) {
- const e = {};
- if (!/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+?(\.git)?\/?$/.test(repo)) e.repo = "Use a public GitHub repo URL, like https://github.com/owner/name.";
- if (task.length < 10) e.task = "Say a bit more: at least 10 characters.";
- else if (task.length > 200) e.task = "Keep it to one line: 200 characters at most.";
- return e;
+const COOKIE = "quc_v";
+const json = (body, status = 200, headers = {}) => Response.json(body, { status, headers: { "cache-control": "no-store", ...headers } });
+const fallback = memoryStore(); // dev and tests only: a deployed Worker always has KV bound
+
+function visitorOf(req) {
+ const m = (req.headers.get("cookie") || "").match(new RegExp(`(?:^|;\\s*)${COOKIE}=([A-Za-z0-9_-]{16,64})`));
+ return m ? m[1] : null;
}
-const NOTICE = "Replays are public. Anyone with the link to a run can watch it, so don't point this at anything you wouldn't show a stranger.";
-
-export const PAGE = `
-
-quc
-
-
-
-
-
-Leave it with the chair.
-Point quc at a public GitHub repo, say in one line what you want done, and go do something else. It works in the corner, quietly, and you can watch the replay.
-
-
-
-Run
-The chair is in the corner. Nothing has been done yet.
-Repo Task
-${NOTICE}
-
-
-
-
-`;
-const headers = (type, extra = {}) => ({ "content-type": type, ...extra });
+async function api(req, env, url) {
+ const store = env.KV || fallback;
+ const path = url.pathname;
+ if (path === "/api/health") return json({ ok: true, rev: env.REV || "", live: !!env.VENICE_API_KEY });
+ if (req.method === "POST") {
+ const origin = req.headers.get("origin");
+ if (origin && origin !== url.origin) return json({ error: "cross-site request" }, 403);
+ }
+ let visitor = visitorOf(req);
+ const setCookie = {};
+ if (!visitor) {
+ visitor = crypto.randomUUID().replace(/-/g, "");
+ setCookie["set-cookie"] = `${COOKIE}=${visitor}; Path=/; Max-Age=31536000; HttpOnly; SameSite=Lax${url.protocol === "https:" ? "; Secure" : ""}`;
+ }
+ const body = req.method === "POST" ? await req.json().catch(() => ({})) : {};
+
+ if (path === "/api/runs" && req.method === "POST") {
+ try {
+ const repo = String(body.repo || "").trim(), task = String(body.task || "").trim();
+ const active = await store.get(`active:${visitor}`);
+ if (!active && !(await underCap(store, visitor, env))) return json({ error: "That's enough for today. The chair needs a rest; come back tomorrow." }, 429, setCookie);
+ const run = await createRun(store, { repo, task, visitor });
+ return json({ id: run.id }, 201, setCookie);
+ } catch (e) {
+ if (e.code === "active") return json({ error: e.message, id: e.id }, 409, setCookie);
+ if (e.code === "invalid") return json({ error: Object.values(e.errors)[0], errors: e.errors }, 400, setCookie);
+ throw e;
+ }
+ }
+
+ const m = path.match(/^\/api\/runs\/([a-z0-9]{6,16})(?:\/(step|safeword|say|patch))?$/);
+ if (!m) return json({ error: "not found" }, 404);
+ const [, id, action] = m;
+ const run = await getRun(store, id);
+ if (!run) return json({ error: "no such run" }, 404, setCookie);
+ const owner = run.visitor === visitor;
+
+ if (!action && req.method === "GET") {
+ const after = Number(url.searchParams.get("after")) || 0;
+ const events = (await getEvents(store, id)).filter((e) => e.seq > after);
+ return json({ run: publicRun(run), events, owner }, 200, setCookie);
+ }
+ if (action === "patch" && req.method === "GET") {
+ const text = await patch(store, id);
+ return new Response(text, { headers: { "content-type": "text/x-diff; charset=utf-8", "content-disposition": `attachment; filename="quc-${id}.patch"` } });
+ }
+ if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
+ if (!owner) return json({ error: "Only the one in the chair can do that." }, 403, setCookie);
+ try {
+ if (action === "step") return json({ run: publicRun(await step(store, id, env)) });
+ if (action === "safeword") return json({ run: publicRun(await safeword(store, id)) });
+ if (action === "say") return json({ reply: await say(store, id, body.text) });
+ } catch (e) {
+ if (e.code) return json({ error: e.message }, 400);
+ throw e;
+ }
+ return json({ error: "not found" }, 404);
+}
export default {
async fetch(req, env = {}) {
- const { pathname } = new URL(req.url);
+ const url = new URL(req.url);
+ try {
+ if (url.pathname.startsWith("/api/")) return await api(req, env, url);
+ } catch (e) {
+ return json({ error: "Something broke in the corner: " + String(e.message || e).slice(0, 200) }, 500);
+ }
if (req.method !== "GET" && req.method !== "HEAD") return new Response("method not allowed", { status: 405 });
- if (pathname === "/api/health") return Response.json({ ok: true, rev: env.REV || "" }, { headers: { "cache-control": "no-store" } });
- if (pathname === "/chair.svg") return new Response(CHAIR, { headers: headers("image/svg+xml", { "cache-control": "public, max-age=3600" }) });
- if (pathname === "/" || pathname === "/index.html" || /^\/run\/[\w-]+$/.test(pathname)) return new Response(PAGE, { headers: headers("text/html; charset=utf-8") });
+ if (url.pathname === "/chair.svg") return new Response(CHAIR, { headers: { "content-type": "image/svg+xml", "cache-control": "public, max-age=3600" } });
+ if (url.pathname === "/" || url.pathname === "/index.html" || /^\/run\/[\w-]+$/.test(url.pathname))
+ return new Response(PAGE, { headers: { "content-type": "text/html; charset=utf-8", "x-frame-options": "DENY", "referrer-policy": "same-origin" } });
return new Response("not found", { status: 404 });
},
};