feat(conductor): one code owner for the ledger item entry (#5912) - #5970
Conversation
|
Pre-push local review dispositions (GPT lane, judged commit 0042ed3; fixes amended into 1e2456f):
|
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All claims verified: First-Principles-Verdict: CONCERNS
What this change shipsIntent: stop the conductor's compaction-surviving ledger entry from being silently lost or corrupted, by giving its format one code owner — a FIX (two defects observed on PR #5652) delivered as an addition.
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 16ec8b8 |
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound codec, but enforcement is still prose: nothing mechanical stops a drifting conductor from hand-writing entries, and the ledger's silent clamp remains untouched. Watch
Suggestions
[DESIGN-REVIEWED] 16ec8b8 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
The goal-conductor's only compaction-surviving state is the per-work-item entry in the session ledger's artifacts map, and its format existed only as prose plus a worked example in SKILL.md. Add scripts/ledger_entry.py as the format's single owner (encode / decode / validate / rotate, mirroring the ledger's real bounds), rewrite SKILL.md to route through the codec, and update the conductor prompt to name both bundled scripts. Closes #5912
1e2456f to
16ec8b8
Compare
|
|
Dispositions for Design Review CONCERNS (verdict on 16ec8b8):
|
|
Dispositions for First Principles CONCERNS (verdict on 16ec8b8):
|
…it silently `_coerce_state` applies six caps, and each one throws caller data away: `_clamp` shortens any text past `_MAX_TEXT`, `tried` and `events` keep only a bounded tail, and `artifacts` stops accepting entries at `_MAX_ARTIFACTS` — in insertion order, so which ones survive is arbitrary from the writer's side. None of it was reported. `record` reads, mutates and writes the coerced record straight back, so the first overflow is persisted and the data is gone for good. The same reader already WARNs when it discards a whole file over `_MAX_STATE_BYTES`. A partial discard is the same loss in a smaller quantity, so report it the same way: one WARNING per read naming each bound hit and how many entries it cost. A wrong-typed field reset to its default is the documented coercion contract, not a discard, and is deliberately not counted. The line is self-limiting — the write-back leaves the next read nothing to trim. Residual named by the reviewer on merged kirodotdev#5970. Spec updated in the same commit per AGENTS.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…it silently `_coerce_state` applies six caps, and each one throws caller data away: `_clamp` shortens any text past `_MAX_TEXT`, `tried` and `events` keep only a bounded tail, and `artifacts` stops accepting entries at `_MAX_ARTIFACTS` — in insertion order, so which ones survive is arbitrary from the writer's side. None of it was reported. `record` reads, mutates and writes the coerced record straight back, so the first overflow is persisted and the data is gone for good. The same reader already WARNs when it discards a whole file over `_MAX_STATE_BYTES`. A partial discard is the same loss in a smaller quantity, so report it the same way: one WARNING per read naming each bound hit and how many entries it cost. A wrong-typed field reset to its default is the documented coercion contract, not a discard, and is deliberately not counted. The line is self-limiting — the write-back leaves the next read nothing to trim. Residual named by the reviewer on merged kirodotdev#5970. Spec updated in the same commit per AGENTS.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Problem / Motivation
The goal-conductor's only compaction-surviving state is the per-work-item entry it writes into the session ledger's
artifactsmap — and that entry format had no code owner. SKILL.md specified it as prose plus a worked example, and the model re-derived the encoding every patrol cycle. Review of PR #5652 caught two real defects this produced: an acceptance spec that lived only in model context (lost on compaction), and an entry documented as a nested JSON object, whichdashboard/handlers/session_ledger.pyrejects withartifacts_not_string_map— so nothing persisted, silently.Why it matters
A conductor that loses an item's acceptance spec cannot evaluate, steer, or resume that item after compaction — the goal silently degrades into orphaned child sessions. Both observed failure modes are silent: the ledger truncates an oversized value (corrupting the stored JSON) rather than rejecting it, and a non-string value never persists at all. A format only enforced by prose will keep regressing.
What changed (motivation → approach → change)
The prose example was the specification, and models drift from prose — so the format needs one code owner the conductor calls instead of re-deriving bytes. Added as a sibling script (not an
accept_eval.pymode) to keep the evaluator single-purpose:src/kiro_crew/builtin_skills/goal-conductor/scripts/ledger_entry.py— stdlib-only CLI codec, JSON on stdin/stdout, structured errors (never a crash), exit 2 only for malformed invocation:encode: fields → the single-line JSON string the ledger accepts; rejects unknown fields, an unknownstatus(a synonym like"done"would silently read as active and never rotate), and a value the ledger would truncate.decode: stored value → structured fields +terminal/completeflags; structured error on malformed input, includingRecursionErroron pathologically nested JSON.validate: enforces the ledger's real bounds before a write (value ≤ 2000, key ≤ 128, ≤ 32 entries, value-is-a-string) — mirrored fromsession_ledger.pyand drift-guarded by test against the ledger's own constants.rotate: collapses terminal entries to a one-line outcome, drops oldest-terminal-first only when over the entry cap, never drops an active item; docstring pins the whole-map write-back contract (the ledger merges, so a partial write-back would invert age-out order).status: runningwith afailscounter (the three-strikes stop's durable state);failmeans finally given up. This prevents rotation from destroying the spec/session/cursor an item still being retried needs.agent.py— the conductor system prompt and installer docstring now name both bundled scripts as the shell grant's purpose.test/test_conductor_agent.py— the doc-ratchet test now pins the codec-owned contract instead of the removed worked example, and_load_evaluatoruses the sharedload_skill_scripthelper (the hand-rolledexec_modulewas leaving__pycache__residue beside the checked-in script).Tests
test/test_conductor_ledger_entry.py(46 tests):session_ledger._MAX_TEXT/_MAX_ARTIFACTS(and the key clamp literal), so a ledger-side change fails this test instead of silently drifting.round, unknownstatus, and values the ledger would truncate.artifacts_not_string_mapshape), non-JSON, non-object, missing status, deep-nestingRecursionError; unknown fields preserved for forward compatibility.cap_exceeded_all_active), opaque/malformed entries preserved, and arunning+failsretrying item surviving rotation whole.session_ledger.recordwrite and reads back byte-identical.Manual verification
N/A — unit coverage sufficient: the codec is a pure JSON transform with a subprocess CLI contract, both exercised directly; the end-to-end test writes through the real ledger store.
Related Issues
Fixes #5912
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)Why no screenshot: backend skill script + SKILL.md prose only; nothing renders in the browser.