Skip to content

Commit 300b640

Browse files
authored
fix(kiro-ide): make hooks work via USER_PROMPT env, not stdin (#471)
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).
1 parent 7fa5479 commit 300b640

58 files changed

Lines changed: 2593 additions & 323 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,72 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [2.2.17] - 2026-07-09
6+
7+
Fixes the Kiro IDE harness's hooks, which previously no-op'd for everything that
8+
needed to know what the agent just did. Kiro IDE delivers hook context through
9+
the `USER_PROMPT` environment variable (not stdin, which it opens but never
10+
writes), and it leaves the tool arguments empty — so the IDE adapter now reads
11+
`USER_PROMPT`, recovers the written file path from the tool result text, and
12+
drives the command/payload-free hooks off the audit trail instead. The audit
13+
logger, sensor firing, runtime-graph recompile, and statusline sync now work in
14+
Kiro IDE without the agent having to compile the graph by hand. Dropping the
15+
stdin read also removes the 2s stdin-race timeout the CLI adapter paid on every
16+
hook; the human-presence mint/block hooks now resolve the project dir from
17+
`process.cwd()`. The Kiro CLI harness is unaffected (it uses a separate adapter
18+
and a working stdin channel). Re-copy your `dist/kiro-ide/.kiro/` to pick up the
19+
fix.
20+
21+
* **Kiro IDE hooks fire correctly**`aidlc-audit-logger`, `aidlc-sensor-fire`,
22+
`aidlc-runtime-compile`, and `aidlc-sync-statusline` now do real work on Kiro
23+
IDE. Artifact writes are audited, sensors run, the runtime graph recompiles on
24+
transitions, and `Current Stage` stays in sync.
25+
* **File paths are resolved relative to the workspace root.** Kiro IDE reports
26+
the written path relative to the project root in the tool-result text; the
27+
adapter resolves it to absolute so `audit-logger`'s record-root check passes.
28+
* **`aidlc-runtime-compile` and `aidlc-sync-statusline` are payload-free on the
29+
IDE** — both gate on the audit tail (latest transition / latest
30+
`STAGE_STARTED`) instead of the tool command or task payload the IDE does not
31+
surface. `aidlc-sync-statusline` is wired to the `shell` tool event.
32+
* **The IDE audit-tail gating is forward-only and idempotent.**
33+
`aidlc-sync-statusline` never rewinds `Current Stage`: it skips when the
34+
workflow is not `Running`, when the pointer is `none`, or when the audit's
35+
latest stage is already completed/skipped. `latestStartedStageSlug` ignores
36+
synthetic `--single` stage-runner rows. `aidlc-runtime-compile`'s IDE path
37+
adds an mtime idempotency guard so a lingering transition (e.g. after
38+
`WORKFLOW_COMPLETED`) does not recompile on every subsequent shell command.
39+
* **The `str_replace`/`fs_append` path extraction is robust.** The adapter
40+
tolerates trailing newlines, strips a `str_replace` ` (N occurrences)`
41+
suffix, and records a visible hook-drop when a write-class tool yields no
42+
extractable path (no silent no-op).
43+
* **The human-presence mint/block hooks survive the stdin removal.** The
44+
`promptSubmit` mint (`HUMAN_TURN`) and the `preToolUse` block floor now
45+
resolve the project dir from `process.cwd()` (the IDE gives no `cwd` payload),
46+
so the Kiro IDE human-presence gate keeps working; dropping the stdin read
47+
also removes the 2s stdin-race the block hook paid on every `preToolUse`.
48+
* **`log-subagent` records the delegate's identity.** The adapter no longer
49+
hardcodes `agent_type: "unknown"`; it extracts the self-identifying first line
50+
(`**Reviewer:** <name>` / `**Agent:** <name>`) from the subagent result and
51+
forwards the result text, so `SUBAGENT_COMPLETED` carries the real agent. The
52+
two reviewer agents (`aidlc-product-lead-agent`,
53+
`aidlc-architecture-reviewer-agent`) now emit that identity marker as the
54+
first line of their response (an Output Contract in their persona, reinforced
55+
in stage-protocol §12a), so the audit trail names which reviewer ran instead
56+
of recording `unknown`.
57+
* **Failed tool calls are not audited as writes.** `audit-and-sensors`
58+
explicitly guards `toolSuccess === false`, so a failed op no longer relies on
59+
its error prose failing to match the path regexes.
60+
* **Opt-in hook debug log.** Enable it to have every hook append its decision
61+
path to `<record>/.aidlc-hooks-health/hook-debug.log` — either set
62+
`AIDLC_HOOK_DEBUG=1`, or `touch aidlc/.aidlc-hook-debug` (the filesystem
63+
marker takes effect on the next hook fire with no IDE restart). Off by
64+
default (no log, no overhead); see the Kiro IDE harness guide.
65+
* **Behaviour for Kiro CLI, Claude Code, and Codex is unchanged.** The shared
66+
core hooks are edited and re-shipped to all four dists, but the new branches
67+
are gated behind the IDE-only `ide-audit-sync` marker (and the opt-in
68+
`AIDLC_HOOK_DEBUG` env var), so the CLI/Claude/Codex payload paths behave
69+
exactly as before.
70+
571
## [2.2.16] - 2026-07-09
672

