TECH — Workflow efficiency hardening: brief conventions, the A1.W output wrapper, and the clash-free ledger-write protocol
TECH — Workflow efficiency hardening
Section titled “TECH — Workflow efficiency hardening”- 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.
Context
Section titled “Context”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 islib/workflow-evaluation/token-rollup.ts(patchReportWithRollup/runCli— 1 direct caller,"processes": [], isolated, LOW blast radius);scripts/analyze_claude_session_tokens.pyis 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)”| Inv | File(s) | Shape | Proposed change |
|---|---|---|---|
| A1.1 | .claude/skills/workflow-orchestration/references/dispatch-primitives.md | convention | Add 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.2 | same (dispatch-primitives.md) | convention | Name 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_changes → git diff --name-only authoritative scope fallback in worktrees (index “never”); large grep → -l/-c first. |
| A1.3 | same (dispatch-primitives.md) | convention | Add 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.5 | CLAUDE.md (Gotchas → General) OR skill-local — PLAN decides | convention | Add (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 block | convention | The 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)”| Inv | File(s) | Shape | Proposed change |
|---|---|---|---|
| A1.W.1 | scripts/kh-output-budget.ts (new, greenfield) | CLI | A 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.2 | same | CLI | On 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.3 | same | CLI | Truncation 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.4 | same | CLI | Degrade-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) | CLI | Stretch 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)”| Inv | File(s) | Shape | Proposed change |
|---|---|---|---|
| A2i.1 | .claude/skills/evaluate-workflow/SKILL.md | convention | State 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-workhorse — peak_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.2 | same (evaluate-workflow/SKILL.md) | convention | All 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.3 | same — wiring is NOT in this file/Subtask | convention (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.4 | same (evaluate-workflow/SKILL.md) | convention | Each 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.md | convention | Updated 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)”| Inv | File(s) | Shape | Proposed change |
|---|---|---|---|
| A3.1 | .claude/skills/evaluate-workflow/SKILL.md | convention | Extend 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.md | convention | Add 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.md | convention | Generalise 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.4 | same (failure-modes.md / the A3.1 loop prose) | convention | The 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) | convention | The 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)”| Inv | File(s) | Shape | Proposed change |
|---|---|---|---|
| B3.1 | .claude/skills/start-session/SKILL.md (Step 2) | convention | Step 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.2 | same | convention | The 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.3 | same | convention | When 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.4 | same | convention | With 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)”| Inv | File(s) | Shape | Proposed change |
|---|---|---|---|
| B4.1 | .claude/skills/handoff/SKILL.md | convention | Add 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.2 | handoff/SKILL.md + a brief-fragment convention | convention | The 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.3 | same | convention | The 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.4 | same | convention | Candidate 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.5 | same | convention | No 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.6 | brief-fragment convention | convention | Transcript-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.7 | handoff/SKILL.md | convention | The 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)”| Inv | File(s) | Shape | Proposed change |
|---|---|---|---|
| C1-T.1 | .claude/skills/spec-driven-implementation/SKILL.md | convention | State 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.2 | spec-driven-implementation/SKILL.md; .claude/agents/task-planner.md; .claude/skills/workflow-orchestration/SKILL.md (lifecycle diagram) + references/lifecycle-detail.md | convention | State 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.3 | spec-driven-implementation/SKILL.md (+ the lifecycle surfaces that name where the tier is recorded) | convention | The 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.4 | same surfaces | convention | The 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.
Command surface
Section titled “Command surface”- New file:
scripts/kh-output-budget.ts(TypeScript,bun-invokable — matches the KH script conventionbun scripts/<name>.ts; not a.shso 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). --fullis the escape command: it sets the budget to ∞ and re-emits the wrapped command’s full stdout. The receipt (below) embeds the exact--fullre-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
constso it is one-line auditable.
Output-budget + receipt contract (A1.W.2)
Section titled “Output-budget + receipt contract (A1.W.2)”When stdout byte-length > budget, the wrapper emits, in order:
- The budgeted content (per §truncation-shape below).
- A machine-visible receipt block — a single line or fenced JSON the agent can parse:
--- kh-output-budget: truncated ---truncated: trueoriginal_length: <bytes>shown: <bytes>escape: bun scripts/kh-output-budget.ts --full --mode <mode> -- <command> [args...]
truncated: trueis the machine-visible marker;original_lengthis the byte count;escapeis 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 forgit diff/git show): emitgit’s own--statsummary first (run the wrapped command with--statinjected, 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.--modedefaulting: if--modeis omitted, inferdiffwhen the wrapped command isgit diff/git show, elsegeneric. The inference is a small allowlist, not a general parser.
Degrade-safe behaviour (A1.W.4)
Section titled “Degrade-safe behaviour (A1.W.4)”- 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
gitfor 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 showmandatory;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.
The protocol (durable statement)
Section titled “The protocol (durable statement)”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.
Evidence — the failure this prevents
Section titled “Evidence — the failure this prevents”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):
.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 inworkflow-orchestration— recommend stating the worker obligation (“never write the ledger in-branch; return intents”) intask-executor.mdand the orchestrator obligation (“apply every intent via ledger-cli on main”) inworkflow-orchestration, each cross-referencing the other..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)..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).CLAUDE.mdgotcha (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 viabun scripts/ledger-cli.tson the MAIN checkout (the ID-90 daemon mutex is per main-checkout dir; an in-branchchore(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 referencesledger-cli.tsas 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):
# --- 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 1fi- Scope: matches the four ledger JSONs + the
tasks//backlog/.mdmirrors exactly (same path set the prose names). Does not touchdocs/reference/*.mdreference docs that are not mirrors. - Degrade-safe: pure read of the staged index (
git diff --cached --name-only); no network, no daemon dependency. Ifgitis unavailable the existingrev-parse --show-toplevelguard at the top of the hook alreadyexit 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 bylaunch-worker.sh. Editingworktree-pre-commit.shonly 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.shalready signals onfinal_report.*); no new transport. - id allocation is Orchestrator-side:
target: nullfor 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/testStrategyin an intent must already be within the ≤250/≤300-char budget gate, since the Orchestrator’sledger-cliwrite will be rejected server-side otherwise (invariant 57). The worker authoring the intent owns getting it within budget first-pass (S281 lesson).
Migration / sequencing notes
Section titled “Migration / sequencing notes”- 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.
- 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. - B3 ⟂ B4 (RESEARCH §8): disjoint files (
start-sessionvshandoff); no dep between them. PLAN may re-cut by file or keep concern-cut. - §LP enforcement rollout caveat: editing
worktree-pre-commit.shaffects 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. - §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.yamlcontract (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-codedledger-cli.tsinvocation examples beyond the unavoidable façade-name reference; {90.22}/{90.23} re-point these. The §LP CLAUDE.md gotcha namesledger-cli.tsas the façade (stable), not a new invocation recipe. - ID-68.28 corpus relocation (note 2): A2i edits to
evaluate-workflow/SKILL.mdmust treat the private-docs path (${KH_PRIVATE_DOCS_DIR}/...workflow-evaluation/) as canonical and must not re-introduce in-repodocs/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(notbun test) for the A1.W wrapper tests; behaviour-not-implementation pertest-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_notetier marker (C1-T.3) ≤300 chars; anyledger_intentsdescription/testStrategywithin ≤250/≤300 first-pass.
Open questions
Section titled “Open questions”| OQ | Topic | TECH disposition / recommendation |
|---|---|---|
| OQ-T1 | §LP back-port to PRODUCT | Recommend 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-T2 | C1-T.3 recording location | Carried 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. |