From bc6d5f12c18fcccddb9a7ce22ab669c4092f7b46 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 14 Oct 2026 03:33:09 +0200 Subject: [PATCH 01/92] docs(skills, swift): the three ScreenCaptureKit traps behind the recorder fixes, replayd per-path streams, contentRect in points, a sheet's stream holding its parent (cherry picked from commit adb33ee991a544247efc749ca23d850ffd84582e) --- .claude/skills/swift/references/gotchas.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.claude/skills/swift/references/gotchas.md b/.claude/skills/swift/references/gotchas.md index b89bd892af..6a13a53680 100644 --- a/.claude/skills/swift/references/gotchas.md +++ b/.claude/skills/swift/references/gotchas.md @@ -263,3 +263,19 @@ Keep measured findings separate from hypotheses; faster CPU numbers alone do not offset one run-loop turn later and check it again a moment after (`MarkdownFileTreeView.applyPendingPlaceScroll`). - **A context menu opened by automation closes within seconds if nothing picks an item.** Run the right-click, `see --scope menu` and the press back-to-back (tools control #530, 2026-10-15). +## ScreenCaptureKit recording β€” 2026-10-14 03:33 + +- **replayd (ScreenCaptureKit's daemon) groups its clients by executable PATH.** When any process started from a path + exits, every live stream of every other process from that same path stops with -3805 (connection interrupted). It + fires at the second process's exit, not at its `stopCapture`, and also when that process only listed windows. Two + `tools control capture record` runs therefore cut each other short until each recorder re-ran from its own disk + clone (8648526b3 on #502, `native/ax-tool/SnapshotSupport/RecorderExecutable.swift`). A hard link to the same file + is not a fix: it made `stopCapture` hang. A 40-line probe without ax-tool reproduced it 7 of 7 from one path and + 0 of 6 from a copy. +- **`SCStreamFrameInfo.contentRect` is in points, the frame's pixel buffer is in pixels.** Using the rect as pixels + crops a Retina frame to its top-left quarter and scales that up 2x. Multiply by `scaleFactor` (from the same info + dictionary) before cropping. +- **A sheet's window stream holds its whole parent window, shrunk to the sheet's size** (a 400x560 sheet arrived as + the 440x760 parent at 0.737). Painting it into the sheet's frame draws a second, smaller parent. When the parent is + also recorded, its own stream already shows the sheet; when the sheet is recorded alone, cut it out of the parent at + the sheet's frame (dd532721a). From 4b88835c5d095272d7626e8986990021ba0123a6 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 14 Oct 2026 04:39:08 +0200 Subject: [PATCH 02/92] docs(skills, babysit): every round gets a full fable-judge read of the summary, the reply file and the whole diff before the push, refusals and deferrals judged on their merits, plants run the commit's own tests (cherry picked from commit ec44a938a991d196d4c34b7b99fa2db19546af24) --- .claude/skills/babysit/SKILL.md | 4 +++- .claude/skills/babysit/references/judging.md | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.claude/skills/babysit/SKILL.md b/.claude/skills/babysit/SKILL.md index 552a85bc57..ddd15abfe8 100644 --- a/.claude/skills/babysit/SKILL.md +++ b/.claude/skills/babysit/SKILL.md @@ -141,7 +141,9 @@ reply arrive (Sol answered in 20 s, Grok in 59 s). During the confirmation perio ## 6. Judge each worker commit (fable-judge, inline) -[references/judging.md](references/judging.md) is the checklist. In short: the diff is the truth, not the report; +πŸ›‘ Invoke the `fable-judge` skill for every round, before the push, and read the worker's summary, its reply file and +the full diff together ([references/judging.md](references/judging.md) is the checklist, the rule comes first there). +Checks plus planted regressions are not a judgement; the code itself is judged. In short: the diff is the truth, not the report; rerun every claimed check yourself; plant the old code back to prove each new test fails without the fix; check the fix against the module's documented contracts and its callers (the #511 defect hid in a caller); check scope, one commit, nothing staged that is not the worker's; check the ready-to-post replies against the final code. Verdict per diff --git a/.claude/skills/babysit/references/judging.md b/.claude/skills/babysit/references/judging.md index 43bcac355d..93007b7647 100644 --- a/.claude/skills/babysit/references/judging.md +++ b/.claude/skills/babysit/references/judging.md @@ -3,6 +3,13 @@ The stance is fable-judge's: a report is a set of claims, not evidence. Judge every commit as if a stranger wrote it, and give the verdict with the evidence. Inline, never delegated: the verdict is the product of this loop. +πŸ›‘ **Rule: invoke the `fable-judge` skill and read the whole thing before every push** (Martin, 2026-10-14). Checks +and planted regressions alone are not a judgement: on the first overnight run four commits (one of 807 lines) went +out with green checks and plants but no line-by-line read. The judge reads, side by side, the worker's summary +report, its reply file (every verdict and every reply text it wants posted) and the FULL diff, tests and sources: +`judge-pack` prints all three in one file. Then it judges the code itself (repo rules in `.claude/rules/code.md` and +`tools.md`, contracts, duplication, type safety, scope), the claims against the code, and every refusal. + ## Checklist, in order 1. **The diff is the truth.** `git show --stat ` and the full diff. One commit, only scope files, nothing staged @@ -26,6 +33,16 @@ it, and give the verdict with the evidence. Inline, never delegated: the verdict 7. **The ready-to-post replies.** Each reply must be true of the final code after your correction: file names, counts, behaviour. Grok's said "the table is in prs.test.ts" after the move; such a line is corrected in the vault copy, not posted as is. + - **Every refusal and deferral is judged on its merits.** A `rejected` needs the file:line that disproves the + finding. A `no-change` or "follow-up" whose fix belongs in this PR (it is small, it is about this PR's change, + or it is not code at all, like a wrong PR description) is not accepted: do it in this round. On 2026-10-14 a + worker answered "the PR description calls login-io.ts a new file" with no-change; the fix was a one-line body + edit. + - **Removed test lines are guilty until explained.** List them (`judge-pack` puts the test diff first); a deleted + or loosened assertion must be replaced by a stronger one, or it is a weakened check. +9. **Plants run the tests the commit changed.** Plant each fixed source and run the test files the worker's commit + touched (`git show --stat`), not the source's folder: on 2026-10-14 two plants "passed" only because the folder + held none of the new tests. 8. **Cherry-pickability** (unified mode): test-pick onto the PR branch before you correct, so the correction can avoid the conflicting lines. From 2bb3b6d8f9eeeaf40daa55d66d298e98188233f5 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 14 Oct 2026 04:50:19 +0200 Subject: [PATCH 03/92] feat(skills, babysit): a run ledger records every prompt, worker reply, judge verdict and push as JSONL and renders the audit note on demand (cherry picked from commit 96447f63ad53c4b81faf858501f433d950a08910) --- .claude/skills/babysit/SKILL.md | 22 +++ .claude/skills/babysit/scripts/ledger.ts | 232 +++++++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 .claude/skills/babysit/scripts/ledger.ts diff --git a/.claude/skills/babysit/SKILL.md b/.claude/skills/babysit/SKILL.md index ddd15abfe8..9eff2de299 100644 --- a/.claude/skills/babysit/SKILL.md +++ b/.claude/skills/babysit/SKILL.md @@ -121,6 +121,28 @@ How each backend commits (measured 2026-10-14 with probe workers in linked workt escalation that the approval driver (step 5) accepts when it matches the brief. The brief therefore tells a Codex worker to run `git add -- ` and `git commit` as two separate commands, never chained with `&&`. +### The run ledger: log every prompt, turn, verdict and push + +Every run keeps `/ledger.jsonl` ([scripts/ledger.ts](scripts/ledger.ts)), one event per line, so the user can +audit afterwards what each worker was told and what it answered, not only the outcome. Append as you go, never +reconstruct at the end: + +- `spawn` and `steer`: the full prompt text (`--file `), with `--worker`, `--model`, `--pr`, `--round`. +- `turn`: the worker's final message when a turn ends (`lastText` of `tools codex wait --json`, Grok's report). +- `judge`: `--meta '{"verdict":"…","evidence":"…"}'`, plus refusals you overruled and the findings you fed back. +- `correction`, `push` (`range`), `post`, `decision`, `incident`, `note`. + +`bun .claude/skills/babysit/scripts/ledger.ts append --run --kind steer --pr 501 --round 2 --worker bs-gk +--model gpt-6-astra --file steer.md`; import `appendEvent` when a script of the run already does the step (the +integrate script logs `push` and `post` itself). The Markdown is built on demand, never kept by hand: +`ledger.ts render --run --out /--Ledger.md` (an overview table per PR, then each +PR's events in time order, prompts and final messages folded into `
`). Render it into the vault with every +report. + +A later round is a `steer` into the SAME worker thread: the worker keeps its context and its prompt cache. The same +PR's next round gets a delta (what the judge decided, what to fix); a new PR for an idle worker gets only the new +PR's facts (worktree, branch, round policy, thread command) plus "same rules as your first brief". + ## 5. Watch while they work Three Monitors, all with `timeout_ms: 1800000`, each re-armed as the very next tool call after its expiry notice diff --git a/.claude/skills/babysit/scripts/ledger.ts b/.claude/skills/babysit/scripts/ledger.ts new file mode 100644 index 0000000000..351336c998 --- /dev/null +++ b/.claude/skills/babysit/scripts/ledger.ts @@ -0,0 +1,232 @@ +#!/usr/bin/env bun +/** + * The babysit run ledger: one JSONL event per line, appended as the run goes, rendered to Markdown on demand. + * Everything a reader needs to audit a run sits in it: every prompt a worker got (spawn and steer, full text), + * each worker turn's final message, the judge's verdict and findings, corrections, pushes and posts. + * (Martin, 2026-10-14: "all the steers, the agent's final messages, the prompts / inputs / judge rounds".) + * + * bun ledger.ts append --run --kind [--pr N] [--round N] [--worker name] [--model m] + * [--text "…" | --file ] [--meta '{"k":"v"}'] [--at ] + * bun ledger.ts render --run [--out ] [--pr N] + * + * Kinds: spawn, steer, turn (a worker turn ended: its final message), judge (verdict + findings), correction, + * push, post, decision, incident, note. The ledger is `/ledger.jsonl`; text is stored whole, the Markdown + * folds long text into
so the page stays readable. + */ +import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { SafeJSON } from "@genesiscz/utils/json"; + +export const LEDGER_KINDS = [ + "spawn", + "steer", + "turn", + "judge", + "correction", + "push", + "post", + "decision", + "incident", + "note", +] as const; +export type LedgerKind = (typeof LEDGER_KINDS)[number]; + +export interface LedgerEvent { + at: string; + kind: LedgerKind; + pr?: number; + round?: number; + worker?: string; + model?: string; + text?: string; + meta?: Record; +} + +export function ledgerPath(run: string): string { + return join(run, "ledger.jsonl"); +} + +export function appendEvent(run: string, event: Omit & { at?: string }): LedgerEvent { + mkdirSync(run, { recursive: true }); + const full: LedgerEvent = { at: event.at ?? new Date().toISOString(), ...event } as LedgerEvent; + appendFileSync(ledgerPath(run), `${SafeJSON.stringify(full, { strict: true })}\n`); + return full; +} + +export function readEvents(run: string): LedgerEvent[] { + const path = ledgerPath(run); + + if (!existsSync(path)) { + return []; + } + + const events: LedgerEvent[] = []; + + for (const line of readFileSync(path, "utf8").split("\n")) { + if (!line.trim()) { + continue; + } + + try { + events.push(SafeJSON.parse(line, { strict: true }) as LedgerEvent); + } catch { + events.push({ at: "", kind: "note", text: `(unreadable ledger line) ${line.slice(0, 200)}` }); + } + } + + return events.sort((a, b) => a.at.localeCompare(b.at)); +} + +const LOCAL = new Intl.DateTimeFormat("cs-CZ", { + timeZone: "Europe/Prague", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", +}); +const ICON: Record = { + spawn: "πŸš€", + steer: "➑️", + turn: "πŸ—£οΈ", + judge: "βš–οΈ", + correction: "πŸ”§", + push: "⬆️", + post: "πŸ’¬", + decision: "❓", + incident: "πŸ›‘", + note: "πŸ“", +}; + +function fold(text: string, summary: string): string[] { + if (text.length <= 600 && text.split("\n").length <= 8) { + return ["", text.trim(), ""]; + } + + return [ + "", + `
${summary} (${text.split("\n").length} lines)`, + "", + "````text", + text.trim(), + "````", + "", + "
", + "", + ]; +} + +export function renderMarkdown(events: LedgerEvent[], options: { title?: string; pr?: number } = {}): string { + const shown = options.pr === undefined ? events : events.filter((e) => e.pr === options.pr || e.pr === undefined); + const prs = [...new Set(shown.map((e) => e.pr).filter((p): p is number => p !== undefined))].sort((a, b) => a - b); + const out: string[] = [`# ${options.title ?? "Babysit run ledger"}`, ""]; + + out.push( + "## Overview", + "", + "| PR | Rounds pushed | Last verdict | Steers | Worker turns |", + "|---|---|---|---|---|" + ); + for (const pr of prs) { + const mine = shown.filter((e) => e.pr === pr); + const pushes = mine.filter((e) => e.kind === "push").map((e) => `r${e.round ?? "?"}`); + const lastJudge = [...mine].reverse().find((e) => e.kind === "judge"); + out.push( + `| #${pr} | ${pushes.join(", ") || "-"} | ${String(lastJudge?.meta?.verdict ?? "-")} | ${mine.filter((e) => e.kind === "steer" || e.kind === "spawn").length} | ${mine.filter((e) => e.kind === "turn").length} |` + ); + } + + out.push(""); + + const general = shown.filter((e) => e.pr === undefined); + if (general.length) { + out.push("## Run-wide events", ""); + for (const e of general) { + out.push( + `- ${ICON[e.kind]} ${LOCAL.format(new Date(e.at))} **${e.kind}** ${e.text?.split("\n")[0]?.slice(0, 200) ?? ""}` + ); + } + + out.push(""); + } + + for (const pr of prs) { + out.push(`## PR #${pr}`, ""); + for (const e of shown.filter((x) => x.pr === pr)) { + const who = [e.worker, e.model].filter(Boolean).join(", "); + const head = `### ${ICON[e.kind]} ${LOCAL.format(new Date(e.at))} ${e.kind}${e.round !== undefined ? ` Β· round ${e.round}` : ""}${who ? ` Β· ${who}` : ""}`; + out.push(head); + if (e.meta && Object.keys(e.meta).length) { + out.push( + "", + ...Object.entries(e.meta).map( + ([k, v]) => `- **${k}**: ${typeof v === "string" ? v : SafeJSON.stringify(v, { strict: true })}` + ) + ); + } + + if (e.text) { + out.push( + ...fold( + e.text, + e.kind === "steer" || e.kind === "spawn" + ? "prompt" + : e.kind === "turn" + ? "worker's final message" + : "text" + ) + ); + } else { + out.push(""); + } + } + } + + return `${out.join("\n").replace(/\n{3,}/g, "\n\n")}\n`; +} + +if (import.meta.main) { + const [verb, ...rest] = process.argv.slice(2); + const arg = (n: string) => { + const i = rest.indexOf(n); + return i >= 0 ? rest[i + 1] : undefined; + }; + const run = arg("--run"); + + if (!run || (verb !== "append" && verb !== "render")) { + console.error("usage: ledger.ts append|render --run … (see the header)"); + process.exit(2); + } + + if (verb === "append") { + const kind = arg("--kind") as LedgerKind | undefined; + + if (!kind || !LEDGER_KINDS.includes(kind)) { + console.error(`--kind must be one of: ${LEDGER_KINDS.join(", ")}`); + process.exit(2); + } + + const file = arg("--file"); + const meta = arg("--meta"); + const event = appendEvent(run, { + kind, + ...(arg("--at") ? { at: arg("--at") } : {}), + ...(arg("--pr") ? { pr: Number(arg("--pr")) } : {}), + ...(arg("--round") ? { round: Number(arg("--round")) } : {}), + ...(arg("--worker") ? { worker: arg("--worker") } : {}), + ...(arg("--model") ? { model: arg("--model") } : {}), + ...(file ? { text: readFileSync(file, "utf8") } : arg("--text") ? { text: arg("--text") } : {}), + ...(meta ? { meta: SafeJSON.parse(meta, { strict: true }) as Record } : {}), + }); + console.log(`ledger: ${event.kind}${event.pr ? ` #${event.pr}` : ""} at ${event.at}`); + } else { + const pr = arg("--pr"); + const md = renderMarkdown(readEvents(run), { ...(pr ? { pr: Number(pr) } : {}) }); + const target = arg("--out"); + + if (target) { + writeFileSync(target, md); + console.log(`${target}: ${md.split("\n").length} lines`); + } else { + process.stdout.write(md); + } + } +} From 1c4aab82c190c326407a7af36ed9ab8e47890ec4 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 14 Oct 2026 04:58:07 +0200 Subject: [PATCH 04/92] feat(skills, babysit): the overnight run's scripts become the skill's (run card, self-fetch brief, steer and wait with ledger, judge pack, integrate, round readiness, worktree setup) (cherry picked from commit e8134b76cc8a83a493bcef993bd9844347f60af6) --- .claude/skills/babysit/SKILL.md | 35 +++- .../babysit/references/brief-self-fetch.md | 91 +++++++++ .claude/skills/babysit/references/judging.md | 7 + .claude/skills/babysit/scripts/fill-brief.ts | 73 +++++++ .claude/skills/babysit/scripts/integrate.ts | 187 ++++++++++++++++++ .claude/skills/babysit/scripts/judge-pack.ts | 107 ++++++++++ .claude/skills/babysit/scripts/link-native.ts | 101 ++++++++++ .claude/skills/babysit/scripts/round-ready.ts | 71 +++++++ .claude/skills/babysit/scripts/run.ts | 71 +++++++ .claude/skills/babysit/scripts/setup-wt.ts | 86 ++++++++ .claude/skills/babysit/scripts/steer.ts | 38 ++++ .claude/skills/babysit/scripts/wait-turn.ts | 69 +++++++ 12 files changed, 932 insertions(+), 4 deletions(-) create mode 100644 .claude/skills/babysit/references/brief-self-fetch.md create mode 100644 .claude/skills/babysit/scripts/fill-brief.ts create mode 100644 .claude/skills/babysit/scripts/integrate.ts create mode 100644 .claude/skills/babysit/scripts/judge-pack.ts create mode 100644 .claude/skills/babysit/scripts/link-native.ts create mode 100644 .claude/skills/babysit/scripts/round-ready.ts create mode 100644 .claude/skills/babysit/scripts/run.ts create mode 100644 .claude/skills/babysit/scripts/setup-wt.ts create mode 100644 .claude/skills/babysit/scripts/steer.ts create mode 100644 .claude/skills/babysit/scripts/wait-turn.ts diff --git a/.claude/skills/babysit/SKILL.md b/.claude/skills/babysit/SKILL.md index 9eff2de299..8a8a6d219e 100644 --- a/.claude/skills/babysit/SKILL.md +++ b/.claude/skills/babysit/SKILL.md @@ -42,6 +42,11 @@ Write it to `/tmp/cc///babysit/run.json` at the start; the scr | `post` | ask | Post the round summary and replies after the push (review-loop step 4), or hold them in the vault. | | `report` | vault note | Optionally also a bus message: `{ "session": "grok-dev", "to": "grokbot-developer" }`. | | `stopAfter` | the user's words | A phase or round to stop at. Stop there even if everything is green. | +| `vault` | `/vault` | The notes folder of this run: reply files, summaries, the rendered ledger. Set it. | +| `judge` | `Claude` | The name the round summary credits as judge ("… from judged by "). | +| `worktrees` | `/worktrees` | Parent of the per-PR worker worktrees `bs-`. | + +Every script below takes `--run ` (or `BABYSIT_RUN`) and reads this card through `scripts/run.ts`. ## 2. Mode: a worktree per worker, or the unified branch @@ -84,7 +89,12 @@ the stop point. After the confirmation period, state the plan in one line and pr and whether unified differs from the PR branch per file. That list is the outer bound. The brief allows the files the findings cite plus their tests, and any other PR file a fix provably needs. Missing a test file in the scope put Grok's tests in the wrong file on 2026-10-14. -7. Baseline the cost: `tools grok usage --json --fresh` (the SuperGrok weekly percentage) for Grok workers. Every +7. **Worktree** (worktree mode): `bun .claude/skills/babysit/scripts/setup-wt.ts --run --pr `. Detached at + the PR head, `bun install --ignore-scripts` plus the repo's patches, the test wrapper's install stamp (else + `scripts/test.ts` reinstalls everything inside the worker's sandbox), native files linked to the main checkout's + twins (`link-native.ts`: on 2026-10-14 a stuck syspolicyd hung every NEW native binary at `_dyld_start`), nested + app `node_modules` linked. About 7 s with a warm bun cache. +8. Baseline the cost: `tools grok usage --json --fresh` (the SuperGrok weekly percentage) for Grok workers. Every worker turn is booked in the cost ledger with its ai-spend price, login and label, and its report (Codex: the `turn_completed` bus event and `status --json` `cost`) ends with one line: API-equivalent, billed, and a Claude Opus/Sonnet estimate for the same tokens. The API-credit @@ -94,7 +104,12 @@ the stop point. After the confirmation period, state the plan in one line and pr ## 4. Brief and dispatch -Fill [references/brief-template.md](references/brief-template.md) per PR (the findings pasted in full, the scope, +Two brief forms. **Self-fetch** (the default since 2026-10-14): +`bun .claude/skills/babysit/scripts/fill-brief.ts --run --pr --round --model "