Skip to content

Commit 1dc217a

Browse files
committed
fix(kiro-ide): make hooks work via USER_PROMPT env, not stdin
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 242953e commit 1dc217a

58 files changed

Lines changed: 2584 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: 66 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.11] - 2026-07-08
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.10] - 2026-07-06
672

773
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.10-blue)
11+
![version](https://img.shields.io/badge/version-2.2.11-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,5 +38,19 @@ If the stage definition lists validation tools, **run them** before writing your
3838
- Cross-reference everything. If it's referenced, it must exist. If it exists, it should be referenced.
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."

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
@@ -869,6 +869,9 @@ If the `run-stage` directive includes a `reviewer` field (non-null), the orchest
869869
- Reads the artifact(s) to evaluate what WAS produced
870870
- Runs any validation tools listed (via shell) and includes results in findings
871871
- Appends a `## Review` section to the primary artifact file with verdict: READY or NOT-READY
872+
- Returns a response whose FIRST line is its identity marker verbatim
873+
(`**Reviewer:** <reviewer-agent-name>`), so the `SUBAGENT_COMPLETED` audit
874+
event records which reviewer ran. The reviewer's persona owns this contract.
872875

873876
3. **Read verdict.** After the reviewer returns, read the `## Review` section from the primary artifact:
874877
- **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)