Skip to content

TECH — Workflow efficiency hardening: brief conventions, the A1.W output wrapper, and the clash-free ledger-write protocol

  • Task: ID-92 — Workflow efficiency hardening — corpus-finding guards, session-lifecycle habits, spec right-sizing
  • Subtask: {92.3} TECH
  • Authored: 11/06/2026 (S341)
  • Consumes (contract): PRODUCT.md ({92.2}) — its numbered Behavior invariants (§A1, §A1.W, §A2i, §A3, §B3, §B4, §C1-T) are the one-to-one mapping target below.
  • Reference inputs: RESEARCH.md ({92.1}, esp. §5 reroute record / private-docs-path canonicality and §8 decomposition hint / file-overlap advisory), adoptability-assessment.md, same dir.
  • Right-sizing posture (per PRODUCT “Decomposition note” + {92.9} tiering): ID-92 takes the PRODUCT + TECH + PLAN tier. TECH is warranted primarily for the two genuinely non-obvious surfaces — the §A1.W greenfield CLI and the §LP clash-free ledger-write hardening (S341 addition). Every other slice is a markdown-convention edit and is given a proportionate proposed-change line per invariant, not over-specified prose.

Fix-surface shape (gitnexus orientation — cited, not paraphrased)

Section titled “Fix-surface shape (gitnexus orientation — cited, not paraphrased)”

PRODUCT’s orientation paragraph already establishes (cited verbatim from PRODUCT §Problem, which carries it from RESEARCH §“Code-intelligence orientation”, same index, 07/06/2026):

