Skip to content

fix(kiro-ide): make hooks work in Kiro IDE (USER_PROMPT env, relative paths, audit-tail gating)#471

Merged
SiddhJog merged 1 commit into
v2from
fix/kiro-hooks
Jul 9, 2026
Merged

fix(kiro-ide): make hooks work in Kiro IDE (USER_PROMPT env, relative paths, audit-tail gating)#471
SiddhJog merged 1 commit into
v2from
fix/kiro-hooks

Conversation

@SiddhJog

@SiddhJog SiddhJog commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Kiro IDE's hooks were effectively dead for everything that depended on what the agent just did — the audit logger, sensor firing, runtime-graph recompile, and statusline sync all silently no-op'd. Root cause, verified live inside a Kiro IDE session: the IDE delivers hook context through the USER_PROMPT environment variable (not stdin, which it opens but never writes), and it leaves the tool arguments empty. The old adapter waited on stdin, timed out, and forwarded an empty payload. This PR reworks the IDE adapter to read USER_PROMPT, recover the written file path from the tool-result text (resolved to absolute), and drive the command/payload-free hooks off the audit trail. The Kiro CLI, Claude Code, and Codex harnesses are untouched — they use a separate adapter and a working stdin channel, and the shared-core additions are inert without the IDE markers/flag. Bumps to 2.1.5.

Summary

1. Read hook context from USER_PROMPT instead of stdin

The IDE never writes stdin (it hangs), and delivers {toolName, toolArgs, toolResult, toolSuccess} via the USER_PROMPT env var. The IDE adapter now parses that env var and drops the dead stdin read + 2s timeout.

Files changed

  • harness/kiro-ide/hooks/aidlc-kiro-adapter.ts → reads USER_PROMPT, removes stdin race

2. Recover the written file path from tool-result text (resolved to absolute)

toolArgs is always empty in the IDE, so the file path is scraped from the tool-result prose (Created the X file. / Replaced text in X / Appended the text to the X file.). The IDE reports it relative to the workspace root, so the adapter resolves it to absolute — fixing audit-logger, which was comparing a relative path against an absolute record root and dropping every write.

Files changed

  • harness/kiro-ide/hooks/aidlc-kiro-adapter.tsextractWrittenPath() + isAbsolute/resolve against project dir
  • core/hooks/aidlc-audit-logger.ts → debug instrumentation at each exit gate (path-gate verdict)

3. Payload-free gating for runtime-compile and sync-statusline

The IDE surfaces neither the shell command nor the task payload, so both hooks now gate on the audit tail via an ide-audit-sync marker. runtime-compile skips the command filter and compiles on a real transition; sync-statusline derives Current Stage from the latest STAGE_STARTED and is rewired from the never-firing spec event onto shell.

Files changed

  • core/hooks/aidlc-runtime-compile.tside-audit-sync bypasses command filter, gates on audit tail
  • core/hooks/aidlc-sync-statusline.ts → audit-tail branch via latestStartedStageSlug()
  • core/tools/aidlc-lib.ts → new latestStartedStageSlug() shared helper
  • harness/kiro-ide/hooks/aidlc-sync-statusline.kiro.hook → trigger specshell

4. Opt-in hook debug log

AIDLC_HOOK_DEBUG=1 makes every hook append its decision path to <record>/.aidlc-hooks-health/hook-debug.log. Off by default — no log, no overhead.

Files changed

  • core/tools/aidlc-lib.tshookDebug() + hookDebugEnabled() (flag-gated)
  • core/hooks/aidlc-audit-logger.ts, core/hooks/aidlc-runtime-compile.ts, core/hooks/aidlc-sync-statusline.ts → debug trace calls
  • harness/kiro-ide/hooks/aidlc-kiro-adapter.ts → adapter-level debug trace

5. Tests, docs, versioning

New headless coverage driven by USER_PROMPT env fixtures (relative-path resolution, runtime-compile audit-tail compile, opt-in debug gate); IDE harness doc updated; 2.1.5 CHANGELOG entry.

Files changed

  • tests/unit/t188-kiro-ide-hook-adapter.test.ts → new IDE adapter test suite (14 cases)
  • tests/unit/gen-coverage-registry.test.ts → register t188 in the none→cli set
  • tests/.coverage-registry.json → regenerated
  • docs/guide/harnesses/kiro-ide.md → env-var hook mechanism + debug-flag section
  • core/tools/aidlc-version.ts → 2.1.5
  • README.md → version badge 2.1.5
  • CHANGELOG.md → 2.1.5 entry
  • dist/** → regenerated (drift-checked)

Testing

  • New t188 IDE adapter suite (14 cases) — all pass; drives the adapter with USER_PROMPT env fixtures, no live IDE needed.
  • Validated end-to-end in a real Kiro IDE session: audit rows land, sensors fire, runtime graph compiles on transitions, Current Stage syncs.
  • Kiro CLI regression-checked live — full audit/sensor/runtime-compile activity, debug log correctly absent (opt-in).
  • Full smoke/unit/integration green except the three pre-existing @anthropic-ai/claude-agent-sdk module-not-found failures (t19, t140, t142), unrelated to this change.

@apackeer

apackeer commented Jul 1, 2026

Copy link
Copy Markdown

Reviewed this in depth. The adapter half is a solid fix for a real problem: the USER_PROMPT discovery is well evidenced, and the env-var parse plus relative-path resolution genuinely revives the audit logger and sensors in the IDE. The concerns below are all in the new payload-free audit-tail gating added to the two shared core hooks, which reconciles from the last STAGE_STARTED event without accounting for states that are legitimately ahead of it. Findings 1-3 can corrupt live workflow state, so I'd hold the merge on those.

1. sync-statusline rolls workflow state backwards after finalize (state corruption)

handleFinalize advances Current Stage to the next slug (core/tools/aidlc-state.ts:1055), or sets Current Stage: none + Status: Completed when there is no next stage (aidlc-state.ts:1060-1062), and emits no STAGE_STARTED. The new ide-audit-sync branch only checks current === auditSlug, so the very next shell command in the IDE sees the last STAGE_STARTED (the stage that just finished), spawns set-status --stage <old-stage>, and that forces Status: Running and flips the completed stage's checkbox back to in-progress (core/tools/aidlc-utility.ts:3027-3034). Net effect: any finalize on Kiro IDE is undone by the next shell command, including resurrecting a fully completed workflow.

Suggested fix: skip the sync when Current Stage is none or Status is Completed/gated, or only sync when the state file is behind the audit rather than ahead of it.

2. latestStartedStageSlug() picks up synthetic --single rows and rewrites the main workflow pointer

report --single emits STAGE_STARTED tagged Workflow: single-stage:<slug> (core/tools/aidlc-orchestrate.ts:2305-2309) under the documented invariant that a single run never touches the main Current Stage. The new helper does not filter on the Workflow field, so after a single run the next IDE shell command sets the main pointer (plus Lifecycle Phase, Active Agent, checkbox) to the single-run stage. The exact filter already exists at core/tools/aidlc-state.ts:150 (startsWith("single-stage:")), so this is a one-line fix in the helper.

3. runtime-compile in ide-audit-sync mode recompiles on every shell command, permanently after WORKFLOW_COMPLETED

With the command filter skipped, the only remaining gate is "a transition event in the last 3 audit blocks", with no idempotency check (no graph-vs-audit mtime, no last-compiled marker), and the compile is a synchronous spawnSync with a 30s timeout that blocks the hook. While a transition sits in the tail, every shell command re-runs a full compile. After WORKFLOW_COMPLETED the tail never changes again, so every shell command in that workspace pays a blocking recompile forever. A last-compiled marker in the health dir, or comparing runtime-graph.json mtime against the audit shard mtimes, would bound it.

4. extractWrittenPath() fails silently on realistic variants, and one variant forwards a wrong path

Verified empirically with bun against the three regexes: JS $ without the m flag does not match before a string-final \n, so a trailing newline or any multi-line/reworded toolResult (e.g. an overwrite wording) fails all three patterns and the write is silently dropped from audit and sensors. That is the same invisible-no-op class this PR fixes, and it is observable only under opt-in AIDLC_HOOK_DEBUG (no recordHookDrop on this path). Separately, Replaced text in (.+)$ has no terminator after the capture, so a result like Replaced text in foo.md (2 occurrences) matches and forwards foo.md (2 occurrences) as the path. t188 covers only the three exact happy-path wordings.

Suggested fix: record a hook drop when a write-class toolName yields no extractable path (so decay is visible to --doctor), and add fixtures for trailing-newline/suffix/overwrite variants.

Smaller items

  • CHANGELOG.md (2.1.5): the "No new commands or flags. ... the Kiro CLI, Claude Code, and Codex harnesses are untouched" bullet contradicts the AIDLC_HOOK_DEBUG=1 bullet two lines above it, and the diff does change shared core hooks shipped to all four dists.
  • docs/guide/harnesses/kiro-ide.md: the updated hook table still lists aidlc-sync-statusline as postToolUse (spec) while this PR flips the .kiro.hook to ["shell"] (the sibling runtime-compile row was corrected).
  • Adapter comment cites tmp/hook-probe-findings.md: tmp/ is gitignored, so that evidence file can never exist in the repo; worth moving the probe findings to a committed location (the comment it replaced pointed at docs/spikes/dist-kiro/findings.md).
  • hookDebug() creates .aidlc-hooks-health/ before any heartbeat exists, which flips --doctor from the fresh-install PASS to the "dir exists but no hooks have fired" FAIL on a debug-enabled fresh install. Transient (first Write/Edit heartbeat clears it), but it misleads exactly the user who just enabled debug to diagnose hooks.
  • Perf note: both hooks now fire on every IDE shell command and each does a full readAllAuditShards read in a fresh bun subprocess; a tail read or mtime cache would cut most of that. hook-debug.log also grows without bound while the flag is exported.

For completeness, two things I checked and did not count against the PR: the CLAUDE_PROJECT_DIR-first project-dir resolution is pre-existing behavior shared by all core hooks (the adapter resolving against the same root keeps the pair self-consistent), and the SUBAGENT_COMPLETED agent_type: "unknown" is strictly more signal than the base, whose stdin gate meant it emitted nothing at all in the IDE.

@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.

Version collision: this PR and #467 both bump to 2.1.5 with a ## [2.1.5] CHANGELOG heading. Per changelog policy (and t68 guards duplicate headings), second-to-merge rebases and re-bumps to 2.1.6. #467 was opened first (30/06 vs 01/07) — worth coordinating merge order.

Comment thread harness/kiro-ide/hooks/aidlc-kiro-adapter.ts
Comment thread harness/kiro-ide/hooks/aidlc-kiro-adapter.ts Outdated
Comment thread harness/kiro-ide/hooks/aidlc-sync-statusline.kiro.hook Outdated
@apackeer

apackeer commented Jul 5, 2026

Copy link
Copy Markdown

Consolidating where this PR stands, since v2 has moved under it. The two existing reviews stand in full: @apackeer's four findings (2026-07-01) and @leandrodamascena's CHANGES_REQUESTED + three inline comments are all unaddressed - findings 1-3 remain merge blockers (they corrupt live workflow state). New items accrued since those reviews:

  1. Rebase target moved: re-bump to 2.2.1, not 2.1.6. v2 is now 2.2.0; 2.1.5 through 2.2.0 are all taken, and 2.2.2-2.2.8 are claimed by in-flight PRs. CHANGELOG heading renames accordingly (t68 enforces).

  2. The adapter gained mint/block human-presence targets after this PR was cut (see harness/kiro-ide/hooks/aidlc-kiro-adapter.ts:72-127 on current v2). They read kiro.cwd off the stdin payload this PR deletes - please re-integrate them on the USER_PROMPT-era adapter (drop the kiro.cwd source; process.cwd() is the existing fallback and correct here). Mishandling this silently kills the human-presence floor on Kiro IDE. Upside worth a CHANGELOG line: your stdin removal also eliminates the 2s stdin race the block hook currently pays on every preToolUse.

  3. t188 is taken. t188-human-presence-gate.test.ts landed on v2 on 2026-07-03. Rename the new suite to the next free slot after rebase and regenerate the coverage registry.

  4. On the .trim() suggestion for extractWrittenPath: necessary but not sufficient. Verified empirically: trim fixes the trailing-newline case but not the suffix capture (Replaced text in foo.md (2 occurrences) still forwards the parenthetical as the path) or reworded/multi-line results, and the earlier finding's recordHookDrop ask still applies. Please treat the two comments as one item: trim + a terminator on the Replaced-text-in capture + drop-recording on a write-class tool with no extractable path + variant fixtures.

  5. Finish [Bug]: AI-DLC v2 log-subagent hook never fires in Kiro IDE — adapter matches tool_name === "subagent" but the IDE sends invoke_sub_agent, so SUBAGENT_COMPLETED is never emitted (and Agent Type is unknown even when it is) #459 rather than half-fixing it. Dropping the subagent tool-name gate correctly revives SUBAGENT_COMPLETED (the .kiro.hook already filters invoke_sub_agent), but hardcoding agent_type: "unknown" discards identity present in the tool result - the delegated agents self-identify on their first line (**Reviewer:** <name> / **Agent:** <name>, see the workaround in [Bug]: AI-DLC v2 log-subagent hook never fires in Kiro IDE — adapter matches tool_name === "subagent" but the IDE sends invoke_sub_agent, so SUBAGENT_COMPLETED is never emitted (and Agent Type is unknown even when it is) #459). Extract it there and forward the result text as the message.

  6. Guard toolSuccess === false explicitly in audit-and-sensors. [Bug]: AI-DLC v2 audit/sensor hooks never fire in Kiro IDE — empty tool_input (payload delivered via USER_PROMPT env, not stdin) #417 documents that failed ops carry it; relying on error prose not matching the path regexes is implicit.

With the earlier findings 1-3 fixed, finding 4 + the inline items folded per point 4, and points 1-3/5-6 above, this is a merge - the USER_PROMPT root-cause work is exactly what we want in. Happy to help with the rebase; the mint/block re-integration is the only non-mechanical piece. If you're short on time, say the word and we'll carry it over the line keeping your commits as the base.

@SiddhJog

SiddhJog commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

I've pushed the hardening in 4e245f7 (findings) + 55beefe (filesystem debug toggle). All four findings are addressed with scoped changes — no structural rework in this PR. I've also run the fixes through 2 Kiro IDE workflow runs; every finding is now confirmed holding in the debug trace, not just unit-verified.

Finding 1 — state resurrection (aidlc-sync-statusline.ts, IDE audit-sync branch). Made forward-only. It now exits without writing when: Status != Running, Current Stage is none/empty, the audit slug already matches state, or the audit's latest stage is already completed/skipped. So a finalize/approve is never undone by the next shell command. Live-confirmed: across ~30+ hook cycles and multiple transitions (intent-capture → requirements-analysis → user-stories), Current Stage only ever moved forward. Trace shows repeated ide-audit-sync auditSlug="user-stories" current="user-stories" exiting at the current === auditSlug guard with no set-status follow-up.

Finding 2 — single-stage rows (latestStartedStageSlug, aidlc-lib.ts). Now filters Workflow: single-stage: blocks so a synthetic --single stage-runner row can't be mistaken for the live stage.

Finding 3 — recompile-forever (aidlc-runtime-compile.ts, IDE path). Added an mtime idempotency guard: if runtime-graph.json is at least as new as the newest audit shard, the tail hasn't changed since the last compile → skip. A real new transition bumps a shard's mtime past the graph and re-enables the compile. This bounds the case where a lingering transition (e.g. after WORKFLOW_COMPLETED) would otherwise recompile on every subsequent shell command. Live-confirmed: debug log shows runtime-compile skip: graph newer than audit (idempotent) graphMtime=… newestShard=… firing (guard hit), while genuine transitions (STAGE_AWAITING_APPROVAL at gate-start) still recompiled correctly.

Finding 4 — path extraction (extractWrittenPath, aidlc-kiro-adapter.ts). Now trims trailing whitespace and strips the str_replace (N occurrences) suffix. When a write-class tool yields no extractable path, the adapter records a visible recordHookDrop instead of a silent no-op. Live-confirmed: every artifact write (stories.md, personas.md, memory.md, etc.) resolved cleanly to an absolute path and emitted ARTIFACT_UPDATED; zero hook-drops in the run.

Smaller items: CHANGELOG reworded; the kiro-ide harness guide's sync-statusline row corrected (shell event, not spec); doctor no longer false-FAILs on the debug-only health dir; and the probe findings are now committed at
kiro-ide-hook-payload.md
.

Debug toggle (55beefe): added touch aidlc/.aidlc-hook-debug as a filesystem alternative to AIDLC_HOOK_DEBUG=1 — takes effect on the next hook fire with no IDE restart (the IDE spawns hook subprocesses, so an env var would need a restart). Off by default, zero overhead on a normal run. This is what produced the traces above.

@SiddhJog SiddhJog force-pushed the fix/kiro-hooks branch 2 times, most recently from d2f2c9f to 1dc217a Compare July 8, 2026 10:19

@apackeer apackeer left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM. Everything from the consolidated comment (2026-07-05) is addressed on the rebased head 1dc217ab; I verified each item and ran checks against it locally.

Verified on this head:

  • Rebase + re-bump: merge-base is the current v2 tip 242953ec; version file, CHANGELOG heading, and README badge agree at 2.2.11 (t68 green).
  • mint/block re-integrated on the USER_PROMPT-era adapter: project dir from process.cwd(), dead kiro.cwd source dropped, full carve-out chain (autonomous mode, off-switch, open-gate predicate, humanActedSinceGate) and the exit-2 contract intact. The 2s stdin-race removal made the CHANGELOG too.
  • t188 renamed to t213 with the coverage registry regenerated.
  • log-subagent finished properly: extractAgentIdentity() recovers the delegate from the **Reviewer:** / **Agent:** first line and forwards the result text as last_assistant_message (which the core hook already reads). Going further and making the marker a persona-level Output Contract in both reviewer agents plus stage-protocol is the right call - it turns a workaround into a contract. S1-S3 pin it.
  • toolSuccess === false guarded explicitly in audit-and-sensors, with absent-field falling through rather than dropping (the right defensive choice). T1/T2 pin both directions.
  • Child spawns use process.execPath rather than bare bun, consistent with the change that landed on v2 after your branch was cut - attentive rebase, appreciated.
  • My local verification: bun scripts/package.ts --check clean on this head (all four dist trees in sync); unit slice --filter "t213|t68|gen-coverage" 3/3 files green, 67 assertions, 0 failures.

Minors - please pick these up in a follow-up PR, none block this merge:

  1. Doctor's fresh-vs-drift check counts only .last files as hook-fired content, so a workspace where the adapter fires but every write drops (a kiro-adapter.drops present, no heartbeat yet) reads "Hook heartbeats: not yet fired" (PASS) where it should flag. Counting .drops as fired-content closes it; alternatively fold it into #516, which already tracks doctor surfacing drop contents.
  2. The runtime-compile mtime idempotency guard (finding 3) is the one review fix without a pinning test. A cheap shape: fire runtime-compile twice with the debug marker on and assert the second run logs the idempotent skip (or that the graph mtime is unchanged).
  3. docs/reference/kiro-ide-hook-payload.md breaks that directory's NN-topic.md naming convention and nothing links to it yet - worth a number and an index entry.
  4. In aidlc-architecture-reviewer-agent.md the new Output Contract section was inserted mid-list, so the last two Key Principles bullets now sit under the Output Contract heading. The product-lead edit appended cleanly; same treatment here.

Merge-sequencing heads-up (coordination, not a fault of this PR): 2.2.11 is currently claimed by four open PRs (#471, #508, #510, #535), and the t213 test slot by three (#471, #508, #535 - different filenames so no git conflict, but two t213s would land in the tree unless the later merger renames). Whichever merges second re-bumps its version and renames its test file per the usual convention; next genuinely free test slot is t218+ given #537/#538 claim t216/t217.

Kiro IDE delivers hook context through the USER_PROMPT environment
variable (not stdin, which it opens but never writes), and leaves
toolArgs empty. The IDE adapter now reads USER_PROMPT, recovers the
written file path from the tool-result prose, and drives the
command/payload-free hooks off the audit trail.

- audit-logger, sensor-fire, runtime-compile, sync-statusline now do
  real work on Kiro IDE; write paths resolved relative to workspace root.
- runtime-compile and sync-statusline gate on the audit tail (payload-free).
- sync-statusline ide-audit-sync is forward-only (never rewinds Current
  Stage): skips when not Running, pointer none, or audit slug already
  completed/skipped.
- runtime-compile IDE path adds an mtime idempotency guard so a lingering
  transition does not recompile on every subsequent shell command.
- extractWrittenPath tolerates trailing newlines, strips a str_replace
  " (N occurrences)" suffix, and records a visible hook-drop when a
  write-class tool yields no extractable path.
- latestStartedStageSlug ignores synthetic --single stage-runner rows.
- opt-in hook debug log: AIDLC_HOOK_DEBUG=1 or touch aidlc/.aidlc-hook-debug.

Kiro CLI, Claude Code, and Codex behaviour unchanged (new branches gated
behind the IDE-only ide-audit-sync marker and the opt-in debug env var).
@SiddhJog SiddhJog merged commit 300b640 into v2 Jul 9, 2026
@SiddhJog SiddhJog deleted the fix/kiro-hooks branch July 9, 2026 06:43
apackeer added a commit that referenced this pull request Jul 9, 2026
Review fixes for leandrodamascena:
- docs/guide/13-customization.md: quote the statusline command and the
  bun-tools permission glob in both the JSON example and the prose, and
  note why the * stays outside the quotes
- docs/reference/14-claude-features.md: quote the statusline,
  session-start, and session-end command examples
- rename the regression test to t219-claude-project-dir-quoting.test.ts
  (t199 already shipped on v2 from #492; t218 landed with #471's
  kiro-ide hook adapter) and update the t40 comment
apackeer added a commit that referenced this pull request Jul 9, 2026
… (2.2.14) (#522)

* fix: quote $CLAUDE_PROJECT_DIR in Claude hook and statusline commands (#519) (2.2.19)

* review: quote CLAUDE_PROJECT_DIR in docs examples, rename test to t219

Review fixes for leandrodamascena:
- docs/guide/13-customization.md: quote the statusline command and the
  bun-tools permission glob in both the JSON example and the prose, and
  note why the * stays outside the quotes
- docs/reference/14-claude-features.md: quote the statusline,
  session-start, and session-end command examples
- rename the regression test to t219-claude-project-dir-quoting.test.ts
  (t199 already shipped on v2 from #492; t218 landed with #471's
  kiro-ide hook adapter) and update the t40 comment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants