Skip to content

refactor(kiro): serve Kiro IDE and Kiro CLI from one distribution - #1157

Open
wowzoo wants to merge 75 commits into
awslabs:mainfrom
wowzoo:feat/kiro-single-harness-v3
Open

wowzoo wants to merge 75 commits into
awslabs:mainfrom
wowzoo:feat/kiro-single-harness-v3

Conversation

@wowzoo

@wowzoo wowzoo commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Replaces #1063, closed unintentionally: a brief visibility change on the head fork detached it
from this repository's fork network, which closed the pull request and made reopening impossible.
Nothing about the change itself was reconsidered, and the head commit is the one #1063 carried
(711e4de).


What this does

kiro and kiro-ide maintain the same .kiro/ shell twice. Kiro IDE and Kiro CLI run the same agent runtime, so this consolidates them into one row, kiro, serving both surfaces, and removes kiro-ide.

Breaking: aidlc config --harness kiro-ide no longer resolves — use --harness kiro.

Why now

The two adapters implement the same contract twice and have never converged:

harness/kiro harness/kiro-ide gap
v2.1.7 415 275 −140
v2.3.0 424 374 −50
v2.6.18 935 743 −192
v2.7.0 1047 2029 +982
this base 1075 2029 +954

690 → 3,104 lines. The point is not that the gap grows monotonically — it narrowed at 2.3.0 and flipped sign at 2.7.0 — but that the same contract is implemented twice and pays maintenance twice every generation. Five call sites already normalized kiro-ide to kiro before this change (aidlc-model-policy.ts, aidlc-plugin.ts, aidlc-plugin-emit.ts, scripts/plugin-hooks-template/compose.ts).

The merged adapter is 3,010 lines — the union, not a sum, because the duplicated halves collapse.

What the merge had to solve

The engine. settings/cli.json gains "chat.agentEngine": "v3", so a plain kiro-cli in the project reaches the agent runtime this row targets with no flag at the call site. Measured both ways on kiro-cli 2.21.1: with the key the session enters that engine, without it it does not.

Hook registration. The row registers through standalone manifests (14 files, 16 registrations) that both surfaces read. Two triggers are surface-specific per docs/features/hooks.md — Session Start is IDE-only, Agent Spawn is CLI-only — so lifecycle responsibilities register on both or they are dead on one surface. Only Prompt Submit, Pre Tool Use and Pre Task Execution can refuse, so every guard that must refuse sits on PreToolUse.

The acting persona. This is the hard part. The pre-merge row registered five guard targets inside each persona's own agent-v1 config and passed the persona as argv — 90 registrations. A v3 hook manifest has no agent-scope field, so that channel has no replacement in kind, and two guards depend on it: state-transition-guard enforces only when it knows a delegate is acting, and reviewer-scope compares the identity against the dispatched reviewer.

What replaces it is measured. Across 615 captured payloads (Kiro CLI 2.18.1 / 2.19.2 / 2.20.1 and IDE 1.x) a delegate's own tool calls do reach the workspace hooks, strictly nested inside the dispatch event's own PreToolUse..PostToolUse window, and the dispatch event names the delegate in one of three places: the subagent_<agent> tool name, tool_input.name, or tool_input.stages[].role. So the adapter opens a window on the dispatch's Pre, closes it on the matching Post, and attributes what happens in between. The ledger is append-only: each open is its own record, a close cancels the whole most-recent group for that dispatch key (a crew dispatch names several personas and must be released as one), and nothing is read before writing, so two adapter processes cannot lose one another's update.

reviewer-scope and review-freeze are registered for the first time on this row. Both must be able to refuse, so both sit on PreToolUse.

IDE 0.x

Not supported, and not silently dropped either: one legacy .kiro.hook ships whose only job is to say so. That format is the only channel a 0.x host reads and a supported one does not run it — measured on 1.0.437, where nothing fired — so the channel does most of the version check. Not all of it: a supported IDE lists the file as legacy beside a Migrate button, and one click makes it a hook that host does run, so the notice also reads the host's own version before it speaks. Both halves are below.

Which trigger, and which exit code, is not a free choice, and 0.12 answers differently per seam. On promptSubmit, exit 2 stops nothing: the hook runner turns any non-zero exit into HookFailedCommandError — "Hook '' command execution failed" — and that seam reads it as a broken hook rather than a refusal. Measured: the turn continued, and the session's own agent spent two minutes diagnosing the notice before writing "enabled": false into the manifest, which does not even disable it (that host keeps enablement in workspace state). On preToolUse, the same exit code does refuse the call. So the notice rides preToolUse with toolTypes: ["*"], writes the denial first, and exits 2.

Both halves carry weight. The exit code is the enforcement — measured on a real 0.12.333, the read was abandoned and the session reported it could not retry because the hook exited 2. The text is why, and it leads the output because this host's instructions tell the model that output denying access forbids retrying. It goes to both streams: stdout || stderr is the success path's rule and does not apply to a non-zero exit, and the delivery measured on a non-zero exit was stderr. The message states the supported versions inline rather than pointing at a file, since a reader in this state has every tool call refused and no docs/ tree in the install.

This refuses per tool call rather than per prompt, which is the better seam: nothing in AI-DLC advances without a tool, and the refusal lands where work would have begun. The target still reads neither stdin nor the normalized payload (a 0.x host opens stdin and never closes it) and still confirms the legacy channel through USER_PROMPT, which is populated on this trigger too, carrying the tool input as JSON rather than the user's prompt; only its presence is read.

The channel alone is no longer proof of the host, so the target also checks one. A supported IDE does not run this file, but it lists it as legacy with a Migrate button — one click makes it a current-generation hook that a supported host does run, and USER_PROMPT is populated there too, so that gate would not catch it. Migrated, on preToolUse, the notice would deny every tool call on a fully supported install, and the button is in the hooks panel of every one of them. So the target reads the version line out of VSCODE_IPC_HOOK (0.12-main.sock on 0.12.333, 1.0.-main.sock on 1.0.437, both measured) and speaks only for a 0.x major. An absent or unparseable value is silence as well: informing is this target's only job, and refusing work on a supported install is the worse failure, at the cost that a 0.x host which stops setting the variable gets no notice. The guide tells readers to leave the hook alone rather than migrate it.

Defects found and fixed on the way

Each of these built and typechecked clean before the fix, which is why they are worth listing:

  • Three guards were registered, dispatched to, and enforced nothing. reviewer-scope, guard-tool-call and deliver-stage-rules read the raw payload, and none was in the set that decides whether the adapter reads stdin at all — so the payload was "", the parsed object was {}, and each returned 0 on its first field access.
  • tool_response is not always a string. Two captured shapes ({items:[{Text}]}, {success,result:[...]}) were classified malformed, which discards the whole event: no audit row, and the Stop hook's inflight marker never cleared.
  • A matcher named execute_pwsh that the canonicalizer did not translate, so the call reached the adapter and returned 0 — the gate read as present and enforced nothing on Windows.
  • write was missing from three matchers while the adapter canonicalizes it, so a frozen-artifact write and a reviewer-scope violation under that spelling reached nothing.
  • subagent_response was classified as an opaque mutation, diverting every delegate report during code-generation into the legacy recovery path.
  • A batch write produced one audit row instead of one per target, and the written path was scraped from result prose even on the surface that populates the input.
  • aidlc-plugin-emit emitted a plugin's compose hook on SessionStart only, so on Kiro CLI a plugin never composed and nothing said so.
  • dist-release/kiro shipped no .vscode/settings.json, so the native channel lost the kiroAgent.trustedCommands pre-trust and an untrusted project command silently never runs.
  • The dispatcher read hook stdin unbounded in two places; a host that never closes stdin wedged the hook.
  • The row merge dropped four working behaviours, and only the unit suites noticed. The roll-forward seam wrote a per-session marker while three separate readers — the continue-workflow carve-out and, in other processes, the done-guard and the doctor bundle — still read the flat one, so the latch was dead. A compiled install lost the engine noun from its argv, breaking every off-band terminal command. All fourteen personas shipped with no tools, MCP or shell grants and no resources. And a space switch stopped re-pointing Markdown agents, so it could not put the conductor back. package.ts --check, all three typecheck configs and Biome passed throughout; the unit tier is what failed, and each of the four now has a regression test.
  • One correction lands in the shared onboarding skeleton, so every row's generated file changes. Three bullets told the reader to type a command that cannot exist (the runtime invocation token where the user-facing skill token belongs), the DocumentKB paragraph shipped twice with the two copies disagreeing, a hook claim was true of no row, one cross-reference pointed the wrong way, and both section slots glued their first heading to the preceding paragraph. Claude Code, Codex, Copilot, Cursor, Kiro and OpenCode outputs all move as a result; the alternative was correcting one row and leaving the same text wrong in five.
  • Deleting a row deletes the fixes that landed in it. fix: bind Plan Approval to content and attempt, add human-only exits, guard Kiro IDE's Windows shell; feat: Change Control #1000 and fix: restore Plan Approval forwarding and harness hook execution #1097 hardened harness/kiro-ide/ while this branch was open, and none of what they added is IDE-0.x-specific, because this row still ships the legacy Plan Approval axis. Six things move here. The shell tool is named execute_bash, execute_pwsh (Windows) or shell, and ten branches tested a literal — one predicate now backs every shell decision, because an unrecognised name failed open on that host. The unattributable-mutation refusal is gated on an active Code Generation window, so the next that starts the loop is no longer refused with no workflow. The safe-read set gains the names the runtime actually sends. Legacy mediation records the planned workspace source, without which core refuses the decision and nothing is recorded at all. A failed mediation step is a refusal with a reason on stderr rather than a silent drop that left the write window latched. And source drift before approval no longer dead-ends: the canonical planning write — the remedy — stays permitted, and the refusal says to re-present the plan. The guide and the reference prose state that contract for one row rather than two, and the conductor's own skill carries the legacy contract it has to execute: which tools may write before approval, that the directive's protected choices are shown verbatim and mapped back to canonical answers and never copied into shared files, and that recovery needs an exact human Recover Plan Approval.
  • Folding the rows turned a harness-name check into dead code, and a guard went silent. retainedTransportForCurrentState skipped its reuse path for the legacy window by name (installedHarnessName() === "kiro-ide"). One row makes that literal permanently false, so a live legacy window took the reuse path and the publication that rotates its protected choices never happened: next answered run-stage where ownership had to refuse (t327 went 4/0 → 2/4). The carve-out exists because of the window, not the row, so it now asks the window — a derived legacy session whose host record matches, the same predicate the file already used a few hundred lines above. Reading the environment alone would have caught a supported IDE 1.x session and a CLI run from a VS Code terminal, both of which keep the reuse. core/ is left with no "kiro-ide" literal anywhere, which is the state that makes this class of silence impossible rather than merely absent. Typecheck and the build never object to a literal like this; the suite is what caught it.
  • Every dispatch leaked a delegation window, and the comment asserting otherwise was the only thing checking it. The ledger write ran for every input target on the reasoning, stated in its own comment, that a repeat for the same event was "a no-op rather than a double count". It is not: opening mints a fresh group per call while a close cancels only the most-recent group for that key, and dispatch tools match two ledger-writing targets on PreToolUse against one on PostToolUse. So every dispatch opened two groups and closed one, and the leftover window kept the lifecycle guard refusing the main session's own verbs with a finished delegate named as the caller — measured on a live run, twice, where the only way forward was editing the ledger by hand. One target owns the ledger now: the only one registered on both edges of a dispatch. Typecheck, Biome and package.ts --check are all blind to this; the regression test delivers one dispatch event to both targets and asserts one close releases it.
  • The row ships two keyless HTTP MCP entries instead of four uvx launchers. The merged row inherited Claude's registry, where four of the five entries spawn a local process from a floating @latest package. That is a heavier default than a row serving both surfaces wants, and the AWS documentation case it existed for is covered over plain HTTP, so the registry is now context7 plus aws-knowledge-mcp-server, both type: http and both disabled. Claude keeps its five deliberately: the two harnesses provision MCP differently, and the guide and glossary prose describing the five is Claude-framed, so it stays true and is untouched. Two consequences the type checker does not see — the shared config diagnostics resolved its shipped-server list per harness, because one list would report a false defaults-drift on whichever harness ships fewer; and the persona frontmatter grants one token per entry, so all fourteen personas move from five grants to two.
  • Deleting a row deletes the fixes that landed in it — a second time, and one of them I first mis-inventoried. Rebasing replayed this PR's deletion of the IDE row over four upstream changes to it, none IDE-0.x-specific. disclose_context joins the Plan Approval safe-read set, because denying it stopped a Windows customer mid-workflow and it has no write surface. Two hook manifests match all three shell spellings rather than execute_bash alone, since the engine verbs arrive through whichever one the host sends and a missing spelling silently stops that hook. fix: restore native Kiro IDE Plan Approval recovery and mediation #1111's new native-recovery suite pointed at the deleted row's dist, so its coverage moved to the merged row rather than disappearing. And its legacy spawns now route through the compiled binary's own subcommand: I had recorded that helper as "already covered here" on the strength of a grep count for the environment variable it reads, but those hits were elsewhere, and on a compiled install passing a source path to the binary makes the recovery fail closed. A grep count is not evidence that a code path uses what it counts.
  • The skill claimed there is no statusline. The row wires none, which is what the sentence meant, but as written it is false on a CLI where a human configured one — and the renderer ships in this row's tools. Narrowed to "this row wires no statusline", keeping the part that matters to the conductor explicit: a statusline reaches it through nothing, so position still has to be surfaced in prose.

Tests

t331 is new and encodes the wiring contract rather than the current file list: every required responsibility on every trigger it needs, a refusing responsibility only on a blocking trigger, a single-surface trigger always paired, every manifest target existing in the adapter, and every matcher covering the tool names the canonicalizers translate. That last assertion is what caught the execute_pwsh and write gaps.

t147 (36 → 42 cases) keeps its live-captured payload fixtures and gains regression cases for the delegation window: one crew dispatch is one window however many personas it named; two identical dispatches need two closes; reviewer-scope still enforces when a second persona is inflight; a crew with no review in flight leaves the drop log clean; a dispatch record naming a delegate that is not inflight is recorded as a drop; the 0.x notice fires only on the legacy channel.