the ID-92 fix surfaces are overwhelmingly markdown workflow artefacts (.claude/skills/*, .claude/agents/* — out of the code-intel allowlist). The only code-touching surface is lib/workflow-evaluation/token-rollup.ts (patchReportWithRollup / runCli — 1 direct caller, "processes": [], isolated, LOW blast radius); scripts/analyze_claude_session_tokens.py is Python (ast-dataflow does not cover) and is 48.23 scope. The A1 stretch wrapper (§A1.W) is a greenfield CLI surface — no existing symbols match.

Greenfield confirmation for §A1.W (S341, this Subtask, fresh gitnexus_query run against the live knowledge-hub index, 11/06/2026):

gitnexus_query({query: "tool output truncation wrapper git diff stat budget", repo: "knowledge-hub"}) returned no tool-output budgeting/truncation-wrapper execution flow. The two ranked processes were proc_146_post → ExtractStructuredPairs and proc_147_post → ExtractTablePairs (both lib/source-documents/document-diff.ts:extractQAPairs — Q&A pair extraction, unrelated domain). The only “truncate”/“budget” symbols in the definition set are pipeline embedding-input truncation (scripts/cocoindex_pipeline/flow.py:_truncate_embedding_input, embed_content_text) and a test budget helper (test_cocoindex_flow_embedding.py:_budget_in_tokens) — embedding-token budgeting, a different domain. No git diff/git show output wrapper, no truncated: true receipt emitter, no --full escape-command surface exists. A grep sweep of scripts/ and lib/ for truncated.*true|output.budget|--full.*re-emit|escape.command returned only two false-positive files (lib/intelligence/flag-analyser.ts, lib/queue/dispatch.ts — neither an output wrapper). §A1.W is greenfield.

No impact analysis is run on the markdown surfaces (.claude/skills/*, .claude/agents/*) — they are exempt per the code-intel allowlist. The only code-touching surface ID-92 actually edits is the greenfield A1.W CLI (new file) and, IF the §LP programmatic guard is adopted, the existing worktree-pre-commit.sh + its launch-worker.sh install block (both out-of-allowlist shell, LOW blast radius — install path verified below). token-rollup.ts is cited by PRODUCT as the lone adjacent code surface but ID-92 does not edit it (it is 48.23-wiring scope).

Prior art the A1.W contract is grounded in

Section titled “Prior art the A1.W contract is grounded in”

read-turn.sh --full (.claude/skills/session-driver-cmux/scripts/read-turn.sh) is the existing “truncate-with-escape-command” pattern in the repo: it renders a worker’s last turn and truncates tool_result blocks to the first 5 lines by default, with --full un-truncating (see its trunc(text) jq function, lines 114–116, and the --full flag, lines 24–27). The A1.W receipt/escape contract is the generalisation of this pattern from “5-line jq render” to “byte-budgeted git/search output with a machine-visible receipt + a ready-to-run escape command”. The wrapper does NOT reinvent the idea; it formalises the existing truncate-default / --full-recover shape into a budgeted, receipt-bearing CLI.


Proposed changes (one-to-one mapping against PRODUCT invariants)

Section titled “Proposed changes (one-to-one mapping against PRODUCT invariants)”

Each row names the file(s) touched, the edit shape (prose-convention vs new-code vs CLI), and how it satisfies the invariant. “Convention” = markdown prose an agent consumes at dispatch time; “CLI” = the one greenfield code surface; “code-shell” = the §LP guard.

§A1 — Unbounded tool outputs (all convention edits)

Section titled “§A1 — Unbounded tool outputs (all convention edits)”
InvFile(s)ShapeProposed change
A1.1.claude/skills/workflow-orchestration/references/dispatch-primitives.mdconventionAdd a result-size discipline block to the dispatch-brief composition convention: every code-/diff-/search-touching brief must carry “--stat-first then targeted expansion; expect hard size bounds on raw dumps; filter-at-source (paginate, scope, name fields) before a tool emits into context.” States the discipline as a brief-composition requirement, not agent goodwill.
A1.2same (dispatch-primitives.md)conventionName the specific high-risk tools + at-source mitigation, verbatim per A1.2: git diff/show--stat first then path-scoped/head-bounded hunks; mempalace_search → default (no-wing) query + client-side filter (matches the CLAUDE.md MemPalace partial-search gotcha); gitnexus detect_changesgit diff --name-only authoritative scope fallback in worktrees (index “never”); large grep → -l/-c first.
A1.3same (dispatch-primitives.md)conventionAdd the report-to-file-return-path rule for load-bearing artefacts that can exceed the 64K tool-output cap: a sub-agent writes the full report to a file and returns the path + a short summary, never the full body inline. Cites the two observed cap hits on worker handoff reports.
A1.4.claude/agents/task-executor.md; .claude/agents/task-checker.md; .claude/skills/session-driver-cmux/SKILL.md (brief fragments)convention(a) task-executor.md gains an output-size discipline line (it currently has none); (b) task-checker.md’s existing per-commit git show --stat mandate is generalised beyond git-diff to the same result-size discipline; (c) session-driver-cmux brief fragments carry the discipline so sub-orchestrator dispatches inherit it.
A1.5CLAUDE.md (Gotchas → General) OR skill-local — PLAN decidesconventionAdd (or defer) a one-line CLAUDE.md gotcha recording the result-size discipline as a standing convention. TECH leaves the lands-as-gotcha-vs-skill-local choice to PLAN per A1.5; if it lands as a gotcha it goes under Gotchas → General as a single bullet, no new section.
A1.6(no file) — stated inside the A1.1 blockconventionThe A1.1–A1.5 block states explicitly that none of it introduces a programmatic block — they are conventions (discipline); the durable enforcement is the A1.W wrapper, and until it ships discipline is the form. The limitation is named, not hidden.

A1 sequencing: A1.1–A1.5 are the “ship-first” convention sweep; A1.W is sequenced after them (A1.W.5) so the conventions land even if the wrapper slips.

§A1.W — Tool-boundary output wrapper (the one greenfield CLI — full design in the dedicated section below)

Section titled “§A1.W — Tool-boundary output wrapper (the one greenfield CLI — full design in the dedicated section below)”
InvFile(s)ShapeProposed change
A1.W.1scripts/kh-output-budget.ts (new, greenfield)CLIA KH-owned wrapper CLI over the highest-volume output-emitting commands (≥ git diff/git show; grep/search a candidate) that enforces an output budget at the source. Durable form of the A1 doctrine. (Command surface, flags, defaults: §A1.W design below.)
A1.W.2sameCLIOn over-budget output: truncate-with-receipt — machine-visible truncated: true marker, original_length (byte count), and a ready-to-run escape command (--full) that re-emits untruncated. One-step recovery; nothing silently dropped.
A1.W.3sameCLITruncation preserves diagnostically-useful ends: --stat-first for diffs; head+tail (not head-only) for command output where failures appear at the end. Truncated diff still shows file-level summary; truncated log still shows last lines.
A1.W.4sameCLIDegrade-safe: if the underlying command fails the wrapper surfaces the real exit code + stderr (no swallowing — KH no-silent-failure doctrine); if output is under budget it passes through unchanged with no marker. Opt-in via the brief conventions (A1.1) — does not silently replace raw git for un-briefed callers.
A1.W.5(sequencing constraint, consumed by {92.4} PLAN)CLIStretch slice: PLAN sequences it after A1.1–A1.5 so the conventions ship even if the wrapper slips. Scope bounded to the named commands — not a general shell-output interceptor (the gsh “be the interpreter” model is explicitly out of scope per the adoptability §(iii) “no action” disposition).

§A2i — Guard definitions (define-now / wire-later — one writer on one file)

Section titled “§A2i — Guard definitions (define-now / wire-later — one writer on one file)”
InvFile(s)ShapeProposed change
A2i.1.claude/skills/evaluate-workflow/SKILL.mdconventionState the definitions of three guards in prose now, each with thresholds + response protocol, each marked “computed once the ID-48.23 per-role fields ship”: (1) Orchestrator-as-workhorsepeak_context > 400K AND sub:main flat-token ratio < 0.2 → name the orchestrate-don’t-implement anti-pattern, route to the recurring-finding surface; (2) Recurring-issue thrash (A3 flag) — dispatch request count > 3× role median → A3 resolve-at-source loop; (3) Unbounded-output detection (A1 metric) — session tool-results dir bytes > threshold → surface offending tool + brief.
A2i.2same (evaluate-workflow/SKILL.md)conventionAll three definitions land in one edit (single writer on one file) — the A1 detection-metric and the A3 thrash-flag are folded into this same edit, not authored by three separate Subtasks. (Forcing function for PLAN: the A2i Subtask absorbs the A1-metric + A3-flag definitions.)
A2i.3same — wiring is NOT in this file/Subtaskconvention (split)The wiring (reading 48.23 per-role/peak/per-dispatch fields, computing flags) is a later slice, not gated inside ID-92. The ID-48.23 dependency MUST NOT be a Subtask-level dep (sibling-only); the define-now/wire-later split satisfies it. TECH states the marker text shape (“pending 48.23”) so a later wiring Subtask has an anchor.
A2i.4same (evaluate-workflow/SKILL.md)conventionEach definition references the corpus evidence (24/460 >400K-peak; 135/127/139-request thrash; 17.1MB/4.7MB output findings) so a reader understands why each threshold sits where it does.
A2i.5.claude/agents/workflow-evaluator.mdconventionUpdated in lockstep: the evaluator role learns the three flags exist as report columns once wired; the report-column set is named even while values read “pending 48.23”. (Sibling-dep: A2i.5 edits a different file from A2i.1–A2i.4, so it can be the same Subtask or a tight sibling — PLAN’s call; recommend same Subtask, two files, since the column-naming must match the definitions exactly.)

Corpus-path constraint (RESEARCH §5 / cross-task note 2): the A2i edit touches evaluate-workflow/SKILL.md, whose corpus/report paths already point at the private-docs path (${KH_PRIVATE_DOCS_DIR}/...workflow-evaluation/) while the data is mid-relocation (ID-68.28). Treat the private-docs path as canonical — do not re-introduce in-repo docs/workflow-evaluation/ paths.

§A3 — Recurring-issue thrash: resolve-at-source loop (convention edits)

Section titled “§A3 — Recurring-issue thrash: resolve-at-source loop (convention edits)”
InvFile(s)ShapeProposed change
A3.1.claude/skills/evaluate-workflow/SKILL.mdconventionExtend the existing recurring-finding surface (bucket by canonical key, flag count ≥ 3 across distinct sessions) from “surface the pattern” into a four-step resolve-at-source loop: (1) flag → (2) diagnose root cause → (3) fix at the source (skill/brief/gotcha) → (4) verify recurrence drops next sweep. Loop terminates on verified-drop, not first-flag.
A3.2.claude/skills/triage-finding/SKILL.mdconventionAdd a recurrence-aware path: a recurrence-class finding is committed work (per §0 committed-work rule) and routes to the Task List, not the backlog. The decision tree gains the branch “is this a recurrence of a prior flagged finding?”.
A3.3.claude/skills/workflow-orchestration/references/failure-modes.mdconventionGeneralise the existing “3 Checker FAILs → escalate” ceiling into a bounded-retry budget for fix-dispatch loops: a fix-Executor re-dispatch loop has a stated request/iteration ceiling and on hitting it pauses for human decision rather than thrashing — converting an unbounded >3×-median loop into a structural ceiling-then-pause.
A3.4same (failure-modes.md / the A3.1 loop prose)conventionThe loop does not re-litigate findings already marked ignored/won’t-fix unless materially different (no-mistakes round-history discipline): a recurrence flag fires on genuinely-recurring issues, not on re-surfacing a closed decision.
A3.5(split note for PLAN)conventionThe A3 flag itself lives in the §A2i evaluate-workflow edit (one writer); the A3 Subtask defines the loop + routing + budget text. The only sibling-dep between A3 and A2i is if the loop prose references the flag’s metric name — otherwise independent (PLAN expresses this dep at the Subtask level if and only if the reference is made).

Ledger-surface churn constraint (RESEARCH §5 / cross-task note 1): A3 edits triage-finding + workflow-orchestration/references/failure-modes.md — both embed bun scripts/ledger-cli.ts invocations elsewhere. ID-90’s P3 retirement ({90.22}/{90.23}) re-points these. Do not add NEW hard-coded ledger-cli.ts invocation examples in the A3 edits, or flag them for the {90.23} docs pass.

§B3 — Roadmap context at session start (convention edits)

Section titled “§B3 — Roadmap context at session start (convention edits)”
InvFile(s)ShapeProposed change
B3.1.claude/skills/start-session/SKILL.md (Step 2)conventionStep 2 reads the roadmap theme that owns the session’s active Task, resolved via the active Task’s capability_theme field — a single owning-theme record (~1–2KB of the 23KB/12-theme roadmap). (OQ-1: owning-theme only; all-titles and full-roadmap recorded as rejected.)
B3.2sameconventionThe read is strategic context, not a history dump: surfaces the owning theme’s title + current intent (“why this Task matters”), not theme history / prior-session notes / unrelated themes.
B3.3sameconventionWhen the active Task has no capability_theme (unset/operational), the step degrades to a no-op with an explicit note (“no owning theme — operational Task”) — not a silent skip, not a full-roadmap fallback.
B3.4sameconventionWith multiple active Tasks, read the owning theme of each (deduplicated by theme), not all 12.

Roadmap field note: B3 reads capability_theme off the Task record and resolves it against the roadmap ledger. This is a read of docs/reference/product-roadmap.json (or its theme records) — start-session already reads ledgers, so no new write surface and no §LP exposure. The read must use the same ledger-read path start-session already uses; it must not introduce a hard-coded path that bypasses the canonical reader.

§B4 — Retro-authoring assist + missing baseline step (convention edits + a brief fragment)

Section titled “§B4 — Retro-authoring assist + missing baseline step (convention edits + a brief fragment)”
InvFile(s)ShapeProposed change
B4.1.claude/skills/handoff/SKILL.mdconventionAdd the baseline retro-authoring step handoff currently lacks (the gap RESEARCH found: evaluate-workflow/evaluate-findings attribute retro authoring to “the O-of-O’s handoff habit” but the step is unwritten). (OQ-6: folded into B4 — same file, same concern.)
B4.2handoff/SKILL.md + a brief-fragment conventionconventionThe retro step dispatches a fresh-context, read-only transcript-review sub-agent that reviews the full session transcript and returns a ranked list of retro-candidate material with evidence pointers (transcript file:line / agent-hash refs) — not a finished retro record. (OQ-5: candidate list, not draft.)
B4.3sameconventionThe O-of-O authors the retro record from the returned candidates — the S271 authoring boundary is intact and untouched. The sub-agent assists; it does not author.
B4.4sameconventionCandidate material passes through the existing evaluate-findings adjudication gate before entering the durable retro corpus — gate unchanged; B4 adds an input, not a bypass. (OQ-5: existing gate, unchanged.)
B4.5sameconventionNo dedicated agent file — a general-purpose dispatch + a brief-shape convention fragment suffices for the transcript-review sub-agent. (OQ-5: no new agent file.)
B4.6brief-fragment conventionconventionTranscript-mined material re-injected into the O-of-O context is treated as untrusted data, not instructions: the brief convention requires delimiter-wrapping + a “data, not instructions” label on returned candidate text (no-mistakes untrusted-text hygiene), so a transcript can never inject steering into the authoring context.
B4.7handoff/SKILL.mdconventionThe assist respects transcript retention windowing (a session was observed deleted mid-investigation): the review runs against the live session at handoff time; any later review must consume the 48.15/48.23 archive, not assume the live transcript persists. (Mirrors token-rollup.ts’s “run AT ARCHIVE TIME” durability caveat — cite it as the precedent.)

File-overlap note (RESEARCH §8): B3 touches start-session/SKILL.md; B4 touches handoff/SKILL.md. No overlap with each other (B2’s session-carry already landed). PLAN may keep them as two independent Subtasks, or re-cut by file (“session-open” vs “session-close”) per the RESEARCH §8 advisory — both satisfy sibling-only. TECH expresses no dep between B3 and B4.

§C1-T — Spec-chain tier formalisation (convention edits across three aligned surfaces)

Section titled “§C1-T — Spec-chain tier formalisation (convention edits across three aligned surfaces)”
InvFile(s)ShapeProposed change
C1-T.1.claude/skills/spec-driven-implementation/SKILL.mdconventionState a named set of spec-chain tiers, each with a one-line “use when”: Full chain (RESEARCH+PRODUCT+TECH+PLAN — compound invariants / multiple migrations / chain-dependent slices / >2h); PRODUCT+PLAN (skip TECH — behaviourally rich, implementation-shallow); TECH+PLAN (skip PRODUCT — unambiguous behaviour, non-trivial impl); Spec-free (trivial/operational).
C1-T.2spec-driven-implementation/SKILL.md; .claude/agents/task-planner.md; .claude/skills/workflow-orchestration/SKILL.md (lifecycle diagram) + references/lifecycle-detail.mdconventionState identically across all three surfaces (the surfaces {92.9} aligned): Orchestrator decides the tier at Task open; Planner may recommend an upgrade mid-{N.1} if RESEARCH surfaces compound invariants. The decision rule + who-decides are the same words in each file.
C1-T.3spec-driven-implementation/SKILL.md (+ the lifecycle surfaces that name where the tier is recorded)conventionThe chosen tier is recorded in the Task’s status_note as a one-line marker (e.g. spec tier: PRODUCT+PLAN) so any role sees which artefacts to expect without inferring from which files exist. (OQ-7: status_note, not a spec-dir marker file — keeps the choice in the ledger; spec-dir-marker is the recorded rejected alternative. PO to confirm recording location — see OQ below.)
C1-T.4same surfacesconventionThe tiers do not erode the heavy path: choosing a light tier is a recorded decision with a stated “use when”; an under-specified Task that later reveals compound invariants escalates to a heavier tier (recorded as a status_note update), never silently proceeds under-specified.

write-product-spec exclusion (RESEARCH §8): the C1-T file set is the three lifecycle surfaces above. write-product-spec is droppable from the set — the scope/right-size rule lives upstream in spec-driven-implementation, not in the per-artefact authoring skill. PLAN should not add write-product-spec to the C1-T edit set.

status_note budget (HARD gate): the spec tier: … marker is appended to the Task status_note, which is ≤300 chars HARD-enforced by the budget gate (invariant 57, task-view patch-server). The marker is a single short clause — authors must keep status_note within budget when adding it; the tier marker is designed to be a few chars.


§A1.W design — the tool-boundary output wrapper (greenfield CLI)

Section titled “§A1.W design — the tool-boundary output wrapper (greenfield CLI)”

This is the one slice that genuinely benefits from a TECH pass. Grounded in the read-turn.sh --full truncate-default/--full-recover prior art.

  • New file: scripts/kh-output-budget.ts (TypeScript, bun-invokable — matches the KH script convention bun scripts/<name>.ts; not a .sh so the receipt JSON + arg parsing are first-class).
  • Invocation shape: bun scripts/kh-output-budget.ts [--budget <bytes>] [--full] [--mode diff|log|generic] -- <command> [args...] The wrapper spawns the wrapped command, captures stdout/stderr/exit-code, and applies the budget to stdout only (stderr always passes through verbatim — see degrade-safe).
  • --full is the escape command: it sets the budget to ∞ and re-emits the wrapped command’s full stdout. The receipt (below) embeds the exact --full re-invocation so the consumer can copy-paste it.
  • Default budget: a single named constant (recommend 32 KB, well under the 64K tool-output cap A1.3 cites, leaving headroom for the receipt envelope). PLAN/Executor may tune; the value is a single const so it is one-line auditable.

When stdout byte-length > budget, the wrapper emits, in order:

  1. The budgeted content (per §truncation-shape below).
  2. A machine-visible receipt block — a single line or fenced JSON the agent can parse:
    --- kh-output-budget: truncated ---
    truncated: true
    original_length: <bytes>
    shown: <bytes>
    escape: bun scripts/kh-output-budget.ts --full --mode <mode> -- <command> [args...]
    truncated: true is the machine-visible marker; original_length is the byte count; escape is the ready-to-run un-truncating re-invocation. Nothing is silently dropped — the receipt always carries the recovery path.

Truncation shape — preserve diagnostically-useful ends (A1.W.3)

Section titled “Truncation shape — preserve diagnostically-useful ends (A1.W.3)”
  • --mode diff (default for git diff/git show): emit git’s own --stat summary first (run the wrapped command with --stat injected, or parse the leading stat block if the caller already passed --stat), then as many leading hunk bytes as fit the budget. A truncated diff always shows the file-level summary.
  • --mode log / --mode generic (commands where failures surface at the end): head + tail split of the budget (e.g. 60% head / 40% tail) with an elision marker between, so the last lines (where errors live) are never dropped. Head-only is explicitly wrong here.
  • --mode defaulting: if --mode is omitted, infer diff when the wrapped command is git diff/git show, else generic. The inference is a small allowlist, not a general parser.
  • Underlying command fails: the wrapper surfaces the real exit code and stderr — it exits with the wrapped command’s exit code and passes stderr through verbatim (never swallows). This is the KH no-silent-Supabase-failure doctrine applied to a CLI: an error must never be hidden behind a truncation envelope.
  • Output under budget: pass through unchanged, no receipt, no marker — the wrapper is invisible for small outputs.
  • Opt-in only (A1.W.4 + A1.W.5): the wrapper is invoked only where the brief conventions (A1.1) name it. It does not alias or shadow raw git for un-briefed callers — there is no global shell interception (that is the gsh “be the interpreter” model, out of scope per adoptability §(iii)). Scope is bounded to the named commands (git diff/git show mandatory; grep/search candidate).

Test strategy for the wrapper (for {92.4} PLAN / the Executor)

Section titled “Test strategy for the wrapper (for {92.4} PLAN / the Executor)”

Behaviour-not-implementation (test-philosophy.md): (1) over-budget diff → receipt present, truncated: true, --stat summary retained, escape command re-emits full; (2) under-budget → pass-through, no marker; (3) wrapped command non-zero exit → real exit code + stderr surfaced, no swallow; (4) --mode log over-budget → last lines retained (tail not dropped). Run via bun run test (not bun test). Tests assert the contract (receipt shape, recovery, degrade), not internal byte arithmetic.


§LP — Clash-free ledger-write protocol (first-class S341 hardening)

Section titled “§LP — Clash-free ledger-write protocol (first-class S341 hardening)”

Provenance flag: this section is a net-new S341 hardening input, NOT derived from a numbered PRODUCT invariant. It is promoted here from an interim skill note into a specified, durable hardening. It belongs to the same family as PRODUCT’s carryover-discipline / orchestrator-as-workhorse themes (B1/B2/B5 carryover conventions

  • the A2i.1 orchestrate-don’t-implement guard): the orchestrator owns ledger writes, workers never do. See the OQ below for the back-port recommendation.

Orchestrator-owns-ALL-ledger-writes. Workers — sub-orchestrators and executors dispatched into their own worktrees (via session-driver-cmux or Agent isolation: "worktree") — MUST NOT mutate or commit the ledger JSONs (docs/reference/{task-list,product-backlog,product-roadmap,product-retros}.json) or their docs/reference/{tasks,backlog}/*.md mirrors in their worktree branch. Workers instead RETURN ledger-write intents; the Orchestrator applies every write via bun scripts/ledger-cli.ts <verb> against the MAIN checkout.

Mechanism (why an in-branch write is unsafe)

Section titled “Mechanism (why an in-branch write is unsafe)”

The ID-90 ledger daemon serialises writes behind one mutex per ledger directory — it de-conflicts only writers that all target the same main-checkout ledger directory. An in-branch chore(ledger) commit in a worker worktree bypasses the mutex entirely: the worktree’s docs/reference/ is a different working tree, the daemon never sees the write, and id-allocation runs unserialised. Routing every write through the single main-checkout mutex makes collisions structurally impossible, not merely discouraged.

bl-287/bl-288 3-way backlog-id collision (prior wave): three parallel sub-orchestrators each committed chore(ledger) deltas in their own branches, so bl-287 and bl-288 were each allocated to THREE different items. Recovery cost manual de-dup, re-ID (→ bl-289/290/291), and journal replay. This is the exact clash the single mutex exists to prevent and an in-branch commit defeats.

Current interim state (TECH formalises BEYOND these)

Section titled “Current interim state (TECH formalises BEYOND these)”

The protocol exists today only as two interim skill notes:

  • .claude/skills/session-driver-cmux/SKILL.md:186–202 — “Ledger writes — clash-free protocol”.
  • .claude/skills/workflow-orchestration/SKILL.md:419–429 — Ledger field-discipline, “Ledger writes are MAIN-checkout-only — never in a worker branch.”

Both are prose-only and live in two skill files; neither is enforced, and the executor surface (task-executor.md) carries no statement of the protocol at all — yet executors in worktrees are exactly the class that committed the colliding deltas.

Proposed change — (a) durable prose target surfaces

Section titled “Proposed change — (a) durable prose target surfaces”

Consolidate + promote the protocol to three authoritative prose surfaces (the two interim notes stay as cross-references pointing at the canonical statement; do not delete them, but de-duplicate to a single canonical body):

  1. .claude/agents/task-executor.md (NEW statement — the gap): add the protocol to the executor operating instructions. Executors are the worktree class that caused the collision; the protocol must be stated where the executor reads it. Canonical body lives here or in workflow-orchestration — recommend stating the worker obligation (“never write the ledger in-branch; return intents”) in task-executor.md and the orchestrator obligation (“apply every intent via ledger-cli on main”) in workflow-orchestration, each cross-referencing the other.
  2. .claude/skills/workflow-orchestration/SKILL.md (consolidate the existing 419–429 note into the canonical orchestrator-side statement; keep it as the home of the field-discipline table).
  3. .claude/skills/session-driver-cmux/SKILL.md (the existing 186–202 note becomes a cross-reference to the canonical body — session-driver is where worker worktrees are created, so a pointer here is correct, but the full body need not be duplicated).
  4. CLAUDE.md gotcha (General): add a one-line bullet — “Workers never write the ledger in-branch: sub-orchestrators + worktree executors RETURN ledger-write intents; the Orchestrator applies every write via bun scripts/ledger-cli.ts on the MAIN checkout (the ID-90 daemon mutex is per main-checkout dir; an in-branch chore(ledger) commit bypasses it — bl-287/288 3-way collision).” This raises the protocol to standing-convention visibility alongside the existing worktree-isolation gotchas. (Respects the RESEARCH §5 ledger-churn note: this gotcha references ledger-cli.ts as the façade name, which is unavoidable and stable; it adds no new invocation example that {90.23} would need to re-point.)

Proposed change — (b) programmatic enforcement: the worktree-pre-commit.sh guard

Section titled “Proposed change — (b) programmatic enforcement: the worktree-pre-commit.sh guard”

DECISION: YES — propose the programmatic guard. Rationale: the A1 doctrine is “enforce at source, never via agent discipline”; the same doctrine applies here. Prose alone already failed (the two interim notes existed and the collision still happened). A pre-commit hook in the worker worktree makes the protocol enforced-at-source, exactly mirroring A1.W’s relationship to A1.1–A1.5.

Insertion point (verified): worktree-pre-commit.sh (.claude/skills/session-driver-cmux/scripts/worktree-pre-commit.sh) already exists (ID-48.12, currently prettier-only) and is already copied into every worker worktree’s hooks dir by launch-worker.sh (verified, lines 349–374: cp "$HOOK_SOURCE" "${WORKTREE_HOOKS_DIR}/pre-commit"; chmod +x). The guard is a small addition to an existing, already-installed hook — no new install plumbing, LOW blast radius.

Proposed guard behaviour (prepend, before the prettier step, so a ledger violation aborts the commit early):

Terminal window
# --- Block in-branch ledger writes (ID-92 §LP clash-free protocol) ---
# Workers MUST NOT commit ledger JSONs or their .md mirrors in a worktree branch
# (the ID-90 daemon mutex is per main-checkout dir; an in-branch chore(ledger)
# commit bypasses it — bl-287/288 3-way collision). Return ledger-write intents
# to the Orchestrator instead.
LEDGER_STAGED=$(git diff --cached --name-only --diff-filter=ACMR \
| grep -E '^docs/reference/(task-list|product-backlog|product-roadmap|product-retros)\.json$|^docs/reference/(tasks|backlog)/.*\.md$' \
|| true)
if [ -n "$LEDGER_STAGED" ]; then
echo "pre-commit: BLOCKED — worker worktrees must not commit ledger files:" >&2
echo "$LEDGER_STAGED" >&2
echo "Return ledger-write intents to the Orchestrator (ID-92 §LP)." >&2
exit 1
fi
  • Scope: matches the four ledger JSONs + the tasks//backlog/ .md mirrors exactly (same path set the prose names). Does not touch docs/reference/*.md reference docs that are not mirrors.
  • Degrade-safe: pure read of the staged index (git diff --cached --name-only); no network, no daemon dependency. If git is unavailable the existing rev-parse --show-toplevel guard at the top of the hook already exit 0s.
  • Belt-and-braces, not the only line: the prose surfaces (a) remain the primary statement; the hook is the enforcement-at-source backstop for the case where a worker ignores the prose (which is what happened).
  • Caveat the Executor must know: the hook lives in the worktree’s own .git/worktrees/<name>/hooks/pre-commit (per-worktree, not the shared .git/hooks), and is not tracked by the worktree’s git history — it is installed fresh at worktree creation by launch-worker.sh. Editing worktree-pre-commit.sh only affects future worktrees; existing live worktrees keep their copy until re-launched. PLAN should note this so the rollout expectation is correct.

Proposed change — (c) the worker return-intent contract shape

Section titled “Proposed change — (c) the worker return-intent contract shape”

Workers return ledger-write intents in their final_report.yaml under a ledger_intents block (the existing final-report channel the Orchestrator already reads). Shape:

ledger_intents:
- verb: set-status # ledger-cli verb the Orchestrator will run
target: "92.3" # Subtask/Task/backlog id
args:
status: done
journal: "TECH authored; mapping verified one-to-one"
- verb: add-backlog
target: null # id is allocated by the Orchestrator under the mutex
args:
title: ""
description: "" # ≤250 chars (budget gate)
...
  • Why final_report.yaml: it is the existing worker→orchestrator return channel (watch-fleet.sh already signals on final_report.*); no new transport.
  • id allocation is Orchestrator-side: target: null for create verbs — the Orchestrator allocates the id under the single mutex, which is the whole point (the worker never picks an id, so two workers can never pick the same one).
  • Budget-aware: description/testStrategy in an intent must already be within the ≤250/≤300-char budget gate, since the Orchestrator’s ledger-cli write will be rejected server-side otherwise (invariant 57). The worker authoring the intent owns getting it within budget first-pass (S281 lesson).

  1. A1.W after A1.1–A1.5 (A1.W.5): the brief-convention sweep ships first; the wrapper is the stretch slice sequenced after, so conventions land even if the wrapper slips.
  2. A2i is one writer on evaluate-workflow/SKILL.md (A2i.2): the A1-metric + A3-flag definitions fold into the single A2i edit; the A3 Subtask owns only the loop/routing/ budget text. The single sibling-dep (A3 → A2i) exists only if the loop prose names the flag metric.
  3. B3 ⟂ B4 (RESEARCH §8): disjoint files (start-session vs handoff); no dep between them. PLAN may re-cut by file or keep concern-cut.
  4. §LP enforcement rollout caveat: editing worktree-pre-commit.sh affects future worktrees only (per-worktree hook copy at launch); live worktrees keep their installed copy. Sequence the §LP guard edit before the next wave’s worker launches so the wave inherits it.
  5. §LP prose ⟂ guard: the prose surfaces (a) and the hook (b) are independent edits and can be separate Subtasks (prose is convention-only; the hook is code-shell). The final_report.yaml contract (c) is convention attached to the prose surfaces.

Constraints carried from RESEARCH §5 (must not regress)

Section titled “Constraints carried from RESEARCH §5 (must not regress)”
  • ID-90 ledger-surface churn (note 1): A3 (triage-finding, failure-modes.md) and any §LP prose must not add NEW hard-coded ledger-cli.ts invocation examples beyond the unavoidable façade-name reference; {90.22}/{90.23} re-point these. The §LP CLAUDE.md gotcha names ledger-cli.ts as the façade (stable), not a new invocation recipe.
  • ID-68.28 corpus relocation (note 2): A2i edits to evaluate-workflow/SKILL.md must treat the private-docs path (${KH_PRIVATE_DOCS_DIR}/...workflow-evaluation/) as canonical and must not re-introduce in-repo docs/workflow-evaluation/ paths — the relocation is half-done and the skill already points at the private path.

KH quality bars (Executors will be held to these)

Section titled “KH quality bars (Executors will be held to these)”
  • UK English throughout (colour/organisation/behaviour; DD/MM/YYYY).
  • No silent failures: the A1.W wrapper surfaces real exit code + stderr (no swallow); the §LP hook blocks loudly with a diagnostic, never silently.
  • bun run test (not bun test) for the A1.W wrapper tests; behaviour-not-implementation per test-philosophy.md.
  • No barrel re-exports / direct imports if the A1.W CLI factors any shared helper (recommend it stays self-contained in scripts/kh-output-budget.ts).
  • Budget gates (invariant 57): any status_note tier marker (C1-T.3) ≤300 chars; any ledger_intents description/testStrategy within ≤250/≤300 first-pass.
OQTopicTECH disposition / recommendation
OQ-T1§LP back-port to PRODUCTRecommend YES — add a numbered carryover-discipline invariant (e.g. a §B6 “ledger-write ownership” or fold into the existing carryover theme) so the §LP hardening has a PRODUCT contract the Checker can verify against, not just a TECH section. The protocol is behaviour with a testable acceptance criterion (worker commit of a ledger file is blocked; intent round-trips through final_report.yaml). Planner recommends; does not edit PRODUCT. Orchestrator/PO call.
OQ-T2C1-T.3 recording locationCarried from PRODUCT OQ-7 unresolved: status_note (chosen) vs spec-dir marker (rejected). PO to confirm. TECH proceeds on status_note per OQ-7’s resolution; flagged so the Checker knows it is PO-pending, not Planner-invented.