@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +160 -333
- package/README.md +1 -1
- package/dist/agent-src/commands/evals/roadmap.json +8 -0
- package/dist/agent-src/commands/fix/ci.md +39 -1
- package/dist/agent-src/commands/pr/create.md +65 -4
- package/dist/agent-src/commands/roadmap/next.md +28 -5
- package/dist/agent-src/commands/work.md +16 -0
- package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
- package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
- package/dist/agent-src/rules/design-review-after-ui-write.md +25 -8
- package/dist/agent-src/rules/ui-audit-gate.md +25 -8
- package/dist/agent-src/scripts/gate_execute.ts +395 -0
- package/dist/agent-src/scripts/resume_probe.ts +422 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
- package/dist/agent-src/scripts/update_roadmap_progress.ts +53 -2
- package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
- package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
- package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
- package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
- package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
- package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
- package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
- package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +43 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-N7FEST6D.js → chunk-7PZ4FSZB.js} +351 -174
- package/dist/cli-delegate/{chunk-3B72LSZL.js → chunk-F3YUF7SA.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +224 -104
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +40 -18
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +2018 -1320
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1652 -892
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +1 -1
- package/dist/scripts/condense.js.map +1 -1
- package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
- package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/gate_execute.ts +395 -0
- package/src/agent-src/scripts/resume_probe.ts +422 -0
- package/src/agent-src/scripts/roadmap_gates.ts +138 -4
- package/src/agent-src/scripts/update_roadmap_progress.ts +53 -2
- package/src/config/ci-local-parity.yml +13 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +111 -5
- package/src/config/gate-violation-baselines.json +2 -2
- package/src/config/rule-activation-census.json +41 -0
- package/src/config/rule-stub-ceilings.json +52 -0
- package/src/scripts/_dispatch.bash +4 -0
- package/src/scripts/_lib/evaluator_contract.ts +159 -0
- package/src/scripts/_lib/gh_transient.ts +80 -0
- package/src/scripts/_lib/release_material.ts +16 -5
- package/src/scripts/_lib/review_independence.ts +111 -0
- package/src/scripts/_lib/session_register.ts +14 -0
- package/src/scripts/_lib/skill_catalogue.ts +20 -0
- package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
- package/src/scripts/_lib/turn_end_refusals.ts +482 -0
- package/src/scripts/ai_council/_py_json.ts +96 -0
- package/src/scripts/ai_council/cli_call_budget.ts +357 -0
- package/src/scripts/ai_council/clients.ts +80 -143
- package/src/scripts/ai_council/config.ts +25 -11
- package/src/scripts/ai_council/probe_store.ts +196 -0
- package/src/scripts/ai_council/qualification.ts +425 -0
- package/src/scripts/ai_council/qualification_wiring.ts +243 -0
- package/src/scripts/ai_council/quorum_wiring.ts +211 -0
- package/src/scripts/ai_team/team_dispatch.ts +40 -6
- package/src/scripts/capture_skill_catalogue.ts +264 -4
- package/src/scripts/check_evaluator_schema.ts +249 -0
- package/src/scripts/check_pr_ci_current.ts +409 -0
- package/src/scripts/check_review_schema.ts +195 -0
- package/src/scripts/check_rule_activation_census.ts +451 -0
- package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
- package/src/scripts/condense.ts +1 -1
- package/src/scripts/council_cli.ts +141 -191
- package/src/scripts/dispatch_r2_reviewer.ts +8 -1
- package/src/scripts/hook_manifest.yaml +72 -5
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/dispatch_hook.ts +43 -1
- package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
- package/src/scripts/hooks_doctor.ts +112 -0
- package/src/scripts/interruption_report.ts +443 -0
- package/src/scripts/lint_evidence_artifacts.ts +359 -0
- package/src/scripts/lint_roadmap_blockers.ts +65 -0
- package/src/scripts/lint_roadmap_complexity.ts +7 -1
- package/src/scripts/release.ts +38 -53
- package/src/scripts/rule_activation_census.ts +292 -0
- package/src/scripts/schemas/evaluator-output.schema.json +60 -0
- package/src/scripts/schemas/review-findings.schema.json +31 -0
- package/src/scripts/self_review_gate.ts +6 -0
- package/src/scripts/session_register_hook.ts +30 -1
- package/src/scripts/sync_pr_branch.ts +271 -0
- package/src/scripts/test_council_qualification.ts +213 -0
- 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
|
+
}
|