t245 and t148 were rewritten onto the Markdown shape — the properties they pinned (no blanket shell trust, no nested delegation, no model pin, every registration reaching this row's adapter) all still matter. t245's expected registration table is now generated from the shipped manifests so a matcher change cannot leave it stale by hand.

Three tests pinned the absence of enforcement this row now has (reviewer-scope registration, review-freeze registration, the doc-parity claim) and were inverted rather than deleted. The matching carve-out in stage-protocol-reviewer.md — "on a harness without reviewer-scope enforcement (Kiro IDE today), do not write the record" — is gone with them, after checking every shipped row's registration: no harness lacks it now.

Verified

bun install --frozen-lockfile          ok
bun scripts/package.ts                 ok
bun scripts/package.ts --check         deterministic across two builds, all six rows
bun run typecheck                      0 errors (all three configs)
dist/kiro doctor                       0 problems
aidlc-graph.ts compile --check         exit 0

bun tests/run-tests.ts --smoke --unit, on this head with upstream/main a0ee4415 merged in — 305 files, 8,321 assertions, one failing assertion.

That failure is t150-codex-packaging, in the case that asserts doctor enforces Codex 0.145.0 as the compact-session reload floor. It is a 5,000 ms timeout: the doctor child process returned status −1 rather than 0, and the runner reported "killed 1 dangling process". The same file passes 13/0 in isolation on the same tree, in 9.47 s for the whole file, so the single case is under the limit when it is not competing for the machine. This branch does not modify that test, and while it does touch harness/codex/manifest.ts, the failing case checks a version floor in doctor rather than anything the manifest carries.

The two suites the earlier body named as environment-sensitive both pass in this run: t255-workspace-sync and t314-source-freshness-receipts. That is the first completed tier in which they pass together, which is what the earlier explanation predicted. The published figures before this run (297 files, 8,195 assertions) came from before the rebase, and the sentence that explained them blamed "the machine under --parallel 8" — wrongly, because the runner pins every smoke and unit file to serial execution, so that flag creates no concurrency in either tier. Bare bun test tests/unit still does not complete on this machine, which is why per-suite numbers come from isolated runs; the totals above come from the repository's own runner.

The merge itself is clean — 0 conflicts — even though the overlap with upstream/main has grown from six files to nine, three of them files this repository's own merged PRs changed (#1158 and #1160: core/aidlc-common/protocols/stage-protocol-reviewer.md, tests/unit/t266-review-class.test.ts, tests/unit/t279-reviewer-turn-budget.test.ts). On that merged tree typecheck, lint and package.ts --check are clean. t279's per-harness assertion needed no roster edit because HARNESS_MATRIX is discovered from the tree rather than hardcoded; all six shipped rows carry the reviewer clause, and kiro-ide is absent by design.

Isolated suites on the merged tree above: t266 15/0 · t279 15/0 · t271 47/0 · t239 13/0 · t331 25/0 · t147 45/0.

Isolated suites measured earlier, before upstream/main was merged in (counts have since grown where upstream added assertions): t245 43/0 · t148 16/0 · t218 74/0 · t221 64/0 · t243 84/0 · t244 0 fail · t68 7/0 · t264 · t265 · t293 · t294.

Replayed on throwaway fixtures with real captured payload shapes: dispatch Pre → the window holds the persona; the delegate running aidlc-orchestrate.ts next → exit 2 naming it; dispatch Post → window empty and the same command passes; orchestrate_subagent Pre → the window holds stages[].role; two distinct personas inflight → exit 2 naming both; the 0.x notice → exit 0 with no output on a supported host, exit 2 with the notice when USER_PROMPT is set.

Attribution inside a parallel delegation window, every branch measured on a throwaway fixture and pinned by a test in t147:

inflight dispatch record result
two delegates names one of them that identity is forwarded, the guard refuses (exit 2), and the possible false refusal of the other delegate's call is recorded as a drop
two delegates names a delegate that is not inflight fail open, recorded as a drop
two delegates none — the ordinary state of a crew stage fail open, and nothing is recorded
one delegate the window alone is the identity; no record is consulted

The 0.x notice, on a real Kiro IDE 0.12.333. Installed the shipped kiro tree into an empty project, opened it in that build, and asked for a file read. The hook fires at the tool call, the agent abandons the read, relays the notice, and does not try to work around it. Two earlier shapes were measured on the same host and rejected: promptSubmit + exit 2 arrived as a failed hook, and the session's own agent then spent two minutes diagnosing it and wrote "enabled": false into the manifest — the same request now completes in ten seconds. Also measured there: the "enabled" field in a .kiro.hook does not gate execution (that host keeps per-hook enablement in workspace state), so editing it neither disables the notice nor is a supported way to.

And on a supported one, IDE 1.0.437. The same tree, the same request: the read succeeds and no legacy hook runs — confirmed by two silent .kiro.hook probes that would have recorded a firing to a file, and by the shipped notice staying quiet. "The legacy channel does not fire on a supported host" is therefore measured on the current generation, not inherited from an older one. What that host does do is list the file as legacy with a Migrate button, and it populates USER_PROMPT for a runCommand hook exactly as 0.x does — so the notice reads the host version from VSCODE_IPC_HOOK and stays silent unless the major is 0. Verified against the running host's real socket path rather than a fixture literal.

The same session is incidental evidence for the row itself on the IDE surface: session-start, record-human-turn, terminal-command, enforce-approval-gate, plan-approval-guard, reviewer-scope and continue-workflow all fired from the standalone manifests, the engine advanced its turn counter and created the intent record, and .drops stayed empty.

A guarded call inside a parallel delegation window, on Kiro CLI --v3. Three delegates opened before any of them closed, and guarded calls landed inside that window:

12:41:19.694  PreToolUse   subagent_aidlc-quality-agent        window 1
12:41:27.258  PreToolUse   subagent_aidlc-developer-agent      window 2  (2 inflight)
12:41:27.796  PreToolUse   execute_bash                        guarded, 2 inflight
12:41:36.838  PreToolUse   subagent_aidlc-devsecops-agent      window 3  (3 inflight)
12:41:37.437  PreToolUse   execute_bash                        guarded, 3 inflight

No delegate closed between those rows, so the concurrency is the capture's own ordering rather than an inference. execute_bash and fs_write calls continued to land with two or three delegates inflight for the rest of the window. That is the combination the second Not verified bullet said had only been replayed on a fixture, and it is now on the ledger of a live run.

The branch that window feeds was exercised with it. Three delegates inflight and no reviewer dispatch record is the ambiguous case: the adapter resolves no identity, the core hook fails open, and — because a recordless crew is the ordinary state of that stage rather than a failure — nothing is written to .drops. The run has no kiro-adapter.drops file at all, which is what the unit case for that branch asserts.

The reviewer read-scope bound, enforcing, on both surfaces. Two live runs from this shipped tree reached CONSTRUCTION and ran per-unit reviews, which is what writes the dispatch record the guard needs. The record is written as the protocol specifies:

.aidlc-reviewer-dispatch.json
  reviewer  aidlc-architecture-reviewer-agent
  stage     functional-design
  unit      shared-kernel
  exempt    7 paths — the declared upstream artifacts plus the stage file

The guard then refused, and the refusals are on the ledger as REVIEWER_SCOPE_BLOCKED rows:

surface refusals units what was refused
Kiro CLI 7 shared-kernel, identity-access, ordering-session, customer-web Bash, target <project-dir> — and one with target <record-root>
Kiro IDE 1 auth-foundation Bash, target <project-dir>

The first refusal is representative: the reviewer ran cd <project-root> && git status --porcelain, which reaches every unit's artifacts, and the hook stopped it — that row has a PreToolUse with no PostToolUse, so the refusal is the hook's exit, not a tool result. One later refusal names <record-root> rather than the project root, so the bound is not merely a project-root check.

What passed in the same window is the stronger half. Six cd <record>/inception && … commands ran unrefused during those reviews — reading stories.md, requirements.md, the story map and specific FR blocks. All of them are on the dispatch record's exempt list. The guard therefore discriminates: declared upstream reads pass, sweeps of the record root or the project root do not.

No workaround followed a refusal. After the block the reviewer read its own review file, returned its verdict, and the stage advanced (log reviewstate unit complete --wave --unit shared-kernel). There is no later call that reaches the same breadth through another tool.

One limit, stated because the ledger cannot show it otherwise: every refusal so far is on Bash. The guard also matches the search and listing tools, and those matchers have not been exercised — not because the reviewer avoids them (the runs use grep_search 25 times, list_directory 16, and file_search 7) but because inside these review windows the reviewer routes its searching through the shell instead: 53 execute_bash calls against 1 grep_search in one window, and 741 against 25 across the run. So the Bash matcher carries the load here, and the search matchers remain covered by tests rather than by this run.

Not verified — please weigh this

  • No completed end-to-end /aidlc run is included here. Two runs from this shipped tree — a Kiro CLI --v3 project and an IDE 1.x project — have carried a workflow through IDEATION and INCEPTION and into CONSTRUCTION. On the CLI surface five units have been through functional-design with their own per-unit reviews, two of them across a NOT-READY → revision → READY cycle; on the IDE surface two units have. Every standalone hook fires, the rituals run, and one run surfaced a real defect in this row that is now fixed with a regression test (see the delegation-ledger bullet). What remains unverified is the tail of CONSTRUCTION — code-generation, build-and-test and ci-pipeline — rather than the phase or the wiring.

Release metadata

Deliberately absent: no aidlc-version.ts bump, no README badge change, no CHANGELOG heading — following the maintainers' "we will bump version when releasing". AGENTS.md and CONTRIBUTING.md currently say the opposite; that divergence is worth settling separately and is not this PR's to decide. The entry a release would need:

Consolidate the two Kiro distributions into one. kiro now serves Kiro IDE and Kiro CLI from a single .kiro/ tree; the kiro-ide distribution is removed. Breaking: aidlc config --harness kiro-ide no longer resolves — use --harness kiro. Upgrade: an existing kiro-ide install becomes a kiro install by re-running aidlc config --harness kiro; a manual copy must first delete the retired surfaces an overlay cannot remove (.kiro/agents/aidlc.json, .kiro/agents/aidlc-*-agent.json, and the .kiro/hooks/aidlc-*.kiro.hook files other than the 0.x notice) — see the cleanup block in docs/guide/harnesses/kiro.md. Kiro IDE 0.x is not supported.

Follow-ups this PR does not take

  • The row carries two permission vocabularies. The conductor declares capability globs (permissions), the personas declare tool-settings regexes (toolsSettings). Both are supported — docs/features/custom-agents.md states existing JSON keeps working unmodified and the newer fields are optional — so each file keeps the shape it shipped with before the merge rather than being rewritten here. Unifying them is a separate, mechanical change.
  • The other native rows now route through the same dispatcher, and this PR does not touch them. fix: route native Copilot and Cursor hooks through the adapter dispatcher #1065 moved Copilot's and Cursor's native hooks onto the adapter dispatcher, which is the shape this row already had and the reason its two adapters could be folded into one. Whether the three dispatchers should share more than a shape — a common entry point rather than one adapter per row — is worth asking once this lands, and is deliberately out of scope here.

Review history

Reviewed adversarially five times before submission (Codex, in a separate harness). The last round's five blockers — crew-dispatch window accounting, the two missing matcher names, reviewer-scope failing open under ambiguity, and a check-then-truncate race in the ledger — are fixed here with regression tests. I am the reviewer on other PRs in this repo; treating my own work the same way seemed like the minimum.


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.

wowzoo added 30 commits September 12, 2026 08:51
The `.kiro/` shell is the same on Kiro IDE 1.x and Kiro CLI because both run the
same agent runtime, so the row's product name should not claim one surface.

- `harness/kiro/manifest.ts`: `productName` "Kiro CLI" -> "Kiro", and
  `configNextStep` now names both entry paths instead of only `kiro-cli chat`.
- `core/tools/aidlc-doctor.ts`: the product label is a hardcoded map that does
  not read the generated `harness.json`, so the same rename is needed there;
  without it `doctor` still prints `Project (.kiro, Kiro CLI)`.

Verified: `bun scripts/package.ts kiro` regenerates with `productName: "Kiro"`
and the matching `configNextStep` in `tools/data/harness.json`, and `doctor`
prints `Project (.kiro, Kiro)` with `0 problems`.
One harness serves Kiro IDE 1.x and Kiro CLI, so the code no longer carries two
names for the same `.kiro/` shell. Five sites already normalized `kiro-ide` to
`kiro` before use; those normalizations become identities and are removed.

Membership and routing (no behaviour change):
- `aidlc.ts`: `AdapterHarness` union and the harness-leaf map.
- `aidlc-init.ts`: `modelHarness()`.
- `aidlc-orchestrate.ts`: `isKiroRoutingHarness()` (three predicates).
- `aidlc-model-policy.ts`, `aidlc-plugin.ts`: unions plus their `kiro-ide -> kiro`
  normalizers.
- `aidlc-settings.ts`: the harness list and the emitted `$schema` properties.
- `aidlc-config-diagnostics.ts`: `kiro || kiro-ide` collapses to `kiro`.

Behaviour that was keyed on the IDE row now keys on `kiro`, because that row
serves the IDE:
- the `kiro-ide-chat-model` pending action,
- `.vscode/settings.json` in the diagnosed file set and its
  `kiroAgent.trustedCommands` check,
- the legacy Plan Approval path in `aidlc-orchestrate.ts`. Its build range is not
  documented as 0.x-only, and an argument-less payload has been observed on 1.x,
  so it is preserved rather than dropped; leaving the old literal would have
  killed it silently, since the comparison is on a string the compiler cannot
  check.

Two merges needed a judgement, both recorded in the code:
- the harness CLI requirement is now optional with an install hint naming both
  entry paths, because a project opened only in the IDE needs no `kiro-cli`;
- the model/effort capability keeps the CLI's `true`, and its message says that
  `chat.modelDefaults` is read by Kiro CLI and ignored by Kiro IDE, where the
  chat model is set in the IDE.

Dual-generation stdin handling is gone: both supported surfaces write and close
stdin, so `aidlc engine adapter` reads once for every target. The single
exception is the `legacy-ide-notice` target, whose host (Kiro IDE 0.x) opens
stdin and never closes it; reading there would hang before the notice printed.

`bun run typecheck` is clean for `core/`, `harness/` and the adapters; the
remaining errors are five test call sites that still pass `"kiro-ide"`.
- t293: the `kiro-ide` case asserted a harness that expresses neither model nor
  effort. That capability split is gone with the row merge, so the case is
  removed rather than repointed, and the honesty note now pins the merged
  wording (Kiro CLI reads `chat.modelDefaults`; Kiro IDE does not).
- t294: `probeHarnessCli` now pins the merged contract - `kiro-cli` is optional
  because a project opened only in Kiro IDE needs no separate CLI - and the
  provider-idempotence loop, the trust fixture and the pending-action install
  all use the `kiro` tree.

Both files pass (t293 17/0, t294 23/0) and `bun run typecheck` is clean across
all three project configs.

Note for the rest of the sweep: three of these call sites were invisible to the
type checker because the harness name arrives as a plain string (`install(...)`,
`cpSync(join(DIST, ...))`, `temp(...)`). The compiler catches the typed unions
only, so the remaining test files must be found by search and by running them.
… manifests

The registration channel the supported surfaces read is the standalone
`hooks/aidlc-*.json` manifest, not the agent-v1 `hooks` dict, so the row now
carries that shape:

- `agents/aidlc.md` replaces the fifteen agent-v1 `.json` configs. Hook wiring
  lives in the manifests; a JSON twin would only restate the persona and drift
  from it. The fourteen persona `.md` files are core projections and are
  unchanged.
- eleven standalone manifests are projected, each dispatching
  `engine adapter kiro <target>`.
- `steering/aidlc-active-memory.md` comes along, since steering is Kiro's
  always-on layer on both surfaces.
- the nine `*.kiro.hook` files are gone. That manifest format is the Kiro IDE
  0.12-era channel and it does not fire on 1.x; only two of the nine were even
  projected, so seven were dead weight in the tree. The upgrade notice for a 0.x
  host is authored separately.
- `settings/{cli,mcp}.json` stay: they are this row's own, and the IDE ignores
  them.

Verified on the regenerated tree: agents `json 0 / md 15`, hooks
`standalone 11 / kiro.hook 0`, steering 1, settings 2, project-root `.gitignore`
present, `doctor` `0 problems` and `graph compile --check` exit 0.

Two defects this step surfaced, both invisible to `doctor` and `typecheck`:
- the manifests still dispatched `engine adapter kiro-ide`, which is no longer a
  known adapter harness, so every hook would have failed with a usage error;
  they now dispatch `kiro`.
- three of the eleven call targets this row's adapter does not implement
  (`enforce-approval-gate`, `record-human-turn`, `terminal-command-guard`). The
  adapter union merge is the next step; until it lands those three hooks would
  not run. Neither `doctor` nor the type checker asks whether a manifest's
  target exists, so this was found by walking the manifests against the
  adapter's target set.
…ce, and pin the engine

The row's shape change left three consumers assuming Kiro agent JSON, so every
projection refresh failed. Reproduced before the fix:

    aidlc config models --preset balanced --project --yes
    error: …/projection/.kiro/agents/aidlc-architect-agent.json: missing agent surface

- `aidlc-model-policy.ts`: the writer no longer opens a per-agent JSON surface,
  and `modelPolicySurfaceDrift()` reads `settings/cli.json` instead of an agent
  file.
- `scripts/package.ts`: `projectKiroAgentJson()` and its call site are removed.
  `agents/*.json` no longer exists in this row, so the branch was unreachable and
  would have read as "the model dial is projected here" to the next reader. The
  `settings/cli.json` projection stays.
- `aidlc-config-diagnostics.ts`: the trust file list followed agent `.json` and
  the 0.12-era `.kiro.hook` files, both gone. It now follows the channel that
  declares commands - the standalone `hooks/*.json` manifests.

The capability declaration is corrected to what the surface can actually carry.
It first kept the CLI's `model: true, effort: true`; measuring the shipped
command showed neither lands: with no model key on a Markdown agent the writer
has no model to key an effort default by, so `settings/cli.json` came back
byte-identical after a `--model … --effort max` request. The row therefore
declares `model: false, effort: false`, and `modelPolicySurfaceDrift()` now
reports the request as inexpressible rather than silently dropping it.

`settings/cli.json` gains `"chat.agentEngine": "v3"`. It belongs in the same
commit as the shape: without it plain `kiro-cli` enters its 2.x engine, which
reads neither this row's Markdown conductor nor its standalone manifests.

`readStdinWithTimeout()` is removed from the dispatcher - it existed only for the
0.12 channel that no longer has a caller there.

Verified: `aidlc config models` completes on the regenerated tree and prints the
honesty note; generated `cli.json` carries the pin; t293 17/0, t294 23/0
(both now exercise the current shape); `bun run typecheck` and `bun run lint`
(732 files) clean.
… them

The row merge left two adapters with overlapping but not equal target sets, so
three shipped manifests dispatched a target this row did not implement. For a
PreToolUse gate that fails OPEN: an adapter with no handler returns null and the
dispatcher exits 0, so `enforce-approval-gate` would have reported success while
gating nothing.

The gate comes first. `t331` walks the shipped manifests against the adapter in
both directions - every dispatched target must be implemented, every target the
merge brought in must survive, and no manifest may dispatch another row's name.
Run against the pre-merge tree it reproduced exactly the three unimplemented
targets; it is green now.

The merge takes the IDE adapter as the base rather than meeting in the middle.
That is measured, not stylistic: it carries 35 functions against the CLI
adapter's 8, and the CLI's own five are dispatch helpers, while the IDE side owns
the payload semantics, the terminal latch, plan approval and the turn counter.
Five targets are ported onto it - `deliver-stage-rules`, `guard-tool-call`,
`review-freeze`, `reviewer-scope`, `state-transition-guard` - with the two
helpers they need (`canonicalTool`, `kiroDispatch`). 17 targets, 2395 lines.

Verified: `t331` 3/0; `bun run lint` clean across 733 files; `doctor` 0 problems
on the regenerated tree; `aidlc engine adapter kiro session-start` dispatches and
answers with the payload's own session id.

Worth knowing for anyone editing this file: `tsconfig.json` excludes
`harness/*/hooks/*-adapter.ts`, so `bun run typecheck` does not parse it. A
syntax error here passes typecheck and is caught only by lint.
…e list

The first version of this gate walked the manifests that exist against the
adapter, so an absent manifest was an absence nothing looked for. It passed 3/0
on a tree where two blocking hooks were unregistered and the lifecycle hook
reached one surface. That is the failure mode the gate was added to prevent.

It now encodes what the row must register, from two grounded places: the
pre-merge agent-v1 `hooks` dict this row carried, which named the event for each
target, and the manifests the IDE row shipped. Five assertions:

- every required responsibility is registered on every trigger it needs;
- a responsibility that must refuse a tool call sits on a blocking trigger -
  `docs/features/hooks.md` gives Prompt Submit, Pre Tool Use and Pre Task
  Execution as the only ones that can block, so a guard on Post*/Stop is a
  bystander;
- a single-surface trigger is paired - the same table gives Session Start as IDE
  only and Agent Spawn as CLI only, so a lifecycle hook needs both names or it is
  dead on one surface. The IDE-only file and task triggers are exempt by name,
  since they have no CLI counterpart to pair with;
- every trigger spelling is one the docs define;
- every dispatched target exists in the adapter, and no manifest dispatches
  another row's harness name.

Against the current tree it reports three real defects the previous version
passed: `session-start` is registered on SessionStart only, so it never fires on
the CLI; and `review-freeze` and `state-transition-guard` have hook bodies in the
tree with no manifest, so neither guard is registered at all.

`bun run lint` clean (719 files); no typecheck errors from this file.
…ards

The wiring gate reported three defects; this closes the wiring half of all three.

- `aidlc-session-start.json` gains an `AgentSpawn` entry beside `SessionStart`.
  `docs/features/hooks.md` gives Session Start as IDE-only and Agent Spawn as
  CLI-only, so one shell serving both surfaces registers both names or the start
  hook is dead on the CLI. The pre-merge agent-v1 config registered exactly this
  event for the same target, so the pairing restores what the row already had.
- `aidlc-review-freeze.json` and `aidlc-state-transition-guard.json` are added.
  Both hook bodies already shipped in the tree with no manifest, so neither guard
  was registered at all. They sit on `PreToolUse` because they must refuse: the
  same doc table gives Prompt Submit, Pre Tool Use and Pre Task Execution as the
  only blocking triggers, so the PostToolUse audit hook cannot substitute.
- both are declared in `harness/kiro/manifest.ts`; a manifest the packager does
  not project is not shipped, which is how nine `.kiro.hook` files sat in this row
  with only two projected.

Matchers use the alias set the terminal guard already measures,
`^(execute_bash|execute_pwsh|shell)$`, so the IDE spellings are covered rather
than assumed; review-freeze adds the write tools the audit hook matches.
`state-transition-guard` is not a duplicate of `terminal-command-guard`: that one
keeps terminal commands deterministic, this one protects workflow lifecycle state.

Verified: manifests 11 -> 13; the wiring gate is 6/0.

🔴 Not yet working at run time. Dispatching either new target answers
`aidlc: kiro is not defined` and exits 0 - so the guards are registered and
failing OPEN. The three blocking targets that came from the IDE base dispatch
cleanly, so the fault is confined to the five blocks ported from the CLI adapter,
whose run-local contract does not exist in this base. That refactor is the next
commit; the row retirement stays uncommitted until it lands.
…ract

The ported blocks were registered but failed OPEN: dispatching either answered
`aidlc: kiro is not defined` and exited 0, so the guards gated nothing. They were
CLI-adapter code carrying that adapter's run-local context into a base that does
not have it. They are rewritten in the base's idiom rather than translated:

- read `ide.toolName` / `ide.toolArgs`, guarded by `ide.malformedFields` first,
  the way `terminal-command-guard` does;
- both targets join `PAYLOAD_TARGETS`, without which the normalized context
  arrives empty and every check silently passes;
- `return 0` / `return 2` instead of `process.exit`, matching the reject contract
  the neighbouring guards use;
- one shared `runCoreHook(hook, payload, cwd)` replaces the two hand-rolled
  spawns, and `TERMINAL_TOOLS` replaces the inline tool-name comparisons - the
  alias set the terminal guard already measures, so the IDE spellings are covered
  rather than assumed.

Three placement faults were found by running the thing, not by reading it:
the blocks had landed inside `buildForward()`, so their exit codes were returned
where a forward was expected (`fwd.input.tool_name` threw); the helpers sat below
their callers; and `projectDir` is local to `run()`, so a top-level helper cannot
see it - `runCoreHook` now takes the directory, and the guards pass
`process.cwd()` as the neighbouring `enforce-approval-gate` does.

Verified by dispatch, both directions:
- `state-transition-guard` with `aidlc-state.ts approve` -> exit 2 with the
  refusal on stderr; with a plain `echo` -> exit 0.
- a blocked verb had to be chosen from `BLOCKED_STATE_TRANSITIONS`; my first probe
  used `complete-stage`, which is not in that set, and the pass it produced was
  the probe's fault, not the guard's.
Wiring gate 6/0. `bun run lint` clean (721 files). Generated-adapter typecheck
errors 44 -> 33; every remaining one belongs to the three blocks not yet moved
(`guard-tool-call`, `deliver-stage-rules`, `reviewer-scope`).
…s glob

The Kiro docs are explicit: "When using custom agents, steering files are not
automatically included. You must explicitly add them to the agent's `resources`
configuration to load steering context" (Steering with custom agents). This row's
conductor carried no `resources`, so the always-on layer never reached it -
including `steering/aidlc-active-memory.md`, which this row ships for exactly that
purpose.

The glob is `file://{{HARNESS_DIR}}/steering/**/*.md`, the form the same doc gives,
and the packager substitutes it to `.kiro/` as with any other authored surface.

This also closes half of the `deliver-stage-rules` question. That target delivers
the active stage's rules, and the reason it looked replaceable was an assumption
that steering arrives on its own; it does not.
… auto-load

The row merge moved the conductor from agent-v1 JSON to Markdown and carried
only one of its four `resources` entries across, plus none of its delegation
trust. Both losses are silent: the agent still starts, and doctor, typecheck and
`graph compile --check` all pass, because nothing asks whether a declared
resource is reachable.

What was lost and why each one matters:

  skill://.kiro/skills/*/SKILL.md   The conductor's whole contract is "follow the
                                    aidlc skill exactly". Skills are not
                                    auto-loaded for a custom agent
                                    (features/skills.md), so the instruction
                                    pointed at nothing.
  file://aidlc/spaces/…/memory/     The workflow's own memory.
  file://AGENTS.md                  The onboarding contract at the project root.
  toolsSettings.subagent.
    trustedAgents (14)              Every routed stage delegates to a persona.
                                    Unlisted, each delegation raises an approval
                                    prompt the conductor cannot answer.

`permissions` has no subagent capability, so delegation trust stays under
`toolsSettings` where the schema puts it; the two coexist. The 14 names mirror
core/agents/ exactly (measured, not copied) and are spelled out rather than
globbed, because `aidlc-*` would also pre-trust whatever a dropped-in plugin
adds.

Verified on the generated tree: dist/kiro/.kiro/agents/aidlc.md parses as YAML
with 4 resources, 14 trustedAgents, 3 permission rules and no unsubstituted
{{HARNESS_DIR}}; every resource target exists in the tree; doctor reports
0 problems.
The persona-scoped axis this row lost with the merge is back, without the
agent-scope field the v3 hook schema does not have.

WHY IT WAS LOST. The pre-merge row registered five hook targets inside each
persona's own agent-v1 config and passed the persona as argv (90 registrations).
Standalone manifests carry `trigger`/`matcher`/`action`/`timeout`/`enabled`/
`confirm` and nothing that names an agent, so that channel has no replacement in
kind. Two guards depended on it: state-transition-guard enforces ONLY when it
knows a delegate is acting (an empty agent_type returns 0), so without it a
delegated agent could run lifecycle verbs the main session reserves; and
reviewer-scope compares agent_type against the dispatched reviewer.

WHAT REPLACES IT, MEASURED. Across 615 captured payloads (Kiro CLI 2.18.1 /
2.19.2 / 2.20.1 and IDE 1.x) a delegate's OWN tool calls do reach the workspace
hooks, strictly nested inside the dispatch event's PreToolUse..PostToolUse window
(CLI: dispatch Pre, then the delegate's execute_bash, then dispatch Post; IDE:
same, with the delegate's MCP call and read_file nested too). The dispatch event
names the delegate in one of three places: the `subagent_<agent>` tool name,
`tool_input.name`, or `tool_input.stages[].role`. So the window itself carries the
identity the payload omits: open on the dispatch's Pre, close on its Post, and
attribute what happens in between.

  - New latch under aidlc/.aidlc-sessions/kiro-delegation/<session>/, keyed by the
    dispatch payload so a redelivery of one event is idempotent (several
    manifests can match one tool call, and each match invokes this adapter).
  - Entries expire after 6h. A window is closed by an event a crashed session
    never sends, and a stuck entry would make state-transition-guard refuse the
    MAIN session's own lifecycle verbs indefinitely.
  - Ambiguity is reported, not guessed. Parallel delegation is real (measured:
    three dispatches opened before any closed). state-transition-guard gets every
    inflight name, because its enforcement does not depend on WHICH delegate is
    acting, only that one is. reviewer-scope COMPARES the name, so it takes one
    only when exactly one is inflight and otherwise fails open — the convention
    the core hook already states for its other uncertainties.

Four defects found on the way, each one fixed here:

  - `orchestrate_subagent` was recognized nowhere. Its payload puts personas in
    `tool_input.stages[].role`, the shape the `subagent` alias handles, but only
    `subagent` was on that path, so an orchestrated delegation produced no
    dispatch at all: guards saw nothing, the audit recorded nothing.
  - reviewer-scope was never registered on this row, so a blocking guard was
    unreachable. It has a manifest now, matching reads as well as writes because
    reading outside the reviewed artifact is the violation it exists for. Its old
    `scoped_registration: true` fallback is dropped: that was sound only because
    the registration itself was the reviewer's, and from a global manifest it
    would claim every unattributed call — including the conductor's own.
  - review-freeze tested `canonicalWriteTool(tool) === null` when that function
    returns "", so the gate never fired; and its matcher omitted create_file,
    delete_file, apply_patch and edit_file, all of which that canonicalizer
    translates.
  - guard-tool-call, reviewer-scope and deliver-stage-rules had been ported INTO
    buildForward(), where their `return 0` / `return 2` became the Forward value
    and the callers then read `.hook` off a number. Moved to the early-return
    chain where the other guards live, with the declarations they were using
    without having (KiroHookInput, KiroDispatch, firstNonBlank, projectEnv, the
    raw `kiro` payload, rmSync).

Verified:
  bun run typecheck                     37 errors -> 0 (all three configs)
  bun test t331                         7 pass / 0 fail (2 new assertions)
  bun test t293 t294                    39 pass / 1 fail; the failure is the
                                        pre-existing dist/kiro-ide expectation
  dist/kiro doctor                      0 problems
  aidlc-graph.ts compile --check        exit 0
  replay on a throwaway fixture, real captured payload shapes:
    dispatch Pre  -> latch holds the persona
    delegate runs `aidlc-orchestrate.ts next` -> exit 2, names the persona
    dispatch Post -> latch empty; the same command then passes
    orchestrate_subagent Pre -> latch holds stages[].role
    two distinct personas inflight -> exit 2 naming both
    reviewer-scope with no dispatch record -> exit 0 (fails open, no crash)

NOT verified here: attribution when two DIFFERENT personas are inflight AND one
issues a guarded call. No capture contains a nested call inside a parallel window,
so the joined-name path above is reasoned from the nesting property rather than
observed. It errs toward refusing.
The 31 files of harness/kiro-ide/ go away and every registry that named the row
loses that name. One Kiro row remains, serving Kiro IDE and Kiro CLI from the
same .kiro/ shell. The content is not lost - it is in git history, and its
adapter's targets were merged into the kiro adapter first.

Registries updated: PluginTargetKind and HarnessManifest.plugin.kind drop the
"kiro-ide" member (so the compiler now rejects it rather than accepting a name
with no row), doctor's productNames map, verify-release's distribution list,
plugin-test's compose env, package.ts and onboarding.ts header comments.

Five things the mechanical rename left behind, each fixed here. All five build
and typecheck clean, which is why none of them would have been noticed by a gate:

  - scripts/plugin-hooks-template/compose.ts: `const isKiroIde = false` sent .kiro
    down the CLI branch, which looks for agents/<persona>.json and reads
    trustedAgents out of agents/aidlc.json. The merged row has neither - it is a
    Markdown row. So plugin composition on Kiro was looking for files that no
    longer exist, and every dispatched stage would have been rejected with a
    remediation telling the author to write agent-v1 JSON. Now .kiro uses the
    Markdown surface and reads trustedAgents from the conductor's frontmatter.
  - The same file's stricter dispatchable predicate is gone rather than rerouted.
    It required a non-empty `tools:` grant plus well-formed permissions.rules,
    and the 14 personas ship from core/ with neither field while being dispatched
    successfully today; a controlled-experiment subagent authored with no
    permissions block also ran its shell command with no prompt. Requiring those
    fields would have reported every persona as an absent dispatch surface. For a
    Markdown agent the presence of the file is the whole check.
  - core/tools/aidlc-plugin-emit.ts emitted the plugin compose hook on
    SessionStart only. docs/features/hooks.md gives Session Start as IDE-only, so
    on Kiro CLI a plugin would simply never compose and nothing would say so. It
    now ships both SessionStart and AgentSpawn, matching the row's own
    aidlc-session-start.json.
  - core/tools/aidlc-utility.ts gated the settings/cli.json doctor check on an
    agent-v1 aidlc.json being present. Markdown-only means that check stopped
    running entirely - and cli.json is the file that pins the agent engine and
    activates the workspace default agent. It is unconditional now, and the
    conductor check names aidlc.md instead of aidlc.{json,md}.
  - Two dead duplicate branches from the bulk rename: scripts/build-binaries.ts
    ran harnessRuntimeGate(..., "kiro", ".kiro") twice, and
    core/tools/aidlc-runner-gen.ts had two `harnessName === "kiro"` tests where
    the first shadowed the second, so the CLI-only wording won and the merged
    both-surfaces wording was unreachable. Also aidlc-init.ts's first-run hint
    named only Kiro CLI; it names both entry points now, like the manifest's
    configNextStep.

Verified: bun scripts/package.ts clean, bun run typecheck 0 errors across all
three configs, dist/kiro doctor 0 problems, and the emitted test-pro plugin
manifest carries both triggers with the plugin's agent shipping as .md (which the
corrected surface extension now matches).

Test-tier state is NOT clean yet and this commit does not claim it: the harness
matrix, drive calibration and several smoke/e2e files still assert the kiro-ide
row and the agent-v1 JSON conductor. That migration is the next commit.
Finishes the merge: nothing in tests or docs still describes a second Kiro row,
and the tier is at parity with the base commit.

DOCS. docs/guide/harnesses/{kiro-cli,kiro-ide}.md become one kiro.md - the two
files described the same install of the same tree for the two surfaces of one
runtime. The merged guide carries the IDE's model requirement and its
non-interactive-PATH tip, both entry points, and a hooks chapter DERIVED from the
shipped manifests rather than transcribed (the old IDE table had drifted: it
still named aidlc-mint and aidlc-block, and its log-subagent matcher predated
orchestrate_subagent). The upgrade cleanup block is kept and extended: the
agent-v1 conductor, the persona JSONs and the `.kiro.hook` registrations are all
retired now, and an overlay copy cannot delete what it no longer ships. Every
roster and inbound link follows (README, docs/README, the harness index, getting
started, cli-commands, customization, glossary, codex-cli).

Two prose surfaces were wrong for the merged row rather than merely stale, and a
test caught each:
  - the conductor SKILL told the user to "exit or restart Kiro CLI" for a fresh
    session, which is wrong advice to someone reading it in the IDE;
  - question-rendering.md called itself "the Kiro CLI harness annex" and said
    "Kiro CLI has no structured-question tool" - true of the row, misattributed
    to one of its surfaces.

TESTS. 50 files. Mostly mechanical (a harness name leaves a list, a dist path
repoints), plus these judgement calls:
  - t148 rewritten: 13 of its 20 cases asserted the agent-v1 shape. The
    properties they pinned all still matter, so they were re-expressed against
    the Markdown conductor - a match-scoped shell grant instead of a blanket one,
    no `subagent` on a persona, no model pin, every registration reaching this
    row's adapter - plus the new ones the merge creates: the resources a custom
    agent does not auto-load, and the engine pin.
  - t245 renamed and repointed, and taught that a manifest may carry two hooks
    (one lifecycle responsibility needs both surface-specific triggers; a
    delegation needs both of its edges). Its per-file shape contract complements
    t331's coverage contract - neither subsumes the other.
  - t218 renamed to name what it covers (channel normalization, not a row).
  - Three tests pinned the ABSENCE of enforcement this row now has, so they were
    inverted rather than deleted: reviewer-scope registration (t221), review-freeze
    registration (t264), and the "IDE-native enforcement only" doc parity (t239).
    The matching carve-out in core/aidlc-common/protocols/stage-protocol-reviewer.md
    - "on a harness without reviewer-scope enforcement (Kiro IDE today), do not
    write the record" - is gone with them: no shipped harness lacks it now, so
    `reviewerScopeRegistration: "unsupported"` has no subject either.
  - harness-matrix: the kiro-ide row is gone; the kiro row's capabilities follow
    the merged reality (Markdown agents, manifest-registered reviewer-scope, a
    plugin wiring file it now emits). `kiroAgentJson` and its `kiro-resources`
    sibling are deleted rather than left false everywhere - a capability no row
    has is a selector that silently matches nothing, and three tests were already
    keying off it.
  - manifestGrantsIdeAgentTools also accepts an authored agents/*.md, because a
    row that writes its conductor outright read as granting nothing.

FOUR MORE DEFECTS, all found by a test after the paths were repointed:

  1. dist-release/kiro shipped no .vscode/settings.json. The
     `kiroAgent.trustedCommands` pre-trust was a nativeRootIntegration on the
     folded row only, so the native channel lost it - and an untrusted project
     command on the IDE surface silently never runs. Carried onto this row: the
     integration follows the surface that needs it, not the row name.
  2. The dispatcher read hook stdin unbounded, in TWO places (an early buffer
     before the pinned-version handoff, and the adapter route itself). A host
     that never closes stdin wedged the hook forever. Both go through a ceiling
     now, mirroring the one the adapter's own entry point already documents as
     defensive; when the ceiling fires the caller must also exit explicitly,
     because a pending Bun.stdin.text() keeps the loop alive.
  3. My own bulk rename deleted four POSITIONAL arguments that happened to sit on
     their own line: two `distSurface(<harness>, ...)` calls, a
     `join(REPO_ROOT, "dist", <harness>, ...)`, a `composeSynthetic(..., <harness>,
     cb)`, an install `--harness <name>`, a `[".kiro", <name>]` fixture tuple, and
     the harness slot of both t218 dispatcher helpers. Each still compiled. Found
     by auditing every removal site against git, not by reading the diff.
  4. t188's plugin-compose expectations followed compose.ts: the remediation text
     names a Markdown agent now, and the case that varied frontmatter GRANT shape
     was rebuilt on the two axes that actually govern (the file exists, and the
     conductor trusts the name) - grant completeness does not, which is why the
     stricter predicate went away in the first place.

Verified, sequentially (two batteries at once cross-contaminate):
  bun run typecheck              0 errors
  bun test tests/unit            failure set IDENTICAL to base 78a6879
                                 (25, all in t186/t208/t209 - pre-existing)
  bun test tests/smoke+integration
                                 52 failures vs base's 52, differing by one
                                 live-SDK case in each direction (t72 here,
                                 t-journey-workspace there) - model flake, and
                                 every remaining failure is in a suite base
                                 fails too
  tests/.coverage-registry.json  regenerated (it is generated, not authored)
Review found four P1s. Three are fixed here; the fourth (the fail-open /
fail-closed asymmetry between reviewer-scope and state-transition-guard) is a
policy decision and is still open.

P1-1 — the delegation latch collapsed identical concurrent dispatches. Keyed by
sha256(tool_name + tool_input), two dispatches of the same persona with the same
prompt hashed to one entry and the FIRST close released both, so enforcement
dropped while a delegate was still running. Read-modify-write also had no lock,
so two adapter processes opening windows at once could lose one another's update.
Both go away with an append-only NDJSON ledger: each open is its own record, each
close cancels the most recent open with a matching key (LIFO), and an append is a
single write with nothing read first. It compacts only when nothing is inflight,
where truncation cannot race an append. A torn final line is skipped rather than
discarding the ledger.

P1-3 — reviewer-scope's matcher named execute_pwsh, but canonicalTool translated
only `shell` and `execute_bash`, so a pwsh call matched the manifest, reached the
adapter, and returned 0. The gate read as present and enforced nothing on Windows.
canonicalTool now maps every TERMINAL_TOOLS spelling, and t331's matcher-parity
list carries execute_pwsh and shell for both terminal-capable guards - it was that
list's omission that let the mismatch through.

P1-4 — subagent_response was classified as an opaque mutation. It is the shell a
delegate reports through: it names no path and writes nothing, so "a write whose
target went missing" is the wrong question for it. Two changes, because one was
not enough: it leaves mutationCapableTool (keeping it out of the legacy recovery
machinery), AND plan-approval-guard returns early for it, because the core guard
treats any tool it does not recognize as mutation-capable and refused a call that
mutates nothing.
  Reproduce before: bun test tests/unit/t147-kiro-hook-adapter.test.ts \
    -t '1f: response shells stay inert for pre-dispatch hooks'   # expected 0, got 2

t147 migration, partial. Three cases asserted the agent-v1 registration channel
(14 persona JSONs each registering `state-transition-guard <its own name>`), which
is what the latch replaces. They are rewritten onto the mechanism that governs
now - open a window, the guard enforces against the delegate; close it, the main
session runs the same verb freely - which also gives the latch the regression
tests the review asked for, including 5d2 for the identical-dispatch collapse.

🔴 THIS COMMIT DOES NOT CLAIM t147 IS GREEN. 25 pass / 11 fail, against 21/14 at
the previous commit and 35/0 on base 78a6879. The remaining 11 are merge
regressions in the adapter with at least three distinct causes, all still open:
  - bare `subagent` crew payloads reach the core guard unrecognized
    ("unknown mutation-capable tool: subagent") instead of the dispatch path;
  - something writes .aidlc-subagent-inflight where log-subagent must not;
  - record-human-turn records no HUMAN_TURN on the unattended prompt path.

🔴 AND A CORRECTION. The previous commit claimed "unit failure set IDENTICAL to
base". That was wrong. Both tier logs were truncated - no summary line, 364 and
352 lines for a 272-file tier - and the claim was computed from them without
checking that they were complete. t147 alone is a 13-case regression they did not
show. Nothing in this branch's verification should be trusted above a log that
ends in a summary line.

Verified here: bun run typecheck 0 errors; t331 / t245 / t148 / t221 / t264
158 pass / 0 fail; t147 improves 21/14 -> 25/11.
t147 exercises the adapter against live-captured payloads. It was 21 pass / 14
fail after the row merge and 35 / 0 on base - the merged adapter is IDE-derived,
so every CLI-surface shape it never saw was silently dropped. Six distinct gaps,
each one a lost audit row or an unenforced guard rather than a crash:

  - `tool_response` is not always a string. Two captured shapes exist -
    `{items:[{Text}]}` from a crew completion and `{success,result:[...]}` from a
    write - and both were classified malformed, which discards the WHOLE event.
    No SUBAGENT_COMPLETED row, and the Stop hook's inflight marker never cleared.
    Decoded now; a value that is no transport at all (number, boolean) still
    reports malformed, because that is the case worth surfacing.
  - The crew shape (`subagent` + `stages[].role`) reached plan-approval-guard
    unrecognized, so the core guard answered "unknown mutation-capable tool:
    subagent" instead of the Code Generation refusal. Routed through kiroDispatch,
    which already normalizes it and already skips malformed stages.
  - log-subagent read identity only from the tool name and the result prose, so a
    crew or direct dispatch recorded `Agent Type: unknown` - the persona is in the
    PAYLOAD (`stages[].role`, `name`). And an empty result dropped the row even
    when the payload named the delegate; the drop guard exists to prevent an
    identity-less fiction, which is not what that is.
  - audit-and-sensors scraped the written path from result prose only. That holds
    for the IDE's captured writes, which carry empty inputs, but the CLI POPULATES
    them - so it audited the wrong path or none. Input first, prose as fallback.
  - A batch write names every target in `operations[]` and the core audit hook
    records one artifact per invocation (it reads `file_path`; there is no `paths`
    handling), so a batch of two produced one row. Invoked per path now.
  - canonicalWriteTool did not know `write`, which the payload fixture's own
    provenance calls part of the defensive adapter vocabulary, and ignored the
    `command` mode that distinguishes a create from an edit - so an edit under
    that spelling was audited as a Write.

Two contracts stated in code that were only implied before:

  - A delete is not an artifact write. The write-audit manifest's matcher already
    said so by omitting delete_file, and t147 pins that a delete leaves no audit
    heartbeat - but the direct and dispatcher entry points bypass that matcher, so
    the adapter says it too. (This also answers the review's question about that
    matcher: the omission is deliberate, not a gap.)
  - subagent_response is a dispatch auxiliary, named once and used by both the
    opaque-mutation classifier and the plan-approval early return.

t147: 21/14 -> 31/5. Five remain, all still open and none of them these shapes:
the unattended prompt path records no HUMAN_TURN (verb-intercept vs the separate
record-human-turn registration), todo_list state sync, the deliver-stage-rules
advisory warning (5b/5c), and reviewer-scope's defensive vocabulary (5f).

Verified: bun run typecheck 0; t331 / t245 / t148 / t264 / t265 140 pass / 0 fail.
t147 is green: 36 pass / 0 fail, from 31/5 at the previous commit, 21/14 when the
row merge landed, and 35/0 on base 78a6879.

THE ONE THAT MATTERED. reviewer-scope, guard-tool-call and deliver-stage-rules
read the RAW payload (`kiro`), and none of them was in INPUT_TARGETS - the set that
decides whether `run()` reads stdin at all. So `input` was "", `kiro` stayed `{}`,
and each returned 0 on its first field access. Three guards that were registered,
dispatched to, and enforced nothing; `doctor`, `graph compile --check` and the type
checker all pass, because the payload arrives as JSON on a stream and its absence
looks exactly like a tool call that needs no guarding. Adding those three names
fixed three separate t147 cases at once. sync-workflow-state joins them for the
same reason but stays out of PAYLOAD_TARGETS on purpose: a malformed payload there
must fall back to the audit-tail reconciliation rather than drop the event.

sync-workflow-state also gains the CLI path it lost. The IDE gives no task
payload, so the merged adapter read the slug from the audit tail for everyone. The
CLI DOES give it - a todo_list create whose task description ends in "[slug]" - and
reading it directly is exact where the audit tail is a reconstruction. Payload
first, marker as fallback.

The mint moved, and a test still described where it used to be. verb-intercept
minted HUMAN_TURN on the pre-merge CLI row, where hook wiring lived inside the
agent config and there was no second registration to double-count with. This row
registers aidlc-record-human-turn.json separately, so verb-intercept must NOT mint
- re-adding it would record two turns per prompt. The test now pins both halves:
verb-intercept does not mint, record-human-turn does, and AIDLC_UNATTENDED
withholds the ledger event.

Two comments named manifests that do not ship: aidlc-mint.json and
aidlc-block.json, for what are actually aidlc-record-human-turn.json and
aidlc-enforce-approval-gate.json. A reader following either name finds nothing.

Verified: bun run typecheck 0; t147 36/0; t331 / t245 / t148 / t221 / t264 / t265 /
t218 278 pass / 0 fail.
…nd CI

Codex's review found the documentation migration incomplete, and it was right in
the way that matters most: the literal `kiro-ide` grep I judged by cannot see a
sentence that describes the split without naming the row.

harness/kiro/onboarding.fills.ts is the worst of it, because it RENDERS INTO THE
SHIPPED AGENTS.md - a file loaded every session. It said the install runs "on the
**Kiro CLI harness**", made Kiro CLI >= 2.6 a hard prerequisite (wrong for an
IDE-only project), and told every reader that "the `/aidlc` session runs from
`agents/aidlc.json`; all 14 expert roles have JSON configs". Those files do not
exist. It now names the row and both of its entry points, and points at
agents/aidlc.md.

Changing that text changes the rendered AGENTS.md bytes, which surfaced an
upgrade-path obligation the packager enforces: the manifest's
legacySignatures.wholeFileHashes must still recognize the variant that shipped
before this change, or an upgrade treats a stale unmarked AGENTS.md as user-owned
content and leaves it. Added, with the reason, plus the current render in t243's
expectation (the packager appends it, so that list moves whenever this text does).

Reference prose, per site rather than by search-and-replace, because some
"Kiro IDE" / "Kiro CLI" sentences are legitimately about a SURFACE and must stay:
  - 06-hooks-and-tools: the reviewer-scope Identity paragraph described the
    per-agent registration that no longer exists; it now describes the delegation
    window, including the parallel-persona case where the adapter declines and
    records a drop. The reviewer-scope registration row, the per-harness
    registration sentence, the deliver-stage-rules row, the marker-mtime row and
    the harness-dir sentence follow. The Stop-block table keeps its two SURFACE
    rows (the IDE cannot block, the CLI can) and loses the retired v2 engine row.
  - 14-claude-features: the primitive map had a Kiro CLI column and a Kiro IDE
    column; they are one column now, and t239 reads index 2 instead of 3.
  - 05-agent-system, 13-customization, 18-plugin-mechanism: the agent-v1 JSON and
    the "tools: + permissions.rules are required" premise are gone. 18 also states
    the measured truth that those fields are OPTIONAL on a Kiro agent - the 14
    shipped personas carry neither - so the remedy for a rejected plugin stage is
    the file plus conductor trust, not frontmatter.

.github/workflows/release.yml still installed and doctored a `kiro-ide` harness in
both its PowerShell and bash lifecycle loops. t244 pins those exact strings, which
is how it was found - I had updated the test's expectation in an earlier commit and
not the workflow it describes.

P1-2, the last of the review's P1s, is resolved as a documentation defect rather
than a behavior change. The two persona-scoped guards DO differ under ambiguity,
and the reason is what each needs: state-transition-guard needs only presence, so
two inflight personas still answer its question; reviewer-scope compares identity,
so it cannot. My earlier comment claimed the code "errs toward refusing" while it
did the opposite for reviewer-scope. The comments now state the asymmetry and its
reason at both sites, and reviewer-scope records a visible drop instead of
declining silently.

CHANGELOG gains an Unreleased entry with the Breaking and Upgrade notes, including
the retired surfaces an overlay copy cannot delete. AIDLC_VERSION is deliberately
NOT bumped: the release workflow rejects a tag that does not match it, so the bump
belongs to whoever cuts the release.

Verified: bun run typecheck 0; t243 84/0; t244 36 tests 0 fail; t239 / t221 /
t266 / t147 134 pass / 0 fail.
Codex's re-review of the previous four commits kept REQUEST_CHANGES with five
blockers. All five were real; four were defects I introduced and one was a policy
I decided to ignore and should not have.

BLOCKER 1 - a crew dispatch opened one window per persona and a close released
only one. openDelegation appends an open per agent under the same dispatch key, and
the replay cancelled the most recent match, so a two-persona crew left one open
inflight until the 6h TTL - and the lifecycle guard kept refusing the MAIN
session's own verbs after the crew had finished. Records carry a `group` now, one
per openDelegation call: a close cancels the whole most-recent group for that key,
while two IDENTICAL dispatches still need two closes because they are two groups.
A pre-group record replays as its own group so an in-flight upgrade strands
nothing. Regression: t147 5d3.

BLOCKER 2 - `write` was missing from the review-freeze, reviewer-scope and
write-audit matchers. The payload fixture carries `tool_name: "write"` and the
adapter canonicalizes it, so a frozen-artifact write and a reviewer-scope
violation under that spelling reached nothing, and the write went unaudited. Every
matcher is now derived from the canonicalizers rather than hand-listed, and t331's
parity lists carry `write` and `read` - it was that omission that let this pass.

BLOCKER 3 - plain `subagent` was missing from the log-subagent matcher while
DISPATCH_TOOL_NAMES included it. The Pre edge could open a window through a
matcher-free registration, but the Post edge never fired: stale latch, and no
completion audit for the crew shape the fixture actually carries. t331 called the
list "all three shapes" while checking `subagent_<name>`; it is four shapes.

BLOCKER 4 - reviewer-scope still failed open with two different personas inflight.
I had reclassified this as a documentation defect. That was wrong: passing an empty
identity lets a delegate read outside its review whenever a second delegate happens
to be inflight, which is the gap the persona axis exists to close. The adapter now
resolves the ambiguity from the dispatch record the core hook itself reads - if the
reviewer it names is among the inflight personas, that is the identity. The
residual cost is a possible false refusal of another delegate's out-of-scope call,
which is the direction to err in and is recorded either way. Regression: t147 5g.

BLOCKER 5 - the ledger compaction was a check-then-truncate race, and my comment
claimed it could not race "when nothing is inflight". Nothing stops another adapter
process from opening a window in that gap. Compaction is gone; records are small
and expire by TTL, so an unbounded ledger costs disk rather than correctness.

RELEASE METADATA IS DELIBERATELY ABSENT. The previous version of this commit
bumped core/tools/aidlc-version.ts, the README badge and a CHANGELOG heading,
because AGENTS.md:54-60 and CONTRIBUTING.md:56 both say every user-visible PR must.
Those documents are stale: the maintainers asked on 2026-09-08 that a PR NOT carry
the bump - "we will bump version when releasing" - and asked for exactly that
change to be reverted on another PR. So this branch touches none of the three, and
the Breaking / Upgrade notes a release entry would need live in the PR body where
whoever cuts the release can lift them. The documented policy and the practice
disagreeing is worth raising separately; it is not this PR's to settle.

Also from the re-review:
  - sync-workflow-state matched only `execute_bash`, so the CLI's todo_list
    PostToolUse never reached the target the previous commit taught to parse it.
    Only the direct adapter test was green.
  - `legacy-ide-notice` existed as two special-cases in the dispatcher and nowhere
    else. It is implemented now, per the design: ONE legacy `.kiro.hook` on
    promptSubmit, because that format is the only channel an unsupported Kiro IDE
    0.x reads AND it does not fire on a supported one - the channel is the version
    check. The target reads neither stdin nor the normalized payload (a 0.x host
    never closes stdin), confirms the legacy channel through USER_PROMPT, prints
    the upgrade notice and exits 2. Measured on a throwaway fixture: no
    USER_PROMPT -> exit 0 and no output; USER_PROMPT set -> exit 2 with the notice.
    t245 and t148 pin that this is the ONLY legacy file, and t147 9 pins both arms.
    Still unverified on a real 0.12 host - this machine has none.
  - docs claimed the projection adds `tools:` and `permissions.rules` to Kiro
    personas while also saying the 14 personas carry neither. The generated files
    carry neither; the capability frontmatter is the CONDUCTOR's. 05-agent-system
    and 09-porting now say that, and 09 notes frontmatterAdditions has no user.
  - t245's expected registration table is regenerated FROM the shipped manifests,
    so a matcher change cannot leave it stale by hand again.

Not addressed here: the double stdin timeout (dispatcher + adapter route each
bound the same held-open stream, measured ~1.3s at a 500ms ceiling) and whether
process.exit truncates stdout. Both are latency and neither loses a decision.

Verified: typecheck 0; `bun scripts/package.ts --check` deterministic across two
builds for all six rows; t147 39/0, t245 43/0, t331 7/0, t148 16/0, t68 7/0,
t221 / t239 / t264 / t265 / t218 all pass.
CI's contract check runs `bun run check` = typecheck AND `biome check
--error-on-warnings`. I only ever ran typecheck locally, so four findings rode the
branch to CI, and every one of them is residue of my own edits:

  - an `x !== null && x.foo` that biome wants as an optional chain;
  - `validIdePermissionRule` in compose.ts, left behind when the over-strict
    dispatchability predicate that was its only caller went away - and removing it
    exposed two more (`inlineYamlListHasValue`, `blockYamlListHasValue`) that only
    IT still used, so the dead set was three functions deep;
  - `existsSync` in t220 and `readdirSync` in t266, unused since the assertions
    that needed them were rewritten onto the Markdown shape.

The lesson is the gate, not the findings: a green typecheck says nothing about
lint, and `bun run check` is the one command that matches what CI asks.

Verified: bun run check 0 (722 files); t147 39/0, t220 47/0, t266, t188 93/0 -
200 pass / 0 fail across the four suites the removals touch.
…e suites

The 36 unit failures on this branch were not stale expectations. Four of them
named real losses, and the tests were the only thing still asking for them.

- The roll-forward seam. verb-intercept kept the IDE row's terminal dedup and
  lost the CLI row's flat markers, while continue-workflow's carve-out — plus
  aidlc-orchestrate's done-guard and the doctor bundle, both in separate
  processes that cannot know a Kiro session id — still read them. Writer and
  reader never met, so the latch was dead. Restores the turn clock, the
  read-only/config-alias latch, the forwarding latch, and the engine
  pre-dispatch alongside the per-session latch.

- The `engine` noun. runTerminalCommand spawned `[executable, ...args]`, so
  every off-band terminal command was malformed on a compiled install.

- The persona grants. frontmatterAdditions went missing with the kiro-ide
  manifest, so all 14 delegation targets shipped with no tools, no MCP grants,
  no shell allowlist and no resources. Restored as frontmatter, reproducing the
  agent-v1 model the row shipped before the merge field for field.

- The active-space re-pointer. It only handled agents/*.json, so a space switch
  left every Markdown agent pinned to `default`. The new arm is anchored on both
  `file://` and the glob tail, because the persona body's
  `spaces/<active-space>/memory/{org,team,project}.md` is a placeholder a looser
  pattern would overwrite.

The conductor's `permissions` comment claimed the schema has no subagent
capability; it does. What actually lives in toolsSettings.subagent is which
agents may be spawned and which skip the prompt.

Test moves: agent configs are read out of frontmatter; t252 drops the conductor
from the behavioural vectors (it states the same policy as capability globs) and
asserts that declared policy separately rather than guessing Kiro's glob
matcher; t308 names its matcher-bearing manifests instead of counting them;
t316's shared-leaf guard moves to the .aidlc pair, which is still shared;
t299 stops expecting a "(not probed)" row now that every harness is probed.

Verified: bun run check clean; smoke+unit 286 files, the 8 affected files green.
Two unrelated failures remain — t255 passes in isolation (a 2s signal race under
--parallel 8) and t298 fails identically without this change.
…int the prose the merge invalidated

The reviewer-scope ambiguity branch recorded a drop whenever more than one
delegate was inflight and no identity could be attributed - including when no
dispatch record existed at all, which is the ordinary state of a crew stage.
Measured: three guarded calls during a two-delegate crew with no review in
flight appended three lines. A non-empty .drops file is a release signal here,
so a normal stage was filling it. Record a drop only when a record exists -
enforcement was expected and did not happen - and say which of the two ways it
could not be attributed.

Three prose sites still described the channel this row removed: the Kiro CLI
adapter asserting scoped_registration from per-agent JSON configs, and Kiro IDE
shipping no registration at all. The field itself stays - OpenCode is now its
only sender and still needs it - so only the attribution changes, and the
porting guide keeps the pattern while gaining the two mechanisms that replaced
it.

t147 5g2/5g3 pin both halves of the branch, and 5g now also asserts the
attribution drop it always produced but never checked.
Measured on a real Kiro IDE 0.12.333, which we had never had until now. Two
premises behind the notice were wrong.

Exit 2 is not a refusal there. The host's hook runner turns any non-zero exit
into HookFailedCommandError - "Hook '<name>' command execution failed" - and
nothing downstream reads the code as a denial. So the notice arrived as a BROKEN
HOOK, and the session's own agent did what that framing invites: it investigated
for two minutes, concluded a current build was being misidentified, and wrote
"enabled": false into the manifest.

promptSubmit cannot refuse anything there either. What 0.12 does honour is text
on a preToolUse hook: its own instructions tell the model that when the output
denies access it is FORBIDDEN from retrying and MUST NOT proceed under any
circumstances. So the notice now rides preToolUse with toolTypes ["*"], exits 0,
and writes the denial FIRST on stdout - stdout because the runner takes
`stdout || stderr`. Re-measured on the same host with the shipped artifact: the
read was abandoned, the notice relayed to the user, no attempt to disable it, and
23s instead of 1m58s.

The USER_PROMPT gate survives the move: that variable is populated on this
trigger too, carrying the tool input as JSON rather than the user's prompt. Only
its presence is read.

This refuses per tool call rather than per prompt, which is the better seam -
nothing in AI-DLC advances without a tool. It is enforcement by instruction, not
a hard block, and the guide now says so instead of implying the IDE stops the
turn.
A supported IDE does not run a `.kiro.hook` - measured on 1.0.437, where nothing
fired - but it does LIST one as `legacy` beside a `Migrate` button, and one click
turns this manifest into a current-generation hook the host does run. The
USER_PROMPT gate cannot catch that: 1.x populates USER_PROMPT for a runCommand
hook too. Migrated, on preToolUse, the notice would deny every tool call on an
install AI-DLC fully supports, and the button sits in the hooks panel of every
such install.

So the target reads the host's version instead of trusting the channel.
VSCODE_IPC_HOOK names the socket by version line - `0.12-main.sock` on 0.12.333,
`1.0.-main.sock` on 1.0.437, both measured, hence the optional third dot - and
anything that is not a 0.x major stays silent. So does an absent or unparseable
value: informing is this target's only job, and refusing work on a supported
install is the worse error, so silence is the failure mode to prefer. The cost is
that a 0.x host which stops setting the variable gets no notice.

Verified against the running host's real socket path, not a fixture literal:
silent on 1.0.437, denial on the 0.12.333 value. The guide now says to leave the
legacy hook alone rather than migrate it.
The row advertised `Kiro CLI >= 2.6` in five places. That predates the engine it
now targets: the v3 engine first shipped in 2.8.0, behind `kiro-cli --v3`, and
this row is v3-only. A 2.6 install would read the tree, ignore the engine pin in
`.kiro/settings/cli.json` as an unknown key, come up on the 2.x engine, and never
read the standalone hook manifests - working badly rather than refusing.

The floor is now 2.21.1, which is where the pin was measured rather than where
the feature began. The honest reason it is not lower: `chat.agentEngine` is
absent from the published settings reference and from every changelog entry
between 2.2.0 and 2.21.0, so there is no public record of when it started
working, and the version where we watched a plain `kiro-cli` reach the engine is
the only defensible claim. The prose points anyone on an older 2.x at `--v3` and
says plainly that we have not tested that path.
…or all of them

Audited the generated AGENTS.md against the tree it ships beside. The row merge
left prose describing one surface, or the pre-merge wiring, as if it were still
true, and the shared skeleton carried defects that reach every harness.

This row (`harness/kiro/onboarding.fills.ts`):

- "blocking stop hook" was listed among the features the install relies on. Two
  paragraphs of our own guide say the opposite - `Stop` cannot refuse a turn on
  either Kiro surface, which is why every guard that must refuse sits on
  PreToolUse - and the same AGENTS.md contradicts the claim further down.
- "the workspace default agent" was listed the same way. It comes from
  `settings/cli.json`, which the IDE ignores, so it is not a feature both
  surfaces have; the bullet then invited the reader to delete it.
- The activation bullet marked only its last clause as IDE-specific, leaving an
  IDE reader to assume the engine pin and the precedence note applied to them.
  It also omitted the shipped `chat.modelDefaults` effort default.
- "all 14 expert roles" miscounted its own list, which the same sentence breaks
  into 11 personas, 2 review-only agents and the composer. And delegation goes
  through more than one tool name, as the log-subagent matcher shows.

The shared skeleton (`core/templates/onboarding.md`), affecting every row:

- Three bullets told the reader to type `{{INVOKE}}-knowledge`, which renders to
  `bun .kiro/tools/aidlc.ts-knowledge` - a command that cannot exist. The
  user-facing token is `{{SKILL_INVOKE}}`, so these are `/aidlc-knowledge` and
  friends. Nine spellings fixed.
- The DocumentKB bullet shipped twice, and the two copies disagreed: one
  documented `summarize`, the other did not. Kept the one that does.
- "All 17 hooks are TypeScript (`.ts`)" is true of no row here: hooks are
  declared as data, and the count differs per harness. The sentence's real point
  - no executable bits - survives without either claim.
- "unlike the three above" pointed at the wrong bullets; they are below.
- The skill directory was said to hold the stage protocol and per-phase stage
  files. It holds the orchestrator and question rendering; those files ship
  under `aidlc-common/`.
- Both `{{SLOT:...}}` section markers sat with no blank line above, so a filled
  slot glued its first heading to the preceding paragraph and an emptied one
  glued the next template heading. Every row shipped two glued headings; all six
  are now clean.
The conductor's shell grant was `bun .kiro/tools/aidlc-*`. Shell match patterns
are globs where `*` is any sequence (features/permissions.md), so that requires a
literal `-` after `aidlc` and never matches `bun .kiro/tools/aidlc.ts` — which is
the only shell command the orchestrator skill issues, 22 spellings out of 22. The
conductor therefore prompted on every turn of its own forwarding loop.

The glob text is stock, inherited from `harness/kiro-ide/agents/aidlc.md`, but the
effect is this branch's: before the merge the CLI row's conductor was an agent-v1
config whose `allowedCommands` regex — `bun (run )?["']?\.kiro/tools/[A-Za-z0-9._-]+\.ts…`
— did cover the dispatcher. Keeping the IDE row's Markdown agent kept the narrower
grant, so CLI users lost a pre-approval they had.

Restored as `{{INVOKE}} engine *` rather than by widening the glob to `aidlc*`.
The native channel pre-approves only `aidlc engine *` and leaves the other route
namespaces (`config`, `compose`, `intent`, `space`) prompting; expressing the same
boundary here keeps the two channels honest instead of trusting more on one of
them. A path glob cannot say it, because the namespace lives in the arguments.

The token spelling is what makes both channels work: `{{INVOKE}}` projects to the
bun dispatcher on one and to `aidlc` on the other, where it collapses onto the
line the tool glob already becomes — so the release rewrite now drops the
duplicate. Writing the bun spelling literally instead made the release projection
produce `aidlc engine engine *`, which its own guard rejected.

The prose bullet described the personas' allowlist, not this agent's: it claimed a
`bun run` spelling and quoted paths that only the persona regex has, called the
grant read-only when the agent holds `write` and `shell`, and said "ONLY" while
omitting the filesystem writes and the 14 pre-trusted personas. Rewritten from the
file, with the deliberate prompts named and the IDE's Agent Autonomy caveat added
— that setting is evaluated before any of this, so Supervised mode prompts anyway.
Both were mine, from the round that fixed the onboarding prose.

`Hooks are declared as data` is false for the opencode row, which registers its
hooks through an auto-discovered TypeScript plugin rather than a wiring file
(`harness/opencode/manifest.ts:19` calls it exactly that). The sentence exists to
say no hook needs an executable bit, so it now says only that, and claims no
mechanism on behalf of six different harnesses.

`the only command the orchestrator skill issues` overstated a real measurement.
The skill issues 22 shell calls: 19 on the dispatcher's engine route, and three
`config` routes that the grant deliberately leaves prompting on both channels,
since they rewrite the install. The comment now says which and how many.
Changing the shared onboarding text moved the root file's hash, and the install
descriptor's `legacySignatures.wholeFileHashes` is how `aidlc config` recognizes
a tree it has written before. The packager appends the CURRENT render to whatever
the manifest declares, so a text change silently drops the previous render from
that set unless it is promoted — an install carrying it would stop being
recognized as ours. The precedent is in this branch already: the last commit to
touch this text (`fcf3aed6`) promoted the outgoing hash into the manifest and
added the incoming one to t243. Same two moves here, for the kiro and codex rows
— the only ones whose `AGENTS.md` is a managed-block root integration.

t243 now passes 84/0. It was the only suite that asked this question, and none of
the targeted runs around the prose commits included it.
…t has none

The prose said 0.12 has no exit-code refusal at all. Our own control run says
otherwise: `kiro-test/kiro-ide-0.x-test/RETEST-0.12.333.md` records a preToolUse
hook exiting 2 refusing a read outright, with no retry. The mistake was mine and
it was a generalisation from the wrong seam — I read the host's hook runner as far
as `HookFailedCommandError` and concluded "never a refusal", without following the
rejection into the preToolUse caller. What 0.12 actually does is seam-specific:
exit 2 on `promptSubmit` stops nothing (measured — the turn continued and the
notice arrived framed as a broken hook), while exit 2 on `preToolUse` refuses the
call.

So the notice now carries both halves. The exit code is the enforcement, honoured
by the host rather than by the model's goodwill; the denial text is the
explanation, and it still leads, because this host's preToolUse contract tells the
model that output denying access forbids retrying. Written to both streams
deliberately: `stdout || stderr` is the success path's rule and does not apply to
a non-zero exit, and the delivery we actually measured on a non-zero exit was via
stderr.

Unchanged: the USER_PROMPT gate, and the host-version guard that keeps a migrated
copy of this hook silent on a supported IDE. That guard matters more now, not
less — with exit 2 a migrated hook would refuse every tool call there.

Verified on the shipped tree: 0.x socket + payload → exit 2 with the denial
leading both streams; supported socket → exit 0, silent. t147 41/0, check clean.
The behaviour on a real 0.12.333 host is the user's re-test, not yet run.
wowzoo added 2 commits September 15, 2026 22:42
…ranch

Clean: 0 conflicts. `awslabs#1185` touches five files this branch had just edited
(`aidlc-config-diagnostics.ts`, `aidlc-init.ts`, `15-troubleshooting.md`, `t296`,
`t304`) but on other lines — a comment extension, a new troubleshooting row, and
new assertions.

Checked for the semantic collision the textual merge cannot report, because the
last absorption hid exactly this: after merging, no test asserts the product name
`Kiro CLI` and no prose describes Kiro as two harness rows, so upstream's new
`t296`/`t304` assertions read this row's own wording. This branch's provider
prose and the two-server registry text both survived. The `kiro-ide -> kiro`
upgrade wiring in `aidlc-init.ts` is intact: five `distributionUpgradesTo` call
sites, two `currentDistribution`, and the two read-only diagnostics sites still
untouched.

Gates: install, package, typecheck, lint and `package.ts --check` rc=0, the last
deterministic across all six rows. t296+t304+t281 40/0, t243 88/0.
…its surfaces

`t299` asserted `In Kiro CLI, effort dials do not apply`, but that sentence
interpolates `descriptor.productName` (aidlc-init.ts), and this row declares
`Kiro` because one row serves both surfaces. CI caught it on shard 2/4.

Third instance of the same class after `comes with Kiro IDE` in t294 and
`comes with Kiro CLI` in t296/t304, and the one my grep missed: I had searched
for `with Kiro CLI`, which does not match `In Kiro CLI`. The net that would have
caught all four is every test expectation carrying `Kiro CLI` or `Kiro IDE` as a
product name; swept now, and the remaining hits are test names, skip messages,
and assertions that name a surface on purpose (how to start a session on each,
the 0.x unsupported notice), which CI confirms still hold.

t299 14 pass / 1 skip.

@rafaelgsr rafaelgsr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for consolidating the Kiro CLI and IDE distributions.

I reviewed the current head 4c5c2cf7094f20462c0a6089cf0e957ed808d2ea against base a517a06166448a456d3934d68a216b627b65547a.

The direction aligns with the project's one-core/many-harness architecture and replaces PR #1063's scope. I found three issues:

  1. [P1] Preview-release lifecycle checks still include kiro-ide. .github/workflows/preview-release.yml:440 and :574 invoke a distribution that this PR removes. The command exits 4, so both Windows and Unix preview-release jobs will fail. Please remove that row and add coverage that keeps preview and stable release rosters synchronized.

  2. [P2] Persisted machine defaults are not migrated. core/tools/aidlc-init.ts:4170 compares default-harness directly without applying the retired-distribution successor map. A machine retaining kiro-ide cannot configure a fresh project without explicitly passing --harness kiro. Please normalize or migrate the saved default and cover this upgrade path.

  3. [P3] Plugin documentation still calls .kiro ambiguous. docs/harness-engineering/10-authoring-a-plugin.md:577 still distinguishes Kiro CLI from Kiro IDE. Please update it for the unified distribution.

I made no changes to the author branch.

Validation performed:

  • bun scripts/package.ts --check — passed
  • TypeScript checks — passed
  • Focused Kiro adapter, manifest, MCP, smoke, and diagnostic tests — passed
  • Current remote CI — all required checks green
  • Both blocking upgrade/release failures reproduced manually on the current head

One remaining risk is that a completed live Kiro workflow tail has not been independently verified.

@leandrodamascena leandrodamascena left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for consolidating the Kiro IDE and CLI distributions and for carrying the upstream fixes into the merged row.

I reviewed the current head 4c5c2cf against current main 891669aa9, with merge base a517a061. The repository already treats both Kiro surfaces as the same .kiro runtime, so the consolidation matches the current architecture. I found three blocking compatibility and release defects.

  1. P1: Preview publication still invokes the retired distribution. .github/workflows/preview-release.yml:440 and :574 still include kiro-ide. Both lifecycle jobs invoke aidlc config --harness kiro-ide, which now exits 4. The next scheduled or manually dispatched preview will fail before publication. Remove the retired entry from both lists and add coverage for the preview workflow inventory.

  2. P1: Previously valid model settings prevent migration. core/tools/aidlc-settings.ts:199-212 now rejects kiro-ide in per-agent model maps. The previous release accepted that key. During aidlc config --harness kiro, settings are resolved before the project stamp changes, so the command exits 4 with unknown harness "kiro-ide". Read the retired key as a compatibility alias for kiro, define the conflict behavior when both keys exist, and add an upgrade regression test.

  3. P2: Read-only configuration and the interactive setup walk reject retired project stamps. selectedDiagnosticHarness, prepareModelsSection, and runSetupWalk pass the persisted kiro-ide identity directly to modelHarness, which now rejects it. I reproduced exit 2 from both config providers --show and config models --show. Apply the one-way successor mapping when reading existing project identity and when matching --harness kiro, while continuing to write kiro.

Validation performed on a local merge with current main: bun run typecheck passed, bun scripts/package.ts --check passed, reviewer-scope tests passed 120/120, and the focused Kiro/configuration suites passed 117/117. The current GitHub CI is green. The disclosed absence of a completed end-to-end Construction tail remains a release risk after these blockers are fixed.

wowzoo added 3 commits September 16, 2026 06:45
Review round on 4c5c2cf found six issues across two reviewers; five share one
cause. The first cut of the upgrade path fixed the places that PERFORM the
upgrade and left every place that READS an identity a previous release wrote, so
a user holding the retired row was refused before reaching the upgrade at all.

- `modelHarness` (core/tools/aidlc-init.ts) resolves the retired id at the gate.
  That is one change for the diagnostics sections, the models section and the
  setup walk, all three of which reviewers reproduced as exit 2 from
  `config providers --show` and `config models --show`. Resolving at the gate
  rather than at its 20-odd call sites is the point: the previous attempt patched
  call sites and missed three.
- `normalizeHarnessModelMap` (core/tools/aidlc-settings.ts) reads the retired key
  as its successor instead of throwing `unknown harness "kiro-ide"`. Throwing
  there put the upgrade out of reach, because settings resolve before the stamp is
  rewritten — the command that would remove the key could not run while the key
  was present. Conflict behaviour is defined: the current key wins, the retired
  one is applied only in its absence, and the outcome does not depend on key
  order. A key that never existed is still a hard error, named as written.
- The configured machine default is normalized, so a machine that recorded the
  retired row configures a fresh project without an explicit `--harness`.
- `.github/workflows/preview-release.yml` no longer invokes the retired row in
  either shell. `release.yml` had already been updated and this one had not, which
  is precisely how it stayed invisible; `t332` now reads both rosters and the
  shipped one.
- `docs/harness-engineering/10-authoring-a-plugin.md` no longer calls `.kiro`
  ambiguous.

The successor map moves to `aidlc-model-policy.ts` beside the harness roster. It
was local to `aidlc-init.ts`, which is why consumers elsewhere never saw it.

Regression tests, each verified failing at the parent with the message the
reviewers quoted: `t298` (retired key read, conflict order, unknown key still
throws), `t243` (both read-only sections exit 0 and report `kiro`; a machine
default of the retired row still lands `kiro`), `t332` (preview and stable
rosters match the shipped one — `Expected - 0 / Received + 1` before).

Gates: typecheck, lint and `package.ts --check` rc=0, deterministic across all
six rows. t243 89/0 · t294+t296+t304 58/0 · t298+t332+t330+t293+t299 69/0.
…ranch

Clean: 0 conflicts. `awslabs#1179` touches `core/tools/aidlc-sensor-traceability.ts` and
its suite, neither of which this branch had edited.

Checked for the semantic collision a textual merge cannot report, since the last
two absorptions each hid one: the shipped rosters and product-name assertions
still read this row's own wording, and the `kiro-ide -> kiro` resolution added in
the review round is intact.

`tests/unit/t281-*.test.ts` carries two files sharing the id — the MCP registry
one and the traceability one. Both are present upstream and at the merge base, so
that collision is not this branch's and is left alone.

Gates: install, package, typecheck, lint and `package.ts --check` rc=0, the last
deterministic across all six rows. t281 (both) + t332 + t298 55/0.
… path

CI on 5750cf4 failed four checks — three unit shards and the deterministic
integration job — and every failing case spawns a hook or a subprocess: t07, t131,
t144, t221, t241, t265b. One cause: the previous commit turned
`aidlc-settings.ts`'s `import type` of `aidlc-model-policy.ts` into a runtime
import, and `aidlc-lib.ts` is loaded by every spawned hook. `t144`'s
`LIB_SIBLINGS` is the list of modules the lib needs at runtime; that module is not
in it, because until now the edge was erased at compile time. The child failed at
import and exited 1.

Adding the module to `LIB_SIBLINGS` would have turned the checks green while
widening the graph every hook loads, which is the opposite of where awslabs#1180 is
taking high-frequency hooks. So the resolver moves to `aidlc-runtime-paths.ts`
instead: that module already owns distribution identity (`legacyDistribution`,
`harnessIdentity`), imports nothing from this project so it cannot cycle, and is
already a lib sibling — no hook loads anything new.

That is the third home for these three lines (local to aidlc-init.ts, where no
other consumer saw them; aidlc-model-policy.ts, which broke the hook path; now
here). The comment records the route so the next move has the reasons.

Gates: typecheck, lint and `package.ts --check` rc=0. t144+t07 26/0 ·
t131+t241+t221 157/0 · t265+t298+t332+t294 111/0.
@wowzoo

wowzoo commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator Author

Thank you both — the reproductions made these quick to confirm, and five of the six turned out to share one cause.

The first cut of the upgrade path fixed the places that perform the upgrade and left every place that reads an identity a previous release wrote. So a user holding the retired row was refused before reaching the upgrade at all — the worst shape this could take, and I had scoped it that way deliberately: I left the read-only sections alone on the reasoning that they should keep reporting the id the project actually carries, and wrote that reasoning into a comment. Reproducing your exit 2 settled it.

Current head is bff9376d, CI green, and it also absorbs 891669aa (#1179) so the branch is not behind.

@rafaelgsr 1 / @leandrodamascena 1 — preview publication invokes the retired row. Removed from both shells in .github/workflows/preview-release.yml. release.yml had already been updated and this one had not, which is exactly how it stayed invisible, so t332 now reads the PowerShell list, both bash lists, and the shipped roster and requires all three to match.

@leandrodamascena 2 — previously valid model settings block migration. normalizeHarnessModelMap reads the retired key as its successor instead of throwing. Conflict behaviour, since you asked for it to be defined: the current key wins, the retired one applies only in its absence, and the outcome does not depend on which key the file lists first. A key that never existed is still a hard error, named as the user wrote it.

@leandrodamascena 3 — read-only configuration and the setup walk reject retired stamps. Resolved at the gate rather than at the call sites: modelHarness maps a retired id to its successor, which covers selectedDiagnosticHarness, prepareModelsSection and runSetupWalk together. Patching call sites is what produced this round's defect — I picked four and missed three — and the returned type still has no retired member, so every write keeps emitting kiro.

@rafaelgsr 2 — persisted machine defaults are not migrated. The configured default is normalized, so a machine that recorded the retired row configures a fresh project without an explicit --harness.

@rafaelgsr 3 — plugin documentation still calls .kiro ambiguous. Updated; .aidlc (Copilot vs OpenCode) is now the only ambiguous root named.

The successor map moved to aidlc-runtime-paths.ts. It began local to aidlc-init.ts, which is why none of the consumers you found ever saw it — and worth flagging since it is visible in the history: my first move put it in aidlc-model-policy.ts, which turned a compile-time import type in aidlc-settings.ts into a runtime import and broke every spawned-hook suite, because aidlc-lib.ts loads settings and that module is not one of the siblings a hook pulls in. CI caught it. Adding it to that sibling list would have gone green while widening what every hook loads, which is the opposite of where #1180 is taking high-frequency hooks, so it now lives beside legacyDistribution and harnessIdentity in a module that imports nothing from this project and is already on the lib path.

On the residual risk you both named. It is real and I am not going to claim it away. I do not think this PR is the right place to close it: validating a full Construction tail takes days of wall clock, main moved five times while this round was open, and each move costs this branch another absorption — waiting does not buy confidence so much as it guarantees the branch is never reviewed at a version that is still current. My plan is to validate the tail against a preview release once this lands, which is what that channel is for, and to bring anything it surfaces as focused follow-up pull requests rather than as more commits here. That is already how the last few went out (#1163, #1171, #1186). A gated Classic-scope run is under way locally in the meantime, on a tree built from this exact head, and I will report what it shows either way.

@apackeer

Copy link
Copy Markdown
Contributor

Thanks for the fast turnaround on the first round and for the reproductions in the reply — the six items are fixed where you said they are, and moving the successor map into aidlc-runtime-paths.ts is the right home for it.

I reviewed the current head 130f5d4ff3efd4d3f8a09bba4dddca5d9696a615 against d8f8c0c3d81614ed9f6ed9be71713ea84a4d0bdc (merge base identical). Alignment rests on @rafaelgsr's and @leandrodamascena's reviews; the delta since 4c5c2cf7 stays within that scope. The findings below were reproduced on bff9376d against dist/kiro built from that head; the only change since is the upstream #1186 merge, which touches none of the cited files.

Blocking

1. P1 — A refused dispatch leaks a delegation window and wedges the main session.
harness/kiro/hooks/aidlc-kiro-adapter.ts:1432-1449 opens the ledger group on log-subagent's PreToolUse edge before any sibling guard has decided. When plan-approval-guard refuses the dispatch (exit 2 on subagent_aidlc-developer-agent before a Plan Approval receipt exists — its designed job), Kiro blocks the tool and emits no PostToolUse, so the close at :1445-1446 never runs and the open lives until DELEGATION_TTL_MS (:279, 6 h). state-transition-guard (:1963-1968) then forwards the stale persona as agent_type, and the main session's own aidlc-orchestrate.ts next is refused with Delegated agent "aidlc-developer-agent" cannot run aidlc-orchestrate.ts next …. Reproduced twice independently; a subsequent record-human-turn does not clear it. This is the same wedge the double-open fix addressed, re-created by any refused, cancelled, or interrupted dispatch. Please cancel the group when a dispatch is refused (or open only once the dispatch is accepted), and add a t147 case: refused dispatch + log-subagent Pre edge → main-session next still allowed.

2. P2 — guard-tool-call is not registered anywhere.
The first-next argument-fidelity guard is only referenced inside the adapter (:1296, :1549); no harness/kiro/hooks/*.json names it. All six shipped execute_bash PreToolUse guards accept a bare bun .kiro/tools/aidlc.ts engine orchestrate next after verb-intercept latched the user's arguments, while invoking the target directly on the same payload returns 2. t331 omits it from the required set, so the contract test stays green. Please register it (all three shell spellings) or fold its check into terminal-command-guard, and add it to t331.

3. P2 — doctor crashes on an unrefreshed kiro-ide project.
core/tools/aidlc-config-diagnostics.ts:2214 casts selected.distribution as ModelHarness without currentDistribution, so probeHarnessCli reads HARNESS_CLI["kiro-ide"] (removed) and throws TypeError: undefined is not an object (evaluating 'spec.command') at :778. Base doctor passes on the same project (52 passed / 4 warnings); head exits 1. Same root cause as the round-1 P2, one more reader. Please route it through the successor resolver and cover doctor itself in the retired-stamp regression (t243 currently checks providers/models --show only).

4. P2 — A failed write with a populated input path is audited as ARTIFACT_UPDATED.
The new input-path preference at aidlc-kiro-adapter.ts:2944-2963 makes rawPath non-empty, and the failure-prose classification sits inside if (!rawPath), so a str_replace failure (Caught an error while replacing string … found multiple times, or the structured {success:false,result:[…]} form) reaches the success path. Reproduced: file byte-identical, head emits ARTIFACT_UPDATED, base emits nothing. Please classify failure before choosing the success path; t218 F4d covers this prose only with empty tool_input.

5. P2 — enforce-approval-gate reads state from process.cwd(), not the resolved project.
aidlc-kiro-adapter.ts:1395-1405 uses process.cwd() for stateFilePath/humanActedSinceGate even though projectDir is resolved at :728 from --project-dir/AIDLC_PROJECT_DIR. The same guarded payload returns 2 from the project root and 0 from a foreign cwd; the pre-merge CLI guard-tool-call presence check returned 2 from the foreign cwd. Please use projectDir and audit the other merged process.cwd() branches (review-freeze in particular).

6. P2 — Source kiro installs fail config trust --check.
aidlc-config-diagnostics.ts:1911-1932 now requires .vscode/settings.json with aidlc engine * for every kiro install, but harness/kiro/manifest.ts ships that file in nativeRootIntegrations only and the source projection invokes Bun, not aidlc. An unmodified dist/kiro source projection returns exit 1 with kiro-ide-trust-unreadable; base returned 0; doctor on the same tree reports 0 problems, so the two diagnostics disagree. Please gate the check on the projection's command channel.

Non-blocking

  • P2 (disclosure) Per-agent model/effort policy is no longer expressible on kiro (HARNESS_HONESTY.kiromodel:false, effort:false, core/tools/aidlc-model-policy.ts:136-142), and t293:801-827 was rewritten from "the model landed" to "inexpressible". The pre-merge kiro row expressed it and Kiro's agent configuration reference still lists model. Either restore it through the Markdown frontmatter writer or name it as a second breaking change in the release-note draft and warn from config models.
  • P3 hook_event_name is compared exactly as "PreToolUse"/"PostToolUse" (aidlc-kiro-adapter.ts:1443-1446, :3133) while the repo's live-captured CLI fixtures (tests/fixtures/kiro-hook-payloads/payloads.json) spell preToolUse/postToolUse. Your v3 capture shows PreToolUse, so this is a fixture/code mismatch rather than an observed bypass — but t147 builds its own PascalCase payloads, so the suite cannot tell the two apart. Please normalize the comparison, drive one t147 case with the fixture spelling, and record which spelling each host emits in kiro-ide-hook-payload.md.
  • P3 t332:372-393 applies the PowerShell roster regex to preview-release.yml only; release.yml:402 carries its own $harnesses = @(...) list that is unasserted (it matches today).
  • P3 The body says t245's expected table "is now generated from the shipped manifests"; EXPECTED_V2_REGISTRATIONS (t245:47) is a hand-maintained constant.
  • P3 Rename residue: duplicate rmSync(kiroProject…) at scripts/build-binaries.ts:1881-1882; the plugin.kind comment at scripts/manifest-types.ts:203-204 is truncated mid-sentence.

One question

Does IDE 1.0.437 populate USER_PROMPT for the v2 UserPromptSubmit shell hook (the body reports it does for runCommand)? If so, aidlc-kiro-adapter.ts:764-776 classifies the session as the legacy channel and record-human-turn writes the legacy Plan Approval host marker (:2463-2465) on a supported host — the 0.x notice already distrusts that signal and checks VSCODE_IPC_HOOK, while the channel classifier does not. Not filed as a finding because no host was measured here.

Validation

Temporary worktrees only; no changes to the branch. bun scripts/package.ts --check deterministic across all six rows; bun run typecheck 0 errors on all three configs; t147, t331, t332, t245, t148, t327, t68, t298, t293, t243 (retired), t221, t264, t239, t281, t294, t230, t252 green; t265 55/0 on 130f5d4f. Each blocker has a throwaway reproduction against dist/kiro; #3, #4 and #6 were diffed against a projection built from base 891669aa. Also confirmed: the six round-1 fixes; the managed upgrade from a real kiro-ide projection (twelve legacy .kiro.hook files removed, notice retained); all 14 personas carry tools/toolsSettings/resources and the two MCP grants in both channels; Claude's MCP diagnostics unchanged; shared core/tools changes gated to .kiro; release metadata untouched.

Remaining risk

No completed Construction tail on either surface — #1 sits exactly there. An unclean manual overlay leaves the twelve old .kiro.hook files in place and doctor does not flag them. CI on 130f5d4f is green.

Recommendation: changes requested for #1#6.

wowzoo added 3 commits September 16, 2026 23:18
@apackeer reproduced six defects against dist/kiro built from bff9376. All six
are real on this head; each fix below carries a test that fails at its parent.

1. A refused dispatch leaked a delegation window and wedged the main session.
   log-subagent opens the window on the dispatch's PreToolUse edge, before any
   sibling guard has decided. When a guard refuses, Kiro blocks the tool and
   sends no PostToolUse, so the only close that dispatch would ever get never
   arrived and the open lived for DELEGATION_TTL_MS. Every ledger consumer then
   read a delegate that was never running: the main session's own `next` was
   refused with the delegate named as the caller, and a later human turn did not
   clear it. The window is now cancelled at the two places this adapter turns a
   refusal into Kiro's reject contract, so a guard added later inherits it, and
   liveDelegationOpens counts a close whose open is not on the ledger yet -
   hook order across sibling registrations is not guaranteed. The pending close
   consumes the whole group, not its first persona, or a crew dispatch stayed
   live. (t147 5d5)

2. guard-tool-call was not registered anywhere. Both of its exit-2 branches -
   the first-`next` argument fidelity check and the same-turn roll-forward
   backstop - were reachable only by a hand-typed dispatcher call, while t180
   kept asserting them by invoking the target directly. All six registered
   execute_bash PreToolUse guards return 0 on a bare `next` that the latch says
   dropped the user's arguments. Registered with the terminal-tool matcher and
   pinned in t331 (responsibility + matcher) and t245 (manifest schema).

3. doctor crashed on an unrefreshed kiro-ide project: the runtime probe indexed
   HARNESS_CLI with the raw stamp and threw `undefined is not an object
   (evaluating 'spec.command')`, so the user holding the row that needs
   upgrading could not ask what state it was in. The stamp is a READ, so it now
   resolves through currentDistribution like every other reader, and the probe
   reports an unknown id instead of throwing - a diagnostic must never be the
   thing that crashes. (t243, in the retired-stamp regression)

4. A failed write with a populated input path was audited as ARTIFACT_UPDATED.
   The failure classification lived inside the `!rawPath` branch, which was
   sound only while the path came from prose: preferring the tool input broke
   that proxy, because the CLI populates the input even when the tool refuses.
   Classify the failure first, whatever the path came from. (t218 F4g)

5. Three guards read process.cwd() rather than the resolved project, so the same
   guarded payload was refused from the project root and allowed from anywhere
   else - enforce-approval-gate, state-transition-guard and review-freeze. All
   three use projectDir now, and the comment that justified the cwd read is
   corrected. Arming this floor takes the runner's off-switch removed, a stage
   at [?], and an audit that tracks presence without a HUMAN_TURN. (t147 5h)

6. Source kiro installs failed `config trust --check` while doctor on the same
   tree reported zero problems. `.vscode/settings.json` trusts the literal
   `aidlc engine *`, which only the native channel runs, and the manifest emits
   the file from nativeRootIntegrations only. The check is gated on the
   projection descriptor - where the release build declares that file - so the
   channel that ships it is the channel judged on it. (t294)

Also: the new hook json had to enter every roster that names these files, not
just the two the fix touched - t308 pins the matcher-bearing manifests by name,
and the kiro harness guide's hook table listed all the others.
…ranch

# Conflicts:
#	tests/unit/gen-coverage-registry.test.ts
@wowzoo

wowzoo commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator Author

Thank you — six reproductions with exact coordinates, and every one of them held when I re-measured it. Two of your six also overturned a judgement I had already written down, which I have said plainly below rather than quietly fixing.

Current head is 6ec8715f, base 9288f80f, CI 22 pass / 0 fail. One commit carries the six fixes; the two after it absorb #1166 and #1195.

1 (P1) — a refused dispatch leaked a delegation window. Confirmed exactly as described, including that a later record-human-turn does not clear it: I reproduced open → next refused → human turn → still refused → PostToolUse close → allowed. The window is now cancelled at the two places this adapter turns a refusal into Kiro's reject contract, rather than inside any one guard, so a guard added later inherits it. Cancelling alone was not sufficient: hook order across sibling registrations is not guaranteed, and the replay discarded a close whose open had not been appended yet — so liveDelegationOpens now counts a pending close, and the pending close consumes the whole group, since one dispatch names several personas and cancelling only the first left the rest live. t147 5d5 is your case (refused dispatch + the opening edge → main-session next still allowed).

Your walk of the other leak paths was more complete than mine, and I want to record what I did not fix: cancellation, a crash, a malformed PostToolUse, and a tool_input that differs between the edges (the key is a hash of it) all still leak until the TTL. A record-human-turn sweep would cover them, and the blocking-dispatch contract makes it safe, but it is a behaviour change beyond the refusal you filed, so I left it out of this round rather than smuggle it in.

2 (P2) — guard-tool-call is not registered. Confirmed, and I had the scope wrong in the narrower direction. My first reproduction had it return 0, because --scope on a project with no state file takes the engine pre-dispatch branch, which clears the forwarding latch; I concluded the exposure was limited to turns that write that latch. That is only half of it. The same target carries a second exit-2 branch — the same-turn roll-forward backstop, armed by any classified terminal command and by the --config pre-dispatch — which is independent of the forwarding latch, and t180 asserts both by invoking the target directly. So the tests were green while nothing enforced either one in production. Registered with the terminal-tool matcher, pinned in t331 (responsibility and matcher) and in t245 (manifest schema), and the roster in the Kiro harness guide now lists it.

Adding it also had to enter a roster neither of us named: t308 pins the matcher-bearing manifests by name, and the local tier caught that. Fixing the two rosters the change touched was not enough.

3 (P2) — doctor crashes on an unrefreshed kiro-ide project. Confirmed with your stack: exit 1 at aidlc-config-diagnostics.ts:778, while the base reports 0 problems, 3 warnings on the same fixture. Reading a stamp is a read, so it goes through currentDistribution like every other reader — and separately, the probe no longer throws on an id the CLI table does not know, because a diagnostic must not be the thing that crashes. The retired-stamp regression now runs doctor before the migration and requires a report rather than a stack trace; problems are allowed there, since the project genuinely has one until it upgrades.

4 (P2) — a failed write with a populated input path is audited as ARTIFACT_UPDATED. Confirmed, with a base control: the same payload writes nothing at the base and an ARTIFACT_UPDATED row at this head. Your diagnosis of why is what I would not have found quickly — an empty path had been standing in for "the write failed", and preferring the tool input silently retired that proxy. The classification now runs before the path is used at all. t218 F4g is the populated-input twin of F4d, and it also asserts no drop, since a refused write is the tool working correctly.

5 (P2) — enforce-approval-gate reads process.cwd(). You were right and my first pass was wrong about it. I reported to my own notes that the divergence "did not reproduce", when what actually happened is that my fixture never armed the floor: the test runner sets AIDLC_SKIP_HUMAN_PRESENCE_GUARD=1 globally, and humanActedSinceGate returns true on a ledger with no presence-tracking events at all. With the off-switch removed, a stage at [?], and an audit carrying a non-HUMAN_TURN event, the case fails at the parent exactly where you said it would. Fixed at all three sites — enforce-approval-gate, state-transition-guard and review-freeze — since your "audit the other merged process.cwd() branches" turned out to name two more, and the comment that justified reading the cwd is corrected. t147 5h drives the same payload from the project and from a foreign cwd and requires the verdict not to move.

One correction to my own test scaffolding, in case it matters elsewhere: the adapter's run() takes _extraArgs and never reads it, so --project-dir passed to the adapter directly is ignored. My earlier probe only worked because it also set AIDLC_PROJECT_DIR.

6 (P2) — source kiro installs fail config trust --check. Confirmed, and the disagreement between the two diagnostics is the part that decided the fix: exit 1 from trust --check and 0 problems from doctor on the same untouched tree. I gated it on the projection descriptor rather than on the running invocation — the release build adds .vscode/settings.json to rootIntegrations, a source projection does not — so the diagnostic reports on the project in front of it instead of on whichever binary is asking, and the native channel still reports both the missing entry and an unreadable file. t294 covers source-exempt and native-enforced on the same fixture.

One thing I did not do: deliver-stage-rules. I noticed it is also absent from every hook JSON and from t331, and nearly filed it alongside #2 as the same defect. It is not: t245 asserts that aidlc-deliver-stage-rules.json must not exist, the core hook explains that Kiro CLI gets stage rules through native resource preload and the IDE through always-included steering, and the steering file is inclusion: always. So the absence is a decision, not a gap. The comment above canonicalTool that says these targets are "engine- and shell-invoked … t331 names them" is stale in the same breath, since t331 did not name two of the three; that is a prose defect this branch introduced and I will correct it.

Non-blocking items. I verified all five and none of them is fixed in this round: the HARNESS_HONESTY.kiro disclosure (model: false, effort: false, with t293 rewritten to "inexpressible"), the hook_event_name casing against the captured fixtures (preToolUse/postToolUse, ten occurrences, versus the PascalCase comparison), t332 applying the PowerShell roster regex to preview-release.yml only while release.yml:402 carries its own unasserted list, the t245 claim in the PR body (EXPECTED_V2_REGISTRATIONS is a hand-maintained constant, so the body is wrong and I will fix the body), and the two rename residues (build-binaries.ts calls rmSync(kiroProject…) twice; the plugin.kind comment in manifest-types.ts breaks off mid-sentence). Your P2 disclosure is the one with a product decision in it, so I would rather answer it in the release note than pick a surface for it unilaterally.

Remaining risk. Unchanged from what I told @rafaelgsr and @leandrodamascena: no completed Construction tail on either surface, and I am not claiming it away. My plan is still to validate it against a preview release once this lands, and to bring what it surfaces as focused follow-up pull requests. Your point that #1 sits exactly there is fair — that is the failure a real tail would have hit, and it is the one blocker in this round that a test could not have found on its own.

@apackeer

Copy link
Copy Markdown
Contributor

Thanks — four of the six are fixed and verified by reproduction (#2, #3, #5, #6), and the honesty about the leak paths you did not take is appreciated.

I reviewed the current head 6ec8715fe3665088ef65d78cac8c671a62a57519 against 9288f80f4c289891e362864c0f3055309648927b (merge base identical). The delta since 130f5d4f is 2b29e93e2 plus two upstream merges that carry nothing PR-authored. Everything below was reproduced on dist/kiro built from this head.

Blocking

1. P1 — #1 is only partially fixed: enforce-approval-gate still leaks the window.
aidlc-enforce-approval-gate.json has no matcher, so it fires on every dispatch; it returns 2 at harness/kiro/hooks/aidlc-kiro-adapter.ts:1415 before either cancelRefusedDispatchWindow site (:3350, :3452), and it is not in INPUT_TARGETS, so adding the call there alone would see no toolName. Reproduced: log-subagent open → gate refuses (2) → HUMAN_TURN appended → gate passes (0) → main-session next refused with Delegated agent "aidlc-developer-agent" cannot run aidlc-orchestrate.ts next; the ledger holds the open with no close, until the 6 h TTL. Please route every exit-2 on a dispatch-shaped PreToolUse through one cancel path (a single exit shim), and add the t147 twin of 5d5 with this guard as the refuser (armed gate, AIDLC_SKIP_HUMAN_PRESENCE_GUARD unset).

2. P2 — the pending-close accounting introduced in 2b29e93e2 fails open.
liveDelegationOpens skips a TTL-expired open at :2349 but still counts its close as pending at :2370-2372, and the next byte-identical dispatch's open is consumed at :2351-2356. So in a session that lives past DELEGATION_TTL_MS and repeats a byte-identical dispatch (a crew subagent/orchestrate_subagent with the same stages[] is the likely shape), the completed dispatch's close becomes a credit that swallows the new window, and the running delegate is invisible to state-transition-guard and reviewer-scope — delegate next → 0, control without ageing → 2. The same over-consumption is reachable inside TTL by any surplus close: a malformed-Pre refusal (close, no open) followed by a valid retry, or two guards refusing one event — which the fix for #1 will make live. Please match closes against expired opens, bound pending closes by DELEGATION_TTL_MS, and make the refusal cancel idempotent per event; add t147 cases for aged open+close → fresh identical open still attributed, and two refusals → later legitimate dispatch still attributed.

3. P2 — #4 still audits a structured failure with a populated path.
:788-789 read only the top-level toolSuccess/tool_success; collectToolResultText (:164-179) discards the nested success, so {success:false, result:['']} and any failure text without one of the four recognised prefixes reach the success path and write ARTIFACT_UPDATED for unchanged bytes (base emits nothing for the structured-empty shape). The fix comment says "the flag stays authoritative" — please make the nested flag authoritative too, and add the populated-path structured case to t218.

Non-blocking

  • P3 shipsNativeTrustFile (core/tools/aidlc-config-diagnostics.ts:1919-1933) returns false on a missing or malformed descriptor, so a native install with .vscode/settings.json absent reports "trust configuration is clean for kiro". Report the unreadable descriptor as its own issue. trustFilesForHarness:1900-1902 still lists .vscode/settings.json for source projections in trust --show.
  • P3 Two raw-stamp readers remain after Reorganized rules files to take up minimal space in context window #3: core/tools/aidlc-utility.ts:3610-3615 makes doctor prescribe aidlc config --harness kiro-ide (which exits 4), and core/tools/aidlc-doctor.ts:161 warns "unsupported harness policy surface: kiro-ide". Wrap both in currentDistribution and assert the fix text in the t243 retired-stamp case.
  • Stale comment at aidlc-kiro-adapter.ts:1367-1368 (record-human-turn "process.cwd()"), plus the canonicalTool comment you already noted.
  • The five round-2 non-blocking items are unchanged, as you said.

Verified

#2: manifest on PreToolUse for all three shell spellings; bare next → 2 through the emitted guards; verbatim first next, --config, --scope, --status paths → no false refusal; t331/t245/t308/t180 green. #3: report, not TypeError, on a real base kiro-ide projection. #5: no executable process.cwd() left; 2 from a foreign cwd with the presence guard armed at all three sites; _extraArgs unreachable from shipped wiring. #6 for the reported case: source → 0/0, release with settings → 0, release missing settings → 1. Crew release, identical-dispatch double close, two-process concurrency and reviewer attribution unregressed within TTL; deliver-stage-rules absence justified (steering inclusion: always + persona resources); guide row accurate; upstream merges clean. bun scripts/package.ts --check deterministic, typecheck 0 errors, t147 47/0, t218 F4 7/0, t243 retired 4/0, t294 28/0, t230/t252/t148 green.

Remaining risk

Unchanged — no completed Construction tail, and #1/#2 above sit in it. CI on 6ec8715f is green.

Recommendation: changes requested for the three blocking items.

wowzoo added 2 commits September 17, 2026 11:16
Round 3 found my round-2 fix wrong in both directions, and both are real. The
delegation window is now opened only for a dispatch that was allowed to start,
which removes the need for the accounting that caused the second defect.

1. P1 — the window still leaked, through a refuser I had not considered.
   `enforce-approval-gate` carries no matcher, so it fires on every dispatch, and
   its exit 2 is far upstream of the two places I had hooked. Choosing call sites
   is what produced both this and the round-1 defect, so the fix is not a third
   site: the log-subagent PreToolUse edge - the only target whose matcher is
   exactly the dispatch tools, and which sees both edges - now decides admission
   and appends the open only after it passes. The human-presence floor moved to
   one predicate (`approvalFloorRefuses`) with two callers, and the Plan Approval
   verdict comes from the same core hook the sibling guard would have run. Both
   matcherless gates stand down on a dispatch shape so they cannot refuse one
   after this edge admitted it; `enforce-approval-gate` joins INPUT_TARGETS for
   that reason alone. (t147 5d5, rewritten)

2. P2 — the pending-close accounting I added failed open. An expired open is
   skipped at replay, so its close matched nothing and was remembered as a credit
   for that key; the next byte-identical dispatch's open was then consumed by it
   and never went live, hiding a running delegate from state-transition-guard and
   reviewer-scope. Surplus closes reached the same state within the TTL, and
   fixing item 1 by adding cancel sites would have made that live. The accounting
   is gone: a refusal is not a ledger fact, a close means the dispatch reported,
   and an unmatched close is dropped. Telling "the same event twice" from "two
   identical events" needs an id this payload does not carry, so nothing depends
   on one. (t147 5d6)

3. P2 — a structured failure with a populated path was still audited. The
   transport's flag can sit inside the result envelope (`{success:false,
   result:['']}`) and the text collector walked only the text keys, so this shape
   carried neither a flag nor recognisable error prose. The nested flag now fills
   the same `toolSuccess` the top-level one does, so the existing explicit-false
   branch decides it - no new policy. Honouring a nested `true` over the prose
   heuristic is the same reading the top-level flag already gets. (t218 F4h)

Non-blocking, all four with the coordinates from the review: an unreadable
projection descriptor is now its own trust issue instead of silently meaning "this
channel owes nothing" (it made a native install with the file deleted report
clean), `trust --show` no longer lists `.vscode/settings.json` for a source
projection, and the two remaining raw-stamp readers go through
`currentDistribution` - `doctor` was prescribing `--harness kiro-ide`, which exits
4, and reporting a retired stamp as an unsupported policy surface. Two stale
comments corrected: the orphaned banner above `canonicalTool`, whose claims about
t331 were both false, and the record-human-turn note that still said
`process.cwd()`.

Per the reviewer's read on awslabs#1: this is the failure a completed Construction tail
would have hit, which is why it took two rounds to find.
@wowzoo

wowzoo commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Thank you — and the framing of #2 as a fail-open that my own fix introduced is exactly right. I have taken a different route than the one you proposed for #1, so I want to put that difference first rather than bury it.

Current head is 502cefed, base d378d0df, CI 22 pass / 0 fail.

#1 and #2 together, because the second is what rules out the obvious fix for the first

You proposed routing every exit-2 on a dispatch-shaped PreToolUse through one cancel path. I started there, and then #2 talked me out of it: a cancel path only works if a refusal can be written down as a fact, and this payload has no event identity to write it against. delegationKey is a content hash precisely because nothing in the payload distinguishes two dispatches — so "cancel the window this event opened" and "cancel the window some earlier identical event opened" are the same instruction. Any shim keeps that ambiguity; it just centralises where it is created.

So the window is no longer opened before admission. The log-subagent PreToolUse edge — the only target whose matcher is exactly the dispatch tools, and which already sees both edges — now decides whether a dispatch may start, and appends the open only if it may. A refusal appends nothing at all, which means there is no credit to spend and no ordering to reconcile: pendingCloses, cancelledGroups and cancelRefusedDispatchWindow are all gone, and an unmatched close is simply dropped.

Two consequences worth checking, since both are behaviour changes and not refactors:

  • The human-presence floor is now one predicate (approvalFloorRefuses) with two callers, and the Plan Approval verdict on a dispatch comes from the same core hook the sibling guard would have run.
  • Both matcherless gates now stand down on a dispatch shape. They have to: whoever refuses a dispatch must also be whoever decides to open its window, or the refusal strands what the other one opened — which is the wedge in your Kiro CLI support and multi-platform compatible architecture #1. enforce-approval-gate joins INPUT_TARGETS for that single purpose, since without the payload it cannot tell a dispatch from anything else. Every other tool it sees is still judged exactly as before.

The process.exit(2) you would have found next is also gone (guard-tool-call's argument-fidelity refusal now returns). It was the one exit that no wrapper could have caught.

t147 5d5 is rewritten to assert the new owner refuses and that both gates return 0 on the same payload; t147 5d6 is your aged-open case — it ages the ledger the adapter itself wrote rather than reconstructing the session hash, then requires a fresh byte-identical dispatch to still be attributed. Both fail on the round-2 adapter.

One test moved rather than being added: t218's "populated 1.x PreToolUse write and dispatch payloads are blocked before approval" asserted the refusal through plan-approval-guard. The contract it protects — a dispatch is refused before approval — is intact, so the dispatch half now goes through log-subagent, and the case also pins that the guard returns 0 on it. I checked that aidlc-log-subagent.json's matcher covers every spelling isDispatchToolName accepts, so nothing arrives at only the guard.

#3

Fixed, but deliberately not as a new rule about nested flags. The nested success now fills the same ide.toolSuccess the top-level one does, so the existing explicit-false branch decides it and no second policy is introduced. That also means a nested true gets the same standing the top-level flag already has against the prose heuristic. I stopped short of widening the failure-prose matching, which is the other half of what you saw: without a captured payload for those shapes I would be guessing at the transport's vocabulary. t218 F4h covers the structured-empty case with a populated path.

Non-blocking, all four

  • shipsNativeTrustFile returns null on a missing or malformed descriptor, and the caller reports kiro-projection-descriptor-unreadable rather than treating it as "this channel owes nothing". Your case — a native install with the file deleted reporting clean — was the sharp end of collapsing those two.
  • trustFilesForHarness applies the same channel test, so trust --show no longer lists .vscode/settings.json for a source projection.
  • Both remaining raw-stamp readers go through currentDistribution: doctor was prescribing aidlc config --harness kiro-ide, which exits 4, and reporting a retired stamp as an unsupported policy surface — telling the user their install is unsupported when it needs the upgrade this release performs.
  • Both stale comments are corrected, including the orphaned banner above canonicalTool whose claims about t331 were both false.

The five round-2 non-blocking items are still unchanged, and the HARNESS_HONESTY disclosure is still the one I would rather answer in the release note than decide unilaterally.

What I did not verify locally this round

I ran typecheck and relied on CI for the suites, rather than a local tier. That is a deliberate change of gate on my side — a full local tier takes longer here than CI does, and three of its failures (t244, t255, t314) reproduce at this PR's own base on this machine, so they say nothing about the branch. What I did keep local is the parent-failure proof above, since CI cannot do it. If you would rather see a specific suite run locally before you re-review, name it and I will.

Remaining risk

Unchanged, and your point stands: no completed Construction tail, and both #1 and #2 live in it. #2 in particular is a failure I introduced while fixing #1, which is the strongest argument yet that this area needs the tail rather than another round of reading. My plan is still to validate it against a preview release once this lands and to bring what it surfaces as separate pull requests.

@apackeer

Copy link
Copy Markdown
Contributor

Thanks — the redesign is the right instinct: a refusal should never be a ledger fact, and the three round-3 blockers now have passing reproductions with failing round-2 controls (5d5, 5d6, F4h all fail on 6ec8715f).

I reviewed the current head 502cefed8ed26d352904481f6002255f428e3072 against d378d0df (merge base identical); origin/main has since moved to be94bde7 and the trial merge is clean — typecheck, package.ts --check, and t230/t147/t218/t243/t294/t331 (413/0) pass on the merged tree. Alignment unchanged. Everything below was reproduced on dist/kiro built from this head, with d378d0df and 6ec8715f controls.

Blocking

1. P1 — The stand-downs turn two independent refusers into zero. harness/kiro/hooks/aidlc-kiro-adapter.ts:1424 and :2636 return 0 for every dispatch shape unconditionally on whether the admitter ran. Reproduced four ways: (a) aidlc-log-subagent.json deleted → unapproved developer dispatch passes plan-approval-guard and enforce-approval-gate with 0 (base and 6ec8715f: 2). (b) tool_input:[] (or tool_response:42, top-level prompt:{}) sets malformedFields, which skips the admitter block at :1451 while :1424 still stands down on the name → the presence floor is bypassed at an ordinary [?] gate (6ec8715f: 2). (c) subagent_aidlc-developer-agent with tool_input.name:"aidlc-quality-agent": planApprovalRefusesDispatch :2401-2424 prefers the argument identity and asks core about the quality agent → 0, then openDelegation attributes the window to the developer from the suffix (6ec8715f refused via the generic mutation fallback). (d) With an unresolved opaque-write window (readPlanApprovalLegacyWindows), the guard arm :2649-2678 would refuse, but the dispatch returns at :2636 first and the admitter never consults the windows (6ec8715f: 2, "legacy write did not complete PostToolUse mediation").

The requirement, rather than a prescription: a gate must not fail open when the admitter did not run, and a refusal after admission must not strand the window. Making the admitter and the gates evaluate the same predicate on the same identity (suffix authoritative, the same kiroDispatch derivation the ledger uses, the same legacy-window check) is necessary but I do not think it is sufficient on its own: the core guard's admission path calls beginCodeGeneration (core/hooks/aidlc-plan-approval-guard.ts:1007-1037), which mutates state under withAuditLock — publishes the generation boundary, rebases the receipt, and throws on source drift or lock contention (core/tools/aidlc-testing-posture.ts:2461-2555) — so two separate evaluations of the "same" predicate are not atomic and the second can refuse where the first admitted. Whatever shape you choose, please test it under both hook orders (admitter first, gate first), with an intervening source change between the two evaluations under strict and relaxed Change Control, and with the two hooks running concurrently, and add t147 cases for (a)–(c). Note t147:1253-1260 and t218:1583-1585 currently pin the stand-down (weakened from asserting 2).

2. P2 — Admission depends on an exact PreToolUse spelling that the repo's own captures and Kiro's docs contradict. ide.event is the raw string (:815, :859); the admitter runs only at :1459 === "PreToolUse", :1424 stands down on the name alone, and :3304 gates the completion forward. tests/fixtures/kiro-hook-payloads/payloads.json (verbatim captures) and https://kiro.dev/docs/hooks/types/ spell preToolUse. On that spelling the floor has no owner for any dispatch, and the Pre-edge registration writes a SUBAGENT_COMPLETED row at dispatch time (including for a refused developer dispatch). Your live v3 and IDE 1.x runs opened windows, which means those hosts sent PascalCase — so this may be a fixture-era spelling; but e6b0c0a58 made the question load-bearing for a fail-open. Please normalise case at parse and drive one admission case with the fixture payload verbatim; the round-2 request to record the observed spelling per host in kiro-ide-hook-payload.md still stands.

3. P2 — planApprovalRefusesDispatch discards the core hook's output. :2414-2428 reduces runCoreHook to .code === 2. Refusal: the AIDLC-UNIT / AIDLC-TESTING-CONTRACT remedy is replaced by the generic line at :1481-1487, so the conductor has no deterministic retry (6ec8715f relayed it via :3544). Admission: under relaxed Change Control the core hook's beginCodeGeneration records CHANGE_ACCEPTED and prints the one-time "1 file changed since this plan was approved…" notice — recorded, never surfaced. Please relay stdout and stderr from the admission call.

Non-blocking

  • P3 shipsNativeTrustFile (core/tools/aidlc-config-diagnostics.ts:1930-1938) casts parsed JSON: {}, [], 42, rootIntegrations:null with .vscode/settings.json deleted → trust --check reports clean. Malformed JSON and a missing file correctly fail.
  • P3 isDispatchToolName accepts bare subagent_ (:2349-2354); the matcher subagent_.+ never delivers it, so both gates stand down and nothing admits. Not observed from a host.
  • P3 No test for kiro-projection-descriptor-unreadable, trust --show omitting .vscode/settings.json on a source projection, or the doctor fix text (t243:4119-4121 asserts only no-TypeError).
  • Round-2 non-blocking items unchanged, as you said. Behaviour change worth a line in the body: a non-developer direct dispatch during code-generation is now forwarded as Task+subagent_type and allowed where round 2 refused it as an unknown mutation-capable tool.

Verified

R3 #1/#2/#3 fixed with controls; retired-stamp doctor/providers --show/models --show on a real base-era kiro-ide install, then upgrade; source/native trust matrix; guard-tool-call exit 2 through both entrypoints on all three shell spellings; bounded stdin on the now-input-dependent gate; 502cefed byte-identical to the clean merge-tree of e6b0c0a58×d378d0df; merged-tree validation as above.

Remaining risk

Unchanged: no completed Construction tail, and #1 sits exactly where round 3's did. The author-admitted crash/cancel/malformed-Post leaks are neither better nor worse. CI on 502cefed is green.

Recommendation: changes requested for #1#3.

wowzoo added 9 commits September 17, 2026 16:57
Round 4 found my round-3 fix fail-open four ways, all verified here: the two
stand-downs returned 0 for any dispatch shape without asking whether the admitter
had run. This is the third consecutive reversal in this seam, so the invariant is
now stated rather than implied — a gate refuses on its own evidence, and the
recoverable cost of a duplicate refusal is accepted.

1. P1 — both stand-downs are gone. `enforce-approval-gate` and the plan guard judge
   a dispatch like any other tool call again. With the log-subagent manifest absent,
   its matcher not firing, or its own block skipped because the payload was
   malformed, the floor previously had no owner at all; the plan guard also skipped
   its legacy opaque-write arm on every dispatch. The `INPUT_TARGETS` entry added
   only to power the stand-down is removed with it. admission-before-open stays, but
   as an early refusal and a narrower window — not as the sole owner of the
   decision. A gate refusing after admission leaves a window with no close, and that
   is now reclaimed rather than prevented (below).
   (t147 5d5 rewritten, 5d7 malformed bypass, t218 dispatch assertion restored to 2)

2. P1 (same finding, identity half) — one derivation, and it is the ledger's.
   `kiroDispatch` treats the `subagent_` suffix as authoritative with arguments as
   fallback, matching this harness's rule that a platform-provided identity outranks
   an agent-authored one. My admission check derived it argument-first, so
   `subagent_aidlc-developer-agent` carrying `tool_input.name: "aidlc-quality-agent"`
   asked core about the quality agent and then had its window attributed to the
   developer. The `invoke_sub_agent` developer fallback stays OUT of kiroDispatch: it
   is a fail-closed policy, not an identity, and putting it there would let a
   completion audit record the developer for a delegate named in prose. (t147 5d8)

3. Windows are reclaimed by a `sweep` boundary, not by more closes. A close cancels
   the most recent group for its key, so appending one at a human turn would cancel a
   dispatch the NEXT turn legitimately opened with the same bytes — round 3's defect
   again. A sweep record abandons every open appended before it and leaves later ones
   untouched. `record-human-turn` appends it, which is sound because a dispatch on
   this row is blocking: anything still believed inflight when a human speaks is
   finished or gone. This also covers the crash/cancel/malformed-Post leaks I
   admitted in round 3 and left open. (t147 5d6, 5d9 both hook orders)

4. P2 — `hook_event_name` is normalised at parse (`canonicalHookEvent`). The hosts
   measured so far send PascalCase, but this repo's captured fixtures and Kiro's docs
   spell `preToolUse`, and my admission edge branched on an exact match — putting a
   security decision on the spelling of one field. Unknown names pass through so a
   new trigger shows up in a drop rather than being silently renamed. Same lesson as
   the USER_PROMPT/stdin channel fix: accept both, do not replace one.

5. P2 — the admission path fails CLOSED on unreadable state while the advisory gate
   keeps failing open. `approvalFloorRefuses` takes the answer as an argument so
   neither caller inherits the other's default; `humanActedSinceGate` learned the
   same lesson (ENOENT-only skip) after treating "could not read" as "was empty"
   inverted it to fail-open.

Not done, and not silently: `planApprovalRefusesDispatch` still discards the core
hook's stdout/stderr (round-4 awslabs#3), so the AIDLC-UNIT remedy is not relayed. The
concurrent-hook case the reviewer asked for is also not written yet.
Round 4 awslabs#3, and the concurrency pin the review asked for.

- `runCoreHook` already piped stdout and dropped it. It now returns it, and the
  dispatch admission relays both streams: the core guard's stderr carries the
  AIDLC-UNIT / AIDLC-TESTING-CONTRACT remedy, which is the conductor's only
  deterministic retry, and its stdout carries the one-time "N file(s) changed since
  this plan was approved" notice that relaxed Change Control prints exactly once. My
  generic line stays only as the fallback for a guard that said nothing. Adding a
  field leaves the two other callers byte-identical. (t147 5d10, fails at the parent)

- t147 5d11 pins the concurrent case: two adapter PROCESSES admitting byte-identical
  dispatches at the same moment leave two opens in two groups. This one PASSES at the
  parent by design — it is a regression pin for the property the append-only ledger
  already provides (a keyed map once collapsed them and the first close released
  both), not evidence of a new defect.
Open since round 2 of the awslabs#1157 review. The axis is engine GENERATION, not surface —
"IDE versus CLI" is the wrong way to reason about it, and that is the part worth
writing down.

Counted across every capture on this machine:
- Kiro CLI, agent-v1 engine (2.6.1, 2.18.1): camelCase. Source is our own fixture,
  whose `_provenance` records both captures and states "payload field names are
  verbatim".
- Kiro IDE 1.x, every build from 1.0.89 to 1.0.337: PascalCase, camelCase 0 —
  1.0.89 (41), 1.0.116 (26), 1.0.138 (31), 1.0.165 (108), 1.0.203 (61), 1.0.212 (73),
  1.0.309 (275), 1.0.337 (111), from the per-version archives in the kiro-ide-1.x-test
  controlled-experiment tree.
- Current unified row, both surfaces: PascalCase, camelCase 0 (CLI 140/139/7/3,
  IDE 129/127/6/2).

🔴 This corrects the first draft of this section, which guessed the camelCase fixture
was a 0.12-era capture. It is not: IDE 0.12 carries no `hook_event_name` field at all
(its contract is USER_PROMPT with camelCase FIELD names), and the fixture's own
provenance names kiro-cli. The published hook docs spell it camelCase too, which
matches agent-v1 rather than anything measured here.

The adapter does not depend on the answer — `canonicalHookEvent` folds both at parse —
but the admission edge branches on this value, so the next reader should not have to
rediscover which spelling belongs to which generation.
…claims

The first minor transition on the controlled-experiment tree (73 captured
events) keeps PascalCase — ten builds now, camelCase 0 — but it moves which
arm of the delegation identity carries the agent name: invoke_sub_agent
returns with a populated argument object and subagent_<agent> is absent, so
the suffix arm's precedence is a code guarantee rather than an observed shape.
…t claim

t239 scans every doc for a "<N>-event" phrase and requires it to equal the
audit registry's event count; "a 73-event capture" tripped that pin even
though it counted hook firings, not audit event types.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation github

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants