@event4u/agent-config 13.0.0 → 14.0.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 (118) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +160 -333
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/evals/roadmap.json +8 -0
  5. package/dist/agent-src/commands/fix/ci.md +39 -1
  6. package/dist/agent-src/commands/pr/create.md +65 -4
  7. package/dist/agent-src/commands/roadmap/next.md +28 -5
  8. package/dist/agent-src/commands/work.md +16 -0
  9. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
  10. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
  11. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
  12. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
  13. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
  14. package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
  15. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
  16. package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
  17. package/dist/agent-src/rules/design-review-after-ui-write.md +25 -8
  18. package/dist/agent-src/rules/ui-audit-gate.md +25 -8
  19. package/dist/agent-src/scripts/gate_execute.ts +395 -0
  20. package/dist/agent-src/scripts/resume_probe.ts +422 -0
  21. package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
  22. package/dist/agent-src/scripts/update_roadmap_progress.ts +53 -2
  23. package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
  24. package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
  25. package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
  26. package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
  27. package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
  28. package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
  29. package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
  30. package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
  31. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  32. package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
  33. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  34. package/dist/agent-src/templates/roadmaps.md +43 -3
  35. package/dist/cli/registry.js +1 -0
  36. package/dist/cli/registry.js.map +1 -1
  37. package/dist/cli-delegate/{chunk-N7FEST6D.js → chunk-7PZ4FSZB.js} +351 -174
  38. package/dist/cli-delegate/{chunk-3B72LSZL.js → chunk-F3YUF7SA.js} +1 -1
  39. package/dist/cli-delegate/cmd_conformance.js +224 -104
  40. package/dist/cli-delegate/cmd_doctor.js +2 -2
  41. package/dist/cli-delegate/cmd_handoff.js +1 -1
  42. package/dist/discovery/deprecation-report.md +1 -1
  43. package/dist/discovery/discovery-manifest.json +40 -18
  44. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  45. package/dist/discovery/discovery-manifest.summary.md +3 -3
  46. package/dist/discovery/dormancy-report.md +1 -1
  47. package/dist/discovery/orphan-report.md +1 -1
  48. package/dist/discovery/packs.json +6 -5
  49. package/dist/discovery/trust-report.md +2 -2
  50. package/dist/discovery/workspaces.json +5 -4
  51. package/dist/hooks/dispatch.js +2018 -1320
  52. package/dist/mcp/registry-manifest.json +3 -3
  53. package/dist/mcp/server.json +2 -2
  54. package/dist/mcp/server.mjs +1652 -892
  55. package/dist/router.json +1 -1
  56. package/dist/scripts/condense.js +1 -1
  57. package/dist/scripts/condense.js.map +1 -1
  58. package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
  59. package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
  60. package/dist/ui/index.html +1 -1
  61. package/package.json +1 -1
  62. package/src/agent-src/scripts/gate_execute.ts +395 -0
  63. package/src/agent-src/scripts/resume_probe.ts +422 -0
  64. package/src/agent-src/scripts/roadmap_gates.ts +138 -4
  65. package/src/agent-src/scripts/update_roadmap_progress.ts +53 -2
  66. package/src/config/ci-local-parity.yml +13 -0
  67. package/src/config/evaluator-budgets.json +4 -4
  68. package/src/config/gate-coverage.yml +111 -5
  69. package/src/config/gate-violation-baselines.json +2 -2
  70. package/src/config/rule-activation-census.json +41 -0
  71. package/src/config/rule-stub-ceilings.json +52 -0
  72. package/src/scripts/_dispatch.bash +4 -0
  73. package/src/scripts/_lib/evaluator_contract.ts +159 -0
  74. package/src/scripts/_lib/gh_transient.ts +80 -0
  75. package/src/scripts/_lib/release_material.ts +16 -5
  76. package/src/scripts/_lib/review_independence.ts +111 -0
  77. package/src/scripts/_lib/session_register.ts +14 -0
  78. package/src/scripts/_lib/skill_catalogue.ts +20 -0
  79. package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
  80. package/src/scripts/_lib/turn_end_refusals.ts +482 -0
  81. package/src/scripts/ai_council/_py_json.ts +96 -0
  82. package/src/scripts/ai_council/cli_call_budget.ts +357 -0
  83. package/src/scripts/ai_council/clients.ts +80 -143
  84. package/src/scripts/ai_council/config.ts +25 -11
  85. package/src/scripts/ai_council/probe_store.ts +196 -0
  86. package/src/scripts/ai_council/qualification.ts +425 -0
  87. package/src/scripts/ai_council/qualification_wiring.ts +243 -0
  88. package/src/scripts/ai_council/quorum_wiring.ts +211 -0
  89. package/src/scripts/ai_team/team_dispatch.ts +40 -6
  90. package/src/scripts/capture_skill_catalogue.ts +264 -4
  91. package/src/scripts/check_evaluator_schema.ts +249 -0
  92. package/src/scripts/check_pr_ci_current.ts +409 -0
  93. package/src/scripts/check_review_schema.ts +195 -0
  94. package/src/scripts/check_rule_activation_census.ts +451 -0
  95. package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
  96. package/src/scripts/condense.ts +1 -1
  97. package/src/scripts/council_cli.ts +141 -191
  98. package/src/scripts/dispatch_r2_reviewer.ts +8 -1
  99. package/src/scripts/hook_manifest.yaml +72 -5
  100. package/src/scripts/hooks/concern_registry.ts +4 -0
  101. package/src/scripts/hooks/dispatch_hook.ts +43 -1
  102. package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
  103. package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
  104. package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
  105. package/src/scripts/hooks_doctor.ts +112 -0
  106. package/src/scripts/interruption_report.ts +443 -0
  107. package/src/scripts/lint_evidence_artifacts.ts +359 -0
  108. package/src/scripts/lint_roadmap_blockers.ts +65 -0
  109. package/src/scripts/lint_roadmap_complexity.ts +7 -1
  110. package/src/scripts/release.ts +38 -53
  111. package/src/scripts/rule_activation_census.ts +292 -0
  112. package/src/scripts/schemas/evaluator-output.schema.json +60 -0
  113. package/src/scripts/schemas/review-findings.schema.json +31 -0
  114. package/src/scripts/self_review_gate.ts +6 -0
  115. package/src/scripts/session_register_hook.ts +30 -1
  116. package/src/scripts/sync_pr_branch.ts +271 -0
  117. package/src/scripts/test_council_qualification.ts +213 -0
  118. package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
