@event4u/agent-config 14.1.0 → 14.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +291 -181
  3. package/agents/templates/.ai-council.yml.example +35 -0
  4. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +83 -12
  5. package/dist/agent-src/rules/roadmap-progress-sync.md +48 -4
  6. package/dist/agent-src/scripts/resume_probe.ts +154 -9
  7. package/dist/agent-src/scripts/roadmap_gates.ts +210 -6
  8. package/dist/agent-src/skills/roadmap-management/SKILL.md +9 -4
  9. package/dist/agent-src/templates/agent-settings.md +70 -0
  10. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  11. package/dist/agent-src/templates/scripts/telemetry/remote.ts +571 -0
  12. package/dist/agent-src/templates/scripts/telemetry/settings.ts +222 -44
  13. package/dist/cli/python/workspace_drive.js +8 -3
  14. package/dist/cli/python/workspace_drive.js.map +1 -1
  15. package/dist/cli/python/workspace_hosts.js +16 -12
  16. package/dist/cli/python/workspace_hosts.js.map +1 -1
  17. package/dist/cli/registry.js +2 -0
  18. package/dist/cli/registry.js.map +1 -1
  19. package/dist/cli-delegate/{chunk-F3YUF7SA.js → chunk-3UCJWMMP.js} +1 -1
  20. package/dist/cli-delegate/chunk-F7DQWYR5.js +69 -0
  21. package/dist/cli-delegate/{chunk-7PZ4FSZB.js → chunk-FBKL6JWT.js} +61 -2
  22. package/dist/cli-delegate/{chunk-MNWRDMR5.js → chunk-PKOBQADP.js} +1 -1
  23. package/dist/cli-delegate/cmd_conformance.js +26 -11
  24. package/dist/cli-delegate/cmd_doctor.js +2 -2
  25. package/dist/cli-delegate/cmd_handoff.js +3 -3
  26. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  27. package/dist/cli-delegate/cmd_settings_set.js +17 -3
  28. package/dist/discovery/deprecation-report.md +1 -1
  29. package/dist/discovery/discovery-manifest.json +4 -4
  30. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  31. package/dist/discovery/discovery-manifest.summary.md +1 -1
  32. package/dist/discovery/dormancy-report.md +1 -1
  33. package/dist/discovery/orphan-report.md +1 -1
  34. package/dist/discovery/packs.json +2 -2
  35. package/dist/discovery/trust-report.md +1 -1
  36. package/dist/discovery/workspaces.json +2 -2
  37. package/dist/hooks/dispatch.js +3773 -1731
  38. package/dist/mcp/registry-manifest.json +2 -2
  39. package/dist/mcp/server.json +2 -2
  40. package/dist/mcp/server.mjs +1070 -408
  41. package/dist/scripts/_lib/host_launch.js +66 -0
  42. package/dist/scripts/_lib/host_launch.js.map +1 -0
  43. package/dist/server/io/yamlIO.js +175 -3
  44. package/dist/server/io/yamlIO.js.map +1 -1
  45. package/dist/server/routes/wizard.js +20 -1
  46. package/dist/server/routes/wizard.js.map +1 -1
  47. package/dist/shared/settingsConsent.js +25 -0
  48. package/dist/shared/settingsConsent.js.map +1 -1
  49. package/docs/contracts/settings-classes.md +11 -0
  50. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +2 -1
  51. package/package.json +1 -1
  52. package/src/agent-src/scripts/resume_probe.ts +154 -9
  53. package/src/agent-src/scripts/roadmap_gates.ts +210 -6
  54. package/src/agent-src/templates/scripts/telemetry/remote.ts +571 -0
  55. package/src/agent-src/templates/scripts/telemetry/settings.ts +222 -44
  56. package/src/config/estate-count-budget.json +120 -0
  57. package/src/config/evaluator-budgets.json +4 -4
  58. package/src/config/gate-coverage.yml +32 -4
  59. package/src/config/gate-violation-baselines.json +63 -63
  60. package/src/config/gitignore-block.txt +5 -0
  61. package/src/config/hook-latency-budget.json +40 -1
  62. package/src/config/hook-token-budget.json +22 -0
  63. package/src/config/rule-stub-ceilings.json +11 -3
  64. package/src/scripts/_dispatch.bash +42 -1
  65. package/src/scripts/_lib/council_fallback_posture.ts +81 -0
  66. package/src/scripts/_lib/council_fallback_wiring.ts +126 -0
  67. package/src/scripts/_lib/council_settings_block.ts +152 -0
  68. package/src/scripts/_lib/headless_invocation.ts +285 -0
  69. package/src/scripts/_lib/host_launch.ts +72 -0
  70. package/src/scripts/_lib/map_to_object.ts +16 -0
  71. package/src/scripts/_lib/roadmap_checkboxes.ts +88 -0
  72. package/src/scripts/_lib/run_checkpoint.ts +409 -0
  73. package/src/scripts/_lib/session_register.ts +32 -1
  74. package/src/scripts/_lib/skill_catalogue_series.ts +91 -0
  75. package/src/scripts/_lib/stdin.ts +35 -1
  76. package/src/scripts/_lib/turn_end_refusals.ts +12 -4
  77. package/src/scripts/_lib/unattended_guard.ts +371 -0
  78. package/src/scripts/ai_council/config.ts +27 -1
  79. package/src/scripts/ai_council/events_log.ts +12 -1
  80. package/src/scripts/ai_council/fallback_config.ts +114 -0
  81. package/src/scripts/ai_council/mid_flight_fallback.ts +295 -0
  82. package/src/scripts/ai_council/orchestrator.ts +251 -292
  83. package/src/scripts/ai_council/py_parity.ts +139 -0
  84. package/src/scripts/ai_council/response_render.ts +99 -0
  85. package/src/scripts/ai_council/spend_gate.ts +123 -0
  86. package/src/scripts/ai_council/transport_resolver.ts +81 -13
  87. package/src/scripts/bench_hook_injection.ts +100 -6
  88. package/src/scripts/bench_hook_latency.ts +442 -16
  89. package/src/scripts/check_estate_count.ts +924 -0
  90. package/src/scripts/compile_hook_manifest.ts +67 -0
  91. package/src/scripts/council_cli.ts +100 -125
  92. package/src/scripts/decision_memo.ts +325 -0
  93. package/src/scripts/dispatch_r2_reviewer.ts +169 -15
  94. package/src/scripts/hook_manifest.json +1 -0
  95. package/src/scripts/hook_manifest.yaml +172 -25
  96. package/src/scripts/hooks/concern_registry.ts +6 -0
  97. package/src/scripts/hooks/dispatch_hook.ts +183 -10
  98. package/src/scripts/hooks/dispatch_issues.ts +26 -0
  99. package/src/scripts/hooks/end_review_nudge_hook.ts +97 -24
  100. package/src/scripts/hooks/hook_stdin.ts +111 -5
  101. package/src/scripts/hooks/injection_budget.ts +595 -0
  102. package/src/scripts/hooks/payload_stub.ts +446 -0
  103. package/src/scripts/hooks/run_continuation_hook.ts +627 -0
  104. package/src/scripts/hooks/session_eol_hook.ts +52 -0
  105. package/src/scripts/hooks/skill_route_hook.ts +172 -11
  106. package/src/scripts/hooks/telemetry_usage_hook.ts +339 -0
  107. package/src/scripts/hooks/tool_result_bytes_hook.ts +27 -0
  108. package/src/scripts/hooks/turn_end_gate_hook.ts +45 -7
  109. package/src/scripts/hooks_doctor.ts +16 -2
  110. package/src/scripts/install-hooks.sh +26 -0
  111. package/src/scripts/interruption_report.ts +330 -13
  112. package/src/scripts/lint_hook_manifest.ts +167 -0
  113. package/src/scripts/roadmap_progress_hook.ts +254 -46
  114. package/src/scripts/run_supervise.ts +623 -0
  115. package/src/scripts/session_register_hook.ts +108 -8
  116. package/src/scripts/sessions_cli.ts +18 -6
  117. package/src/scripts/telemetry_disclosure_hook.ts +244 -0
  118. package/src/server/io/yamlIO.ts +167 -3
  119. package/src/shared/settingsConsent.ts +49 -2
  120. package/dist/cli-delegate/chunk-MMJL4TIX.js +0 -18
@@ -0,0 +1,325 @@
1
+ /**
2
+ * decision_memo — the decision-memo channel (UOTL Phase 4.3, sequenced by
3
+ * road-to-long-horizon-execution Phase 2.2).
4
+ *
5
+ * ## What this is for
6
+ *
7
+ * A question the agent resolves without contacting the user is only
8
+ * legitimate if the resolution is REVIEWABLE afterwards. Before this, a
9
+ * medium-impact resolution left no trace at all: the run either halted (a
10
+ * contact) or decided silently (no record). Neither is what the ladder wants
11
+ * between those two — the memo is.
12
+ *
13
+ * One file per resolution under
14
+ * `agents/runtime/state/decisions/<run>/NNN.md`, monotonic index per run.
15
+ * Local-only: the whole `agents/runtime/` tree is gitignored, so a memo never
16
+ * reaches a commit unless a human copies it out.
17
+ *
18
+ * ## Why a writer script rather than "the agent writes a markdown file"
19
+ *
20
+ * Two properties a prose instruction cannot give:
21
+ *
22
+ * - **The shape cannot drift.** Every memo carries the same five fields the
23
+ * roadmap names — question, chosen option, reasoning, resolver, confidence —
24
+ * because the writer refuses a memo missing any of them. A directory of
25
+ * free-form notes is not a channel, it is a folder.
26
+ * - **The index is monotonic and gap-free per run**, so "003 exists but 002
27
+ * does not" is impossible and a reader can tell a pruned memo from an
28
+ * unwritten one.
29
+ *
30
+ * ## What it deliberately does NOT do
31
+ *
32
+ * It does not decide anything, and it does not gate. A memo is a record of a
33
+ * resolution that already happened; making the writer refuse a resolution
34
+ * would put a decision gate in an observability surface, which is the shape
35
+ * `roadmap-progress` and the interruption ledger both stay clear of.
36
+ *
37
+ * It also carries no locked-class check, and that is not an omission to fix
38
+ * here: `high_impact` and `user_required` are refused at the SCHEMA
39
+ * (`config.ts::_build_decision_resolution`), before any model sees a route.
40
+ * A second check in the writer would read as the enforcement point and put
41
+ * the real one out of mind — the schema is the gate, this is the ledger.
42
+ */
43
+
44
+ import * as fs from 'node:fs';
45
+ import * as path from 'node:path';
46
+ import { fileURLToPath, pathToFileURL } from 'node:url';
47
+
48
+ export const DECISIONS_DIR_REL = path.join('agents', 'runtime', 'state', 'decisions');
49
+
50
+ /** The five fields the roadmap names, and nothing optional among them. */
51
+ export interface DecisionMemo {
52
+ /** The question as it was actually open — not a summary of the answer. */
53
+ question: string;
54
+ /** The option taken, stated so a reader can see what was NOT taken. */
55
+ chosen: string;
56
+ /** Why. The load-bearing field: without it the memo is a log line. */
57
+ reasoning: string;
58
+ /** Which rung resolved it — `agent`, `second-model:<provider>`, `council`. */
59
+ resolver: string;
60
+ /** Self-assessed, `high` | `medium` | `low`. Never a number. */
61
+ confidence: 'high' | 'medium' | 'low';
62
+ }
63
+
64
+ const CONFIDENCE: ReadonlySet<string> = new Set(['high', 'medium', 'low']);
65
+
66
+ /**
67
+ * A run id has to be a filename component. `safe` here is deliberately
68
+ * narrow rather than a sanitiser: a run id that needs escaping is a caller
69
+ * bug, and silently rewriting it would put two runs in one directory.
70
+ */
71
+ export function isSafeRunId(runId: string): boolean {
72
+ return /^[A-Za-z0-9_-]{1,64}$/.test(runId);
73
+ }
74
+
75
+ export function runDir(repoRoot: string, runId: string): string {
76
+ return path.join(repoRoot, DECISIONS_DIR_REL, runId);
77
+ }
78
+
79
+ /**
80
+ * The next index for this run — one past the highest `NNN.md` present.
81
+ *
82
+ * Reads the directory rather than a counter file: a counter that drifts from
83
+ * the directory is a second source of truth, and the directory is the one a
84
+ * human reads.
85
+ */
86
+ export function nextIndex(dir: string): number {
87
+ let entries: string[];
88
+ try {
89
+ entries = fs.readdirSync(dir);
90
+ } catch {
91
+ return 1;
92
+ }
93
+ let max = 0;
94
+ for (const name of entries) {
95
+ const m = /^(\d{3})\.md$/.exec(name);
96
+ if (m === null) continue;
97
+ const n = Number.parseInt(m[1] as string, 10);
98
+ if (Number.isFinite(n) && n > max) max = n;
99
+ }
100
+ return max + 1;
101
+ }
102
+
103
+ /** Refusal reasons, as a list so a caller can report all of them at once. */
104
+ export function validate(memo: Partial<DecisionMemo>): string[] {
105
+ const problems: string[] = [];
106
+ for (const field of ['question', 'chosen', 'reasoning', 'resolver'] as const) {
107
+ const v = memo[field];
108
+ if (typeof v !== 'string' || v.trim() === '') {
109
+ problems.push(`${field} is required and must be non-empty`);
110
+ }
111
+ }
112
+ const c = memo.confidence;
113
+ if (typeof c !== 'string' || !CONFIDENCE.has(c)) {
114
+ problems.push(`confidence must be one of high | medium | low (got ${String(c)})`);
115
+ }
116
+ return problems;
117
+ }
118
+
119
+ export function render(memo: DecisionMemo, index: number, stampedAt: string): string {
120
+ return [
121
+ `# Decision ${String(index).padStart(3, '0')}`,
122
+ '',
123
+ `- **Resolver:** ${memo.resolver}`,
124
+ `- **Confidence:** ${memo.confidence}`,
125
+ `- **Recorded:** ${stampedAt}`,
126
+ '',
127
+ '## Question',
128
+ '',
129
+ memo.question.trim(),
130
+ '',
131
+ '## Chosen',
132
+ '',
133
+ memo.chosen.trim(),
134
+ '',
135
+ '## Reasoning',
136
+ '',
137
+ memo.reasoning.trim(),
138
+ '',
139
+ ].join('\n');
140
+ }
141
+
142
+ export interface WriteResult {
143
+ path: string;
144
+ index: number;
145
+ }
146
+
147
+ /**
148
+ * Write one memo. Throws on a validation failure — a memo that cannot be
149
+ * written is a caller bug and must not degrade into a partial record.
150
+ */
151
+ export function writeMemo(
152
+ repoRoot: string,
153
+ runId: string,
154
+ memo: DecisionMemo,
155
+ opts: { now?: () => Date } = {},
156
+ ): WriteResult {
157
+ if (!isSafeRunId(runId)) {
158
+ throw new Error(
159
+ `decision_memo: run id ${JSON.stringify(runId)} is not a safe filename ` +
160
+ `component (letters, digits, _ and - only, 1..64 chars)`,
161
+ );
162
+ }
163
+ const problems = validate(memo);
164
+ if (problems.length > 0) {
165
+ throw new Error(`decision_memo: refusing an incomplete memo — ${problems.join('; ')}`);
166
+ }
167
+ const dir = runDir(repoRoot, runId);
168
+ fs.mkdirSync(dir, { recursive: true });
169
+ const stamp = (opts.now ?? ((): Date => new Date()))().toISOString();
170
+ // Claim the slot with `wx` and retry on collision, rather than
171
+ // read-then-write.
172
+ //
173
+ // R2 review, finding 10. `nextIndex` followed by a plain `writeFileSync`
174
+ // is a read-then-write with no lock: two memos written for the same run
175
+ // concurrently — the unattended or parallel lane this module exists for,
176
+ // or simply two shell calls — compute the same index and the second
177
+ // silently overwrites the first. That breaks the exact property the header
178
+ // sells as this module's reason to exist ("monotonic and gap-free per run,
179
+ // so a reader can tell a pruned memo from an unwritten one"), because a
180
+ // LOST memo leaves no gap at all.
181
+ //
182
+ // The bound is generous and finite: a loop that cannot fail would hide a
183
+ // permission error as a hang.
184
+ const MAX_ATTEMPTS = 64;
185
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
186
+ const index = nextIndex(dir);
187
+ const file = path.join(dir, `${String(index).padStart(3, '0')}.md`);
188
+ try {
189
+ fs.writeFileSync(file, render(memo, index, stamp), { encoding: 'utf-8', flag: 'wx' });
190
+ return { path: file, index };
191
+ } catch (err) {
192
+ // EEXIST means another writer took this index between the read and
193
+ // the write — re-read and take the next one. Anything else (EACCES,
194
+ // ENOSPC) is a real failure and must not be retried into a hang.
195
+ if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err;
196
+ }
197
+ }
198
+ throw new Error(
199
+ `decision_memo: could not claim an index for run ${JSON.stringify(runId)} ` +
200
+ `after ${MAX_ATTEMPTS} attempts — another writer is taking every slot`,
201
+ );
202
+ }
203
+
204
+ /** Every memo of one run, in index order. */
205
+ export function listMemos(repoRoot: string, runId: string): string[] {
206
+ const dir = runDir(repoRoot, runId);
207
+ let entries: string[];
208
+ try {
209
+ entries = fs.readdirSync(dir);
210
+ } catch {
211
+ return [];
212
+ }
213
+ return entries
214
+ .filter((n) => /^\d{3}\.md$/.test(n))
215
+ .sort()
216
+ .map((n) => path.join(dir, n));
217
+ }
218
+
219
+ const USAGE = `usage: decision_memo write --run <id> --question <q> --chosen <c> \\
220
+ --reasoning <r> --resolver <who> --confidence <high|medium|low>
221
+ decision_memo list --run <id>
222
+
223
+ Records one autonomously-resolved question so it is reviewable after the run.
224
+ Writes agents/runtime/state/decisions/<run>/NNN.md — local-only (gitignored).
225
+
226
+ --run takes ANY slug, and that freedom has a cost worth stating (R2 round 4,
227
+ finding 3): interruption_report attributes memos to a run by joining on the
228
+ session-derived run id, so a memo written under a hand-picked slug is counted
229
+ by 'run:supervise --digest' (which counts directories) and NOT by the report's
230
+ median_memos_per_run (which joins). The two disagree about the same files, and
231
+ the report's zero reads as "no memos" rather than "no join".
232
+
233
+ To be attributable, pass the id the ledger uses:
234
+ agent-config run:supervise --digest # lists the session ids in play
235
+ A free-form slug is still valid and still recorded — it is simply a memo about
236
+ a run the report cannot name.
237
+
238
+ Locked classes (high_impact, user_required) are refused at the CONFIG SCHEMA,
239
+ never here: this is the ledger, not the gate.
240
+ `;
241
+
242
+ function argValue(argv: string[], flag: string): string | null {
243
+ const i = argv.indexOf(flag);
244
+ if (i === -1 || i + 1 >= argv.length) return null;
245
+ return argv[i + 1] ?? null;
246
+ }
247
+
248
+ export function main(argv: string[] = process.argv.slice(2)): number {
249
+ const sub = argv[0] ?? '';
250
+ if (sub === '' || sub === '--help' || sub === '-h') {
251
+ process.stdout.write(USAGE);
252
+ return sub === '' ? 2 : 0;
253
+ }
254
+ const repoRoot = argValue(argv, '--root') ?? process.cwd();
255
+ const runId = argValue(argv, '--run');
256
+ if (runId === null) {
257
+ process.stderr.write('decision_memo: --run <id> is required\n');
258
+ return 2;
259
+ }
260
+
261
+ if (sub === 'list') {
262
+ const memos = listMemos(repoRoot, runId);
263
+ if (memos.length === 0) {
264
+ process.stdout.write(`decision_memo: no memos for run ${runId}\n`);
265
+ return 0;
266
+ }
267
+ for (const m of memos) {
268
+ process.stdout.write(`${path.relative(repoRoot, m)}\n`);
269
+ }
270
+ return 0;
271
+ }
272
+
273
+ if (sub !== 'write') {
274
+ process.stderr.write(`decision_memo: unknown subcommand '${sub}'\n${USAGE}`);
275
+ return 2;
276
+ }
277
+
278
+ const memo: Partial<DecisionMemo> = {
279
+ question: argValue(argv, '--question') ?? '',
280
+ chosen: argValue(argv, '--chosen') ?? '',
281
+ reasoning: argValue(argv, '--reasoning') ?? '',
282
+ resolver: argValue(argv, '--resolver') ?? '',
283
+ confidence: (argValue(argv, '--confidence') ?? '') as DecisionMemo['confidence'],
284
+ };
285
+ const problems = validate(memo);
286
+ if (problems.length > 0) {
287
+ process.stderr.write(`decision_memo: ${problems.join('; ')}\n`);
288
+ return 2;
289
+ }
290
+ try {
291
+ const res = writeMemo(repoRoot, runId, memo as DecisionMemo);
292
+ process.stdout.write(`${path.relative(repoRoot, res.path)}\n`);
293
+ return 0;
294
+ } catch (exc) {
295
+ process.stderr.write(`${exc instanceof Error ? exc.message : String(exc)}\n`);
296
+ return 2;
297
+ }
298
+ }
299
+
300
+ /**
301
+ * The realpath-comparing form, not a `basename` test: an installed projection
302
+ * and macOS `/var` → `/private/var` both make `argv[1]` a symlink whose URL
303
+ * differs from `import.meta.url`, and a naive comparison silently no-ops the
304
+ * CLI. Same shape as `interruption_report.ts::_isCliEntry`.
305
+ */
306
+ function _isCliEntry(): boolean {
307
+ if (process.argv[1] === undefined) {
308
+ return false;
309
+ }
310
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
311
+ if (import.meta.url === argvUrl) {
312
+ return true;
313
+ }
314
+ try {
315
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
316
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
317
+ return here === argv;
318
+ } catch {
319
+ return false;
320
+ }
321
+ }
322
+
323
+ if (_isCliEntry()) {
324
+ process.exit(main());
325
+ }
@@ -426,35 +426,170 @@ export function expectedHashes(args: {
426
426
  return {
427
427
  scope_hash: sha256(args.scopeDiffText),
428
428
  roadmap_hash: args.roadmapText == null ? 'none' : sha256(args.roadmapText),
429
- ac_hash: args.acText == null ? 'none' : sha256(args.acText),
429
+ // ONE predicate, shared with the prompt builder. An empty extraction
430
+ // used to hash to e3b0c44298fc…b855 — the SHA-256 of the empty string,
431
+ // which looks like a real hash in the manifest and re-derives
432
+ // identically on `--verify-current`, so the gate confirmed a criteria
433
+ // set that was never there. `'none'` is the value that already means
434
+ // "no input" for `roadmap_hash`; an absent AC block gets the same word.
435
+ ac_hash: hasAcceptanceCriteria(args.acText) ? sha256(args.acText as string) : 'none',
430
436
  };
431
437
  }
432
438
 
433
439
  /**
434
- * Extract the `## Acceptance Criteria` section — from that heading (inclusive)
435
- * to the next `## ` heading or EOF. No section → empty string.
440
+ * Does an extraction carry acceptance criteria?
441
+ *
442
+ * The single predicate behind BOTH the manifest's `ac_hash` and the reviewer
443
+ * prompt's roadmap line. It exists because the first version of this fix encoded
444
+ * the same question twice — truthiness in {@link expectedHashes}, an explicit
445
+ * `!== null && !== ''` at the call site — and the two agreed only for as long as
446
+ * `''` stayed the sole reachable falsy value. Refining one alone (this `.trim()`,
447
+ * for instance) would then have made the manifest record a hash while the prompt
448
+ * told the reviewer there were no criteria: the same two-places-disagree failure
449
+ * this module was being repaired for, one level up. Caught by the R2 review of
450
+ * that very fix.
451
+ *
452
+ * Whitespace-only counts as absent. The inline collector can emit blank lines
453
+ * between a criterion and its loose-list continuation, so a result that is
454
+ * structurally non-empty and semantically empty is reachable.
455
+ */
456
+ export function hasAcceptanceCriteria(acText: string | null | undefined): boolean {
457
+ return typeof acText === 'string' && acText.trim() !== '';
458
+ }
459
+
460
+ /**
461
+ * Extract a roadmap's acceptance criteria in either form the tree uses.
462
+ *
463
+ * Two forms, because roadmap authors write both and the reviewer needs whichever
464
+ * one is present:
465
+ *
466
+ * 1. A `## Acceptance Criteria` section — from that heading (inclusive) to the
467
+ * next `## ` heading or EOF.
468
+ * 2. Inline `- **AC-n:**` bullets, declared per phase with no section heading —
469
+ * collected with their indented continuation lines.
470
+ *
471
+ * Neither form → empty string, and every caller must treat that as "no criteria"
472
+ * rather than as an extracted-and-empty block: see {@link expectedHashes} and the
473
+ * prompt's roadmap line.
474
+ *
475
+ * The heading form is tried first and wins outright when found. Measured
476
+ * 2026-08-18 over the 44 active roadmaps: 21 heading-only, 7 inline-only, 0
477
+ * carrying both, 16 carrying neither — so the two forms do not currently
478
+ * co-occur and a precedence rule is a guard against a future file rather than a
479
+ * choice between live populations.
480
+ *
481
+ * WHY THE INLINE FORM WAS MISSING, stated because this is the second instance of
482
+ * the same defect class in this one function. The first was case sensitivity
483
+ * (`## Acceptance criteria` extracted nothing, found by the zcs-close R2 review
484
+ * 2026-08-09). The second is this: an inline-only roadmap yielded `''`, nothing
485
+ * downstream noticed, and the reviewer was handed a 0-byte
486
+ * `acceptance-criteria.md` under a prompt that said the criteria had been
487
+ * extracted. The R2 reviewer is the independent check on AC conformance, so
488
+ * blinding it while the artefact reads as though it checked is the
489
+ * gate-that-scans-nothing-exits-green shape one layer up.
436
490
  */
437
491
  export function extractAcceptanceCriteria(roadmapText: string): string {
438
492
  const lines = roadmapText.split('\n');
439
493
  let start = -1;
440
494
  for (let i = 0; i < lines.length; i++) {
441
- // Case-insensitive: the tree carries both `## Acceptance Criteria` and
495
+ // Case-insensitive, and NOT anchored at the end of the line.
496
+ //
497
+ // Case first: the tree carries both `## Acceptance Criteria` and
442
498
  // `## Acceptance criteria`; a case-sensitive match silently extracted
443
499
  // nothing for the latter (found by the zcs-close R2 review, 2026-08-09).
444
- if (/^## acceptance criteria\s*$/i.test(lines[i] as string)) {
500
+ //
501
+ // The end-anchor was the same defect wearing a different mask, and it was
502
+ // LIVE: `\s*$` rejected a heading carrying a qualifier, so
503
+ // `## Acceptance criteria (per phase, on promotion to ready)` and
504
+ // `## Acceptance criteria (anti-dump — the review-s own rule)` extracted
505
+ // nothing. Two roadmaps, measured 2026-08-18 — and they had been counted
506
+ // as "declares no criteria" by the very census that motivated this fix,
507
+ // which is how a matcher tuned to its own measurement hides its misses.
508
+ if (/^##\s+acceptance criteria\b/i.test(lines[i] as string)) {
445
509
  start = i;
446
510
  break;
447
511
  }
448
512
  }
449
- if (start === -1) return '';
450
- let end = lines.length;
451
- for (let i = start + 1; i < lines.length; i++) {
452
- if (/^## /.test(lines[i] as string)) {
453
- end = i;
454
- break;
513
+ if (start !== -1) {
514
+ let end = lines.length;
515
+ for (let i = start + 1; i < lines.length; i++) {
516
+ if (/^## /.test(lines[i] as string)) {
517
+ end = i;
518
+ break;
519
+ }
520
+ }
521
+ return lines.slice(start, end).join('\n');
522
+ }
523
+ return extractInlineAcceptanceCriteria(lines);
524
+ }
525
+
526
+ /**
527
+ * An inline criterion bullet, at any indent and with either bullet marker.
528
+ *
529
+ * Indent-tolerant so a nested criterion is emitted as its own row rather than
530
+ * folded into its parent as a continuation line — folding made the extraction
531
+ * shape, and therefore `ac_hash`, a function of indentation depth. Neither the
532
+ * `*` marker nor a nested criterion occurs in the tree today (measured
533
+ * 2026-08-18 across all 44 active roadmaps: zero of each), so this is a guard
534
+ * against a future file, not a fix for a live population.
535
+ */
536
+ const AC_BULLET_RE = /^\s*[-*]\s+\*\*AC-/;
537
+
538
+ /**
539
+ * Collect inline `- **AC-n:**` bullets and their continuation lines.
540
+ *
541
+ * A continuation line is indented and non-blank. Bullets are emitted in file
542
+ * order and separated by nothing but their own newline: each one is
543
+ * self-labelling (`AC-0`, `AC-1`, …), so a synthetic section heading would be
544
+ * text this function invented rather than text the roadmap declared.
545
+ *
546
+ * Two stopping rules, and the second is the subtle one:
547
+ *
548
+ * - another criterion bullet ends the current one, at any indent — see
549
+ * {@link AC_BULLET_RE};
550
+ * - a blank line ends it ONLY when the next non-blank line is not an indented
551
+ * continuation. Markdown loose lists separate a bullet from its second
552
+ * paragraph with exactly that blank line, so breaking on the blank
553
+ * unconditionally truncated the criterion and emitted a PARTIAL extraction —
554
+ * which then hashed to a real value and read as complete. That is the
555
+ * looks-like-success shape this whole function exists to remove, relocated
556
+ * from "empty but claimed" to "half but claimed"; caught by the R2 review of
557
+ * the first version. No roadmap uses a loose continuation today (measured
558
+ * 2026-08-18: zero across all 44), so this is latent-defect removal.
559
+ */
560
+ function extractInlineAcceptanceCriteria(lines: readonly string[]): string {
561
+ const out: string[] = [];
562
+ for (let i = 0; i < lines.length; i++) {
563
+ if (!AC_BULLET_RE.test(lines[i] as string)) continue;
564
+ out.push(lines[i] as string);
565
+ let j = i + 1;
566
+ while (j < lines.length) {
567
+ const line = lines[j] as string;
568
+ // A sibling or nested criterion owns itself; hand it back to the
569
+ // outer loop rather than absorbing it.
570
+ if (AC_BULLET_RE.test(line)) break;
571
+ if (/^\s+\S/.test(line)) {
572
+ out.push(line);
573
+ j++;
574
+ continue;
575
+ }
576
+ if (line.trim() !== '') break;
577
+ // Blank: look past the run of blanks for an indented continuation.
578
+ let k = j;
579
+ while (k < lines.length && (lines[k] as string).trim() === '') k++;
580
+ const resumes =
581
+ k < lines.length &&
582
+ /^\s+\S/.test(lines[k] as string) &&
583
+ !AC_BULLET_RE.test(lines[k] as string);
584
+ if (!resumes) break;
585
+ for (let b = j; b < k; b++) out.push(lines[b] as string);
586
+ j = k;
455
587
  }
588
+ // Resume the outer scan AT the line that stopped the walk, so a
589
+ // criterion bullet that ended this one is still matched.
590
+ i = j - 1;
456
591
  }
457
- return lines.slice(start, end).join('\n');
592
+ return out.join('\n');
458
593
  }
459
594
 
460
595
  /**
@@ -576,11 +711,29 @@ function reviewerPrompt(args: {
576
711
  headSha: string;
577
712
  scopeHash: string;
578
713
  roadmapGiven: boolean;
714
+ acGiven: boolean;
579
715
  changedFiles: readonly string[];
580
716
  }): string {
581
- const roadmapLine = args.roadmapGiven
582
- ? '- roadmap under review: `roadmap.md` (Acceptance Criteria extracted to `acceptance-criteria.md`)'
583
- : '- roadmap under review: none (`acceptance-criteria.md` is empty)';
717
+ // Three states, not two. Branching on `roadmapGiven` alone told the reviewer
718
+ // "Acceptance Criteria extracted" whenever a roadmap was supplied — including
719
+ // when the extraction returned nothing and the file beside it was 0 bytes. A
720
+ // reviewer told the criteria are in a file it finds empty has no way to tell
721
+ // an extraction failure from a roadmap that genuinely declares none, so it
722
+ // cannot report the gap.
723
+ //
724
+ // The empty branch reports the EXTRACTION, never the roadmap. Its first
725
+ // version said "it declares NO acceptance criteria" — a claim the extractor
726
+ // cannot establish, since an unrecognised shape produces the identical empty
727
+ // result. That turned a silent failure into an affirmative false statement
728
+ // handed to the one independent check on AC conformance, which is strictly
729
+ // worse: silence invites a look, a false assertion forecloses it. Caught by
730
+ // the R2 review of the first version, and it was not hypothetical — the
731
+ // trailing-text heading it names was live in two roadmaps.
732
+ const roadmapLine = !args.roadmapGiven
733
+ ? '- roadmap under review: none (`acceptance-criteria.md` is empty)'
734
+ : args.acGiven
735
+ ? '- roadmap under review: `roadmap.md` (Acceptance Criteria extracted to `acceptance-criteria.md`)'
736
+ : '- roadmap under review: `roadmap.md` — NO acceptance criteria could be EXTRACTED from it, in either recognised form (an `## Acceptance criteria` heading, or inline `- **AC-n:**` bullets), so `acceptance-criteria.md` is empty. Two different things produce that result and the dispatcher cannot tell them apart: the roadmap declares none, or it declares them in a shape the extractor does not recognise. Open `roadmap.md`, decide which, and report a finding if the criteria are there.';
584
737
  return [
585
738
  `# R2 completion review — ${args.slug}`,
586
739
  '',
@@ -1037,6 +1190,7 @@ function runDispatch(args: Args): number {
1037
1190
  headSha,
1038
1191
  scopeHash: hashes.scope_hash,
1039
1192
  roadmapGiven: roadmapText !== null,
1193
+ acGiven: hasAcceptanceCriteria(acText),
1040
1194
  changedFiles,
1041
1195
  });
1042
1196
  const manifest = deriveManifest({
@@ -0,0 +1 @@
1
+ {"fingerprint":"4834904a:65187","manifest":{"schema_version":1,"concerns":{"chat-history":{"script":"src/scripts/chat_history.ts","args":["hook-dispatch"],"fail_closed":false,"severity":"advisory","needs_payload_bodies":["result"]},"hot-context":{"script":"src/scripts/hot_context_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"handoff-context":{"script":"src/scripts/handoff_context_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"roadmap-progress":{"script":"src/scripts/roadmap_progress_hook.ts","args":[],"fail_closed":false,"severity":"advisory","needs_payload_bodies":["input"]},"onboarding-gate":{"script":"src/scripts/onboarding_gate_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"context-hygiene":{"script":"src/scripts/context_hygiene_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"verify-before-complete":{"script":"src/scripts/before_complete_hook.ts","args":[],"fail_closed":false,"severity":"advisory","needs_payload_bodies":["input","result"]},"ship-diff-volume":{"script":"src/scripts/hooks/ship_diff_volume_hook.ts","args":[],"fail_closed":false,"severity":"advisory","needs_payload_bodies":["input"]},"minimal-safe-diff":{"script":"src/scripts/minimal_safe_diff_hook.ts","args":[],"fail_closed":false,"severity":"advisory","needs_payload_bodies":["input"]},"injection-scan":{"script":"src/scripts/injection_scan_hook.ts","args":[],"fail_closed":false,"severity":"advisory","needs_payload_bodies":["input","result"]},"block-no-verify":{"script":"src/scripts/hooks/block_no_verify.ts","args":[],"fail_closed":true,"severity":"blocking","needs_payload_bodies":["input"]},"block-kernel-rule-writes":{"script":"src/scripts/hooks/block_kernel_rule_writes.ts","args":[],"fail_closed":true,"severity":"blocking","needs_payload_bodies":["input"]},"block-config-weakening":{"script":"src/scripts/hooks/block_config_weakening.ts","args":[],"fail_closed":false,"severity":"blocking","needs_payload_bodies":["input"]},"rtk-wrap":{"script":"src/scripts/hooks/rtk_wrap_hook.ts","args":[],"fail_closed":false,"severity":"advisory","needs_payload_bodies":["input"]},"design-slop":{"script":"src/scripts/hooks/design_slop_hook.ts","args":[],"fail_closed":false,"severity":"advisory","needs_payload_bodies":["input"]},"ui-route-nudge":{"script":"src/scripts/hooks/ui_route_nudge_hook.ts","args":[],"fail_closed":false,"severity":"advisory","needs_payload_bodies":["input"]},"code-graph-nudge":{"script":"src/scripts/hooks/code_graph_nudge_hook.ts","args":[],"fail_closed":false,"severity":"advisory","tools":["Grep","Glob","Read"],"needs_payload_bodies":["input"]},"memory-learn":{"script":"src/scripts/memory_learn_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"first-run-gate":{"script":"src/scripts/first_run_gate_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"profile-staleness":{"script":"src/scripts/profile_staleness_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"wrapper-freshness":{"script":"src/scripts/wrapper_freshness_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"surface-probe":{"script":"src/scripts/surface_probe_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"team-review-gate":{"script":"src/scripts/team_review_gate_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"pr-url-reminder":{"script":"src/scripts/pr_url_reminder_hook.ts","args":[],"fail_closed":false,"severity":"advisory","needs_payload_bodies":["input","result"]},"language-mirror":{"script":"src/scripts/language_mirror_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"git-authorization":{"script":"src/scripts/git_authorization_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"block-unauthorized-git":{"script":"src/scripts/hooks/block_unauthorized_git.ts","args":[],"fail_closed":true,"severity":"blocking","needs_payload_bodies":["input"]},"evidence-independence":{"script":"src/scripts/hooks/evidence_independence.ts","args":[],"fail_closed":false,"severity":"blocking","needs_payload_bodies":["input"]},"session-canary":{"script":"src/scripts/session_canary_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"council-availability":{"script":"src/scripts/council_availability_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"telemetry-disclosure":{"script":"src/scripts/telemetry_disclosure_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"self-repair":{"script":"src/scripts/self_repair_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"session-register":{"script":"src/scripts/session_register_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"orchestration-record":{"script":"src/scripts/hooks/orchestration_record_hook.ts","args":[],"fail_closed":false,"severity":"advisory","tools":["Agent","Task"],"needs_payload_bodies":["input","result"]},"telemetry-usage":{"script":"src/scripts/hooks/telemetry_usage_hook.ts","args":[],"fail_closed":false,"severity":"advisory","tools":["Skill"],"needs_payload_bodies":["input"]},"delegation-nudge":{"script":"src/scripts/hooks/delegation_nudge_hook.ts","args":[],"fail_closed":false,"severity":"advisory","nudge_rank":1},"skill-route":{"script":"src/scripts/hooks/skill_route_hook.ts","args":[],"fail_closed":false,"severity":"advisory","nudge_rank":2},"end-review-nudge":{"script":"src/scripts/hooks/end_review_nudge_hook.ts","args":[],"fail_closed":false,"severity":"advisory","skip_on_refusal_retry":true},"turn-end-gate":{"script":"src/scripts/hooks/turn_end_gate_hook.ts","args":[],"fail_closed":false,"severity":"blocking"},"edit-shape":{"script":"src/scripts/hooks/edit_shape_hook.ts","args":[],"fail_closed":false,"severity":"advisory","tools":["Write"],"needs_payload_bodies":["input"]},"reread-guard":{"script":"src/scripts/hooks/reread_guard_hook.ts","args":[],"fail_closed":false,"severity":"advisory","tools":["Read","Edit","Write"],"needs_payload_bodies":["input"]},"session-eol":{"script":"src/scripts/hooks/session_eol_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"run-continuation":{"script":"src/scripts/hooks/run_continuation_hook.ts","args":[],"fail_closed":false,"severity":"blocking"},"interruption-ledger":{"script":"src/scripts/hooks/interruption_ledger_hook.ts","args":[],"fail_closed":false,"severity":"advisory","skip_on_refusal_retry":true},"subagent-ledger":{"script":"src/scripts/hooks/subagent_ledger_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"tool-result-bytes":{"script":"src/scripts/hooks/tool_result_bytes_hook.ts","args":[],"fail_closed":false,"severity":"advisory"},"spawn-guard-shadow":{"script":"src/scripts/hooks/spawn_guard_shadow_hook.ts","args":[],"fail_closed":false,"severity":"advisory","tools":["Agent","Task"]}},"roles":{"worker":{"drop":["delegation-nudge","skill-route","end-review-nudge","council-availability","team-review-gate","self-repair","session-eol","interruption-ledger"]}},"platforms":{"augment":{"session_start":["chat-history","hot-context","handoff-context","session-canary","council-availability","telemetry-disclosure","first-run-gate","onboarding-gate","verify-before-complete","minimal-safe-diff","profile-staleness","wrapper-freshness","surface-probe","session-register"],"session_end":["chat-history","memory-learn","session-register","roadmap-progress"],"stop":["chat-history","hot-context","verify-before-complete","self-repair","session-register","roadmap-progress"],"pre_tool_use":["block-no-verify","block-unauthorized-git","evidence-independence","block-kernel-rule-writes","block-config-weakening","rtk-wrap","design-slop","ui-route-nudge","code-graph-nudge","ship-diff-volume","reread-guard"],"post_tool_use":["chat-history","roadmap-progress","context-hygiene","verify-before-complete","minimal-safe-diff","injection-scan","pr-url-reminder","orchestration-record","telemetry-usage","tool-result-bytes","edit-shape"]},"claude":{"session_start":["chat-history","hot-context","handoff-context","session-canary","council-availability","telemetry-disclosure","first-run-gate","onboarding-gate","verify-before-complete","minimal-safe-diff","profile-staleness","wrapper-freshness","surface-probe","session-register"],"session_end":["chat-history","memory-learn","session-register","roadmap-progress"],"stop":["chat-history","hot-context","verify-before-complete","team-review-gate","end-review-nudge","turn-end-gate","self-repair","session-register","session-eol","interruption-ledger","roadmap-progress","run-continuation"],"user_prompt_submit":["chat-history","verify-before-complete","minimal-safe-diff","language-mirror","delegation-nudge","skill-route","git-authorization","session-canary","self-repair","session-register"],"pre_tool_use":["block-no-verify","block-unauthorized-git","evidence-independence","block-kernel-rule-writes","block-config-weakening","rtk-wrap","design-slop","ui-route-nudge","code-graph-nudge","ship-diff-volume","reread-guard","spawn-guard-shadow"],"post_tool_use":["chat-history","roadmap-progress","context-hygiene","verify-before-complete","minimal-safe-diff","injection-scan","pr-url-reminder","orchestration-record","telemetry-usage","tool-result-bytes","edit-shape","language-mirror"],"pre_compact":["language-mirror","hot-context"],"subagent_start":["subagent-ledger"],"subagent_stop":["subagent-ledger"]},"cowork":{"session_start":["chat-history","hot-context","handoff-context","session-canary","council-availability","telemetry-disclosure","first-run-gate","onboarding-gate","verify-before-complete","minimal-safe-diff","profile-staleness","wrapper-freshness","surface-probe","session-register"],"session_end":["chat-history","memory-learn","session-register","roadmap-progress"],"stop":["chat-history","hot-context","verify-before-complete","self-repair","session-register","roadmap-progress"],"user_prompt_submit":["chat-history","verify-before-complete","minimal-safe-diff","language-mirror","git-authorization","session-canary","self-repair","session-register"],"pre_tool_use":["block-no-verify","block-unauthorized-git","evidence-independence","block-kernel-rule-writes","block-config-weakening","rtk-wrap","design-slop","ui-route-nudge","code-graph-nudge","ship-diff-volume","reread-guard","spawn-guard-shadow"],"post_tool_use":["chat-history","roadmap-progress","context-hygiene","verify-before-complete","minimal-safe-diff","injection-scan","pr-url-reminder","orchestration-record","telemetry-usage","tool-result-bytes","edit-shape"],"subagent_start":["subagent-ledger"],"subagent_stop":["subagent-ledger"]},"cursor":{"session_start":["chat-history","hot-context","handoff-context","session-canary","council-availability","telemetry-disclosure","first-run-gate","onboarding-gate","verify-before-complete","minimal-safe-diff","profile-staleness","wrapper-freshness","surface-probe","session-register"],"session_end":["chat-history","memory-learn","session-register","roadmap-progress"],"stop":["chat-history","hot-context","verify-before-complete","self-repair","session-register","roadmap-progress"],"user_prompt_submit":["chat-history","verify-before-complete","minimal-safe-diff","language-mirror","git-authorization","session-canary","self-repair","session-register"],"post_tool_use":["chat-history","roadmap-progress","context-hygiene","verify-before-complete","minimal-safe-diff","injection-scan","pr-url-reminder","orchestration-record","telemetry-usage","tool-result-bytes","edit-shape"]},"cline":{"session_start":["chat-history","hot-context","handoff-context","session-canary","council-availability","telemetry-disclosure","first-run-gate","onboarding-gate","verify-before-complete","minimal-safe-diff","profile-staleness","wrapper-freshness","surface-probe","session-register"],"session_end":["chat-history","memory-learn","session-register","roadmap-progress"],"stop":["chat-history","hot-context","verify-before-complete","self-repair","session-register","roadmap-progress"],"user_prompt_submit":["chat-history","verify-before-complete","minimal-safe-diff","language-mirror","git-authorization","session-canary","self-repair","session-register"],"post_tool_use":["chat-history","roadmap-progress","context-hygiene","verify-before-complete","minimal-safe-diff","injection-scan","pr-url-reminder","orchestration-record","telemetry-usage","tool-result-bytes","edit-shape"]},"windsurf":{"session_start":["chat-history","hot-context","handoff-context","session-canary","council-availability","telemetry-disclosure","first-run-gate","onboarding-gate","verify-before-complete","profile-staleness","wrapper-freshness","surface-probe","session-register"],"stop":["chat-history","hot-context","verify-before-complete","self-repair","session-register"],"user_prompt_submit":["chat-history","verify-before-complete","language-mirror","git-authorization","session-canary","self-repair","session-register"]},"gemini":{"session_start":["chat-history","hot-context","handoff-context","session-canary","council-availability","telemetry-disclosure","first-run-gate","onboarding-gate","verify-before-complete","minimal-safe-diff","profile-staleness","wrapper-freshness","surface-probe","session-register"],"session_end":["chat-history","memory-learn","session-register","roadmap-progress"],"stop":["chat-history","hot-context","verify-before-complete","self-repair","session-register","roadmap-progress"],"user_prompt_submit":["chat-history","verify-before-complete","minimal-safe-diff","language-mirror","git-authorization","session-canary","self-repair","session-register"],"post_tool_use":["chat-history","roadmap-progress","context-hygiene","verify-before-complete","minimal-safe-diff","injection-scan","pr-url-reminder","orchestration-record","telemetry-usage","tool-result-bytes","edit-shape"]},"copilot":{"fallback_only":true}},"native_event_aliases":{"augment":{"SessionStart":"session_start","SessionEnd":"session_end","Stop":"stop","PostToolUse":"post_tool_use","PreToolUse":"pre_tool_use"},"claude":{"SessionStart":"session_start","SessionEnd":"session_end","Stop":"stop","UserPromptSubmit":"user_prompt_submit","PostToolUse":"post_tool_use","PreToolUse":"pre_tool_use","PreCompact":"pre_compact","SubagentStart":"subagent_start","SubagentStop":"subagent_stop"},"cowork":{"SessionStart":"session_start","SessionEnd":"session_end","Stop":"stop","UserPromptSubmit":"user_prompt_submit","PostToolUse":"post_tool_use","PreToolUse":"pre_tool_use","PreCompact":"pre_compact","SubagentStart":"subagent_start","SubagentStop":"subagent_stop"},"cursor":{"sessionStart":"session_start","sessionEnd":"session_end","stop":"stop","beforeSubmitPrompt":"user_prompt_submit","postToolUse":"post_tool_use","preToolUse":"pre_tool_use","preCompact":"pre_compact"},"cline":{"TaskStart":"session_start","TaskResume":"session_start","TaskComplete":"session_end","TaskCancel":"stop","UserPromptSubmit":"user_prompt_submit","PostToolUse":"post_tool_use","PreToolUse":"pre_tool_use","PreCompact":"pre_compact"},"windsurf":{"pre_user_prompt":"user_prompt_submit","post_cascade_response":"stop","post_setup_worktree":"session_start"},"gemini":{"SessionStart":"session_start","SessionEnd":"session_end","BeforeAgent":"user_prompt_submit","AfterAgent":"stop","BeforeTool":"pre_tool_use","AfterTool":"post_tool_use"}}}}