@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +291 -181
- package/agents/templates/.ai-council.yml.example +35 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +83 -12
- package/dist/agent-src/rules/roadmap-progress-sync.md +48 -4
- package/dist/agent-src/scripts/resume_probe.ts +154 -9
- package/dist/agent-src/scripts/roadmap_gates.ts +210 -6
- package/dist/agent-src/skills/roadmap-management/SKILL.md +9 -4
- package/dist/agent-src/templates/agent-settings.md +70 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/telemetry/remote.ts +571 -0
- package/dist/agent-src/templates/scripts/telemetry/settings.ts +222 -44
- package/dist/cli/python/workspace_drive.js +8 -3
- package/dist/cli/python/workspace_drive.js.map +1 -1
- package/dist/cli/python/workspace_hosts.js +16 -12
- package/dist/cli/python/workspace_hosts.js.map +1 -1
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-F3YUF7SA.js → chunk-3UCJWMMP.js} +1 -1
- package/dist/cli-delegate/chunk-F7DQWYR5.js +69 -0
- package/dist/cli-delegate/{chunk-7PZ4FSZB.js → chunk-FBKL6JWT.js} +61 -2
- package/dist/cli-delegate/{chunk-MNWRDMR5.js → chunk-PKOBQADP.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +26 -11
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +17 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +4 -4
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +3773 -1731
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1070 -408
- package/dist/scripts/_lib/host_launch.js +66 -0
- package/dist/scripts/_lib/host_launch.js.map +1 -0
- package/dist/server/io/yamlIO.js +175 -3
- package/dist/server/io/yamlIO.js.map +1 -1
- package/dist/server/routes/wizard.js +20 -1
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/shared/settingsConsent.js +25 -0
- package/dist/shared/settingsConsent.js.map +1 -1
- package/docs/contracts/settings-classes.md +11 -0
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +2 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/resume_probe.ts +154 -9
- package/src/agent-src/scripts/roadmap_gates.ts +210 -6
- package/src/agent-src/templates/scripts/telemetry/remote.ts +571 -0
- package/src/agent-src/templates/scripts/telemetry/settings.ts +222 -44
- package/src/config/estate-count-budget.json +120 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +32 -4
- package/src/config/gate-violation-baselines.json +63 -63
- package/src/config/gitignore-block.txt +5 -0
- package/src/config/hook-latency-budget.json +40 -1
- package/src/config/hook-token-budget.json +22 -0
- package/src/config/rule-stub-ceilings.json +11 -3
- package/src/scripts/_dispatch.bash +42 -1
- package/src/scripts/_lib/council_fallback_posture.ts +81 -0
- package/src/scripts/_lib/council_fallback_wiring.ts +126 -0
- package/src/scripts/_lib/council_settings_block.ts +152 -0
- package/src/scripts/_lib/headless_invocation.ts +285 -0
- package/src/scripts/_lib/host_launch.ts +72 -0
- package/src/scripts/_lib/map_to_object.ts +16 -0
- package/src/scripts/_lib/roadmap_checkboxes.ts +88 -0
- package/src/scripts/_lib/run_checkpoint.ts +409 -0
- package/src/scripts/_lib/session_register.ts +32 -1
- package/src/scripts/_lib/skill_catalogue_series.ts +91 -0
- package/src/scripts/_lib/stdin.ts +35 -1
- package/src/scripts/_lib/turn_end_refusals.ts +12 -4
- package/src/scripts/_lib/unattended_guard.ts +371 -0
- package/src/scripts/ai_council/config.ts +27 -1
- package/src/scripts/ai_council/events_log.ts +12 -1
- package/src/scripts/ai_council/fallback_config.ts +114 -0
- package/src/scripts/ai_council/mid_flight_fallback.ts +295 -0
- package/src/scripts/ai_council/orchestrator.ts +251 -292
- package/src/scripts/ai_council/py_parity.ts +139 -0
- package/src/scripts/ai_council/response_render.ts +99 -0
- package/src/scripts/ai_council/spend_gate.ts +123 -0
- package/src/scripts/ai_council/transport_resolver.ts +81 -13
- package/src/scripts/bench_hook_injection.ts +100 -6
- package/src/scripts/bench_hook_latency.ts +442 -16
- package/src/scripts/check_estate_count.ts +924 -0
- package/src/scripts/compile_hook_manifest.ts +67 -0
- package/src/scripts/council_cli.ts +100 -125
- package/src/scripts/decision_memo.ts +325 -0
- package/src/scripts/dispatch_r2_reviewer.ts +169 -15
- package/src/scripts/hook_manifest.json +1 -0
- package/src/scripts/hook_manifest.yaml +172 -25
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/dispatch_hook.ts +183 -10
- package/src/scripts/hooks/dispatch_issues.ts +26 -0
- package/src/scripts/hooks/end_review_nudge_hook.ts +97 -24
- package/src/scripts/hooks/hook_stdin.ts +111 -5
- package/src/scripts/hooks/injection_budget.ts +595 -0
- package/src/scripts/hooks/payload_stub.ts +446 -0
- package/src/scripts/hooks/run_continuation_hook.ts +627 -0
- package/src/scripts/hooks/session_eol_hook.ts +52 -0
- package/src/scripts/hooks/skill_route_hook.ts +172 -11
- package/src/scripts/hooks/telemetry_usage_hook.ts +339 -0
- package/src/scripts/hooks/tool_result_bytes_hook.ts +27 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +45 -7
- package/src/scripts/hooks_doctor.ts +16 -2
- package/src/scripts/install-hooks.sh +26 -0
- package/src/scripts/interruption_report.ts +330 -13
- package/src/scripts/lint_hook_manifest.ts +167 -0
- package/src/scripts/roadmap_progress_hook.ts +254 -46
- package/src/scripts/run_supervise.ts +623 -0
- package/src/scripts/session_register_hook.ts +108 -8
- package/src/scripts/sessions_cli.ts +18 -6
- package/src/scripts/telemetry_disclosure_hook.ts +244 -0
- package/src/server/io/yamlIO.ts +167 -3
- package/src/shared/settingsConsent.ts +49 -2
- 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
|
-
|
|
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
|
-
*
|
|
435
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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"}}}}
|