773
The architecture reviewer's read scope is now bounded to the artifacts under review plus the shared inception contracts the stage `consumes:`. On per-unit Construction stages (`functional-design`, `nfr-requirements`, `nfr-design`, `infrastructure-design`, `code-generation`) the reviewer previously had no read-scope constraint and its persona pushed a "cross-reference everything" sweep, so reviewing unit N read sibling units' `construction/<other-unit>/` directories and per-unit cost grew linearly with unit count. Stage-protocol §12a now extends the per-unit reviewer pass-list with `directive.consumes`, the persona and knowledge scope "cross-reference" to what was passed, and the four harness orchestrators (Claude Code, Kiro CLI, Kiro IDE, Codex CLI) append the same bound to their reviewer step. Cross-unit contract verification runs against the shared inception artifacts; sibling-unit files may be spot-checked only when the current unit's design explicitly names an integration point in them. **Upgrade:** re-copy your `dist/<harness>/` shell into the project.
@@ -53,6 +119,7 @@ Gate-revision backstop: the conductor sometimes revises a stage's artifact at an
53119
* `Revision Count` now reflects a revision made at an open gate even when the conductor skipped the `reject` verb: `approve` backfills the count and the audit pair. Run `reject` yourself when you request changes so your feedback text is still recorded.
54120
* NEW `Recovered: true` variants of the `GATE_REJECTED` and `STAGE_REVISING` audit rows, emitted by the approve-time backstop when it backfills a skipped rejection.
55121
* NEW `AIDLC_SKIP_REVISION_BACKSTOP=1` off-switch disables the backstop (mirrors `AIDLC_SKIP_ARTIFACT_GUARD`); the test suite sets it globally so existing approve flows are unaffected.
122+
56123
## [2.2.10] - 2026-07-06
57124

58125
Workspace detection now recognizes git submodules. A workspace whose code lives in uninitialized submodules (empty dirs plus a `.gitmodules` file) previously scanned as Greenfield, so reverse-engineering was auto-skipped and every design stage ran with zero code understanding. The scanner gains a sixth brownfield signal: a parseable `.gitmodules` with at least one submodule path entry classifies the workspace Brownfield. When submodule paths are uninitialized, the scan warns and names the remedy (`git submodule update --init --recursive`) at birth, in the doctor report, and on `detect`. Languages stay as scanned (Unknown is truthful until the submodules are fetched). **Upgrade:** re-copy your `dist/<harness>/` shell into the project.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ A native implementation of the **AI-DLC methodology** (AI-Driven Development Lif
88

99
The methodology lives once, in a harness-neutral `core/`; each harness adds a thin surface that decides how it shows up on that harness. So you edit the methodology in one place, and every harness distribution is generated from it — no harness gets special treatment. (See [Repository layout](#repository-layout) for how the pieces fit together.)
1010

11-
![version](https://img.shields.io/badge/version-2.2.16-blue)
11+
![version](https://img.shields.io/badge/version-2.2.17-blue)
1212
![license](https://img.shields.io/badge/license-MIT--0-green)
1313
![Kiro IDE](https://img.shields.io/badge/harness-Kiro%20IDE-orange)
1414
![Kiro CLI](https://img.shields.io/badge/harness-Kiro%20CLI-orange)

core/agents/aidlc-architecture-reviewer-agent.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ If the stage definition lists validation tools, **run them** before writing your
3838
- Cross-reference everything within the artifacts under review and the contracts you were passed. If it's referenced there, it must exist there or in the passed contracts. If it exists in the artifacts under review, it should be referenced. Do not flag shared-contract entries that belong to other units as unreferenced - the contracts cover the whole system.
3939
- Think one layer deeper. The design says "use a queue" — but what about ordering? Retries? Dead letters?
4040
- Implementation is the test. If you can't mentally trace a request through the system end-to-end, it's incomplete.
41+
42+
## Output Contract
43+
44+
The FIRST line of the response you return to the orchestrator MUST be your
45+
identity marker, verbatim:
46+
47+
```
48+
**Reviewer:** aidlc-architecture-reviewer-agent
49+
```
50+
51+
This is how the audit trail records WHICH reviewer ran (the `SUBAGENT_COMPLETED`
52+
event reads it from your first line). Do not omit it, reword it, or place other
53+
text before it. After that line, give your verdict (READY / NOT-READY) and
54+
findings as usual.
4155
- Run the tools. They catch structural issues. You catch architectural issues. Together = thorough.
4256
- READY means "a developer could build this system without architectural guidance beyond this document."
4357

core/agents/aidlc-product-lead-agent.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,17 @@ You are a senior product leader — the person who signs off before work goes to
3636
- Be specific. "Story S-4 has no acceptance criteria for the error case" beats "needs more detail."
3737
- Don't rewrite. Say what's wrong and what good looks like. The builder fixes.
3838
- READY means "engineering can start without coming back to ask questions."
39+
40+
## Output Contract
41+
42+
The FIRST line of the response you return to the orchestrator MUST be your
43+
identity marker, verbatim:
44+
45+
```
46+
**Reviewer:** aidlc-product-lead-agent
47+
```
48+
49+
This is how the audit trail records WHICH reviewer ran (the `SUBAGENT_COMPLETED`
50+
event reads it from your first line). Do not omit it, reword it, or place other
51+
text before it. After that line, give your verdict (READY / NOT-READY) and
52+
findings as usual.

core/aidlc-common/protocols/stage-protocol.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,9 @@ If the `run-stage` directive includes a `reviewer` field (non-null), the orchest
875875
- Verifies cross-unit contract claims against the passed shared inception contracts, not by sweeping or searching sibling units' design directories (no cross-unit grep or glob patterns); opens another unit's file only when the current unit's design explicitly names it as an integration point, and only that file
876876
- Runs any validation tools listed (via shell) and includes results in findings
877877
- Appends a `## Review` section to the primary artifact file with verdict: READY or NOT-READY
878+
- Returns a response whose FIRST line is its identity marker verbatim
879+
(`**Reviewer:** <reviewer-agent-name>`), so the `SUBAGENT_COMPLETED` audit
880+
event records which reviewer ran. The reviewer's persona owns this contract.
878881

879882
3. **Read verdict.** After the reviewer returns, read the `## Review` section from the primary artifact:
880883
- **READY** → proceed to §13 learnings ritual then the approval gate

core/hooks/aidlc-audit-logger.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type ClaudeCodeHookInput,
1414
docsRoot,
1515
errorMessage,
16+
hookDebug,
1617
hooksHealthDir,
1718
isClaudeCodeHookInput,
1819
isoTimestamp,
@@ -21,6 +22,7 @@ import {
2122
} from "../tools/aidlc-lib.ts";
2223

2324
const projectDir = resolveProjectDirFromHook(import.meta.url);
25+
hookDebug(projectDir, "audit-logger", "invoked", { projectDir, cwd: process.cwd() });
2426

2527
// Write health heartbeat
2628
const healthDir = hooksHealthDir(projectDir);
@@ -30,15 +32,22 @@ writeFileSync(join(healthDir, "audit-logger.last"), isoTimestamp(), "utf-8");
3032
// Read JSON from stdin. If stdin is a TTY (interactive shell, test harness
3133
// running under `bash -x`-inheriting pipeline), no JSON is coming — exit
3234
// cleanly instead of blocking on the terminal read.
33-
if (process.stdin.isTTY) process.exit(0);
35+
if (process.stdin.isTTY) {
36+
hookDebug(projectDir, "audit-logger", "exit: stdin isTTY");
37+
process.exit(0);
38+
}
3439

3540
const input = await Bun.stdin.text();
3641
let parsed: ClaudeCodeHookInput;
3742
try {
3843
const raw: unknown = JSON.parse(input);
39-
if (!isClaudeCodeHookInput(raw)) process.exit(0);
44+
if (!isClaudeCodeHookInput(raw)) {
45+
hookDebug(projectDir, "audit-logger", "exit: not ClaudeCodeHookInput", { input: input.slice(0, 200) });
46+
process.exit(0);
47+
}
4048
parsed = raw;
4149
} catch {
50+
hookDebug(projectDir, "audit-logger", "exit: stdin parse failed", { input: input.slice(0, 200) });
4251
process.exit(0);
4352
}
4453

@@ -54,7 +63,11 @@ const fileNorm = auditFileValue; // forward-slash form for all path matching bel
5463
// the bare space record root — the write is logged iff it lands under that root.
5564
const recordRoot = docsRoot(projectDir).replace(/\\/g, "/").replace(/\/$/, "");
5665
const underRecord = fileNorm === recordRoot || fileNorm.startsWith(`${recordRoot}/`);
57-
if (!underRecord) process.exit(0);
66+
hookDebug(projectDir, "audit-logger", "path-gate", { tool, file: fileNorm, recordRoot, underRecord });
67+
if (!underRecord) {
68+
hookDebug(projectDir, "audit-logger", "exit: not under record root");
69+
process.exit(0);
70+
}
5871

5972
// Don't log writes to an audit shard itself (avoid recursion). The shard is
6073
// audit/<host>-<clone>.md under the record dir; the bare audit.md guard also
@@ -64,13 +77,17 @@ if (
6477
file.endsWith("\\audit.md") ||
6578
/[/\\]audit[/\\][^/\\]+\.md$/.test(file)
6679
) {
80+
hookDebug(projectDir, "audit-logger", "exit: write to audit shard (recursion guard)");
6781
process.exit(0);
6882
}
6983

7084
const auditFile = auditFilePath(projectDir);
7185

7286
// Don't auto-create the audit trail — the orchestrator creates it at workflow start.
73-
if (!existsSync(auditFile)) process.exit(0);
87+
if (!existsSync(auditFile)) {
88+
hookDebug(projectDir, "audit-logger", "exit: audit file missing", { auditFile });
89+
process.exit(0);
90+
}
7491

7592
// Extract the context breadcrumb: the path relative to the record root (the
7693
// per-intent record dir on the new layout, or the flat `aidlc-docs/` root).
@@ -123,10 +140,12 @@ try {
123140
File: auditFileValue,
124141
Context: context,
125142
}, projectDir);
143+
hookDebug(projectDir, "audit-logger", "emitted", { eventType, file: auditFileValue, context });
126144
} catch (e) {
127145
// Hook must be a no-op on any audit emission failure to avoid breaking the
128146
// user's tool call. Record the drop so `--doctor` can surface it, then
129147
// exit cleanly.
148+
hookDebug(projectDir, "audit-logger", "exit: emit threw", { eventType, error: errorMessage(e) });
130149
recordHookDrop(projectDir, "audit-logger", errorMessage(e));
131150
process.exit(0);
132151
}

core/hooks/aidlc-runtime-compile.ts

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,27 @@
2121
// compile's own audit emits cannot re-trigger the compile.
2222

2323
import { spawnSync } from "node:child_process";
24-
import { mkdirSync, writeFileSync } from "node:fs";
24+
import { mkdirSync, statSync, writeFileSync } from "node:fs";
2525
import { join } from "node:path";
2626
import {
2727
activeIntent,
2828
activeSpace,
29+
auditShards,
2930
type ClaudeCodeHookInput,
3031
errorMessage,
32+
hookDebug,
3133
hooksHealthDir,
3234
isClaudeCodeHookInput,
3335
isoTimestamp,
3436
readAllAuditShards,
3537
recordHookDrop,
3638
resolveProjectDirFromHook,
39+
runtimeGraphPath,
3740
harnessDir,
3841
} from "../tools/aidlc-lib.ts";
3942

4043
const projectDir = resolveProjectDirFromHook(import.meta.url);
44+
hookDebug(projectDir, "runtime-compile", "invoked");
4145

4246
// 1. TTY guard — exit cleanly when invoked outside a piped stdin context
4347
// (interactive shell, test harness running under `bash -x`).
@@ -68,8 +72,20 @@ const command: string = parsed.tool_input?.command ?? "";
6872
const aidlcTransitionTool = /\bbun\b.*\.(?:claude|kiro|codex)\/tools\/aidlc-(state|jump|bolt|utility)\.ts\b/;
6973
const aidlcOrchestrateReport = /\bbun\b.*\.(?:claude|kiro|codex)\/tools\/aidlc-orchestrate\.ts\b.*\breport\b/;
7074
const aidlcRuntimeRef = /\bbun\b.*\.(?:claude|kiro|codex)\/tools\/aidlc-runtime\.ts\b/;
71-
if (aidlcRuntimeRef.test(command)) process.exit(0);
72-
if (!aidlcTransitionTool.test(command) && !aidlcOrchestrateReport.test(command)) process.exit(0);
75+
// IDE audit-tail mode: Kiro IDE does not surface the shell command, so the
76+
// command-based filter cannot run. The adapter sets source="ide-audit-sync" to
77+
// signal "skip the command filter and gate purely on the audit tail" (steps
78+
// 6-7). The audit-tail transition check is the real gate; the command filter is
79+
// only a cheap pre-filter that needs a command string to work.
80+
const ideAuditMode = (parsed.tool_input?.source ?? "") === "ide-audit-sync";
81+
hookDebug(projectDir, "runtime-compile", "command-gate", { ideAuditMode, command: command.slice(0, 120) });
82+
if (!ideAuditMode) {
83+
if (aidlcRuntimeRef.test(command)) process.exit(0);
84+
if (!aidlcTransitionTool.test(command) && !aidlcOrchestrateReport.test(command)) {
85+
hookDebug(projectDir, "runtime-compile", "exit: command not a transition tool");
86+
process.exit(0);
87+
}
88+
}
7389

7490
// 4. Audit read — across EVERY per-clone shard of the ACTIVE intent, NOT this
7591
// hook process's own PID/clone shard. The state tool that wrote the
@@ -82,7 +98,10 @@ if (!aidlcTransitionTool.test(command) && !aidlcOrchestrateReport.test(command))
8298
const space = activeSpace(projectDir);
8399
const intent = activeIntent(projectDir, space) ?? undefined;
84100
const audit = readAllAuditShards(projectDir, intent, space).replace(/\r\n/g, "\n");
85-
if (audit.length === 0) process.exit(0);
101+
if (audit.length === 0) {
102+
hookDebug(projectDir, "runtime-compile", "exit: audit empty");
103+
process.exit(0);
104+
}
86105

87106
// 5. Heartbeat — doctor reads this file's mtime to detect silent-hook failure.
88107
// Kept at the bare (workspace-level) health dir to match where --doctor reads
@@ -111,7 +130,45 @@ const last3 = blocks.slice(-3);
111130
// (before the orchestrator wrote any §13 entries).
112131
const transitionRegex = /^\*\*Event\*\*:\s*(GATE_APPROVED|STAGE_STARTED|STAGE_AWAITING_APPROVAL|AUDIT_MERGED|WORKFLOW_COMPLETED)\s*$/m;
113132
const hasTransition = last3.some((b) => transitionRegex.test(b));
114-
if (!hasTransition) process.exit(0);
133+
hookDebug(projectDir, "runtime-compile", "transition-gate", { hasTransition, last3count: last3.length });
134+
if (!hasTransition) {
135+
hookDebug(projectDir, "runtime-compile", "exit: no transition in audit tail");
136+
process.exit(0);
137+
}
138+
139+
// 7b. Idempotency guard (IDE audit-tail mode only). On the CLI the command
140+
// filter (step 3) already bounds compiles to the one Bash call that emitted
141+
// the transition. In ide-audit-sync mode that filter is skipped, so the
142+
// transition sits in the tail across EVERY subsequent shell command — and
143+
// after WORKFLOW_COMPLETED the tail never changes again, which would make
144+
// every future shell command pay a blocking recompile forever. Bound it by
145+
// mtime: if runtime-graph.json is already at least as new as the newest
146+
// audit shard, the tail hasn't changed since the last compile — skip. A real
147+
// new transition bumps a shard's mtime past the graph and re-enables the
148+
// compile. Cheap stat calls; no new marker file.
149+
if (ideAuditMode) {
150+
try {
151+
const graphMtime = statSync(runtimeGraphPath(projectDir, intent, space)).mtimeMs;
152+
let newestShard = 0;
153+
for (const shard of auditShards(projectDir, intent, space)) {
154+
try {
155+
const m = statSync(shard).mtimeMs;
156+
if (m > newestShard) newestShard = m;
157+
} catch {
158+
// shard vanished mid-read — ignore
159+
}
160+
}
161+
if (graphMtime >= newestShard) {
162+
hookDebug(projectDir, "runtime-compile", "skip: graph newer than audit (idempotent)", {
163+
graphMtime,
164+
newestShard,
165+
});
166+
process.exit(0);
167+
}
168+
} catch {
169+
// runtime-graph.json absent (never compiled) → fall through and compile.
170+
}
171+
}
115172

116173
// 8. Dispatch — sync subprocess. Hook waits for completion. On non-zero
117174
// exit, record the drop for `--doctor` to surface; never block the

0 commit comments

Comments
 (0)