@@ -0,0 +1,443 @@
1
+ /**
2
+ * interruption_report — road-to-user-out-of-the-loop Phase 0 Step 2.
3
+ *
4
+ * Reports the two axes the roadmap's Goal names and pre-registers as separate
5
+ * claims, because a run can ask zero questions and still be slow:
6
+ *
7
+ * CONTACT AXIS asks per run, hand-backs per run, halts per run, and the
8
+ * median user wait between an agent turn and the user's reply.
9
+ * WALL-CLOCK AXIS total elapsed per run versus agent working time — elapsed
10
+ * minus the time the run spent waiting on a human.
11
+ *
12
+ * ── WHAT THIS REPORT REFUSES TO DO ────────────────────────────────────────
13
+ *
14
+ * The step specifies a 30-session window "matching the conformance window".
15
+ * Measured on this tree the day it was written, the rolling history held
16
+ * **5 sessions, all from one day** — the file is a rolling buffer, not an
17
+ * archive, so 30 is a request rather than a guarantee.
18
+ *
19
+ * So the window is a REQUEST and `sessions_found` is reported next to it, with
20
+ * an explicit `window_short` flag when fewer sessions exist than were asked
21
+ * for. A baseline computed over 5 sessions and labelled "30" is exactly the
22
+ * measurement-artifact-as-decision-input failure this repository has already
23
+ * recorded once (`skill-usage-report.md` publishing "Active: 0"). A short
24
+ * window is a real finding about retention; hiding it would make the number
25
+ * look authoritative and the retention problem invisible.
26
+ *
27
+ * ── THE JOIN ──────────────────────────────────────────────────────────────
28
+ *
29
+ * Contacts come from `agents/runtime/state/interruptions.jsonl` (the Phase 0
30
+ * Step 1 concern). Timestamps come from `agents/runtime/.agent-chat-history`.
31
+ * They join on the session tag: the ledger writes `run_id` using
32
+ * `derive_session_tag`, the same derivation that file writes as `s`.
33
+ *
34
+ * A run present in one source and not the other is reported, never silently
35
+ * dropped — a ledger entry with no timestamps yields contact counts and a null
36
+ * wall clock, and history with no ledger entry yields timing and a null contact
37
+ * count. Dropping either would bias the medians in a direction nobody could see.
38
+ *
39
+ * ── SYNTHETIC USER TURNS ──────────────────────────────────────────────────
40
+ *
41
+ * The harness writes task notifications and system reminders into the USER
42
+ * role. Counting those as "the user replied" would collapse every measured wait
43
+ * to near zero and make the contact axis read as free. They are excluded by
44
+ * marker, and the count of what was excluded is reported so the exclusion is
45
+ * auditable rather than a silent filter.
46
+ */
47
+
48
+ import * as fs from 'node:fs';
49
+ import * as path from 'node:path';
50
+ import { fileURLToPath, pathToFileURL } from 'node:url';
51
+
52
+ export const DEFAULT_WINDOW = 30;
53
+
54
+ export interface LedgerRow {
55
+ run_id: string;
56
+ turn: number;
57
+ kind: string;
58
+ class: string;
59
+ roadmap: string | null;
60
+ at: string;
61
+ }
62
+
63
+ export interface HistoryTurn {
64
+ session: string;
65
+ role: 'user' | 'agent';
66
+ ts: string;
67
+ synthetic: boolean;
68
+ }
69
+
70
+ export interface RunReport {
71
+ run_id: string;
72
+ roadmap: string | null;
73
+ asks: number;
74
+ handbacks: number;
75
+ contacts: number;
76
+ halts: number;
77
+ elapsed_minutes: number | null;
78
+ waiting_minutes: number | null;
79
+ working_minutes: number | null;
80
+ }
81
+
82
+ export interface Report {
83
+ window_requested: number;
84
+ sessions_found: number;
85
+ window_short: boolean;
86
+ synthetic_user_turns_excluded: number;
87
+ runs: RunReport[];
88
+ median_contacts_per_run: number | null;
89
+ median_user_wait_minutes: number | null;
90
+ median_elapsed_minutes: number | null;
91
+ median_working_minutes: number | null;
92
+ notes: string[];
93
+ }
94
+
95
+ /**
96
+ * A user-role entry the harness produced, not the human. Matched on the markers
97
+ * the harness actually emits; anything else counts as a real reply.
98
+ */
99
+ const SYNTHETIC_MARKERS = [
100
+ '<task-notification>',
101
+ '<system-reminder>',
102
+ '<local-command-caveat>',
103
+ '<command-name>',
104
+ ];
105
+
106
+ export function isSyntheticUserText(text: string): boolean {
107
+ return SYNTHETIC_MARKERS.some((m) => text.includes(m));
108
+ }
109
+
110
+ export function median(values: readonly number[]): number | null {
111
+ if (values.length === 0) return null;
112
+ const sorted = [...values].sort((a, b) => a - b);
113
+ const mid = Math.floor(sorted.length / 2);
114
+ return sorted.length % 2 === 1 ? sorted[mid]! : (sorted[mid - 1]! + sorted[mid]!) / 2;
115
+ }
116
+
117
+ export function readLedger(root: string): LedgerRow[] {
118
+ const file = path.join(root, 'agents', 'runtime', 'state', 'interruptions.jsonl');
119
+ let raw: string;
120
+ try {
121
+ raw = fs.readFileSync(file, 'utf8');
122
+ } catch {
123
+ return [];
124
+ }
125
+ const out: LedgerRow[] = [];
126
+ for (const line of raw.split('\n')) {
127
+ if (!line.trim()) continue;
128
+ try {
129
+ const o: unknown = JSON.parse(line);
130
+ if (o === null || typeof o !== 'object') continue;
131
+ const r = o as Record<string, unknown>;
132
+ if (typeof r['run_id'] !== 'string') continue;
133
+ out.push({
134
+ run_id: r['run_id'],
135
+ turn: typeof r['turn'] === 'number' ? r['turn'] : 0,
136
+ kind: typeof r['kind'] === 'string' ? r['kind'] : 'none',
137
+ class: typeof r['class'] === 'string' ? r['class'] : 'none',
138
+ roadmap: typeof r['roadmap'] === 'string' ? r['roadmap'] : null,
139
+ at: typeof r['at'] === 'string' ? r['at'] : '',
140
+ });
141
+ } catch {
142
+ // A corrupt line is one lost observation, never a failed report.
143
+ }
144
+ }
145
+ return out;
146
+ }
147
+
148
+ export function readHistory(root: string): { turns: HistoryTurn[]; synthetic: number } {
149
+ const file = process.env['AGENT_CHAT_HISTORY_FILE'] || path.join(root, 'agents', 'runtime', '.agent-chat-history');
150
+ let raw: string;
151
+ try {
152
+ raw = fs.readFileSync(file, 'utf8');
153
+ } catch {
154
+ return { turns: [], synthetic: 0 };
155
+ }
156
+ const turns: HistoryTurn[] = [];
157
+ let synthetic = 0;
158
+ for (const line of raw.split('\n')) {
159
+ if (!line.trim()) continue;
160
+ try {
161
+ const o: unknown = JSON.parse(line);
162
+ if (o === null || typeof o !== 'object') continue;
163
+ const r = o as Record<string, unknown>;
164
+ const t = r['t'];
165
+ if (t !== 'user' && t !== 'agent') continue;
166
+ const session = typeof r['s'] === 'string' ? r['s'] : '';
167
+ const ts = typeof r['ts'] === 'string' ? r['ts'] : '';
168
+ if (!session || !ts) continue;
169
+ const text = typeof r['text'] === 'string' ? r['text'] : '';
170
+ const isSynthetic = t === 'user' && isSyntheticUserText(text);
171
+ if (isSynthetic) synthetic += 1;
172
+ turns.push({ session, role: t, ts, synthetic: isSynthetic });
173
+ } catch {
174
+ // Same reasoning as the ledger: one bad line is not a failed report.
175
+ }
176
+ }
177
+ return { turns, synthetic };
178
+ }
179
+
180
+ const MINUTE_MS = 60_000;
181
+
182
+ function minutesBetween(a: string, b: string): number | null {
183
+ const ta = Date.parse(a);
184
+ const tb = Date.parse(b);
185
+ if (Number.isNaN(ta) || Number.isNaN(tb)) return null;
186
+ return (tb - ta) / MINUTE_MS;
187
+ }
188
+
189
+ /**
190
+ * Per-session wait gaps: an agent turn followed by a REAL user turn. Synthetic
191
+ * user entries are skipped rather than treated as replies, so a run the harness
192
+ * woke up four times still shows the one human wait it actually had.
193
+ */
194
+ export function waitGaps(turns: readonly HistoryTurn[]): number[] {
195
+ const gaps: number[] = [];
196
+ let lastAgentTs: string | null = null;
197
+ for (const turn of turns) {
198
+ if (turn.role === 'agent') {
199
+ lastAgentTs = turn.ts;
200
+ continue;
201
+ }
202
+ if (turn.synthetic) continue;
203
+ if (lastAgentTs === null) continue;
204
+ const gap = minutesBetween(lastAgentTs, turn.ts);
205
+ if (gap !== null && gap >= 0) gaps.push(gap);
206
+ lastAgentTs = null;
207
+ }
208
+ return gaps;
209
+ }
210
+
211
+ export function buildReport(root: string, windowRequested: number): Report {
212
+ const ledger = readLedger(root);
213
+ const { turns, synthetic } = readHistory(root);
214
+
215
+ const bySession = new Map<string, HistoryTurn[]>();
216
+ for (const turn of turns) {
217
+ const list = bySession.get(turn.session);
218
+ if (list) list.push(turn);
219
+ else bySession.set(turn.session, [turn]);
220
+ }
221
+ for (const list of bySession.values()) list.sort((a, b) => a.ts.localeCompare(b.ts));
222
+
223
+ // Newest sessions first, capped at the requested window. Ordering is by the
224
+ // session's LAST turn, so a long-running session that started earliest is
225
+ // still current.
226
+ const ordered = [...bySession.entries()].sort((a, b) => {
227
+ const la = a[1][a[1].length - 1]?.ts ?? '';
228
+ const lb = b[1][b[1].length - 1]?.ts ?? '';
229
+ return lb.localeCompare(la);
230
+ });
231
+ const windowed = ordered.slice(0, windowRequested);
232
+ const windowedIds = new Set(windowed.map(([id]) => id));
233
+
234
+ const ledgerRuns = new Set(ledger.map((r) => r.run_id));
235
+ const runIds = new Set<string>([...windowedIds]);
236
+ // A ledger run with no history is still a run; it contributes contacts and a
237
+ // null wall clock rather than disappearing from the denominator.
238
+ for (const id of ledgerRuns) if (!windowedIds.has(id)) runIds.add(id);
239
+
240
+ const allGaps: number[] = [];
241
+ const runs: RunReport[] = [];
242
+
243
+ for (const runId of runIds) {
244
+ const rows = ledger.filter((r) => r.run_id === runId);
245
+ const contacts = rows.filter((r) => r.kind !== 'none');
246
+ const asks = contacts.filter((r) => r.kind === 'ask').length;
247
+ const handbacks = contacts.filter((r) => r.kind === 'handback').length;
248
+
249
+ // A halt is a contact on the run's LAST recorded turn: the run stopped
250
+ // there and did not resume inside this window. Every other contact was
251
+ // answered and the run continued, which is a different, cheaper event.
252
+ const maxTurn = rows.reduce((m, r) => Math.max(m, r.turn), -1);
253
+ const halts = contacts.filter((r) => r.turn === maxTurn).length;
254
+
255
+ const sessionTurns = bySession.get(runId) ?? [];
256
+ const gaps = waitGaps(sessionTurns);
257
+ allGaps.push(...gaps);
258
+
259
+ let elapsed: number | null = null;
260
+ let waiting: number | null = null;
261
+ let working: number | null = null;
262
+ if (sessionTurns.length >= 2) {
263
+ elapsed = minutesBetween(sessionTurns[0]!.ts, sessionTurns[sessionTurns.length - 1]!.ts);
264
+ waiting = gaps.reduce((a, b) => a + b, 0);
265
+ if (elapsed !== null) working = Math.max(0, elapsed - waiting);
266
+ }
267
+
268
+ runs.push({
269
+ run_id: runId,
270
+ roadmap: rows.find((r) => r.roadmap !== null)?.roadmap ?? null,
271
+ asks,
272
+ handbacks,
273
+ contacts: contacts.length,
274
+ halts,
275
+ elapsed_minutes: elapsed,
276
+ waiting_minutes: waiting,
277
+ working_minutes: working,
278
+ });
279
+ }
280
+
281
+ runs.sort((a, b) => a.run_id.localeCompare(b.run_id));
282
+
283
+ const notes: string[] = [];
284
+ if (ledger.length === 0) {
285
+ notes.push(
286
+ 'interruptions.jsonl is empty or absent — the contact axis has no observations yet. ' +
287
+ 'The Phase 0 concern records on `stop`; a baseline needs sessions to have run since it landed.',
288
+ );
289
+ }
290
+ if (bySession.size === 0) {
291
+ notes.push('no chat history with session tags — the wall-clock axis has no observations.');
292
+ }
293
+ const runsWithLedger = runs.filter((r) => ledgerRuns.has(r.run_id)).length;
294
+ if (runsWithLedger < runs.length) {
295
+ notes.push(
296
+ `${runs.length - runsWithLedger} run(s) in the window have timing but no ledger entry ` +
297
+ '— reported with null contact counts rather than as zero contacts.',
298
+ );
299
+ }
300
+
301
+ return {
302
+ window_requested: windowRequested,
303
+ sessions_found: bySession.size,
304
+ window_short: bySession.size < windowRequested,
305
+ synthetic_user_turns_excluded: synthetic,
306
+ runs,
307
+ notes,
308
+ median_contacts_per_run: median(
309
+ runs.filter((r) => ledgerRuns.has(r.run_id)).map((r) => r.contacts),
310
+ ),
311
+ median_user_wait_minutes: median(allGaps),
312
+ median_elapsed_minutes: median(
313
+ runs.map((r) => r.elapsed_minutes).filter((v): v is number => v !== null),
314
+ ),
315
+ median_working_minutes: median(
316
+ runs.map((r) => r.working_minutes).filter((v): v is number => v !== null),
317
+ ),
318
+ };
319
+ }
320
+
321
+ function fmt(v: number | null, unit = ''): string {
322
+ return v === null ? 'n/a' : `${Math.round(v * 10) / 10}${unit}`;
323
+ }
324
+
325
+ export function renderText(report: Report): string {
326
+ const lines: string[] = [];
327
+ lines.push('interruption_report — road-to-user-out-of-the-loop Phase 0');
328
+ lines.push('');
329
+ lines.push(
330
+ `window: ${report.sessions_found} session(s) found / ${report.window_requested} requested` +
331
+ (report.window_short ? ' ⚠️ SHORT WINDOW — see notes' : ''),
332
+ );
333
+ lines.push(`synthetic user turns excluded: ${report.synthetic_user_turns_excluded}`);
334
+ lines.push('');
335
+ lines.push('CONTACT AXIS');
336
+ lines.push(` median contacts per run: ${fmt(report.median_contacts_per_run)}`);
337
+ lines.push(` median user wait: ${fmt(report.median_user_wait_minutes, ' min')}`);
338
+ lines.push('');
339
+ lines.push('WALL-CLOCK AXIS');
340
+ lines.push(` median elapsed per run: ${fmt(report.median_elapsed_minutes, ' min')}`);
341
+ lines.push(` median agent working time: ${fmt(report.median_working_minutes, ' min')}`);
342
+ lines.push('');
343
+ lines.push(`runs: ${report.runs.length}`);
344
+ for (const run of report.runs) {
345
+ lines.push(
346
+ ` ${run.run_id} asks=${run.asks} handbacks=${run.handbacks} halts=${run.halts}` +
347
+ ` elapsed=${fmt(run.elapsed_minutes)} working=${fmt(run.working_minutes)}` +
348
+ ` roadmap=${run.roadmap ?? '-'}`,
349
+ );
350
+ }
351
+ if (report.window_short) {
352
+ lines.push('');
353
+ lines.push(
354
+ 'SHORT WINDOW — the rolling chat history is a buffer, not an archive. The number ' +
355
+ 'above is computed over the sessions that exist, and is labelled with that count ' +
356
+ 'rather than with the requested window. Do not cite it as an N-session baseline.',
357
+ );
358
+ }
359
+ for (const note of report.notes) {
360
+ lines.push('');
361
+ lines.push(`note: ${note}`);
362
+ }
363
+ return lines.join('\n');
364
+ }
365
+
366
+ const USAGE = `usage: interruption_report [--window N] [--json] [--root PATH]
367
+
368
+ Reports the two axes road-to-user-out-of-the-loop pre-registers:
369
+ contact axis asks / hand-backs / halts per run, median user wait
370
+ wall-clock axis elapsed per run versus agent working time
371
+
372
+ --window N sessions to include, newest first (default ${DEFAULT_WINDOW}).
373
+ A REQUEST, not a guarantee: the chat history is a rolling
374
+ buffer, so the report states how many sessions it actually
375
+ found and flags a short window rather than mislabelling it.
376
+ --json machine-readable output.
377
+ --root PATH repository root (default: cwd).
378
+ `;
379
+
380
+ export function main(argv: string[] = process.argv.slice(2)): number {
381
+ if (argv.includes('-h') || argv.includes('--help')) {
382
+ process.stdout.write(USAGE);
383
+ return 0;
384
+ }
385
+ let windowRequested = DEFAULT_WINDOW;
386
+ let root = process.cwd();
387
+ let asJson = false;
388
+ for (let i = 0; i < argv.length; i += 1) {
389
+ const arg = argv[i];
390
+ if (arg === '--json') {
391
+ asJson = true;
392
+ } else if (arg === '--window') {
393
+ const raw = argv[i + 1];
394
+ const parsed = Number(raw);
395
+ if (raw === undefined || !Number.isFinite(parsed) || parsed <= 0) {
396
+ process.stderr.write(`interruption_report: --window needs a positive number\n`);
397
+ return 2;
398
+ }
399
+ windowRequested = Math.floor(parsed);
400
+ i += 1;
401
+ } else if (arg === '--root') {
402
+ const raw = argv[i + 1];
403
+ if (raw === undefined) {
404
+ process.stderr.write('interruption_report: --root needs a path\n');
405
+ return 2;
406
+ }
407
+ root = raw;
408
+ i += 1;
409
+ } else {
410
+ process.stderr.write(`interruption_report: unknown argument '${String(arg)}'\n`);
411
+ process.stderr.write(USAGE);
412
+ return 2;
413
+ }
414
+ }
415
+
416
+ const report = buildReport(root, windowRequested);
417
+ process.stdout.write(asJson ? `${JSON.stringify(report, null, 2)}\n` : `${renderText(report)}\n`);
418
+ return 0;
419
+ }
420
+
421
+ function _isCliEntry(): boolean {
422
+ if (process.argv[1] === undefined) {
423
+ return false;
424
+ }
425
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
426
+ if (import.meta.url === argvUrl) {
427
+ return true;
428
+ }
429
+ // Symlinked invocation (an installed projection, or macOS /var → /private/var):
430
+ // import.meta.url is the real path while argv[1] keeps the symlink, so the raw
431
+ // URLs differ and the CLI would silently no-op. Compare realpaths.
432
+ try {
433
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
434
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
435
+ return here === argv;
436
+ } catch {
437
+ return false;
438
+ }
439
+ }
440
+
441
+ if (_isCliEntry()) {
442
+ process.exit(main());
443
+ }