From a4b60da763a02c2a5083c7a15c661ce3c3895bea Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 29 Jul 2026 06:37:24 +0200 Subject: [PATCH 1/9] feat(plugin): handoff routing and driver skills, wrap-up port, bump to 1.0.38 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gt:handoff had grown two jobs — deciding where work goes, and driving the receiver. Split into a gt:handoff-to router plus per-target drivers, so a new target can be added without touching the router. Retires gt:handoff. --- .claude-plugin/marketplace.json | 4 +- .gitignore | 3 + biome.json | 10 + handoff-tab-lightbox.png | Bin 0 -> 81995 bytes .../genesis-tools/.claude-plugin/plugin.json | 2 +- plugins/genesis-tools/agents/agent-driver.md | 96 ++++ .../genesis-tools/skills/agents-talk/SKILL.md | 18 +- .../skills/handoff-to-codex/SKILL.md | 167 +++++++ .../genesis-tools/skills/handoff-to/SKILL.md | 68 +++ plugins/genesis-tools/skills/handoff/SKILL.md | 104 ----- plugins/genesis-tools/skills/wrap-up/SKILL.md | 312 +++++++++++++ .../skills/wrap-up/scripts/resolve.ts | 423 ++++++++++++++++++ 12 files changed, 1086 insertions(+), 121 deletions(-) create mode 100644 handoff-tab-lightbox.png create mode 100644 plugins/genesis-tools/agents/agent-driver.md create mode 100644 plugins/genesis-tools/skills/handoff-to-codex/SKILL.md create mode 100644 plugins/genesis-tools/skills/handoff-to/SKILL.md delete mode 100644 plugins/genesis-tools/skills/handoff/SKILL.md create mode 100644 plugins/genesis-tools/skills/wrap-up/SKILL.md create mode 100644 plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 114c95b4e7..5e7a9719db 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,7 +1,7 @@ { "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "name": "genesis-tools", - "version": "1.0.37", + "version": "1.0.38", "description": "Plugins for GenesisTools CLI development and management", "owner": { "name": "genesiscz", @@ -11,7 +11,7 @@ { "name": "genesis-tools", "description": "Skills and utilities for working with GenesisTools CLI toolkit. Provides guidance for discovering, executing, and troubleshooting genesis tools with integrated workflow support.", - "version": "1.0.37", + "version": "1.0.38", "author": { "name": "GenesisTools" }, diff --git a/.gitignore b/.gitignore index 01a9759f9a..c3437be5ed 100644 --- a/.gitignore +++ b/.gitignore @@ -231,3 +231,6 @@ vercel-skills .repowise/ .repowise-workspace.yaml .claude/plans/sweep-p6/ + +# skillopt scratch +.skillopt-sleep/ diff --git a/biome.json b/biome.json index eac50dd3f8..b1aca31a12 100644 --- a/biome.json +++ b/biome.json @@ -119,6 +119,16 @@ } } }, + { + "includes": ["plugins/*/skills/*/scripts/**"], + "linter": { + "rules": { + "style": { + "noRestrictedGlobals": "off" + } + } + } + }, { "includes": ["**/*.test.ts", "**/*.test.tsx", "**/__tests__/**", "**/*.data.ts"], "plugins": [] diff --git a/handoff-tab-lightbox.png b/handoff-tab-lightbox.png new file mode 100644 index 0000000000000000000000000000000000000000..14fb6443fae16b69cc0897e88744ab8d605672c1 GIT binary patch literal 81995 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 Dvt`JQ literal 0 HcmV?d00001 diff --git a/plugins/genesis-tools/.claude-plugin/plugin.json b/plugins/genesis-tools/.claude-plugin/plugin.json index fc5056e3b6..bb51ff0e4d 100644 --- a/plugins/genesis-tools/.claude-plugin/plugin.json +++ b/plugins/genesis-tools/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genesis-tools", - "version": "1.0.37", + "version": "1.0.38", "description": "Skills and utilities for working with GenesisTools CLI toolkit. Provides guidance for discovering, executing, and troubleshooting genesis tools with integrated workflow support.", "author": { "name": "GenesisTools" diff --git a/plugins/genesis-tools/agents/agent-driver.md b/plugins/genesis-tools/agents/agent-driver.md new file mode 100644 index 0000000000..20c5c77c3b --- /dev/null +++ b/plugins/genesis-tools/agents/agent-driver.md @@ -0,0 +1,96 @@ +--- +name: agent-driver +description: "Drives one external worker session end to end — spawn, watch, steer, resolve approvals, verify, tear down — keeping its event stream out of the orchestrator's context. One per worker session. Triggers on 'drive the codex session', 'supervise the worker', and gt:handoff-to-codex default mode." +--- + +You drive exactly **one** external worker session end to end. You are not the implementer and you are not the architect — you are the supervisor that keeps a worker on task and reports honestly. + +Your spawn prompt gives you: `BACKEND` (default `codex`), `NAME`, `CWD`, `BRIEF_FILE`, `WRITE_POLICY`, `VERIFY_CMD`, `SCOPE` (paths the worker may touch), and `ESCALATE` (what must come back to the human). + +## 1. Join the bus first + +```bash +tools agents login --agent-name driver_ +``` + +Run it with `run_in_background: true` and follow its **stdout** with `Monitor` (never `2>&1` — stderr is diagnostics and will corrupt the event stream). This is mandatory: it is how the orchestrator steers you and how approvals reach you. + +## 2. Check the brief before spawning + +Read `BRIEF_FILE`. Refuse to spawn and report back if any of these is missing: + +- a self-contained task statement (the worker has none of the orchestrator's context), +- `VERIFY_CMD` with its expected observable output, +- explicit negative constraints, +- a **Stop and report** checkpoint block. + +Refusing here is cheap. A worker that runs 20 minutes in the wrong direction is not. + +## 3. Spawn + +```bash +tools codex spawn --name --write --cwd --prompt-file +``` + +`--write ask` is the default for implementation; omit `--write` (read-only) for review work; `--write allow` only when the orchestrator explicitly declared the scope disposable or worktree-isolated. The session auto-registers as `codex_` — do **not** log that identity in yourself. + +## 4. Watch + +```bash +tools codex status --name +tools codex tail --name --follow # background + Monitor +``` + +Read for these and nothing else: the worker drifting outside `SCOPE`, a verify failure it is patching around, an approval request, a stall, a checkpoint report. + +## 5. Steer + +```bash +tools codex steer --name --body '' +tools codex interrupt --name # when the current turn is already wrong +``` + +Restate the constraints in every correction. Steer early — a short correction beats a rollback. + +## 6. Approvals + +```bash +tools codex approve --name --request +tools codex deny --name --request +``` + +**Approve on your own** only when the action is inside `SCOPE` and inside the declared writable roots. + +**Escalate** — never decide alone — for: scope expansion, new dependencies, new files outside `SCOPE`, public-interface changes, any `git commit`/`push`/branch operation, anything destructive, anything in `ESCALATE`. Escalation is dual-channel: + +```bash +tools agents message --from driver_ --to lead --body 'approval needed: ' +``` + +then a harness `SendMessage` nudge to `lead`, because an idle orchestrator does not wake on bus traffic alone. Then wait — do not guess. + +## 7. Verify yourself + +When the turn completes, run `VERIFY_CMD` **yourself** and read `git diff`. The worker's claim that it passed is not evidence. If verify fails, either steer once more with the actual failure output, or stop and report — never rewrite the worker's code yourself. + +## 8. Tear down and report + +```bash +tools codex stop --name +``` + +Report to `lead` in this shape, and nothing longer: + +``` +VERDICT: +CHANGED: +VERIFY: +STEERS: +OPEN: +``` + +Never report a green state you did not observe. "Stopped at a checkpoint" is a successful outcome, not a failure. + +## Backends other than Codex + +`BACKEND` exists so this agent can drive other workers later. Until another backend is wired, `codex` is the only supported value — if you are given a different one, say so and stop rather than improvising a CLI. diff --git a/plugins/genesis-tools/skills/agents-talk/SKILL.md b/plugins/genesis-tools/skills/agents-talk/SKILL.md index eb3eda7e4d..84c6bf27a9 100644 --- a/plugins/genesis-tools/skills/agents-talk/SKILL.md +++ b/plugins/genesis-tools/skills/agents-talk/SKILL.md @@ -135,21 +135,11 @@ tools agents request --from reviewer --to lead --body 'Approve the auth change?' ## Long-lived Codex teammates `tools codex spawn` creates a persistent app-server session and auto-registers `codex_` on this same bus. Do -not manually log that identity in from the orchestrator. The driver observes inbound controls; the model receives with -the seeded `tools agents login --agent-name codex_ --once --session ` command. +not manually log that identity in from the orchestrator — the model receives with its seeded +`tools agents login --agent-name codex_ --once --session ` command. -Sessions are read-only unless you deliberately choose a write policy: - -```bash -# Supervised worker: untrusted commands and write approvals go to lead. -tools codex spawn --name implementer --write ask --prompt 'Implement the bounded change' - -# Trusted bounded worker: workspace writes without approval prompts. -tools codex spawn --name implementer --write allow --prompt 'Implement the bounded change' -``` - -Omit `--write` (or use `--write deny`) for reviewers. Use `--write ask` as the default when edits are needed; use -`--write allow` only when the task and writable roots are tightly bounded. +Write policies, steering, approvals, and the driver-subagent pattern live in **`gt:handoff-to-codex`**. Load that +skill rather than hand-rolling a spawn from here. ## What you receive on the `login` stream diff --git a/plugins/genesis-tools/skills/handoff-to-codex/SKILL.md b/plugins/genesis-tools/skills/handoff-to-codex/SKILL.md new file mode 100644 index 0000000000..f8e5c1fab1 --- /dev/null +++ b/plugins/genesis-tools/skills/handoff-to-codex/SKILL.md @@ -0,0 +1,167 @@ +--- +name: handoff-to-codex +description: Hand a review or an implementation to Codex and drive the session (spawn, steer, approve, verify). Triggers on /gt:handoff-to-codex, "give this to codex", "let codex implement this", "codex subagent", "run codex on this", "tools codex", or any request to offload coding work to gpt-5.x. +--- + +# handoff-to-codex + +Run OpenAI Codex as a steerable worker under this session: you architect and verify, Codex implements. The backend is `tools codex` — a long-lived `codex app-server` daemon per session, joined to the `tools agents` message bus. Every run must stay correctable mid-flight; the flags below are load-bearing. + +This file is **self-contained on purpose** — the Codex worker itself is pointed at § Receiving end, and it cannot load Claude skills. + +Not sure Codex is the right worker at all? `gt:handoff-to` decides that. Not sure how the bus works? `gt:agents-talk`. Everything needed to *run* a handoff is below. + +## Modes + +| Invocation | Who drives | Blocks the main turn? | +|---|---|---| +| `/gt:handoff-to-codex ` (default) | a `genesis-tools:agent-driver` subagent, spawned in the background | no | +| `/gt:handoff-to-codex --inline ` | this session, via background Bash + Monitor | no | +| `/gt:handoff-to-codex --inline --wait ` | this session, blocking until `turn.completed` | yes | + +**Default to the driver subagent.** It keeps the Codex event stream (thousands of lines) out of this session's context, survives long turns, and gives steering decisions their own context window. Use `--inline` for a short single-turn job where spawning a subagent costs more than it saves; use `--inline --wait` only when the next step here genuinely cannot proceed without the result. + +Driver model: **sonnet** by default; **opus** when there is no committed plan, when architecture or interface shape is at stake, or when approvals will need real scope judgment. + +Spawning the driver (after §1's brief is written and `tools agents login --agent-main --agent-name lead` is running in the background): + +``` +Agent( + subagent_type: "genesis-tools:agent-driver", + model: "sonnet", // or "opus" per above + run_in_background: true, + prompt: "BACKEND: codex\nNAME: \nCWD: \nBRIEF_FILE: /tmp/codex--brief.md\nWRITE_POLICY: ask\nVERIFY_CMD: \nSCOPE: \nESCALATE: " +) +``` + +The driver reports back on the bus as `driver_` and ends with a `VERDICT:` message; keep working until it arrives. + +## 1. Readiness gate + +Do not spawn until all four hold: + +1. The prompt is **self-contained** — Codex has none of this conversation's context. +2. A **verification command** is named, with its expected observable output. +3. **Negative constraints are explicit** — "do NOT create new files", "do NOT commit or push", "do NOT touch `src/x/`", size limits. +4. **Checkpoints are named** (§ Checkpoint contract). + +Write the prompt to a file and pass `--prompt-file`. Inline `--prompt` breaks on embedded quotes, backticks, and `$(...)`. + +## 2. Spawn + +```bash +tools codex spawn \ + --name \ + --write ask \ + --cwd \ + --prompt-file /tmp/codex--brief.md +``` + +Write policy — the only real safety dial: + +| `--write` | sandbox | approvals | use for | +|---|---|---|---| +| omitted / `deny` | read-only | none possible | reviewers, investigations, second opinions | +| `ask` | workspace-write | untrusted → forwarded to `lead` | **default for implementation** | +| `allow` | workspace-write | never prompts | tightly bounded, disposable, or worktree-isolated work only | + +Other flags: `--model` / `--effort`, `--home` (CODEX_HOME override), `--mode review|task`, `--writable-root `, `--session ` when `CLAUDE_CODE_SESSION_ID` can't be discovered, `--no-agents` to disable the bus (don't — the bus is the point). + +Sessions land in `~/.genesis-tools/codex/sessions/.*` (`.jsonl` event log, `.meta.json`, `.daemon.log`). Auth is whatever the Codex CLI is logged into for the effective `CODEX_HOME`; `tools codex` selects no GenesisTools AI account. + +The session auto-registers on the bus as `codex_`. **Never `tools agents login` that identity yourself** — the driver observes it; the model receives with its seeded `--once` command. + +## 3. Checkpoint contract (state it in the brief, every time) + +Codex presses on by default. The brief must say where it stops. Include this block verbatim, filled in: + +```markdown +## Stop and report — do not continue past these +- After : report what changed + the verify output, then WAIT for a reply. +- Before creating any new file, adding any dependency, or changing any public interface: ASK. +- Before any `git commit`, `git push`, branch switch, or destructive command: ASK. +- If the verify command fails twice in a row: STOP and report both failures. Do not keep patching. +- If the task turns out to need work outside : STOP and report the gap. +Report with: tools agents message --from codex_ --to lead --body '' +Check for replies with: tools agents login --agent-name codex_ --once +``` + +## 4. Watch and steer + +```bash +tools codex status --name +tools codex tail --name --follow # background + Monitor +tools codex read --name # thread snapshot +tools codex steer --name --body 'Focus on the auth path; do NOT refactor the router' +tools codex interrupt --name # kill the current turn +tools codex rollback --name --turns 1 # drop turns from the end +tools codex stop --name # tear down +``` + +Repeat the negative constraints in every steering message — the correction is what the model attends to now. + +## 5. Approvals + +With `--write ask`, out-of-policy commands and file changes pause and arrive on the bus as `approval_request` messages to `lead`: + +```bash +tools codex approve --name --request +tools codex deny --name --request +``` + +Driver authority: **approve autonomously** only when the action is inside the declared writable roots and inside the declared task scope. **Escalate to the human** for anything that expands scope, adds a dependency, touches git history, or leaves the declared paths. + +Compact receiver stream for the orchestrator: + +```bash +tools agents login --agent-main --agent-name lead --kinds message,error,approval_request +``` + +Waking a possibly-idle peer needs both channels: `tools agents message ...` (durable payload) **then** a harness `SendMessage` nudge. Agents-channel traffic alone does not re-invoke an idle subagent. + +## 6. Verify, then integrate + +Never trust the worker's self-report. After the turn completes: + +1. Run the verification command yourself. +2. `git diff` — read it, don't skim it. +3. Only then integrate, commit, or hand back. + +Then `tools codex stop --name `. + +## Receiving end — read this if you ARE the Codex worker + +You are a worker inside a Claude Code agent swarm. The orchestrator is `lead`. + +1. Your first action is to report in on the bus. It is not optional: + `tools agents message --from codex_ --to lead --body 'received; starting '` +2. Check for steering between meaningful steps: + `tools agents login --agent-name codex_ --once` +3. Honor the **Stop and report** block in your brief literally. Stopping to ask is the expected behavior, not a failure. +4. Run the verification command yourself before reporting done, and paste its real output. Never report a green state you did not observe. +5. In a read-only sandbox, `tools agents` writes fail with EPERM — narrate progress as short standalone assistant messages instead; the bridge forwards them to `lead`. + +## Fallback: one-shot `codex exec` + +For a job that needs no bus, no daemon, and no mid-flight steering: + +```bash +command codex --sandbox workspace-write exec \ + --json --ignore-user-config --skip-git-repo-check \ + -C -o /tmp/codex--last.md \ + "" 2>&1 | tee /tmp/codex-.log +``` + +- `command codex` — the user's zsh wrapper silently injects `--sandbox danger-full-access`; a worker must get an explicit narrower sandbox. +- `--json` — first event is `{"type":"thread.started","thread_id":"..."}`; capture it or the run is not resumable. +- `--ignore-user-config` — otherwise it loads `~/.codex` config and skills (~450k wasted input tokens) and fires the user's notification hooks. +- `-o ` — read the answer from this file, never by parsing the stream. +- Never `--ephemeral` if you might resume. + +Wait on event types only (`error:` appears in normal red-test output): + +```bash +SECONDS=0; until rg -q '"type":"turn.completed"|"type":"turn.failed"' /tmp/codex-.log 2>/dev/null || [ $SECONDS -ge 600 ]; do sleep 5; done +``` + +Resume: `command codex exec resume --json --skip-git-repo-check -c sandbox_mode="workspace-write" -o /tmp/codex--steer.md ""`. `--skip-git-repo-check` is **not** inherited; `--sandbox`/`--cd` are **not** re-applied on resume — pass sandbox as `-c sandbox_mode=`. diff --git a/plugins/genesis-tools/skills/handoff-to/SKILL.md b/plugins/genesis-tools/skills/handoff-to/SKILL.md new file mode 100644 index 0000000000..6eaa9233a7 --- /dev/null +++ b/plugins/genesis-tools/skills/handoff-to/SKILL.md @@ -0,0 +1,68 @@ +--- +name: handoff-to +description: Offload work to another model or agent, and pick which one (Codex/gpt-5.x, sonnet, opus, fable). Triggers on "give this to codex", "offload this", "hand this off", "second opinion from GPT", "parallelize this across models", "which model should do X" — and use it proactively whenever a bounded, well-specified task should go to a worker while this session reviews. +--- + +# handoff-to — pick the worker, then dispatch + +This skill only answers two questions: **who does it**, and **is it ready to leave**. The mechanics of driving each worker live elsewhere: + +| Worker | Dispatch via | +|---|---| +| Codex / gpt-5.x | `gt:handoff-to-codex` — **mandatory**: invoke that skill; never hand-roll `tools codex` or `codex exec` from here | +| sonnet / opus / fable | `Agent` tool with `model:`, or `Workflow` for fan-out | + +## Model rankings + +Higher = better. **Cost** = what is actually paid (not list price). **Intelligence** = how hard a problem you can hand it unsupervised. **Taste** = UI/UX, code quality, API design, copy. + +| model | cost | intelligence | taste | +|---|---|---|---| +| gpt-5.5 | 9 | 8 | 5 | +| sonnet-5 | 5 | 5 | 7 | +| opus-4.8 | 4 | 7 | 8 | +| fable-5 | 2 | 9 | 9 | + +How to apply: + +- Defaults, not limits. Standing permission to override: if a cheaper model's output misses the bar, rerun with a smarter one without asking. **Judge the output, not the price tag. Escalating costs less than shipping mediocre work.** +- Cost is a tie-breaker only. When axes conflict for anything that ships: intelligence > taste > cost. +- Bulk/mechanical work (clear-spec implementation, data analysis, migrations): gpt-5.5 — effectively free. +- Anything user-facing (UI, copy, API design) needs taste ≥ 7. +- Reviews of plans/implementations: fable-5 or opus-4.8, optionally gpt-5.5 as an extra independent perspective. +- Never Haiku for work that ships (thin wrapper/relay agents are fine). +- gpt-5.5 is only reachable through the Codex CLI. Claude models run via the `Agent`/`Workflow` `model` parameter. + +## Task routing + +| Task | Route | +|---|---| +| Design decisions, naming, interface shape | Stay here — decide first, then hand the decision down | +| Spec'd mechanical implementation, boilerplate, big renames | Codex | +| Test writing against a fixed contract | Codex | +| Second-opinion code review | Codex, read-only | +| Ambiguous / underspecified work | Stay here until spec'd, THEN offload | +| Cross-file refactor requiring judgment calls | Stay here, or opus/fable subagent | +| Long-running bounded sweep while this session reviews | Codex, parallel drivers with `isolation: "worktree"` | + +Rule of thumb: **taste stays here, precision ships out.** + +## Readiness gate (applies to every route) + +Do not dispatch until all four hold. If any fails, the task is not ready to offload — finish specifying it first. + +1. The prompt is **self-contained**: the worker has none of this conversation's context. +2. There is a **verification command** the worker can run itself, with the expected observable output stated. +3. **Negative constraints are explicit** — "do NOT create new files", "do NOT commit", "do NOT touch `src/x/`", size limits. Workers obey these reliably when spelled out, and not otherwise. +4. **Checkpoints are named** — the points at which the worker must stop and report instead of pressing on (see `gt:handoff-to-codex` § Checkpoint contract). + +## Driver-model choice (when the route is Codex) + +The driver is the Claude subagent that owns the Codex session (`genesis-tools:agent-driver`). + +- **sonnet** — default. The driver relays, watches, and approves inside declared bounds. +- **opus** — when there is no committed plan, when architecture or interface shape is at stake, or when approvals will require real judgment about scope. + +## Never trust the self-report + +Whatever the worker says it did, re-run the verification command yourself and read the diff before integrating. diff --git a/plugins/genesis-tools/skills/handoff/SKILL.md b/plugins/genesis-tools/skills/handoff/SKILL.md deleted file mode 100644 index babaf267dd..0000000000 --- a/plugins/genesis-tools/skills/handoff/SKILL.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -name: gt:handoff -description: Create and maintain a compaction-proof progress file (.claude/plans/.handoff.md) next to an implementation plan, so any executor - fresh session, post-compact resume, or parallel subagent - knows exactly where the work stands and which plan lines to read next. Invoke when starting to execute a plan, when resuming one, when handing work to another agent, or when the user says "create a handoff" / "prepare the handoff". ---- - -# handoff — compaction-proof execution state - -The handoff file is the executor's external memory. Context gets compacted, sessions die, agents run in parallel — the handoff file survives all of it. **THE FILE IS THE TRUTH**: if your memory of progress disagrees with the file, the file wins. - -It is also **self-describing**: its own `## PROTOCOL` section teaches any reader the rules, so a fresh agent needs nothing but "Read `.handoff.md` and follow it." - -## Where it lives - -Next to the plan: `.claude/plans/.md` → `.claude/plans/.handoff.md`. -If there is no plan file, the handoff still works — its TASKS section carries the step list itself. - -## The three iron rules - -1. **Read the handoff FIRST** — at session start, after every compaction, before every task. It is small by design (~1–2k tokens) and replaces re-reading the whole plan. -2. **Update it IMMEDIATELY after every task** — never "at the end". An update you postponed dies with the next compaction. -3. **STATE/TASKS are rewritten in place; LOG is append-only.** Never rewrite or delete LOG entries — they are the audit trail. - -## Creating a handoff (start of execution, or when handing off) - -Step 1 — build the plan TOC with real line numbers: - -```bash -rg -n '^#{1,3} ' .claude/plans/.md -wc -l .claude/plans/.md -``` - -Step 2 — Write `.claude/plans/.handoff.md` from this exact skeleton, filling every `<...>` (task list and line ranges come from the TOC you just built; mark independent tasks with the same `[P:n]` group when the plan's DON'T-TOUCH/interface-freeze shows they share no files): - -````markdown -# Handoff: - -## PROTOCOL — read this first, every time -You are executing a plan. This file is your memory; the plan file is your instructions. -1. Read this whole file (it is small). Trust it over anything you remember. -2. Read ONLY the plan's preamble (lines 1-) — goal, covenant, interface freeze. -3. Find **YOU ARE HERE** below. Read ONLY that task's line range from the plan: - `sed -n ',p' .claude/plans/.md` (or Read with offset/limit). -4. Execute the task exactly as the plan says. Do not improvise; deviations go in the - plan's `## Deviations` AND one LOG line here. -5. IMMEDIATELY update this file: flip the task checkbox, move YOU ARE HERE, append one - LOG line. Then go to 3. -6. After any compaction or restart: start again at 1. Never re-read the whole plan. -Rules: STATE/TASKS sections are rewritten in place. LOG is append-only, newest at the -bottom. If a check fails after its ON-FAIL fallback: STOP, log it, report to the user. - -## STATE -- **Plan:** .claude/plans/.md ( lines; preamble = lines 1-) -- **Goal:** -- **Branch/worktree:** @ -- **YOU ARE HERE:** Task — (plan lines -) — | blocked: > -- **Verify:** - -## TASKS -- [ ] Task 1 — (lines -) -- [ ] Task 2 — (lines -) [P:1] -- [ ] Task 3 — (lines -) [P:1] - -## PLAN TOC -- Preamble (goal, covenant, interface freeze, conventions): lines 1- -- Task 1 — : lines - -- ... -- Dry-run trace: lines - -- Deviations: lines <>- - -## LOG -- — handoff created; plan has tasks, none started. -```` - -Step 3 — verify: `wc -l` the handoff (should be well under ~120 lines) and confirm every TASKS line range matches the TOC. - -Step 4 — the handoff prompt for another agent is exactly one sentence: -> Read `.claude/plans/.handoff.md` and follow its PROTOCOL section. Re-read that file after every compaction. - -## Maintaining it (executor duties, after EVERY task) - -1. Flip the checkbox in TASKS. -2. Move **YOU ARE HERE** to the next task (with its line range) and update **Verify**. -3. Append ONE LOG line with a real timestamp (`date '+%F %H:%M'`), stating the observable result, not intentions: - `- 2026-07-08 21:40 — Task 2 done: bun test 14/14 green, committed abc1234. Next: Task 3.` -4. If anything deviated: one LOG line here + the entry in the plan's `## Deviations`. -5. Blocked? Set YOU ARE HERE to `blocked: `, log it, STOP and report — do not skip ahead. - -Keep LOG lines terse. Never trim or rewrite old lines; the file staying append-only is worth more than it staying pretty. - -## Resuming (fresh session / post-compact) - -Read the handoff → read plan preamble lines → read the YOU-ARE-HERE task's line range → work. That's ~2–4k tokens to be fully oriented, no matter how large the plan is. Re-reading the entire plan after a compaction is a protocol violation, not diligence. - -## Parallelizing with subagents - -- Only tasks sharing a `[P:n]` group may run concurrently; anything unmarked is sequential. -- **One writer rule:** subagents NEVER edit the handoff. The orchestrator spawns each subagent with: "Execute ONLY Task of `.claude/plans/.md`, lines -. Read the plan preamble (lines 1-) first. Report the verify output; do not touch other files." The orchestrator updates TASKS/LOG as each returns. -- If two [P] tasks would touch the same file, the [P] marking is wrong — fix the handoff, run them sequentially. - -## Relationship to other conventions - -- Pairs with `plan-it` (gt:plan-it): plan-it plans have greppable `## Task N:` headings, per-task VERIFY, and a `## Deviations` section — a handoff maps onto them 1:1. Works with any plan that has task headings, though. -- The executor should load the `fable-style` skill if available; the handoff governs *where you are*, fable-style governs *how you work*. -- `*.handoff.md` files are chronological/append-only by repo convention — this skill's STATE/TASKS rewrite-in-place blocks are the explicitly declared exception; LOG keeps the append-only audit trail. diff --git a/plugins/genesis-tools/skills/wrap-up/SKILL.md b/plugins/genesis-tools/skills/wrap-up/SKILL.md new file mode 100644 index 0000000000..04cc87a6c3 --- /dev/null +++ b/plugins/genesis-tools/skills/wrap-up/SKILL.md @@ -0,0 +1,312 @@ +--- +name: wrap-up +description: "Write the state doc that lets a fresh agent resume cold — a session wrap-up in Obsidian, or a plan-execution handoff in the repo. Triggers on 'wrap up', 'close out this session', 'summarize and hand off', 'document what we did', 'save/create the handoff', and on starting or resuming execution of a plan. Use it proactively at the end of any substantial multi-commit or PR-merged session, even unasked." +--- + +# wrap-up — durable resume state + +Context gets compacted, sessions die, work spans days. This skill writes the file a fresh agent reads to know exactly where things stand, without re-deriving it from git and scrollback. + +Two modes, two artifacts. Pick by what is being resumed: + +| Mode | Artifact | Lives in | For | +|---|---|---|---| +| **SESSION** (default) | `-.wrapup.md` | Obsidian vault | end of a work session; cross-day, cross-project narrative + forensics | +| **PLAN** | `.claude/plans/.handoff.md` | the repo | mid-execution of a specific plan; which task you are on, what verifies it | + +SESSION mode is Parts 1–2 below; PLAN mode is the last section. + +They compose: a long plan execution uses PLAN mode continuously and SESSION mode once at the end. + +## SESSION mode + +The wrap-up doc is the bridge between sessions. It lives in the user's Obsidian vault (durable, cross-project, outside any one repo), and it is **append-only for history** with **one rewrite-in-place header** for current state. + +Two jobs: figure out *where* the doc lives (target resolution), then *write* it (header + log + forensics). + +## Part 1 — Resolve the target directory + +Work through these tiers in order. Stop at the first that yields a directory. + +### Tier 1 — the Obsidian dir already involved this session +If this session already read from or wrote to a specific Obsidian vault directory (a plan, a handoff, notes for this project), that is the target. You know this from your own session history — no lookup needed. Prefer it over the registry: it reflects where the work actually lived today. + +### Tier 2 — the registry +Otherwise consult `~/.claude/handoff-registry.json`, which maps projects/branches/worktrees to their Obsidian home. Run: + +```bash +bun "$CLAUDE_PLUGIN_ROOT/skills/wrap-up/scripts/resolve.ts" resolve +``` + +It reads your current git toplevel + branch + cwd, matches the most specific entry, and prints `{ found, obsidianDir, docPath, ... }`. If `found:true`, use that `docPath`. The registry shape: + +```json +{ + "entries": [ + { + "projectDir": "/path/to/ProjectRepo", + "branch": "feat/handoff-fixes", + "worktreeDir": "/path/to/ProjectRepo/.claude/worktrees/handoff-part2", + "obsidianDir": "/path/to/Vault/ProjectRepo/Plans" + } + ] +} +``` + +`branch` and `worktreeDir` are optional — omit `branch` for a project-wide entry that matches any branch. `docPath` is optional; when absent the resolver derives `/-.wrapup.md` so each branch accumulates its own file. + +### Tier 2.5 — shared plugin config (automatic) +`resolve` falls back to `~/.genesis-tools/plugins/config.json` by itself when the registry has no match — the result then carries `"source": "config"` and you just use its `docPath`, no user interaction needed. The file is a per-skill map shared by all genesis-tools plugin skills: + +```json +{ + "wrap-up": { + "vaultDir": "~/Vault", + "docDir": ".claude/wrapups", + "registryPath": "~/.claude/handoff-registry.json" + } +} +``` + +All keys optional. `docDir` (absolute, `~/`, or relative to the project toplevel) wins over `vaultDir`, which resolves to `//`. `registryPath` relocates the registry itself. When proposing a target in Tier 3, also suggest adding a `vaultDir` here once — it makes every future project resolve automatically. + +### Tier 3 — infer, propose, register +If neither registry nor config yields a target (`found:false`), don't guess silently and don't dump the doc in a random place. Instead: + +1. **Infer the user's structure.** Look at how the vault is organized for similar work — e.g. `ls` the vault root and any `/` folder, check whether handoffs/plans live under `//Plans/`, `/Handoffs/`, or similar. One or two `ls`/`Glob` calls, not a full crawl. +2. **Propose a target** to the user via `AskUserQuestion` — offer the inferred path as the recommended option plus one or two alternatives, so they confirm or redirect in one click. +3. **Persist the choice** so it's never asked again for this project/branch: + ```bash + bun "$CLAUDE_PLUGIN_ROOT/skills/wrap-up/scripts/resolve.ts" register --obsidian "" --branch "" --worktree "" + ``` +4. Then write the doc there. + +## Part 2 — Write the doc + +**EVERY wrap-up invocation does BOTH, always — never one without the other:** +1. **Rewrite the `YOU-ARE-HERE` header in place** (current state), AND +2. **Append a new `## — ` log section at the bottom** (what this session did, with commit SHAs). + +This is not optional and not skippable — not even for a tiny one-commit session, not even if "the header already says it." The header is *volatile* (it gets overwritten next time, so its content is lost); the **log is the only permanent record**. A wrap-up that updates the header but appends no log section silently erases this session from history the moment the next wrap-up runs. If you truly did nothing worth a paragraph, you still append a one-line dated log entry. Header-only = incomplete wrap-up. + +Locate the file at `docPath`. If it doesn't exist, create it from the template below (header + first log section). If it exists, **update the header in place** and **append a new log section** — never rewrite existing log sections; they are the audit trail. + +### The template (new file) + +```markdown +# Wrap-up: — + + +## You are here () +- **Branch / worktree:** @ +- **State:** +- **Next:** +- **Verify:** +- **Read to resume:** + + +--- + +## — (commits , , …) + +### Goal & context + + +### What happened (chronological) + + +### Files touched + + +### Commits + — `. Every git-touching entry above must map to a SHA here.> + +### Decisions & rationale + + +### Bugs, surprises, gotchas + + +### Verification + + +### Open / deferred / blocked + +``` + +The subsections above are the floor, not the ceiling — a substantive session's log runs long by design. Drop a subsection only when it genuinely has nothing (e.g. no bugs hit → omit "Bugs"); never collapse the whole thing to a terse paragraph. + +### Updating an existing file — one call does everything + +The whole update — rewrite the `YOU-ARE-HERE` block in place, append the new log section, auto-stamp the datetime, and auto-generate the `### Header before → after` snapshot from the outgoing header — is a single `log` invocation. You never hand-transcribe the old header (the script reads it), never compute the datetime (the script stamps it), never do a separate in-place Edit. + +Pipe ONE heredoc split by two sentinel lines: `@@HERE@@` introduces the new state bullets (the script wraps them in `## You are here ()` between the markers), then `@@LOG@@` introduces the log-section body you author. Quote the delimiter (`<<'WRAPUP'`) so backticks and `$` in your prose aren't shell-expanded: + +```bash +bun "$CLAUDE_PLUGIN_ROOT/skills/wrap-up/scripts/resolve.ts" log "" <<'WRAPUP' +@@HERE@@ +- **Branch / worktree:** @ +- **State:** +- **Next:** +- **Verify:** +- **Read to resume:** +@@LOG@@ +## — (commits , …) + +### Goal & context +... +### What happened (chronological) +... +### Files touched +... +### Decisions & rationale +... +WRAPUP +``` + +What the script does, atomically: +- Extracts the current `YOU-ARE-HERE` block, replaces it in place with your new `@@HERE@@` bullets under a freshly-stamped `## You are here ()` title. +- Appends your `@@LOG@@` body as a new section at the bottom, then auto-appends a `### Header before → after` block quoting the outgoing header (verbatim) and the new one (verbatim) — so the volatile resume-pointer's wording survives permanently. **You do not write the before→after block yourself.** +- Prints `{ logged, stamp }`. Use `stamp` (or `date '+%Y-%m-%d %H:%M'`) for the `## ` line in your `@@LOG@@` body. + +It refuses (non-zero exit) if the file doesn't exist (create it from the template with Write first), if a sentinel is missing, or if there's no `YOU-ARE-HERE` block — so a failed call never half-writes. + +Append-only still holds: `log` only rewrites the header region and adds at the bottom; it never touches earlier log sections. To amend an in-place list/table in an old section, edit it directly and prepend `` inside it. + +### What each log section carries (forensics) + +**Err long. The log is the permanent record — a fresh agent must be able to reconstruct the entire session from it without re-running anything, re-reading scrollback, or re-deriving state from git.** When in doubt, include it. Terse wrap-ups are the failure mode; length here is a feature, not bloat. Every substantive session fills all of these: + +- **Goal & context** — the ask, the starting state, the problem being solved. Frame first. +- **Chronological narrative** — the build-log of what actually happened, step by step, *in order*, including dead-ends, failed attempts, and course-corrections. The path is as valuable as the result. This is the meat and it is meant to be long. +- **Files touched** — every path changed/created/deleted, each with WHAT changed and WHY. Not a bare file list. +- **Commit SHA(s)** the section produced or refers to. A git-touching section without its SHA is incomplete. +- **Decisions & rationale** — every non-obvious choice, the alternatives considered and rejected, the tradeoffs accepted. This is precisely what the diff cannot show. +- **Bugs, surprises, gotchas** — errors hit and how resolved, environment quirks, traps the next agent must avoid. +- **Verification** — commands run + observed output (test counts, exit codes). State skipped/failed checks honestly. +- **Open / deferred / blocked** — what's unfinished, what was punted and why, what's blocked, and the concrete next steps. + +## Reading a wrap-up back (cheap resume) + +To orient without reading the whole file, pull just the current-state header: + +```bash +bun "$CLAUDE_PLUGIN_ROOT/skills/wrap-up/scripts/resolve.ts" here +# or with sed: +sed -n '/YOU-ARE-HERE:START/,/YOU-ARE-HERE:END/p' +``` + +The header's own **Read to resume** line tells you how much of the log below to read — usually the header plus the last section or two, not the whole history. + +## Rules + +- **Header rewrites in place; log is append-only — and you ALWAYS do both.** Every invocation: rewrite the `YOU-ARE-HERE` block (the single mutable region) AND append one new dated `##` log section. Never header-only. Log sections are permanent audit trail — never delete or rewrite them. +- **Every log section header carries the full datetime** (`## YYYY-MM-DD HH:MM — topic`), never date-only. Get it from `date '+%Y-%m-%d %H:%M'`. +- **Every git-touching section attaches its commit SHA(s)** inline. +- **Resolve before writing.** Don't dump wrap-ups in an arbitrary directory — walk the three tiers, and register the target once so it's automatic next time. +- **Keep it honest.** Record what actually happened — failed steps, skipped checks, open bugs. A wrap-up that only lists wins misleads the next session. +- **Err long, not terse.** The log section is the permanent audit trail — write it exhaustively (goal, full chronological narrative including dead-ends, files+why, decisions+rejected alternatives, bugs, verification output, open items). A substantive session's log runs long by design; a one-paragraph summary of a multi-commit session is a defect, not brevity. +- **Always end your response with the full absolute path to the wrap-up file as the last line** — after writing or appending, the final line of your reply must be the complete `docPath` (e.g. `/path/to/Vault/ProjectRepo/Plans/....wrapup.md`), so the user can open it in one click. Nothing after it. + +--- + +## PLAN mode — compaction-proof execution state in the repo + +Everything above is SESSION mode. PLAN mode is a different artifact with different rules: a small file next to an implementation plan, rewritten constantly during execution, that answers "which task am I on". + +The handoff file is the executor's external memory. **THE FILE IS THE TRUTH**: if your memory of progress disagrees with the file, the file wins. It is also **self-describing** — its own `## PROTOCOL` section teaches any reader the rules, so a fresh agent needs nothing but "Read `.handoff.md` and follow it." + +### Where it lives + +Next to the plan: `.claude/plans/.md` → `.claude/plans/.handoff.md`. If there is no plan file, the handoff still works — its TASKS section carries the step list itself. + +### The three iron rules + +1. **Read the handoff FIRST** — at session start, after every compaction, before every task. It is small by design (~1–2k tokens) and replaces re-reading the whole plan. +2. **Update it IMMEDIATELY after every task** — never "at the end". An update you postponed dies with the next compaction. +3. **STATE/TASKS are rewritten in place; LOG is append-only.** Never rewrite or delete LOG entries — they are the audit trail. + +### Creating one + +Step 1 — build the plan TOC with real line numbers: + +```bash +rg -n '^#{1,3} ' .claude/plans/.md +wc -l .claude/plans/.md +``` + +Step 2 — Write `.claude/plans/.handoff.md` from this exact skeleton, filling every `<...>` (task list and line ranges come from the TOC you just built; mark independent tasks with the same `[P:n]` group when the plan's DON'T-TOUCH/interface-freeze shows they share no files): + +````markdown +# Handoff: + +## PROTOCOL — read this first, every time +You are executing a plan. This file is your memory; the plan file is your instructions. +1. Read this whole file (it is small). Trust it over anything you remember. +2. Read ONLY the plan's preamble (lines 1-) — goal, covenant, interface freeze. +3. Find **YOU ARE HERE** below. Read ONLY that task's line range from the plan: + `sed -n ',p' .claude/plans/.md` (or Read with offset/limit). +4. Execute the task exactly as the plan says. Do not improvise; deviations go in the + plan's `## Deviations` AND one LOG line here. +5. IMMEDIATELY update this file: flip the task checkbox, move YOU ARE HERE, append one + LOG line. Then go to 3. +6. After any compaction or restart: start again at 1. Never re-read the whole plan. +Rules: STATE/TASKS sections are rewritten in place. LOG is append-only, newest at the +bottom. If a check fails after its ON-FAIL fallback: STOP, log it, report to the user. + +## STATE +- **Plan:** .claude/plans/.md ( lines; preamble = lines 1-) +- **Goal:** +- **Branch/worktree:** @ +- **YOU ARE HERE:** Task — (plan lines -) — | blocked: > +- **Verify:** + +## TASKS +- [ ] Task 1 — (lines -) +- [ ] Task 2 — (lines -) [P:1] +- [ ] Task 3 — (lines -) [P:1] + +## PLAN TOC +- Preamble (goal, covenant, interface freeze, conventions): lines 1- +- Task 1 — : lines - +- ... +- Dry-run trace: lines - +- Deviations: lines <>- + +## LOG +- — handoff created; plan has tasks, none started. +```` + +Step 3 — verify: `wc -l` the handoff (should be well under ~120 lines) and confirm every TASKS line range matches the TOC. + +Step 4 — the handoff prompt for another agent is exactly one sentence: + +> Read `.claude/plans/.handoff.md` and follow its PROTOCOL section. Re-read that file after every compaction. + +### Maintaining it (executor duties, after EVERY task) + +1. Flip the checkbox in TASKS. +2. Move **YOU ARE HERE** to the next task (with its line range) and update **Verify**. +3. Append ONE LOG line with a real timestamp (`date '+%F %H:%M'`), stating the observable result, not intentions: + `- 2026-07-08 21:40 — Task 2 done: bun test 14/14 green, committed abc1234. Next: Task 3.` +4. If anything deviated: one LOG line here + the entry in the plan's `## Deviations`. +5. Blocked? Set YOU ARE HERE to `blocked: `, log it, STOP and report — do not skip ahead. + +Keep LOG lines terse. Never trim or rewrite old lines; append-only is worth more than pretty. + +### Resuming (fresh session / post-compact) + +Read the handoff → read plan preamble lines → read the YOU-ARE-HERE task's line range → work. That is ~2–4k tokens to be fully oriented, no matter how large the plan is. Re-reading the entire plan after a compaction is a protocol violation, not diligence. + +### Parallelizing with subagents + +- Only tasks sharing a `[P:n]` group may run concurrently; anything unmarked is sequential. +- **One writer rule:** subagents NEVER edit the handoff. The orchestrator spawns each subagent with: "Execute ONLY Task of `.claude/plans/.md`, lines -. Read the plan preamble (lines 1-) first. Report the verify output; do not touch other files." The orchestrator updates TASKS/LOG as each returns. +- If two [P] tasks would touch the same file, the [P] marking is wrong — fix the handoff, run them sequentially. + +### Relationship to other conventions + +- Pairs with `gt:plan-it`: its plans have greppable `## Task N:` headings, per-task VERIFY, and a `## Deviations` section — a handoff maps onto them 1:1. Works with any plan that has task headings, though. +- The executor should load the `fable-style` skill if available; the handoff governs *where you are*, fable-style governs *how you work*. +- `*.handoff.md` files are chronological/append-only by repo convention — this mode's STATE/TASKS rewrite-in-place blocks are the explicitly declared exception; LOG keeps the append-only audit trail. +- Handing the work to a non-Claude worker instead of a subagent? That is `gt:handoff-to` (routing) and `gt:handoff-to-codex` (mechanics), not this skill. diff --git a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts new file mode 100644 index 0000000000..2032a9ad62 --- /dev/null +++ b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts @@ -0,0 +1,423 @@ +#!/usr/bin/env bun + +/** + * wrap-up target resolver. + * + * Owns the deterministic half of "where does the wrap-up doc live?": + * - resolve : match the current project/branch/worktree against the registry, + * print the obsidian dir + the derived doc path (or found:false). + * - register: append/update a registry entry after the user confirms a target. + * - here : print ONLY the YOU-ARE-HERE block of a wrap-up file (cheap read). + * - log : atomically append a log section AND rewrite the YOU-ARE-HERE block, + * auto-stamping the datetime and auto-generating the before→after + * snapshot from the outgoing header. stdin carries two parts split by + * sentinel lines: @@HERE@@ (new state bullets) then @@LOG@@ (log body). + * + * The registry lives at ~/.claude/handoff-registry.json: + * { "entries": [ { projectDir, branch?, worktreeDir?, obsidianDir, docPath? }, ... ] } + * A missing/empty `branch` means the entry matches any branch in that project. + * + * Shared plugin config (optional) lives at ~/.genesis-tools/plugins/config.json: + * { "wrap-up": { "registryPath"?, "vaultDir"?, "docDir"? } } + * - registryPath: overrides the registry location. + * - docDir: fallback doc directory when the registry has no match — absolute, + * or relative to the project toplevel (e.g. ".claude/wrapups"). + * - vaultDir: vault root; fallback target becomes /. + * Resolution order: registry match > docDir > vaultDir > found:false. + */ + +import { homedir } from "node:os"; +import { basename, isAbsolute, join } from "node:path"; + +const PLUGIN_CONFIG = join(homedir(), ".genesis-tools", "plugins", "config.json"); +const HERE_START = ""; +const HERE_END = ""; + +interface Entry { + projectDir: string; + branch?: string; + worktreeDir?: string; + obsidianDir: string; + docPath?: string; +} +interface Registry { + entries: Entry[]; +} + +interface WrapUpConfig { + registryPath?: string; + vaultDir?: string; + docDir?: string; +} + +async function loadPluginConfig(): Promise { + const f = Bun.file(PLUGIN_CONFIG); + if (!(await f.exists())) { + return {}; + } + + try { + const parsed = JSON.parse(await f.text()); + return typeof parsed?.["wrap-up"] === "object" && parsed["wrap-up"] !== null ? parsed["wrap-up"] : {}; + } catch (err) { + // Falling back to {} silently would make a corrupt config look like an + // absent one and quietly demote the wrap-up to a different target tier. + console.error(`wrap-up: ignoring unreadable plugin config ${PLUGIN_CONFIG}: ${String(err)}`); + return {}; + } +} + +function expandHome(p: string): string { + return p.startsWith("~/") ? join(homedir(), p.slice(2)) : p; +} + +async function registryPath(): Promise { + const cfg = await loadPluginConfig(); + return expandHome(cfg.registryPath ?? join(homedir(), ".claude", "handoff-registry.json")); +} + +async function sh(cmd: string[]): Promise { + const p = Bun.spawn(cmd, { stdout: "pipe", stderr: "ignore" }); + const out = await new Response(p.stdout).text(); + await p.exited; + return out.trim(); +} + +async function loadRegistry(): Promise { + const path = await registryPath(); + const f = Bun.file(path); + if (!(await f.exists())) { + return { entries: [] }; + } + + try { + const parsed = JSON.parse(await f.text()); + return Array.isArray(parsed?.entries) ? parsed : { entries: [] }; + } catch (err) { + // A malformed registry must not masquerade as an empty one — that would + // silently drop every registered target and resolve to found:false. + console.error(`wrap-up: ignoring unreadable registry ${path}: ${String(err)}`); + return { entries: [] }; + } +} + +async function saveRegistry(reg: Registry): Promise { + await Bun.write(await registryPath(), `${JSON.stringify(reg, null, 2)}\n`); +} + +function slug(s: string): string { + return ( + s + .replace(/[^a-z0-9]+/gi, "-") + .replace(/^-+|-+$/g, "") + .toLowerCase() || "main" + ); +} + +function derivedDocPath(entry: Entry, branch: string): string { + if (entry.docPath) { + return entry.docPath; + } + + const project = basename(entry.projectDir); + return join(entry.obsidianDir, `${project}-${slug(branch)}.wrapup.md`); +} + +async function gitContext() { + const toplevel = await sh(["git", "rev-parse", "--show-toplevel"]); + const branch = await sh(["git", "rev-parse", "--abbrev-ref", "HEAD"]); + // In a worktree, common-dir differs from git-dir; the "main" checkout's + // toplevel is what a project-level entry keys on. + const cwd = process.cwd(); + return { toplevel: toplevel || cwd, branch: branch || "", cwd }; +} + +function matches(entry: Entry, ctx: { toplevel: string; branch: string; cwd: string }): number { + // Higher score = more specific match. 0 = no match. + const paths = [entry.worktreeDir, entry.projectDir].filter(Boolean) as string[]; + const pathHit = paths.some((p) => ctx.toplevel === p || ctx.cwd === p || ctx.cwd.startsWith(`${p}/`)); + if (!pathHit) { + return 0; + } + + if (entry.branch && entry.branch !== ctx.branch) { + return 0; + } + + let score = 1; + if (entry.worktreeDir && (ctx.toplevel === entry.worktreeDir || ctx.cwd.startsWith(`${entry.worktreeDir}/`))) { + score += 2; + } + + if (entry.branch) { + score += 1; + } + + return score; +} + +async function cmdResolve() { + const ctx = await gitContext(); + const reg = await loadRegistry(); + const ranked = reg.entries + .map((e) => ({ e, score: matches(e, ctx) })) + .filter((x) => x.score > 0) + .sort((a, b) => b.score - a.score); + + if (ranked.length === 0) { + // Fallback tier: shared plugin config. docDir (absolute or project-relative) + // wins over vaultDir/; both land as a synthetic non-registered + // entry so the doc path derivation stays uniform. + const cfg = await loadPluginConfig(); + const docDir = cfg.docDir + ? isAbsolute(expandHome(cfg.docDir)) + ? expandHome(cfg.docDir) + : join(ctx.toplevel, cfg.docDir) + : cfg.vaultDir + ? join(expandHome(cfg.vaultDir), basename(ctx.toplevel)) + : null; + + if (docDir) { + const entry: Entry = { projectDir: ctx.toplevel, obsidianDir: docDir }; + console.log( + JSON.stringify( + { + found: true, + source: "config", + obsidianDir: docDir, + docPath: derivedDocPath(entry, ctx.branch), + project: ctx.toplevel, + branch: ctx.branch, + worktree: null, + }, + null, + 2 + ) + ); + return; + } + + console.log(JSON.stringify({ found: false, project: ctx.toplevel, branch: ctx.branch, cwd: ctx.cwd }, null, 2)); + return; + } + + const { e } = ranked[0]; + console.log( + JSON.stringify( + { + found: true, + source: "registry", + obsidianDir: e.obsidianDir, + docPath: derivedDocPath(e, ctx.branch), + project: ctx.toplevel, + branch: ctx.branch, + worktree: e.worktreeDir ?? null, + }, + null, + 2 + ) + ); +} + +async function cmdRegister(args: Record) { + const ctx = await gitContext(); + const entry: Entry = { + projectDir: args.project ?? ctx.toplevel, + obsidianDir: args.obsidian, + ...(args.branch ? { branch: args.branch } : {}), + ...(args.worktree ? { worktreeDir: args.worktree } : {}), + ...(args.doc ? { docPath: args.doc } : {}), + }; + + if (!entry.obsidianDir) { + console.error("register: --obsidian is required"); + process.exit(1); + } + + const reg = await loadRegistry(); + // De-dupe on (projectDir, branch, worktreeDir). + reg.entries = reg.entries.filter( + (x) => + !( + x.projectDir === entry.projectDir && + (x.branch ?? "") === (entry.branch ?? "") && + (x.worktreeDir ?? "") === (entry.worktreeDir ?? "") + ) + ); + reg.entries.push(entry); + await saveRegistry(reg); + console.log( + JSON.stringify( + { registered: entry, registry: await registryPath(), docPath: derivedDocPath(entry, ctx.branch) }, + null, + 2 + ) + ); +} + +async function cmdHere(file: string) { + if (!file) { + console.error("here: pass the wrap-up file path"); + process.exit(1); + } + + const text = await Bun.file(file).text(); + const start = text.indexOf(HERE_START); + const end = text.indexOf(HERE_END); + if (start === -1 || end === -1) { + console.error("here: no YOU-ARE-HERE block found"); + process.exit(1); + } + + console.log(text.slice(start, end + HERE_END.length)); +} + +function nowStamp(): string { + const d = new Date(); + const p = (n: number) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; +} + +function innerBlock(full: string): string { + return full.replace(HERE_START, "").replace(HERE_END, "").trim(); +} + +function blockquote(s: string): string { + return s + .split("\n") + .map((l) => (l.length ? `> ${l}` : ">")) + .join("\n"); +} + +const LOG_USAGE = ` +Expected: pipe ONE heredoc split by two sentinel lines — @@HERE@@ (new state +bullets) then @@LOG@@ (the log-section body you author). Example: + + bun resolve.ts log "" <<'WRAPUP' + @@HERE@@ + - **Branch / worktree:** @ + - **State:** + - **Next:** + - **Verify:** + - **Read to resume:** + @@LOG@@ + ## — (commits , …) + + ### Goal & context + ... + WRAPUP + +The script auto-stamps the datetime, rewrites the YOU-ARE-HERE block in place, +and auto-generates the "Header before → after" snapshot — don't write those.`; + +function failLog(problem: string): never { + console.error(`log: ${problem}\n${LOG_USAGE}`); + process.exit(1); +} + +async function cmdLog(file: string) { + if (!file) { + failLog("no wrap-up file path given (first positional arg)"); + } + + const f = Bun.file(file); + if (!(await f.exists())) { + failLog( + `file does not exist: ${file}\n → create it from the template with Write first, then use 'log' for every session after` + ); + } + + const stdin = (await Bun.stdin.text()).trim(); + if (!stdin) { + failLog("nothing on stdin — you must pipe the @@HERE@@ / @@LOG@@ heredoc in"); + } + + const hIdx = stdin.indexOf("@@HERE@@"); + const lIdx = stdin.indexOf("@@LOG@@"); + const missing = [hIdx === -1 && "@@HERE@@", lIdx === -1 && "@@LOG@@"].filter(Boolean); + if (missing.length) { + failLog(`stdin is missing sentinel line(s): ${missing.join(" and ")}`); + } + + if (lIdx < hIdx) { + failLog("@@LOG@@ appears before @@HERE@@ — order must be @@HERE@@ first, then @@LOG@@"); + } + + const hereBody = stdin.slice(hIdx + "@@HERE@@".length, lIdx).trim(); + const logBody = stdin.slice(lIdx + "@@LOG@@".length).trim(); + if (!hereBody && !logBody) { + failLog("both the @@HERE@@ and @@LOG@@ sections are empty"); + } + + if (!hereBody) { + failLog("the @@HERE@@ section is empty — it needs the new 'You are here' state bullets"); + } + + if (!logBody) { + failLog("the @@LOG@@ section is empty — it needs the log-section body (## datetime header + forensics)"); + } + + const text = await f.text(); + const s = text.indexOf(HERE_START); + const e = text.indexOf(HERE_END); + if (s === -1 || e === -1 || e < s) { + failLog( + `no valid ${HERE_START} … ${HERE_END} block in ${file} — is this a wrap-up file created from the template?` + ); + } + + const oldFull = text.slice(s, e + HERE_END.length); + const stamp = nowStamp(); + const newFull = `${HERE_START}\n## You are here (${stamp})\n${hereBody}\n${HERE_END}`; + const rewritten = text.slice(0, s) + newFull + text.slice(e + HERE_END.length); + + const section = [ + logBody, + "", + "### Header before → after", + "", + "**Before:**", + "", + blockquote(innerBlock(oldFull)), + "", + "**After:**", + "", + blockquote(innerBlock(newFull)), + ].join("\n"); + + const body = `${rewritten.replace(/\s+$/, "")}\n\n${section}\n`; + await Bun.write(file, body); + console.log(JSON.stringify({ logged: file, stamp }, null, 2)); +} + +function parseFlags(argv: string[]): Record { + const out: Record = {}; + for (let i = 0; i < argv.length; i++) { + if (argv[i].startsWith("--")) { + out[argv[i].slice(2)] = argv[i + 1] ?? ""; + i++; + } + } + return out; +} + +const [cmd, ...rest] = process.argv.slice(2); +switch (cmd) { + case "resolve": + await cmdResolve(); + break; + case "register": + await cmdRegister(parseFlags(rest)); + break; + case "here": + await cmdHere(rest[0]); + break; + case "log": + await cmdLog(rest[0]); + break; + default: + console.error( + "usage: resolve.ts [--branch b] [--worktree w] [--project p] [--doc path] | here | log (log reads stdin: @@HERE@@ … @@LOG@@ …)>" + ); + process.exit(1); +} From 7c59173be797e8d8156fc8cac8111597296e7768 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 29 Jul 2026 06:57:30 +0200 Subject: [PATCH 2/9] fix(wrap-up): cache plugin config, surface git failures, cover resolve.ts with tests --- .../skills/wrap-up/scripts/resolve.test.ts | 280 ++++++++++++++++++ .../skills/wrap-up/scripts/resolve.ts | 170 +++++++---- 2 files changed, 395 insertions(+), 55 deletions(-) create mode 100644 plugins/genesis-tools/skills/wrap-up/scripts/resolve.test.ts diff --git a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.test.ts b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.test.ts new file mode 100644 index 0000000000..317230a7e5 --- /dev/null +++ b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.test.ts @@ -0,0 +1,280 @@ +import { describe, expect, it } from "bun:test"; +import { + blockquote, + buildLogBody, + derivedDocPath, + type Entry, + expandHome, + innerBlock, + matches, + parseFlags, + slug, + splitSentinels, +} from "./resolve.ts"; + +const HERE_START = ""; +const HERE_END = ""; + +function docWith(headerBody: string, tail = "\n\n---\n\n## 2026-07-01 10:00 — first session\n\nseed body.\n"): string { + return `# Wrap-up: Demo\n\n${HERE_START}\n## You are here (2026-07-01 10:00)\n${headerBody}\n${HERE_END}${tail}`; +} + +describe("slug", () => { + it("lowercases and dashes a branch name", () => { + expect(slug("feat/Skills-Handoff")).toBe("feat-skills-handoff"); + }); + + it("trims leading and trailing separators", () => { + expect(slug("///feat///")).toBe("feat"); + }); + + it("falls back to 'main' when nothing survives", () => { + expect(slug("")).toBe("main"); + expect(slug("///")).toBe("main"); + }); +}); + +describe("derivedDocPath", () => { + const entry: Entry = { projectDir: "/repos/MyProject", obsidianDir: "/vault/MyProject" }; + + it("derives /-.wrapup.md", () => { + expect(derivedDocPath(entry, "feat/x")).toBe("/vault/MyProject/MyProject-feat-x.wrapup.md"); + }); + + it("gives each branch its own file", () => { + expect(derivedDocPath(entry, "feat/a")).not.toBe(derivedDocPath(entry, "feat/b")); + }); + + it("prefers an explicit docPath over the derived one", () => { + const pinned: Entry = { ...entry, docPath: "/vault/pinned.md" }; + expect(derivedDocPath(pinned, "feat/x")).toBe("/vault/pinned.md"); + }); +}); + +describe("matches", () => { + const ctx = { toplevel: "/repos/Proj", branch: "feat/x", cwd: "/repos/Proj" }; + + it("returns 0 when the path does not match at all", () => { + expect(matches({ projectDir: "/repos/Other", obsidianDir: "/v" }, ctx)).toBe(0); + }); + + it("matches any branch when the entry declares none", () => { + expect(matches({ projectDir: "/repos/Proj", obsidianDir: "/v" }, ctx)).toBe(1); + }); + + it("returns 0 when the entry pins a different branch", () => { + expect(matches({ projectDir: "/repos/Proj", obsidianDir: "/v", branch: "feat/other" }, ctx)).toBe(0); + }); + + it("scores a branch-pinned entry above a project-wide one", () => { + const wide = matches({ projectDir: "/repos/Proj", obsidianDir: "/v" }, ctx); + const pinned = matches({ projectDir: "/repos/Proj", obsidianDir: "/v", branch: "feat/x" }, ctx); + expect(pinned).toBeGreaterThan(wide); + }); + + it("scores a worktree match highest", () => { + const wt = { toplevel: "/repos/Proj/.worktrees/x", branch: "feat/x", cwd: "/repos/Proj/.worktrees/x" }; + const pinned = matches({ projectDir: "/repos/Proj", obsidianDir: "/v", branch: "feat/x" }, wt); + const worktree = matches( + { + projectDir: "/repos/Proj", + obsidianDir: "/v", + branch: "feat/x", + worktreeDir: "/repos/Proj/.worktrees/x", + }, + wt + ); + expect(worktree).toBeGreaterThan(pinned); + }); + + it("matches a cwd nested under the project dir", () => { + const nested = { toplevel: "/repos/Proj", branch: "feat/x", cwd: "/repos/Proj/src/deep" }; + expect(matches({ projectDir: "/repos/Proj", obsidianDir: "/v" }, nested)).toBe(1); + }); + + it("does not match a sibling dir sharing a name prefix", () => { + const sibling = { toplevel: "/repos/ProjOther", branch: "feat/x", cwd: "/repos/ProjOther" }; + expect(matches({ projectDir: "/repos/Proj", obsidianDir: "/v" }, sibling)).toBe(0); + }); +}); + +describe("splitSentinels", () => { + it("splits a well-formed payload", () => { + const res = splitSentinels("@@HERE@@\n- state\n@@LOG@@\n## topic\nbody"); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.hereBody).toBe("- state"); + expect(res.logBody).toBe("## topic\nbody"); + } + }); + + it("rejects empty stdin", () => { + const res = splitSentinels(" \n "); + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.problem).toContain("nothing on stdin"); + } + }); + + it("names both missing sentinels", () => { + const res = splitSentinels("just some prose"); + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.problem).toContain("@@HERE@@"); + expect(res.problem).toContain("@@LOG@@"); + } + }); + + it("names the single missing sentinel", () => { + const res = splitSentinels("@@HERE@@\n- state"); + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.problem).toContain("@@LOG@@"); + expect(res.problem).not.toContain("@@HERE@@ and"); + } + }); + + it("rejects reversed sentinel order", () => { + const res = splitSentinels("@@LOG@@\nbody\n@@HERE@@\n- state"); + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.problem).toContain("order must be"); + } + }); + + it("rejects an empty @@HERE@@ section", () => { + const res = splitSentinels("@@HERE@@\n@@LOG@@\n## topic"); + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.problem).toContain("@@HERE@@ section is empty"); + } + }); + + it("rejects an empty @@LOG@@ section", () => { + const res = splitSentinels("@@HERE@@\n- state\n@@LOG@@\n"); + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.problem).toContain("@@LOG@@ section is empty"); + } + }); +}); + +describe("buildLogBody", () => { + const args = { hereBody: "- **State:** new", logBody: "## 2026-07-29 06:47 — second", stamp: "2026-07-29 06:47" }; + + it("rewrites the header in place with the new stamp", () => { + const res = buildLogBody({ text: docWith("- **State:** old"), ...args }); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.body).toContain("## You are here (2026-07-29 06:47)"); + expect(res.body).toContain("- **State:** new"); + // The old stamp survives only as a quoted line in the snapshot, + // never as a live header. + expect(res.body).not.toContain("\n## You are here (2026-07-01 10:00)"); + expect(res.body).toContain("\n> ## You are here (2026-07-01 10:00)"); + } + }); + + it("keeps exactly one YOU-ARE-HERE block", () => { + const res = buildLogBody({ text: docWith("- **State:** old"), ...args }); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.body.split(HERE_START).length - 1).toBe(1); + expect(res.body.split(HERE_END).length - 1).toBe(1); + } + }); + + it("preserves earlier log sections (append-only audit trail)", () => { + const res = buildLogBody({ text: docWith("- **State:** old"), ...args }); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.body).toContain("## 2026-07-01 10:00 — first session"); + expect(res.body).toContain("seed body."); + expect(res.body.indexOf("first session")).toBeLessThan(res.body.indexOf("second")); + } + }); + + it("appends the before/after snapshot quoting the old and new state", () => { + const res = buildLogBody({ text: docWith("- **State:** old"), ...args }); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.body).toContain("### Header before → after"); + expect(res.body).toContain("> - **State:** old"); + expect(res.body).toContain("> - **State:** new"); + } + }); + + it("is idempotent in shape — logging twice keeps one header and both sections", () => { + const first = buildLogBody({ text: docWith("- **State:** old"), ...args }); + expect(first.ok).toBe(true); + if (!first.ok) { + return; + } + + const second = buildLogBody({ + text: first.body, + hereBody: "- **State:** third", + logBody: "## 2026-07-29 07:00 — third", + stamp: "2026-07-29 07:00", + }); + expect(second.ok).toBe(true); + if (second.ok) { + expect(second.body.split(HERE_START).length - 1).toBe(1); + expect(second.body).toContain("## 2026-07-01 10:00 — first session"); + expect(second.body).toContain("— second"); + expect(second.body).toContain("— third"); + } + }); + + it("reports a file with no YOU-ARE-HERE block", () => { + const res = buildLogBody({ text: "# Just a heading\n\nno block here.\n", ...args }); + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.problem).toContain("no valid"); + } + }); + + it("reports a block whose markers are inverted", () => { + const res = buildLogBody({ text: `# Doc\n${HERE_END}\nbody\n${HERE_START}\n`, ...args }); + expect(res.ok).toBe(false); + }); +}); + +describe("blockquote / innerBlock", () => { + it("quotes every line and keeps blank lines as bare '>'", () => { + expect(blockquote("a\n\nb")).toBe("> a\n>\n> b"); + }); + + it("strips the sentinel markers", () => { + expect(innerBlock(`${HERE_START}\n## You are here\n- x\n${HERE_END}`)).toBe("## You are here\n- x"); + }); +}); + +describe("parseFlags", () => { + it("pairs each --flag with its value", () => { + expect(parseFlags(["--obsidian", "/vault", "--branch", "feat/x"])).toEqual({ + obsidian: "/vault", + branch: "feat/x", + }); + }); + + it("gives a trailing valueless flag an empty string", () => { + expect(parseFlags(["--doc"])).toEqual({ doc: "" }); + }); + + it("ignores positional noise", () => { + expect(parseFlags(["stray", "--branch", "feat/x"])).toEqual({ branch: "feat/x" }); + }); +}); + +describe("expandHome", () => { + it("leaves absolute and relative paths untouched", () => { + expect(expandHome("/abs/path")).toBe("/abs/path"); + expect(expandHome(".claude/wrapups")).toBe(".claude/wrapups"); + }); + + it("expands a leading ~/", () => { + expect(expandHome("~/vault")).not.toContain("~"); + expect(expandHome("~/vault").endsWith("/vault")).toBe(true); + }); +}); diff --git a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts index 2032a9ad62..a2fa814389 100644 --- a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts +++ b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts @@ -33,7 +33,7 @@ const PLUGIN_CONFIG = join(homedir(), ".genesis-tools", "plugins", "config.json" const HERE_START = ""; const HERE_END = ""; -interface Entry { +export interface Entry { projectDir: string; branch?: string; worktreeDir?: string; @@ -50,7 +50,22 @@ interface WrapUpConfig { docDir?: string; } +// One resolve invocation reads this on both the registry-path lookup and the +// docDir fallback; caching keeps it to a single read and stops a corrupt config +// from printing the same warning twice. +let pluginConfigCache: WrapUpConfig | undefined; + async function loadPluginConfig(): Promise { + if (pluginConfigCache) { + return pluginConfigCache; + } + + const cfg = await readPluginConfig(); + pluginConfigCache = cfg; + return cfg; +} + +async function readPluginConfig(): Promise { const f = Bun.file(PLUGIN_CONFIG); if (!(await f.exists())) { return {}; @@ -67,7 +82,7 @@ async function loadPluginConfig(): Promise { } } -function expandHome(p: string): string { +export function expandHome(p: string): string { return p.startsWith("~/") ? join(homedir(), p.slice(2)) : p; } @@ -77,9 +92,16 @@ async function registryPath(): Promise { } async function sh(cmd: string[]): Promise { - const p = Bun.spawn(cmd, { stdout: "pipe", stderr: "ignore" }); - const out = await new Response(p.stdout).text(); - await p.exited; + const p = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" }); + const [out, err] = await Promise.all([new Response(p.stdout).text(), new Response(p.stderr).text()]); + const code = await p.exited; + if (code !== 0) { + // Callers deliberately fall back (cwd / empty branch) so this stays + // non-fatal, but a swallowed failure is indistinguishable from a + // legitimately empty result — say which one happened. + console.error(`wrap-up: \`${cmd.join(" ")}\` exited ${code}${err.trim() ? `: ${err.trim()}` : ""}`); + } + return out.trim(); } @@ -105,7 +127,7 @@ async function saveRegistry(reg: Registry): Promise { await Bun.write(await registryPath(), `${JSON.stringify(reg, null, 2)}\n`); } -function slug(s: string): string { +export function slug(s: string): string { return ( s .replace(/[^a-z0-9]+/gi, "-") @@ -114,7 +136,7 @@ function slug(s: string): string { ); } -function derivedDocPath(entry: Entry, branch: string): string { +export function derivedDocPath(entry: Entry, branch: string): string { if (entry.docPath) { return entry.docPath; } @@ -132,7 +154,7 @@ async function gitContext() { return { toplevel: toplevel || cwd, branch: branch || "", cwd }; } -function matches(entry: Entry, ctx: { toplevel: string; branch: string; cwd: string }): number { +export function matches(entry: Entry, ctx: { toplevel: string; branch: string; cwd: string }): number { // Higher score = more specific match. 0 = no match. const paths = [entry.worktreeDir, entry.projectDir].filter(Boolean) as string[]; const pathHit = paths.some((p) => ctx.toplevel === p || ctx.cwd === p || ctx.cwd.startsWith(`${p}/`)); @@ -278,11 +300,11 @@ function nowStamp(): string { return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; } -function innerBlock(full: string): string { +export function innerBlock(full: string): string { return full.replace(HERE_START, "").replace(HERE_END, "").trim(); } -function blockquote(s: string): string { +export function blockquote(s: string): string { return s .split("\n") .map((l) => (l.length ? `> ${l}` : ">")) @@ -315,59 +337,68 @@ function failLog(problem: string): never { process.exit(1); } -async function cmdLog(file: string) { - if (!file) { - failLog("no wrap-up file path given (first positional arg)"); - } +export type SentinelSplit = { ok: true; hereBody: string; logBody: string } | { ok: false; problem: string }; - const f = Bun.file(file); - if (!(await f.exists())) { - failLog( - `file does not exist: ${file}\n → create it from the template with Write first, then use 'log' for every session after` - ); - } - - const stdin = (await Bun.stdin.text()).trim(); +export function splitSentinels(raw: string): SentinelSplit { + const stdin = raw.trim(); if (!stdin) { - failLog("nothing on stdin — you must pipe the @@HERE@@ / @@LOG@@ heredoc in"); + return { ok: false, problem: "nothing on stdin — you must pipe the @@HERE@@ / @@LOG@@ heredoc in" }; } const hIdx = stdin.indexOf("@@HERE@@"); const lIdx = stdin.indexOf("@@LOG@@"); const missing = [hIdx === -1 && "@@HERE@@", lIdx === -1 && "@@LOG@@"].filter(Boolean); if (missing.length) { - failLog(`stdin is missing sentinel line(s): ${missing.join(" and ")}`); + return { ok: false, problem: `stdin is missing sentinel line(s): ${missing.join(" and ")}` }; } if (lIdx < hIdx) { - failLog("@@LOG@@ appears before @@HERE@@ — order must be @@HERE@@ first, then @@LOG@@"); + return { ok: false, problem: "@@LOG@@ appears before @@HERE@@ — order must be @@HERE@@ first, then @@LOG@@" }; } const hereBody = stdin.slice(hIdx + "@@HERE@@".length, lIdx).trim(); const logBody = stdin.slice(lIdx + "@@LOG@@".length).trim(); if (!hereBody && !logBody) { - failLog("both the @@HERE@@ and @@LOG@@ sections are empty"); + return { ok: false, problem: "both the @@HERE@@ and @@LOG@@ sections are empty" }; } if (!hereBody) { - failLog("the @@HERE@@ section is empty — it needs the new 'You are here' state bullets"); + return { + ok: false, + problem: "the @@HERE@@ section is empty — it needs the new 'You are here' state bullets", + }; } if (!logBody) { - failLog("the @@LOG@@ section is empty — it needs the log-section body (## datetime header + forensics)"); + return { + ok: false, + problem: "the @@LOG@@ section is empty — it needs the log-section body (## datetime header + forensics)", + }; } - const text = await f.text(); + return { ok: true, hereBody, logBody }; +} + +export type LogBuild = { ok: true; body: string } | { ok: false; problem: string }; + +export function buildLogBody({ + text, + hereBody, + logBody, + stamp, +}: { + text: string; + hereBody: string; + logBody: string; + stamp: string; +}): LogBuild { const s = text.indexOf(HERE_START); const e = text.indexOf(HERE_END); if (s === -1 || e === -1 || e < s) { - failLog( - `no valid ${HERE_START} … ${HERE_END} block in ${file} — is this a wrap-up file created from the template?` - ); + return { ok: false, problem: `no valid ${HERE_START} … ${HERE_END} block` }; } const oldFull = text.slice(s, e + HERE_END.length); - const stamp = nowStamp(); const newFull = `${HERE_START}\n## You are here (${stamp})\n${hereBody}\n${HERE_END}`; const rewritten = text.slice(0, s) + newFull + text.slice(e + HERE_END.length); @@ -385,12 +416,37 @@ async function cmdLog(file: string) { blockquote(innerBlock(newFull)), ].join("\n"); - const body = `${rewritten.replace(/\s+$/, "")}\n\n${section}\n`; - await Bun.write(file, body); + return { ok: true, body: `${rewritten.replace(/\s+$/, "")}\n\n${section}\n` }; +} + +async function cmdLog(file: string) { + if (!file) { + failLog("no wrap-up file path given (first positional arg)"); + } + + const f = Bun.file(file); + if (!(await f.exists())) { + failLog( + `file does not exist: ${file}\n → create it from the template with Write first, then use 'log' for every session after` + ); + } + + const split = splitSentinels(await Bun.stdin.text()); + if (!split.ok) { + failLog(split.problem); + } + + const stamp = nowStamp(); + const built = buildLogBody({ text: await f.text(), hereBody: split.hereBody, logBody: split.logBody, stamp }); + if (!built.ok) { + failLog(`${built.problem} in ${file} — is this a wrap-up file created from the template?`); + } + + await Bun.write(file, built.body); console.log(JSON.stringify({ logged: file, stamp }, null, 2)); } -function parseFlags(argv: string[]): Record { +export function parseFlags(argv: string[]): Record { const out: Record = {}; for (let i = 0; i < argv.length; i++) { if (argv[i].startsWith("--")) { @@ -401,23 +457,27 @@ function parseFlags(argv: string[]): Record { return out; } -const [cmd, ...rest] = process.argv.slice(2); -switch (cmd) { - case "resolve": - await cmdResolve(); - break; - case "register": - await cmdRegister(parseFlags(rest)); - break; - case "here": - await cmdHere(rest[0]); - break; - case "log": - await cmdLog(rest[0]); - break; - default: - console.error( - "usage: resolve.ts [--branch b] [--worktree w] [--project p] [--doc path] | here | log (log reads stdin: @@HERE@@ … @@LOG@@ …)>" - ); - process.exit(1); +// Guarded so the pure helpers above can be imported by tests without the CLI +// dispatcher running (and calling process.exit) on import. +if (import.meta.main) { + const [cmd, ...rest] = process.argv.slice(2); + switch (cmd) { + case "resolve": + await cmdResolve(); + break; + case "register": + await cmdRegister(parseFlags(rest)); + break; + case "here": + await cmdHere(rest[0]); + break; + case "log": + await cmdLog(rest[0]); + break; + default: + console.error( + "usage: resolve.ts [--branch b] [--worktree w] [--project p] [--doc path] | here | log (log reads stdin: @@HERE@@ … @@LOG@@ …)>" + ); + process.exit(1); + } } From b705adab3603565df4f8c7e0daf8b84660f646a5 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 29 Jul 2026 06:57:36 +0200 Subject: [PATCH 3/9] docs(wrap-up): state that gt:handoff is retired, not renamed --- plugins/genesis-tools/skills/wrap-up/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/genesis-tools/skills/wrap-up/SKILL.md b/plugins/genesis-tools/skills/wrap-up/SKILL.md index 04cc87a6c3..019db37944 100644 --- a/plugins/genesis-tools/skills/wrap-up/SKILL.md +++ b/plugins/genesis-tools/skills/wrap-up/SKILL.md @@ -310,3 +310,4 @@ Read the handoff → read plan preamble lines → read the YOU-ARE-HERE task's l - The executor should load the `fable-style` skill if available; the handoff governs *where you are*, fable-style governs *how you work*. - `*.handoff.md` files are chronological/append-only by repo convention — this mode's STATE/TASKS rewrite-in-place blocks are the explicitly declared exception; LOG keeps the append-only audit trail. - Handing the work to a non-Claude worker instead of a subagent? That is `gt:handoff-to` (routing) and `gt:handoff-to-codex` (mechanics), not this skill. +- **Replaces the retired `gt:handoff`.** That skill is gone, not renamed: its wrap-up half is SESSION mode above, its plan-handoff half is PLAN mode. If you were reaching for `gt:handoff`, you want this skill — unless you meant offloading work to another model, which is `gt:handoff-to`. From 2d6bdf7cedbae75fa2cd40fae4769b5a67a2549b Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 29 Jul 2026 07:06:40 +0200 Subject: [PATCH 4/9] fix(wrap-up): atomic file writes and scope the biome JSON exemption to wrap-up scripts --- biome.json | 2 +- .../skills/wrap-up/scripts/resolve.ts | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/biome.json b/biome.json index b1aca31a12..edbc4c6ffa 100644 --- a/biome.json +++ b/biome.json @@ -120,7 +120,7 @@ } }, { - "includes": ["plugins/*/skills/*/scripts/**"], + "includes": ["plugins/genesis-tools/skills/wrap-up/scripts/**"], "linter": { "rules": { "style": { diff --git a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts index a2fa814389..bcb1758877 100644 --- a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts +++ b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts @@ -26,6 +26,7 @@ * Resolution order: registry match > docDir > vaultDir > found:false. */ +import { rename } from "node:fs/promises"; import { homedir } from "node:os"; import { basename, isAbsolute, join } from "node:path"; @@ -123,8 +124,18 @@ async function loadRegistry(): Promise { } } +// Write via temp file + rename so an interrupted write can never leave a +// truncated file behind. Both targets are append-only records whose partial +// loss is unrecoverable: the registry holds every project's wrap-up target, +// and the wrap-up doc's log is the only permanent session history. +async function writeAtomic(path: string, body: string): Promise { + const tmp = `${path}.tmp-${process.pid}-${Date.now()}`; + await Bun.write(tmp, body); + await rename(tmp, path); +} + async function saveRegistry(reg: Registry): Promise { - await Bun.write(await registryPath(), `${JSON.stringify(reg, null, 2)}\n`); + await writeAtomic(await registryPath(), `${JSON.stringify(reg, null, 2)}\n`); } export function slug(s: string): string { @@ -442,7 +453,7 @@ async function cmdLog(file: string) { failLog(`${built.problem} in ${file} — is this a wrap-up file created from the template?`); } - await Bun.write(file, built.body); + await writeAtomic(file, built.body); console.log(JSON.stringify({ logged: file, stamp }, null, 2)); } From 87a9541d611e0ca576f6821a7617bc7c91909942 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 29 Jul 2026 07:06:43 +0200 Subject: [PATCH 5/9] docs(codex): pin approval routing to lead, fail loudly on wait timeout, keep --ignore-user-config on resume --- plugins/genesis-tools/agents/agent-driver.md | 6 +++++- plugins/genesis-tools/skills/handoff-to-codex/SKILL.md | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/plugins/genesis-tools/agents/agent-driver.md b/plugins/genesis-tools/agents/agent-driver.md index 20c5c77c3b..bc92a571bd 100644 --- a/plugins/genesis-tools/agents/agent-driver.md +++ b/plugins/genesis-tools/agents/agent-driver.md @@ -3,6 +3,8 @@ name: agent-driver description: "Drives one external worker session end to end — spawn, watch, steer, resolve approvals, verify, tear down — keeping its event stream out of the orchestrator's context. One per worker session. Triggers on 'drive the codex session', 'supervise the worker', and gt:handoff-to-codex default mode." --- +# Agent Driver + You drive exactly **one** external worker session end to end. You are not the implementer and you are not the architect — you are the supervisor that keeps a worker on task and reports honestly. Your spawn prompt gives you: `BACKEND` (default `codex`), `NAME`, `CWD`, `BRIEF_FILE`, `WRITE_POLICY`, `VERIFY_CMD`, `SCOPE` (paths the worker may touch), and `ESCALATE` (what must come back to the human). @@ -59,6 +61,8 @@ tools codex approve --name --request tools codex deny --name --request ``` +The `approval_request` bus message is addressed to `lead`, not to `driver_` (the recipient is hardcoded), so do **not** sit waiting for one on your login stream. You get the request id from the `tools codex tail --name --follow` stream you are already watching in §4, or from `lead` forwarding it. The worker stays paused until you answer. + **Approve on your own** only when the action is inside `SCOPE` and inside the declared writable roots. **Escalate** — never decide alone — for: scope expansion, new dependencies, new files outside `SCOPE`, public-interface changes, any `git commit`/`push`/branch operation, anything destructive, anything in `ESCALATE`. Escalation is dual-channel: @@ -81,7 +85,7 @@ tools codex stop --name Report to `lead` in this shape, and nothing longer: -``` +```text VERDICT: CHANGED: VERIFY: diff --git a/plugins/genesis-tools/skills/handoff-to-codex/SKILL.md b/plugins/genesis-tools/skills/handoff-to-codex/SKILL.md index f8e5c1fab1..f5fb93d22b 100644 --- a/plugins/genesis-tools/skills/handoff-to-codex/SKILL.md +++ b/plugins/genesis-tools/skills/handoff-to-codex/SKILL.md @@ -25,7 +25,7 @@ Driver model: **sonnet** by default; **opus** when there is no committed plan, w Spawning the driver (after §1's brief is written and `tools agents login --agent-main --agent-name lead` is running in the background): -``` +```text Agent( subagent_type: "genesis-tools:agent-driver", model: "sonnet", // or "opus" per above @@ -109,6 +109,8 @@ tools codex approve --name --request tools codex deny --name --request ``` +**The recipient is always `lead`** — it is hardcoded (`leadName: "lead"`, `src/codex/lib/session.ts`), not the driver's name. So in driver mode the bus message lands on the orchestrator, not on `driver_`. The driver picks approvals up from its own `tools codex tail --name --follow` stream, which carries the request id; if `lead` sees the bus message first, it forwards the id to the driver. Either way the session stays paused until someone answers, so an unanswered approval shows up as a stall, not a silent continue. + Driver authority: **approve autonomously** only when the action is inside the declared writable roots and inside the declared task scope. **Escalate to the human** for anything that expands scope, adds a dependency, touches git history, or leaves the declared paths. Compact receiver stream for the orchestrator: @@ -162,6 +164,9 @@ Wait on event types only (`error:` appears in normal red-test output): ```bash SECONDS=0; until rg -q '"type":"turn.completed"|"type":"turn.failed"' /tmp/codex-.log 2>/dev/null || [ $SECONDS -ge 600 ]; do sleep 5; done +rg -q '"type":"turn.completed"|"type":"turn.failed"' /tmp/codex-.log || { echo "TIMEOUT after ${SECONDS}s — turn never terminated"; tail -20 /tmp/codex-.log; } ``` -Resume: `command codex exec resume --json --skip-git-repo-check -c sandbox_mode="workspace-write" -o /tmp/codex--steer.md ""`. `--skip-git-repo-check` is **not** inherited; `--sandbox`/`--cd` are **not** re-applied on resume — pass sandbox as `-c sandbox_mode=`. +The re-check after the loop is not optional: the loop also exits on the deadline, and a timed-out run still leaves a stale `-o` file on disk. Reading that file without confirming a terminal event reports a half-finished turn as a result. On timeout, stop and report — do not resume blindly. + +Resume: `command codex exec resume --json --ignore-user-config --skip-git-repo-check -c sandbox_mode="workspace-write" -o /tmp/codex--steer.md ""`. Nothing is inherited from the original invocation — `--ignore-user-config` and `--skip-git-repo-check` must both be repeated, and `--sandbox`/`--cd` are **not** re-applied on resume, so pass sandbox as `-c sandbox_mode=`. Dropping `--ignore-user-config` on resume silently reloads `~/.codex` config and skills mid-thread. From 6a6ab0a28a0ad061bb678a7e970600d2bca55226 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 29 Jul 2026 07:13:18 +0200 Subject: [PATCH 6/9] fix(wrap-up): per-line biome-ignore per house rule, drop the config override, survive missing git --- biome.json | 10 ----- .../skills/wrap-up/scripts/resolve.test.ts | 17 +++++++++ .../skills/wrap-up/scripts/resolve.ts | 38 +++++++++++++------ 3 files changed, 44 insertions(+), 21 deletions(-) diff --git a/biome.json b/biome.json index edbc4c6ffa..eac50dd3f8 100644 --- a/biome.json +++ b/biome.json @@ -119,16 +119,6 @@ } } }, - { - "includes": ["plugins/genesis-tools/skills/wrap-up/scripts/**"], - "linter": { - "rules": { - "style": { - "noRestrictedGlobals": "off" - } - } - } - }, { "includes": ["**/*.test.ts", "**/*.test.tsx", "**/__tests__/**", "**/*.data.ts"], "plugins": [] diff --git a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.test.ts b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.test.ts index 317230a7e5..cab2263d26 100644 --- a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.test.ts +++ b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.test.ts @@ -8,6 +8,7 @@ import { innerBlock, matches, parseFlags, + sh, slug, splitSentinels, } from "./resolve.ts"; @@ -267,6 +268,22 @@ describe("parseFlags", () => { }); }); +describe("sh", () => { + it("returns trimmed stdout on success", async () => { + expect(await sh(["echo", " hello "])).toBe("hello"); + }); + + it("returns empty string when the command exits non-zero", async () => { + expect(await sh(["false"])).toBe(""); + }); + + it("returns empty string instead of throwing when the binary is missing", async () => { + // Bun.spawn throws on ENOENT; gitContext()'s documented no-git fallback + // depends on this degrading to "" rather than crashing the command. + expect(await sh(["wrap-up-no-such-binary-xyz"])).toBe(""); + }); +}); + describe("expandHome", () => { it("leaves absolute and relative paths untouched", () => { expect(expandHome("/abs/path")).toBe("/abs/path"); diff --git a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts index bcb1758877..153dc89662 100644 --- a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts +++ b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts @@ -73,6 +73,7 @@ async function readPluginConfig(): Promise { } try { + // biome-ignore lint/style/noRestrictedGlobals: standalone script without access to SafeJSON const parsed = JSON.parse(await f.text()); return typeof parsed?.["wrap-up"] === "object" && parsed["wrap-up"] !== null ? parsed["wrap-up"] : {}; } catch (err) { @@ -92,18 +93,26 @@ async function registryPath(): Promise { return expandHome(cfg.registryPath ?? join(homedir(), ".claude", "handoff-registry.json")); } -async function sh(cmd: string[]): Promise { - const p = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" }); - const [out, err] = await Promise.all([new Response(p.stdout).text(), new Response(p.stderr).text()]); - const code = await p.exited; - if (code !== 0) { - // Callers deliberately fall back (cwd / empty branch) so this stays - // non-fatal, but a swallowed failure is indistinguishable from a - // legitimately empty result — say which one happened. - console.error(`wrap-up: \`${cmd.join(" ")}\` exited ${code}${err.trim() ? `: ${err.trim()}` : ""}`); - } +export async function sh(cmd: string[]): Promise { + try { + const p = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" }); + const [out, err] = await Promise.all([new Response(p.stdout).text(), new Response(p.stderr).text()]); + const code = await p.exited; + if (code !== 0) { + // Callers deliberately fall back (cwd / empty branch) so this stays + // non-fatal, but a swallowed failure is indistinguishable from a + // legitimately empty result — say which one happened. + console.error(`wrap-up: \`${cmd.join(" ")}\` exited ${code}${err.trim() ? `: ${err.trim()}` : ""}`); + } - return out.trim(); + return out.trim(); + } catch (err) { + // Bun.spawn throws outright when the binary is missing from $PATH, which + // would crash the whole command instead of taking the documented + // no-git fallback. Degrade to "" like a non-zero exit does. + console.error(`wrap-up: \`${cmd.join(" ")}\` could not run: ${String(err)}`); + return ""; + } } async function loadRegistry(): Promise { @@ -114,6 +123,7 @@ async function loadRegistry(): Promise { } try { + // biome-ignore lint/style/noRestrictedGlobals: standalone script without access to SafeJSON const parsed = JSON.parse(await f.text()); return Array.isArray(parsed?.entries) ? parsed : { entries: [] }; } catch (err) { @@ -135,6 +145,7 @@ async function writeAtomic(path: string, body: string): Promise { } async function saveRegistry(reg: Registry): Promise { + // biome-ignore lint/style/noRestrictedGlobals: standalone script without access to SafeJSON await writeAtomic(await registryPath(), `${JSON.stringify(reg, null, 2)}\n`); } @@ -213,6 +224,7 @@ async function cmdResolve() { if (docDir) { const entry: Entry = { projectDir: ctx.toplevel, obsidianDir: docDir }; console.log( + // biome-ignore lint/style/noRestrictedGlobals: standalone script without access to SafeJSON JSON.stringify( { found: true, @@ -230,12 +242,14 @@ async function cmdResolve() { return; } + // biome-ignore lint/style/noRestrictedGlobals: standalone script without access to SafeJSON console.log(JSON.stringify({ found: false, project: ctx.toplevel, branch: ctx.branch, cwd: ctx.cwd }, null, 2)); return; } const { e } = ranked[0]; console.log( + // biome-ignore lint/style/noRestrictedGlobals: standalone script without access to SafeJSON JSON.stringify( { found: true, @@ -280,6 +294,7 @@ async function cmdRegister(args: Record) { reg.entries.push(entry); await saveRegistry(reg); console.log( + // biome-ignore lint/style/noRestrictedGlobals: standalone script without access to SafeJSON JSON.stringify( { registered: entry, registry: await registryPath(), docPath: derivedDocPath(entry, ctx.branch) }, null, @@ -454,6 +469,7 @@ async function cmdLog(file: string) { } await writeAtomic(file, built.body); + // biome-ignore lint/style/noRestrictedGlobals: standalone script without access to SafeJSON console.log(JSON.stringify({ logged: file, stamp }, null, 2)); } From 57fd247c8ba0c51974d1190c5a09ba4455efe099 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 29 Jul 2026 07:17:35 +0200 Subject: [PATCH 7/9] fix(wrap-up): preserve destination file mode on atomic write and clean up temp on failure --- .../skills/wrap-up/scripts/resolve.test.ts | 54 +++++++++++++++++++ .../skills/wrap-up/scripts/resolve.ts | 23 ++++++-- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.test.ts b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.test.ts index cab2263d26..6892b943e6 100644 --- a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.test.ts +++ b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.test.ts @@ -1,4 +1,7 @@ import { describe, expect, it } from "bun:test"; +import { chmod, mkdtemp, readdir, stat, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { blockquote, buildLogBody, @@ -11,6 +14,7 @@ import { sh, slug, splitSentinels, + writeAtomic, } from "./resolve.ts"; const HERE_START = ""; @@ -268,6 +272,56 @@ describe("parseFlags", () => { }); }); +describe("writeAtomic", () => { + it("replaces the destination's contents", async () => { + const dir = await mkdtemp(join(tmpdir(), "wrapup-atomic-")); + const target = join(dir, "doc.md"); + await writeFile(target, "old contents"); + + await writeAtomic(target, "new contents"); + expect(await Bun.file(target).text()).toBe("new contents"); + }); + + it("creates the file when it does not exist yet", async () => { + const dir = await mkdtemp(join(tmpdir(), "wrapup-atomic-")); + const target = join(dir, "fresh.md"); + + await writeAtomic(target, "hello"); + expect(await Bun.file(target).text()).toBe("hello"); + }); + + it("preserves a restrictive destination mode instead of widening it", async () => { + const dir = await mkdtemp(join(tmpdir(), "wrapup-atomic-")); + const target = join(dir, "private.json"); + await writeFile(target, "{}"); + await chmod(target, 0o600); + + await writeAtomic(target, '{"entries":[]}'); + // rename() installs a new inode; without carrying the mode over this + // would come back as the umask default (commonly 0644). + expect((await stat(target)).mode & 0o777).toBe(0o600); + }); + + it("leaves no temp file behind", async () => { + const dir = await mkdtemp(join(tmpdir(), "wrapup-atomic-")); + const target = join(dir, "doc.md"); + + await writeAtomic(target, "body"); + expect((await readdir(dir)).filter((f) => f.includes(".tmp-"))).toEqual([]); + }); + + it("cleans up the temp file when the rename fails", async () => { + const dir = await mkdtemp(join(tmpdir(), "wrapup-atomic-")); + // A directory as the destination makes rename() fail after the temp + // file has already been written. + const target = join(dir, "adir"); + await Bun.write(join(target, "keep.txt"), "x"); + + await expect(writeAtomic(target, "body")).rejects.toThrow(); + expect((await readdir(dir)).filter((f) => f.includes(".tmp-"))).toEqual([]); + }); +}); + describe("sh", () => { it("returns trimmed stdout on success", async () => { expect(await sh(["echo", " hello "])).toBe("hello"); diff --git a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts index 153dc89662..3b3435cd4c 100644 --- a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts +++ b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts @@ -26,7 +26,7 @@ * Resolution order: registry match > docDir > vaultDir > found:false. */ -import { rename } from "node:fs/promises"; +import { chmod, rename, rm, stat } from "node:fs/promises"; import { homedir } from "node:os"; import { basename, isAbsolute, join } from "node:path"; @@ -138,10 +138,25 @@ async function loadRegistry(): Promise { // truncated file behind. Both targets are append-only records whose partial // loss is unrecoverable: the registry holds every project's wrap-up target, // and the wrap-up doc's log is the only permanent session history. -async function writeAtomic(path: string, body: string): Promise { +export async function writeAtomic(path: string, body: string): Promise { const tmp = `${path}.tmp-${process.pid}-${Date.now()}`; - await Bun.write(tmp, body); - await rename(tmp, path); + const existed = await Bun.file(path).exists(); + try { + await Bun.write(tmp, body); + if (existed) { + // rename() swaps in a brand-new inode created under the current + // umask, so a private 0600 registry or wrap-up doc would silently + // widen to 0644. Carry the destination's mode over to the temp file. + const { mode } = await stat(path); + await chmod(tmp, mode & 0o777); + } + + await rename(tmp, path); + } catch (err) { + // Never leave the half-written temp file next to the real one. + await rm(tmp, { force: true }); + throw err; + } } async function saveRegistry(reg: Registry): Promise { From f7c10c896a5bd165b79104b04804c025b9326dfa Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 29 Jul 2026 07:17:38 +0200 Subject: [PATCH 8/9] docs(codex): reconcile approval routing in section 1 and exit nonzero on wait timeout --- plugins/genesis-tools/agents/agent-driver.md | 2 +- plugins/genesis-tools/skills/handoff-to-codex/SKILL.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/genesis-tools/agents/agent-driver.md b/plugins/genesis-tools/agents/agent-driver.md index bc92a571bd..61d9c7528d 100644 --- a/plugins/genesis-tools/agents/agent-driver.md +++ b/plugins/genesis-tools/agents/agent-driver.md @@ -15,7 +15,7 @@ Your spawn prompt gives you: `BACKEND` (default `codex`), `NAME`, `CWD`, `BRIEF_ tools agents login --agent-name driver_ ``` -Run it with `run_in_background: true` and follow its **stdout** with `Monitor` (never `2>&1` — stderr is diagnostics and will corrupt the event stream). This is mandatory: it is how the orchestrator steers you and how approvals reach you. +Run it with `run_in_background: true` and follow its **stdout** with `Monitor` (never `2>&1` — stderr is diagnostics and will corrupt the event stream). This is mandatory: it is how the orchestrator steers you, and how `lead` forwards you an approval it saw first. Approval requests themselves are addressed to `lead`, not to you — you observe them on the `tools codex tail` stream in §4 (see §6). ## 2. Check the brief before spawning diff --git a/plugins/genesis-tools/skills/handoff-to-codex/SKILL.md b/plugins/genesis-tools/skills/handoff-to-codex/SKILL.md index f5fb93d22b..9a05f19a27 100644 --- a/plugins/genesis-tools/skills/handoff-to-codex/SKILL.md +++ b/plugins/genesis-tools/skills/handoff-to-codex/SKILL.md @@ -164,7 +164,7 @@ Wait on event types only (`error:` appears in normal red-test output): ```bash SECONDS=0; until rg -q '"type":"turn.completed"|"type":"turn.failed"' /tmp/codex-.log 2>/dev/null || [ $SECONDS -ge 600 ]; do sleep 5; done -rg -q '"type":"turn.completed"|"type":"turn.failed"' /tmp/codex-.log || { echo "TIMEOUT after ${SECONDS}s — turn never terminated"; tail -20 /tmp/codex-.log; } +rg -q '"type":"turn.completed"|"type":"turn.failed"' /tmp/codex-.log || { echo "TIMEOUT after ${SECONDS}s — turn never terminated"; tail -20 /tmp/codex-.log; exit 1; } ``` The re-check after the loop is not optional: the loop also exits on the deadline, and a timed-out run still leaves a stale `-o` file on disk. Reading that file without confirming a terminal event reports a half-finished turn as a result. On timeout, stop and report — do not resume blindly. From 7448809db327f955dc14168fe42271f11b4a049a Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 29 Jul 2026 07:42:33 +0200 Subject: [PATCH 9/9] fix(wrap-up): discard subprocess stdout on non-zero exit, clean up test fixtures --- .../skills/wrap-up/scripts/resolve.test.ts | 46 +++++++++++++++---- .../skills/wrap-up/scripts/resolve.ts | 4 ++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.test.ts b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.test.ts index 6892b943e6..eecc9deb7f 100644 --- a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.test.ts +++ b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from "bun:test"; -import { chmod, mkdtemp, readdir, stat, writeFile } from "node:fs/promises"; +import { afterEach, describe, expect, it } from "bun:test"; +import { chmod, mkdir, mkdtemp, readdir, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { @@ -273,8 +273,20 @@ describe("parseFlags", () => { }); describe("writeAtomic", () => { - it("replaces the destination's contents", async () => { + const fixtures: string[] = []; + + async function tempDir(): Promise { const dir = await mkdtemp(join(tmpdir(), "wrapup-atomic-")); + fixtures.push(dir); + return dir; + } + + afterEach(async () => { + await Promise.all(fixtures.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); + }); + + it("replaces the destination's contents", async () => { + const dir = await tempDir(); const target = join(dir, "doc.md"); await writeFile(target, "old contents"); @@ -283,7 +295,7 @@ describe("writeAtomic", () => { }); it("creates the file when it does not exist yet", async () => { - const dir = await mkdtemp(join(tmpdir(), "wrapup-atomic-")); + const dir = await tempDir(); const target = join(dir, "fresh.md"); await writeAtomic(target, "hello"); @@ -291,7 +303,7 @@ describe("writeAtomic", () => { }); it("preserves a restrictive destination mode instead of widening it", async () => { - const dir = await mkdtemp(join(tmpdir(), "wrapup-atomic-")); + const dir = await tempDir(); const target = join(dir, "private.json"); await writeFile(target, "{}"); await chmod(target, 0o600); @@ -303,7 +315,7 @@ describe("writeAtomic", () => { }); it("leaves no temp file behind", async () => { - const dir = await mkdtemp(join(tmpdir(), "wrapup-atomic-")); + const dir = await tempDir(); const target = join(dir, "doc.md"); await writeAtomic(target, "body"); @@ -311,14 +323,20 @@ describe("writeAtomic", () => { }); it("cleans up the temp file when the rename fails", async () => { - const dir = await mkdtemp(join(tmpdir(), "wrapup-atomic-")); - // A directory as the destination makes rename() fail after the temp - // file has already been written. + const dir = await tempDir(); + // rename() onto a NON-EMPTY directory fails, which is what drives the + // cleanup path after the temp file has already been written. Create the + // directory explicitly rather than leaning on Bun.write's implicit + // parent creation, so the setup states its own intent. const target = join(dir, "adir"); - await Bun.write(join(target, "keep.txt"), "x"); + await mkdir(target); + await writeFile(join(target, "keep.txt"), "x"); await expect(writeAtomic(target, "body")).rejects.toThrow(); expect((await readdir(dir)).filter((f) => f.includes(".tmp-"))).toEqual([]); + // The destination is untouched, which pins that the rejection came from + // the rename onto the directory and not from something incidental. + expect(await readdir(target)).toEqual(["keep.txt"]); }); }); @@ -331,6 +349,14 @@ describe("sh", () => { expect(await sh(["false"])).toBe(""); }); + it("discards stdout when the command exits non-zero", async () => { + // `false` prints nothing, so it cannot tell whether stdout is being + // dropped or was simply empty. A failing command that DOES print is + // the case that matters: git can write to stdout and still fail, and + // passing that through would be read as a real toplevel or branch. + expect(await sh(["sh", "-c", "echo not-a-real-branch; exit 3"])).toBe(""); + }); + it("returns empty string instead of throwing when the binary is missing", async () => { // Bun.spawn throws on ENOENT; gitContext()'s documented no-git fallback // depends on this degrading to "" rather than crashing the command. diff --git a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts index 3b3435cd4c..09a6b5c72e 100644 --- a/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts +++ b/plugins/genesis-tools/skills/wrap-up/scripts/resolve.ts @@ -103,6 +103,10 @@ export async function sh(cmd: string[]): Promise { // non-fatal, but a swallowed failure is indistinguishable from a // legitimately empty result — say which one happened. console.error(`wrap-up: \`${cmd.join(" ")}\` exited ${code}${err.trim() ? `: ${err.trim()}` : ""}`); + // Discard whatever landed on stdout: a failed `git rev-parse` can + // still print, and passing that through would be taken for a real + // toplevel or branch name. + return ""; } return out.trim();