feat(conductor): isolate the work-ledger flow in a kirocrew-ledger-conductor agent - #9277
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of All the diff hunks are accounted for and match the description; the depth cap is enforced server-side in Design-Verdict: PASS Root-cause retraction with a named alternative rejected for cause, a reversibility story for the new public name, and regression pins on every re-mount surface. [DESIGN-REVIEWED] 584cfab |
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) — ✅ PASSPremise-level review of All verification is done: the rename is complete, nothing dangles, the author's counts check out. Final review: First-Principles-Verdict: PASS Retracting an unchosen charter change at its cause — the RFC's own mount decision — and re-homing the flow where opting in is explicit. What this change shipsIntent: stop every existing conductor user from being silently moved onto the work-ledger procedure #9152 mounted on their agents, while keeping that procedure reachable for people who choose it. A FIX (of a rollout decision), carried by one declared addition.
WatchThe RFC's own rollout note says this agent exists to be retired, yet shipping it mints a permanent public name (alias spec through deprecation, [FIRST-PRINCIPLES-REVIEWED] 584cfab |
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: |
a33764a to
39c3d7e
Compare
|
Intent: Take the work-ledger flow off the two shipped conductors and put it on a separate |
39c3d7e to
bc13846
Compare
|
self-added: yes
|
|
self-added: no
|
|
self-added: no
|
bc13846 to
bc0cd37
Compare
|
self-added: yes
|
bc0cd37 to
91fa497
Compare
|
self-added: yes
|
|
self-added: no
|
|
self-added: no
|
|
self-added: no
|
…nductor agent #9152 mounted `kirocrew-work` on `kirocrew-conductor` and `kirocrew-pipeline-conductor`, per the RFC's §Agent spec changes. That part is retracted here, and the flow moves to a new agent instead. The two grants alone do not describe the change they came with. The ledger flow **inverts the dispatch order** (`create` -> `session_create` -> `bind` -> seed, where `goal-conductor/SKILL.md` seeds before it records) and **replaces the patrol cycle** (one `work_ledger_read` instead of a per-item transcript read with a stored cursor). Mounting the tools on a shipped agent therefore hands its users a different procedure under the same name, on the agent most likely to be mid-goal when it upgrades. An agent's tool surface is part of its charter. Retracted (A). Both installers drop the `@kirocrew-work` tools entry, the `mcpServers` entry and the two per-tool grants. Verified byte-identical to their pre-#9152 form by generating both specs at 40ff148~1 under the same stubs and diffing. Every #9152 assertion becomes a negative assertion rather than a deletion, on all four surfaces a mount can survive on — `tools`, `mcpServers`, `allowedTools` and the derived KAS `permissions` block, the last silently, since nothing reads `allowedTools` on that backend. Added (B). `kirocrew-ledger-conductor`, a copy of `_install_conductor_agent` with the work mount, `_LEDGER_CONDUCTOR_WORK_GRANTS` (renamed from `_CONDUCTOR_WORK_GRANTS`) and its own prompt. Every property that installer's docstring argues for is kept and re-asserted, because a copy is where those drift: no `fs_write` and no `code`, no whole-server auto-approve, grants filtered through the governance ceiling with a SEL record for each withhold, and `permissions` derived from the filtered list. The worker half is mounted (it is one server) and deliberately not granted — to a root conductor it answers only `not_bound`, and a second-level conductor reaches it through the gate. Two mechanical helpers, `_filter_auto_approve` and `_conductor_mcp_servers`, so the third installer is not a third copy of the same forty lines. Full extraction of the installer body is deliberately not done: the file documents "one standalone installer per generated agent" as its pattern, and rewriting two shipped installers inside a retraction is the wrong risk. New builtin skill `goal-ledger-conductor`, carrying `accept_eval.py` as a real file (the skill-scope gate refuses a symlink) and NOT `ledger_entry.py` — that codec exists to squeeze an item into a 2000-character `artifacts` value, and this flow has a store. `goal-conductor` and `_CONDUCTOR_SYSTEM_PROMPT` are untouched: they are what the un-migrated conductor runs on. Docs. The feature-map row and `agents.md` name who mounts the ledger and who does not; `pipeline-conductor.md`'s "unbuilt" paragraph is corrected. The RFC goes to v2.1 with a rollout note recording why the flow is isolated and the two criteria for folding the agents back together — one goal run end to end, including a `blocked` or `question` and a second-level conductor, and Phase 3's `watch: "work-ledger"` gate merged so patrol stops paying a turn per quiet interval. ## Pattern harvest Defect class: a spec change that adds tools to a shipped agent whose bundled procedure the tools contradict. The grant diff looks additive and reviews as additive; what actually ships is a different operating procedure under an unchanged agent name. Rule candidate: when a change adds an MCP server to an existing agent spec, the review has to ask whether that agent's prompt and skill describe the procedure the new tools imply. If they do not, the tools belong on a new spec or the procedure ships in the same commit. A grep-shaped gate cannot judge this; the tractable half is a test-level convention, which this commit establishes — a negative mount assertion per shipped agent, so a later addition has to delete a test that says why the mount was withheld.
91fa497 to
584cfab
Compare
|
Rebased onto One line-level delta from the reviewed head Everything else in the payload is unchanged: identical added/removed lines, same 13 files. |
bolichen97
left a comment
There was a problem hiding this comment.
Approving after independent Tech Lead verification.
Phase-3 relationship to #9152 — correct, and the load-bearing claim independently reproduced. I generated both shipped conductor specs under identical stubs at 40ff1482d~1 (pre-#9152) and at this head, dumped with sorted keys: diff -r is clean. Negative control at origin/main shows the real delta the retraction removes — @kirocrew-work in tools, the kirocrew-work mcpServers entry, and the two grants on both allowedTools and the derived KAS permissions block. All four surfaces are clean here, which is the surface set that matters since nothing reads allowedTools on the KAS backend.
Not a new builtin app. No app manifest, no app tree, nothing under the App Kit surface: this is a new agent SPEC (kirocrew-ledger-conductor.json, added to OWNED_KIRO_AGENT_FILES, owned-but-not-required) plus a builtin skill under src/kiro_crew/builtin_skills/, which is exactly where AGENTS.md's LLM-facing-capabilities rule requires a skill any shipped feature references to live. No new CLI command and no new MCP tool — the kirocrew-work server, its four verbs, the store, the routes and the kirocrew-worker spec from #9152 are untouched, so the statelessness rule is not re-engaged.
No regression on #9152's merged work. mcp_work.py, work_ledger.py, the dashboard handlers, validation.py, sandbox.py and security/paths.py are all absent from the diff. goal-conductor/SKILL.md, its scripts and _CONDUCTOR_SYSTEM_PROMPT are byte-for-byte untouched. The _CONDUCTOR_WORK_GRANTS → _LEDGER_CONDUCTOR_WORK_GRANTS rename leaves no dangling reference.
Security properties of the copied installer hold. No fs_write and no code; execute_bash mounted and never granted; no whole-server auto-approve; grants filtered through _may_auto_approve with a SEL record naming this installer per withhold, and the audit failing does not break the install; permissions derived from the filtered list rather than restated. work_brief granted (reads the caller's own bound item or answers not_bound) and work_report deliberately left gated is the right split for a write that crosses a dispatch relationship. The skill feeds the evaluator through a quoted heredoc and names the printf '%s' '<json>' shape as the thing not to do.
Verification run locally: 206 tests pass across test_ledger_conductor_agent.py, test_conductor_agent.py, test_pipeline_conductor_agent.py, test_worker_agent.py. accept_eval.py copy confirmed byte-identical to the shipped original and a regular file, not a symlink. The mypy exclusion regex is anchored to goal-ledger-conductor/scripts/accept_eval.py$, so the original stays type-checked.
CI: 64 check-runs, 56 success / 8 skipped / 0 failure on this head. All five AI lanes clean (Design PASS, First Principles PASS, GPT no blocking, Opus no blocking, UX Review success), plus PR Hygiene, PR Scope, Builtin Skill Scope Gate, CodeQL and Semgrep. Every AI finding across four rounds carries a disposition comment; the two deferrals are recorded in the artifacts that own them (#9309 for goal-conductor's remaining printf sites, the RFC rollout note for the agent-name alias story).
One residual, accepted rather than waved: the end-to-end run of the new agent against a live multi-item goal is not done, and it is correctly named as the first of the two merge criteria in the RFC rollout note rather than claimed. Since the new spec is additive and both shipped conductors are provably back to their pre-#9152 form, that gap cannot regress an existing user.
Adds the third conductor spec: an agent that owns ONE security audit on ONE target, dispatching one auditor session per attack surface, an independent verifier per finding, and (behind a human yes) a fixer. Design of record is the RFC in #9195, sections "The agent" and "Verified facts". Registration only. The `security-conductor` skill and its bundled scripts land separately; this spec references them by name so the agent has somewhere to read its procedure from, and the prompt already binds the two decisions it must not make by judgment to those scripts' verdicts. - `subagent.py`: the name joins `UNADVERTISED_AGENTS`, so it never appears in a rendered roster. A conductor is dispatched by name by an operator; advertising it invites a caller to hand it work it cannot do. - `agent_files.py`: `SECURITY_CONDUCTOR_AGENT_FILENAME` beside its pipeline sibling, and in `OWNED_KIRO_AGENT_FILES` so the Playwright convergence sweep rewrites it rather than letting it rot. - `agent.py`: `_install_security_conductor_agent()` cloned from `_install_pipeline_conductor_agent()`, called eagerly beside it with the same try/except+debug-log shape, plus `_SECURITY_CONDUCTOR_SYSTEM_PROMPT`. Every security property the sibling installers argue for is kept, and each one carries more weight here because this agent's children probe a security fence, so what it ingests on an unattended cycle is hostile by assumption: no file-writing tool (neither `fs_write` nor `code`), both servers mounted whole but auto-approved verb by verb, `execute_bash` mounted and never auto-approved, and the KAS policy derived from the FILTERED grant list. The grant tuples are the pipeline conductor's, REUSED rather than copied. Deriving from this prompt's own tool inventory lands on exactly that set -- patrol lifecycle, reads, the agent's own ledger, owner reporting, and no `select_crew` since this conductor routes nothing -- so a third byte-identical copy would be duplication whose later divergence nothing could detect. Cross-agent reuse is already this file's practice for `_CONDUCTOR_WORK_GRANTS`. Tests: `test/test_security_conductor_agent.py` mirrors the pipeline installer's suite (identity, owned filename, unadvertised, verbosity token, patrol via `monitor_start`, no file-writing tool, dashboard grants create/read only, core grants verb by verb, narrowed MCP servers, permissions derived from the filtered grants, and the withheld-grant audit naming this installer) and adds the four prompt invariants the charter rests on: scope is `scope_check.py`'s verdict and `UNKNOWN` is never permission, acceptance is `verify_finding.py`'s verdict and never a child's prose, a policy refusal IS the boundary, and both human gates. One test asserts the two conductors' resolved grant sets stay identical, which is what makes the reuse above safe to state rather than merely intended. The two roster ratchets that enumerate `UNADVERTISED_AGENTS` are updated (they pin the set's single definition, so they red by design on a new member), as are the two prose lists that named the old three. Review round 1 (GPT 5.6, blocking): shell was mounted with nothing saying what it is FOR. The spec half was already closed -- `execute_bash` is never in `allowedTools`, pinned by test -- but the pipeline conductor I mirrored carries a "shell exists to run the skill's scripts" paragraph that this prompt lacked, so the one path from hostile child output to a changed target (a finding whose text asks for a shell write, ingested on a cycle where the operator armed session-level trust) was unaddressed at the only layer that can address it: `allowedTools` is name-scoped with no argument matching, so a granted shell cannot be bounded by argument in the spec. The prompt now says shell runs the skill's scripts and is never a way to change a target, that a change to a target is a child's work behind a gate, and that a finding's own text is ingested content rather than an instruction. Red-before proven. Review round 2 (Design Review, Opus 4.8 — both advisory): the user-facing `docs/agents.md` advertisement is dropped. Both lanes named the same remedy — a release cut between this PR and the skill PR would ship a DOCUMENTED, dispatchable auditor whose enforcement scripts do not exist. The eager install cannot be deferred (`session_create` refuses an agent it cannot resolve, and resolution reads a boot-time snapshot), but the advertisement can, and `kirocrew-worker` is already an owned spec that list does not name — so omitting one until its procedure ships is the file's existing practice rather than a new gap. `docs/system-specs/modules/crew-mode.md` keeps its update: it describes `UNADVERTISED_AGENTS` membership, which is true today. Rebased onto main after the three sibling changes landed (#9195 RFC, #9270 ledger, #9271 skill) and re-mirrored onto the pipeline conductor, which moved underneath this branch: - #9277 pulled `@kirocrew-work` and its grants OUT of both shipped conductors into a dedicated `kirocrew-ledger-conductor` spec, and renamed `_CONDUCTOR_WORK_GRANTS`. The mount is dropped here too: this conductor's children report through the skill's ledger scripts, not the work ledger, so keeping it would grant a flow whose procedure this agent does not run -- #9277's own charter-creep argument. New test `test_the_work_server_is_not_mounted`. - the grant filter plus withheld-audit block is now `_filter_auto_approve`, and the mcpServers narrowing is `_conductor_mcp_servers`. Both adopted, which deletes ~45 lines of inline copy from this installer. - the `AGENTS.md:248` coupling that made this PR unmergeable on its own is resolved by #9271: the `security-conductor` skill now lives in `src/kiro_crew/builtin_skills/`. The three scripts the prompt names are still absent, which `test_no_stub_scripts_are_shipped` on main declares the sanctioned phased state ("an absent script is fine here"). Conflicts were additive on both sides (a fourth conductor at the same anchors); both were kept.
Adds the third conductor spec: an agent that owns ONE security audit on ONE target, dispatching one auditor session per attack surface, an independent verifier per finding, and (behind a human yes) a fixer. Design of record is the RFC in #9195, sections "The agent" and "Verified facts". Registration only. The `security-conductor` skill and its bundled scripts land separately; this spec references them by name so the agent has somewhere to read its procedure from, and the prompt already binds the two decisions it must not make by judgment to those scripts' verdicts. - `subagent.py`: the name joins `UNADVERTISED_AGENTS`, so it never appears in a rendered roster. A conductor is dispatched by name by an operator; advertising it invites a caller to hand it work it cannot do. - `agent_files.py`: `SECURITY_CONDUCTOR_AGENT_FILENAME` beside its pipeline sibling, and in `OWNED_KIRO_AGENT_FILES` so the Playwright convergence sweep rewrites it rather than letting it rot. - `agent.py`: `_install_security_conductor_agent()` cloned from `_install_pipeline_conductor_agent()`, called eagerly beside it with the same try/except+debug-log shape, plus `_SECURITY_CONDUCTOR_SYSTEM_PROMPT`. Every security property the sibling installers argue for is kept, and each one carries more weight here because this agent's children probe a security fence, so what it ingests on an unattended cycle is hostile by assumption: no file-writing tool (neither `fs_write` nor `code`), both servers mounted whole but auto-approved verb by verb, `execute_bash` mounted and never auto-approved, and the KAS policy derived from the FILTERED grant list. The grant tuples are the pipeline conductor's, REUSED rather than copied. Deriving from this prompt's own tool inventory lands on exactly that set -- patrol lifecycle, reads, the agent's own ledger, owner reporting, and no `select_crew` since this conductor routes nothing -- so a third byte-identical copy would be duplication whose later divergence nothing could detect. Cross-agent reuse is already this file's practice for `_CONDUCTOR_WORK_GRANTS`. Tests: `test/test_security_conductor_agent.py` mirrors the pipeline installer's suite (identity, owned filename, unadvertised, verbosity token, patrol via `monitor_start`, no file-writing tool, dashboard grants create/read only, core grants verb by verb, narrowed MCP servers, permissions derived from the filtered grants, and the withheld-grant audit naming this installer) and adds the four prompt invariants the charter rests on: scope is `scope_check.py`'s verdict and `UNKNOWN` is never permission, acceptance is `verify_finding.py`'s verdict and never a child's prose, a policy refusal IS the boundary, and both human gates. One test asserts the two conductors' resolved grant sets stay identical, which is what makes the reuse above safe to state rather than merely intended. The two roster ratchets that enumerate `UNADVERTISED_AGENTS` are updated (they pin the set's single definition, so they red by design on a new member), as are the two prose lists that named the old three. Review round 1 (GPT 5.6, blocking): shell was mounted with nothing saying what it is FOR. The spec half was already closed -- `execute_bash` is never in `allowedTools`, pinned by test -- but the pipeline conductor I mirrored carries a "shell exists to run the skill's scripts" paragraph that this prompt lacked, so the one path from hostile child output to a changed target (a finding whose text asks for a shell write, ingested on a cycle where the operator armed session-level trust) was unaddressed at the only layer that can address it: `allowedTools` is name-scoped with no argument matching, so a granted shell cannot be bounded by argument in the spec. The prompt now says shell runs the skill's scripts and is never a way to change a target, that a change to a target is a child's work behind a gate, and that a finding's own text is ingested content rather than an instruction. Red-before proven. Review round 2 (Design Review, Opus 4.8 — both advisory): the user-facing `docs/agents.md` advertisement is dropped. Both lanes named the same remedy — a release cut between this PR and the skill PR would ship a DOCUMENTED, dispatchable auditor whose enforcement scripts do not exist. The eager install cannot be deferred (`session_create` refuses an agent it cannot resolve, and resolution reads a boot-time snapshot), but the advertisement can, and `kirocrew-worker` is already an owned spec that list does not name — so omitting one until its procedure ships is the file's existing practice rather than a new gap. `docs/system-specs/modules/crew-mode.md` keeps its update: it describes `UNADVERTISED_AGENTS` membership, which is true today. Rebased onto main after the three sibling changes landed (#9195 RFC, #9270 ledger, #9271 skill) and re-mirrored onto the pipeline conductor, which moved underneath this branch: - #9277 pulled `@kirocrew-work` and its grants OUT of both shipped conductors into a dedicated `kirocrew-ledger-conductor` spec, and renamed `_CONDUCTOR_WORK_GRANTS`. The mount is dropped here too: this conductor's children report through the skill's ledger scripts, not the work ledger, so keeping it would grant a flow whose procedure this agent does not run -- #9277's own charter-creep argument. New test `test_the_work_server_is_not_mounted`. - the grant filter plus withheld-audit block is now `_filter_auto_approve`, and the mcpServers narrowing is `_conductor_mcp_servers`. Both adopted, which deletes ~45 lines of inline copy from this installer. - the `AGENTS.md:248` coupling that made this PR unmergeable on its own is resolved by #9271: the `security-conductor` skill now lives in `src/kiro_crew/builtin_skills/`. The three scripts the prompt names are still absent, which `test_no_stub_scripts_are_shipped` on main declares the sanctioned phased state ("an absent script is fine here"). Conflicts were additive on both sides (a fourth conductor at the same anchors); both were kept.
Adds the third conductor spec: an agent that owns ONE security audit on ONE target, dispatching one auditor session per attack surface, an independent verifier per finding, and (behind a human yes) a fixer. Design of record is the RFC in #9195, sections "The agent" and "Verified facts". Registration only. The `security-conductor` skill and its bundled scripts land separately; this spec references them by name so the agent has somewhere to read its procedure from, and the prompt already binds the two decisions it must not make by judgment to those scripts' verdicts. - `subagent.py`: the name joins `UNADVERTISED_AGENTS`, so it never appears in a rendered roster. A conductor is dispatched by name by an operator; advertising it invites a caller to hand it work it cannot do. - `agent_files.py`: `SECURITY_CONDUCTOR_AGENT_FILENAME` beside its pipeline sibling, and in `OWNED_KIRO_AGENT_FILES` so the Playwright convergence sweep rewrites it rather than letting it rot. - `agent.py`: `_install_security_conductor_agent()` cloned from `_install_pipeline_conductor_agent()`, called eagerly beside it with the same try/except+debug-log shape, plus `_SECURITY_CONDUCTOR_SYSTEM_PROMPT`. Every security property the sibling installers argue for is kept, and each one carries more weight here because this agent's children probe a security fence, so what it ingests on an unattended cycle is hostile by assumption: no file-writing tool (neither `fs_write` nor `code`), both servers mounted whole but auto-approved verb by verb, `execute_bash` mounted and never auto-approved, and the KAS policy derived from the FILTERED grant list. The grant tuples are the pipeline conductor's, REUSED rather than copied. Deriving from this prompt's own tool inventory lands on exactly that set -- patrol lifecycle, reads, the agent's own ledger, owner reporting, and no `select_crew` since this conductor routes nothing -- so a third byte-identical copy would be duplication whose later divergence nothing could detect. Cross-agent reuse is already this file's practice for `_CONDUCTOR_WORK_GRANTS`. Tests: `test/test_security_conductor_agent.py` mirrors the pipeline installer's suite (identity, owned filename, unadvertised, verbosity token, patrol via `monitor_start`, no file-writing tool, dashboard grants create/read only, core grants verb by verb, narrowed MCP servers, permissions derived from the filtered grants, and the withheld-grant audit naming this installer) and adds the four prompt invariants the charter rests on: scope is `scope_check.py`'s verdict and `UNKNOWN` is never permission, acceptance is `verify_finding.py`'s verdict and never a child's prose, a policy refusal IS the boundary, and both human gates. One test asserts the two conductors' resolved grant sets stay identical, which is what makes the reuse above safe to state rather than merely intended. The two roster ratchets that enumerate `UNADVERTISED_AGENTS` are updated (they pin the set's single definition, so they red by design on a new member), as are the two prose lists that named the old three. Review round 1 (GPT 5.6, blocking): shell was mounted with nothing saying what it is FOR. The spec half was already closed -- `execute_bash` is never in `allowedTools`, pinned by test -- but the pipeline conductor I mirrored carries a "shell exists to run the skill's scripts" paragraph that this prompt lacked, so the one path from hostile child output to a changed target (a finding whose text asks for a shell write, ingested on a cycle where the operator armed session-level trust) was unaddressed at the only layer that can address it: `allowedTools` is name-scoped with no argument matching, so a granted shell cannot be bounded by argument in the spec. The prompt now says shell runs the skill's scripts and is never a way to change a target, that a change to a target is a child's work behind a gate, and that a finding's own text is ingested content rather than an instruction. Red-before proven. Review round 2 (Design Review, Opus 4.8 — both advisory): the user-facing `docs/agents.md` advertisement is dropped. Both lanes named the same remedy — a release cut between this PR and the skill PR would ship a DOCUMENTED, dispatchable auditor whose enforcement scripts do not exist. The eager install cannot be deferred (`session_create` refuses an agent it cannot resolve, and resolution reads a boot-time snapshot), but the advertisement can, and `kirocrew-worker` is already an owned spec that list does not name — so omitting one until its procedure ships is the file's existing practice rather than a new gap. `docs/system-specs/modules/crew-mode.md` keeps its update: it describes `UNADVERTISED_AGENTS` membership, which is true today. Rebased onto main after the three sibling changes landed (#9195 RFC, #9270 ledger, #9271 skill) and re-mirrored onto the pipeline conductor, which moved underneath this branch: - #9277 pulled `@kirocrew-work` and its grants OUT of both shipped conductors into a dedicated `kirocrew-ledger-conductor` spec, and renamed `_CONDUCTOR_WORK_GRANTS`. The mount is dropped here too: this conductor's children report through the skill's ledger scripts, not the work ledger, so keeping it would grant a flow whose procedure this agent does not run -- #9277's own charter-creep argument. New test `test_the_work_server_is_not_mounted`. - the grant filter plus withheld-audit block is now `_filter_auto_approve`, and the mcpServers narrowing is `_conductor_mcp_servers`. Both adopted, which deletes ~45 lines of inline copy from this installer. - the `AGENTS.md:248` coupling that made this PR unmergeable on its own is resolved by #9271: the `security-conductor` skill now lives in `src/kiro_crew/builtin_skills/`. The three scripts the prompt names are still absent, which `test_no_stub_scripts_are_shipped` on main declares the sanctioned phased state ("an absent script is fine here"). Conflicts were additive on both sides (a fourth conductor at the same anchors); both were kept. Co-authored-by: Joe Guo <zejiangg@amazon.com>
Problem / Motivation
#9152 (Phase 2 of the work-ledger RFC) mounted
kirocrew-workonkirocrew-conductorandkirocrew-pipeline-conductorand auto-approved the two conductor verbs on both, exactly as the RFC's §Agent spec changes specifies.The effect is that every existing conductor user is opted into a procedure nobody asked them about. The two grants read as additive, but the flow they enable is not:
goal-conductor(shipped)session_create→session_sendseed → record the itemcreate→session_create→bind→ seedsession_read_messageper item, with a storedsincecursorwork_ledger_readsession_ledgerartifacts, encoded byledger_entry.pyunder a 32-entry cappromitted manuallyaccept_batchSo the two tools arrive on an agent whose prompt and bundled skill describe the other column. An agent's tool surface is part of its charter, and this one landed on the agent most likely to be mid-goal when it upgrades.
Why it matters
A conductor that upgrades mid-goal gets tools its own skill never mentions. Best case it ignores them and nothing changes; worst case it half-adopts the flow — records items in the ledger and in
session_ledgerartifacts, or binds after seeding — and ends with two records that can disagree about what is running. Neither outcome was chosen by the user, and neither is visible to them.The flow itself is fine. What is wrong is who is holding it.
What changed (motivation → approach → change)
Approach. Retract the mount from the two shipped conductors, and put the flow on a third spec that is otherwise a copy of the goal conductor. Isolation over a config flag: a flag would still need
goal-conductor/SKILL.mdto describe both procedures at once, and the two disagree on the load-bearing steps.A — retracted
_install_conductor_agentand_install_pipeline_conductor_agenteach drop the@kirocrew-worktoolsentry, themcpServersentry and the two per-tool grants._CONDUCTOR_WORK_GRANTSis renamed_LEDGER_CONDUCTOR_WORK_GRANTSand retargeted; the tuple and its reasoning are kept for the new agent.Verified byte-identical to pre-#9152, not just to "no work entries": both specs were generated at
40ff1482d~1and on this branch under the same stubs the installer tests use, dumped with sorted keys, and diffed. Identical.B — added
kirocrew-ledger-conductor/kirocrew-ledger-conductor.json, installed eagerly beside its siblings.kirocrew-conductorkirocrew-ledger-conductortoolsexecute_bash,fs_read,web_fetch,session,report,tool_search,@kirocrew-core,@kirocrew-dashboard@kirocrew-workmcpServerskirocrew-core,kirocrew-dashboardkirocrew-work(noautoApprove)allowedToolssession,report,tool_search, 14 core verbs, 4 dashboard verbswork_ledger_read,work_ledger_record,work_briefwork_briefgranted (reads the caller's OWN bound item, ornot_bound);work_reportmounted, not granted_CONDUCTOR_SYSTEM_PROMPT(unchanged)_LEDGER_CONDUCTOR_SYSTEM_PROMPTgoal-conductor(unchanged)goal-ledger-conductoraccept_eval.py,ledger_entry.pyaccept_eval.pyonlyEverything
_install_conductor_agent's docstring argues for is kept and re-asserted, because a copied installer is where those properties drift: nofs_writeand nocode(governance classes it underfilesystem.write), no whole-server auto-approve, every grant filtered through the governance ceiling with a SEL record naming this installer for each withhold, andpermissionsderived from the filtered list rather than restated.The worker half is mounted because
kirocrew-workis one server. Of its two verbs,work_briefis granted: it only reads the caller's own bound item (or answersnot_bound, which is how a root conductor learns it has no parent), and it is a second-level conductor's mandated first call in a session nobody opened — gated, that call is an approval stall before any planning.work_reportis not granted: it writes into the parent's record across a dispatch relationship, and one prompt per round boundary is the accepted cost. A nested conductor's reporting contract (work_briefbefore planning,work_reportat round boundaries) is spelled out in both the prompt and the skill.Two mechanical helpers,
_filter_auto_approveand_conductor_mcp_servers, so the third installer is not a third copy of the same forty lines. Full extraction of the installer body is deliberately not done:agent.pydocuments "one standalone installer per generated agent" as its pattern, and rewriting two shipped installers inside a retraction is the wrong risk to take in this PR._LEDGER_CONDUCTOR_SYSTEM_PROMPTfixes the dispatch order, tells a second-level conductor to report its own item upward withwork_brief/work_report(its parent reads its own ledger, not the child's), states bind before seed and why (an unbound worker's firstwork_briefcannot tell an early call from a broken one), carries the per-item agent table with the never-leave-agent-unset warning, makesdonea claim the evaluator settles, keeps a claimedprout of the acceptance bar until an explicitaction=accept, and notes that patrol is still on a timer untilmonitor_startacceptswatch: "work-ledger".New builtin skill
goal-ledger-conductor, matching the prompt.accept_eval.pyis carried as a real file — the builtin-skill scope gate refuses a symlink before any read, and a wheel carries one poorly — and pinned byte-identical to the shipped copy by a test, because two copies that drift are two acceptance bars. The copy is excluded frommypyinpyproject.toml: two top-level modules namedaccept_evalmake mypy stop with "Duplicate module named" before checking anything, and the original stays checked. The skill feeds the evaluator through a quoted heredoc (<<'ACCEPT_BATCH'), not aprintf '%s' '<json>'argument: the acceptance document is built from ingested text, and a single quote inside afilepath would end the string and hand the rest to the shell, whichexecute_bashruns after one approval. (The shippedgoal-conductorskill carries the sameprintfshape at two sites; tracked in #9309, out of this PR's scope.)ledger_entry.pyis not carried: it exists to squeeze an item into a 2000-characterartifactsvalue under an entry cap, and this flow has a store.goal-conductor/SKILL.md, its scripts and_CONDUCTOR_SYSTEM_PROMPTare untouched, byte for byte, and a test asserts it. They are what the un-migrated conductor runs on. Phase 4's rewrite of that skill and its deletion ofledger_entry.pyare consequently out of scope here.C — docs
docs/feature-map/README.md— the Work ledger row now names who mounts it and who deliberately does not.src/kiro_crew/docs/agents.md— the owned-spec roster gainskirocrew-ledger-conductorandkirocrew-worker(the latter was left out by feat(work-ledger): kirocrew-work tools and kirocrew-worker agent (phase 2) #9152), with a sentence on why the ledger conductor is separate.docs/system-specs/modules/pipeline-conductor.md— itsmcpServersclaim needed no edit (it still says core + dashboard, which the retraction restores), but its "a work-ledger tool family … is unbuilt" paragraph was stale and is corrected.revision: v2.1with a Rollout note recording the retraction, what isolation costs, and the two merge criteria: one goal run end to end on the new agent (items created, bound, seeded, reported, verified, closed, including at least oneblocked/questionand one second-level conductor), and Phase 3'swatch: "work-ledger"gate merged so patrol stops paying a turn per quiet interval. It also states what retiring thekirocrew-ledger-conductorname means: an alias spec identical to the merged conductor for at least one minor release, a release-notes line and akirocrew doctornotice — never a same-change deletion.Tests
New
test/test_ledger_conductor_agent.py(31 tests) in three groups:printfforms are asserted absent; and both the skill and the prompt tell the reader to filteraccept_batchtostatus: doneitems before piping it (accept_batchis status-blind by design, so an unfiltered batch would let aprogressworker's early-satisfiedfilecondition be recorded as a pass and closed under it).fs_write/code; noweb_search/grep/glob;execute_bashmounted and never granted; no mutating session verb granted (session_send,session_stop,chat_folder_move*,task_run,workflow_run,spawn_*); grants pass the governance ceiling and a stripped grant loses its KAS rule with it; a withhold is audited naming this installer; the audit failing does not break the install;{{VERBOSITY_BLOCK}}present; owned but not required; the boot installer calls it.autoApprove; only the conductor half granted, with no whole-server ref; KAS resources exact rather thankirocrew-work/*.create<session_create<bind<session_send), not merely as presence; the agent rule;doneas a claim plusaccept_eval/accept_batch/action=verdict;action=accept;action=closewith noledger_entry; thewatch:note; that it points at its own skill and notgoal-conductor. Plus: the skill ships only the evaluator, as a regular file, byte-identical to the shipped one; the skill body and the prompt agree on the four load-bearing rules; andgoal-conductoris untouched.Inverted rather than deleted in
test_conductor_agent.py,test_pipeline_conductor_agent.pyandtest_worker_agent.py: every #9152 assertion becomes a negative one, on all four surfaces a mount can survive on —tools,mcpServers,allowedTools, and the derived KASpermissionsblock. The last matters most: nothing readsallowedToolson that backend, so a rule left behind there would re-grant the flow silently. Two new prompt-side assertions pin that neither shipped conductor's prompt names a tool it cannot reach. A deletion would have left nothing stopping the mount from coming back; a negative assertion makes a future re-mount delete a test that says why it was withheld.test_worker_agent.py's parametrized "a conductor mounts it" test moves to the one conductor that does; its former subjects get the negative form; the no-write check now covers all three conductors.Manual verification
40ff1482d~1and on this branch under identical stubs,diff -rclean. This is the claim the whole retraction rests on, and no unit test can make it (it is a comparison across commits).black --target-version py310 --check,isort --check-only,flake8,mypyon the touched Python: clean.origin/mainworktree on this host (host-ownership, AF_UNIX path length, missing binaries —test_host_isolation_floor,test_session_storage,test_file_explorer_appand siblings), differing by one port-contention flake intest_app_backend.pythat flips direction between runs.printfinterpolation above), fixed and verified closed. Round 2 (after the server'saccept_batchfilter finding): both clean. Round 3 (after the nested-conductor reporting finding): both clean. Round 4 (after Design Review'swork_briefgrant suggestion): both clean. Opus 4.8 returned no findings in any round.Not verified: an end-to-end run of the new agent against a live goal. That is the first of the two merge criteria in the rollout note, and it needs a real multi-item goal rather than a test fixture.
Related Issues
Follows #9152 (Phase 2) and #8855 (Phase 1). Partially retracts #9152's agent-spec change; the tools, routes, store and
kirocrew-workerspec it added are untouched.Pattern harvest
Defect class: a spec change that adds tools to a shipped agent whose bundled procedure the tools contradict. The grant diff looks additive and reviews as additive; what actually ships is a different operating procedure under an unchanged agent name.
Rule candidate: when a change adds an MCP server to an existing agent spec, the review has to ask whether that agent's prompt and skill describe the procedure the new tools imply. If they do not, the tools belong on a new spec, or the procedure ships in the same commit.
A grep-shaped gate cannot judge that. The tractable half is a test-level convention, which this PR establishes: a negative mount assertion per shipped agent, so a later addition has to delete a test that states why the mount was withheld rather than just adding a line to a list.
no linked issue: this retracts part of an already-merged PR (#9152) and adds an agent the RFC did not name; no tracking issue exists for it.