@event4u/agent-config 8.11.0 → 8.12.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/AGENTS.md +1 -0
- package/CHANGELOG.md +49 -279
- package/README.md +2 -2
- package/dist/agent-src/contexts/execution/roadmap-ci-steps-mechanics.md +117 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +106 -94
- package/dist/agent-src/rules/architecture.md +6 -51
- package/dist/agent-src/rules/code-comment-discipline.md +10 -94
- package/dist/agent-src/rules/decision-revisit-gate.md +9 -77
- package/dist/agent-src/rules/improve-before-implement.md +7 -66
- package/dist/agent-src/rules/no-roadmap-references.md +13 -77
- package/dist/agent-src/rules/persona-governance.md +5 -47
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +6 -32
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +21 -104
- package/dist/agent-src/rules/untrusted-input-defense.md +3 -75
- package/dist/agent-src/skills/agent-docs-writing/SKILL.md +83 -0
- package/dist/agent-src/skills/decision-review/SKILL.md +86 -0
- package/dist/agent-src/skills/module-detect-on-the-fly/SKILL.md +58 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/contexts/knowledge-card.md +12 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +14 -14
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.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/mcp/registry-manifest.json +1 -1
- package/dist/router.json +1 -1
- package/docs/CLAIMS.md +30 -2
- package/docs/architecture.md +1 -1
- package/docs/archive/CHANGELOG-pre-8.12.0.md +304 -0
- package/docs/benchmark.md +39 -0
- package/docs/contracts/ai-council-config.md +9 -4
- package/docs/contracts/persona-schema.md +49 -0
- package/docs/contracts/provider-lifecycle.md +29 -4
- package/docs/decisions/ADR-120-council-chairman-mode.md +70 -0
- package/docs/decisions/ADR-121-knowledge-sensitivity-classes.md +90 -0
- package/docs/decisions/INDEX.md +2 -0
- package/docs/design/council-vs-solo-baseline.md +92 -0
- package/docs/design/utilization-window-criteria.md +51 -0
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +68 -0
- package/docs/guidelines/agent-infra/rule-body-migration-inventory.md +241 -0
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +80 -3
- package/docs/guidelines/code-clarity.md +86 -1
- package/docs/maintainers/branch-protection.md +87 -0
- package/docs/maintainers/system-map.md +85 -0
- package/docs/proof.md +14 -3
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +9 -0
- package/src/scripts/_cli/cmd_doctor.ts +95 -0
- package/src/scripts/_lib/knowledge_global.ts +44 -0
- package/src/scripts/_lib/knowledge_global_promote.ts +150 -0
- package/src/scripts/ai_council/orchestrator.ts +188 -1
- package/src/scripts/bench_persona_placebo.ts +308 -0
- package/src/scripts/bench_quality_rerun.ts +357 -0
- package/src/scripts/build_proof.ts +9 -0
- package/src/scripts/check_artefact_count_messaging.ts +56 -0
- package/src/scripts/check_knowledge_cards.ts +33 -4
- package/src/scripts/check_portability.ts +1 -0
- package/src/scripts/complexity_report.ts +776 -0
- package/src/scripts/consumer_matrix.ts +251 -12
- package/src/scripts/council_cli.ts +56 -1
- package/src/scripts/explain_run.ts +593 -0
- package/src/scripts/knowledge_global_cli.ts +234 -45
- package/src/scripts/rule_backlinks.ts +0 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* explain-run — read-only "why did this happen" report over EXISTING
|
|
4
|
+
* on-disk artefacts (road-to-feedback-8.11.md, Phase 7 — Explainability v0).
|
|
5
|
+
*
|
|
6
|
+
* Given `--task <id>` and/or `--since <ISO date>`, renders one Markdown
|
|
7
|
+
* report covering:
|
|
8
|
+
* 0. A plain-language `## Summary` — one line per section below, derived
|
|
9
|
+
* from the same parsed values (no extra data sources).
|
|
10
|
+
* 1. The resolved rule set for this repo — kernel (always-on) vs
|
|
11
|
+
* trigger-routed (tier_1 / tier_2), with each trigger-routed rule's
|
|
12
|
+
* full trigger set. Source: `dist/router.json`.
|
|
13
|
+
* 2. Rules the audit log actually observed firing in the window (the
|
|
14
|
+
* `rules_applied` field of audit-log-v1 lines — /work and
|
|
15
|
+
* /implement-ticket phase-ends only). Source:
|
|
16
|
+
* `agents/runtime/state/audit/*.jsonl`.
|
|
17
|
+
* 3. Artefact engagement (consulted vs applied) per kind, in window.
|
|
18
|
+
* Source: `.agent-engagement.jsonl` (telemetry.artifact_engagement).
|
|
19
|
+
* 4. Subagent dispatches in window — mode, tiers, token deltas,
|
|
20
|
+
* first_pass_success/escalated. Source: same audit-log-v1 files,
|
|
21
|
+
* `orchestration` sub-object.
|
|
22
|
+
* 5. Hook / loop / freshness state snapshot. Source:
|
|
23
|
+
* `agents/state/context-hygiene.json` (the code path the hook
|
|
24
|
+
* actually writes — see `context_hygiene_hook.ts::STATE_FILE`) or
|
|
25
|
+
* the doc-referenced `agents/runtime/state/context-hygiene.json`.
|
|
26
|
+
* 6. A parked list of "why" questions this v0 cannot answer today,
|
|
27
|
+
* with the reason — these are NOT silently omitted.
|
|
28
|
+
*
|
|
29
|
+
* Every section states its source file and prints an honest
|
|
30
|
+
* "no data — <source> absent or empty" instead of inventing rows. No new
|
|
31
|
+
* state, no daemon, no new capture — this reads artefacts other parts of
|
|
32
|
+
* the suite already write.
|
|
33
|
+
*
|
|
34
|
+
* Kill criterion: if unused after 3 releases (zero invocations/citations),
|
|
35
|
+
* delete — same honest-null convention as `orchestration_savings_report.ts`
|
|
36
|
+
* and the Phase 5 kill criteria in road-to-feedback-8.11.md.
|
|
37
|
+
*
|
|
38
|
+
* Usage:
|
|
39
|
+
* ./scripts-run src/scripts/explain_run \
|
|
40
|
+
* [--task <id>] [--since <ISO-8601>] \
|
|
41
|
+
* [--router <path>] [--audit-dir <path>] [--engagement <path>] \
|
|
42
|
+
* [--hygiene <path>] [--output <path>]
|
|
43
|
+
*
|
|
44
|
+
* Defaults: --router dist/router.json · --audit-dir agents/runtime/state/audit
|
|
45
|
+
* · --engagement .agent-engagement.jsonl · --hygiene tries
|
|
46
|
+
* agents/state/context-hygiene.json then agents/runtime/state/context-hygiene.json.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import * as fs from 'node:fs';
|
|
50
|
+
import * as path from 'node:path';
|
|
51
|
+
import { pathToFileURL } from 'node:url';
|
|
52
|
+
|
|
53
|
+
import { EngagementSchemaError, parse_event, type EngagementEvent } from '../agent-src/templates/scripts/telemetry/engagement.js';
|
|
54
|
+
import { readAuditLines } from './orchestration_savings_report.js';
|
|
55
|
+
|
|
56
|
+
// ── CLI ──────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
const DEFAULT_ROUTER = 'dist/router.json';
|
|
59
|
+
const DEFAULT_AUDIT_DIR = 'agents/runtime/state/audit';
|
|
60
|
+
const DEFAULT_ENGAGEMENT = '.agent-engagement.jsonl';
|
|
61
|
+
// The rule prose (`context-hygiene.md`) cites `agents/runtime/state/…`; the
|
|
62
|
+
// actual writer (`context_hygiene_hook.ts::STATE_FILE`) uses `agents/state/…`
|
|
63
|
+
// — a documented latent divergence (ADR-200 "replicate latent bugs"). Try
|
|
64
|
+
// the real path first, then the documented one, so the report works either
|
|
65
|
+
// way and is honest about which it found.
|
|
66
|
+
const DEFAULT_HYGIENE_CANDIDATES = ['agents/state/context-hygiene.json', 'agents/runtime/state/context-hygiene.json'];
|
|
67
|
+
|
|
68
|
+
/** Cap on how many artefact ids render inline per row before "…+N more". */
|
|
69
|
+
const ID_CAP = 10;
|
|
70
|
+
|
|
71
|
+
export interface Options {
|
|
72
|
+
task: string | null;
|
|
73
|
+
since: string | null;
|
|
74
|
+
router: string;
|
|
75
|
+
auditDir: string;
|
|
76
|
+
engagement: string;
|
|
77
|
+
hygiene: string | null;
|
|
78
|
+
output: string | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseArgs(argv: string[]): Options {
|
|
82
|
+
const opts: Options = {
|
|
83
|
+
task: null,
|
|
84
|
+
since: null,
|
|
85
|
+
router: DEFAULT_ROUTER,
|
|
86
|
+
auditDir: DEFAULT_AUDIT_DIR,
|
|
87
|
+
engagement: DEFAULT_ENGAGEMENT,
|
|
88
|
+
hygiene: null,
|
|
89
|
+
output: null,
|
|
90
|
+
};
|
|
91
|
+
for (let i = 0; i < argv.length; i++) {
|
|
92
|
+
const a = argv[i];
|
|
93
|
+
if (a === undefined) continue;
|
|
94
|
+
const next = (): string | undefined => argv[++i];
|
|
95
|
+
if (a === '--task') opts.task = next() ?? opts.task;
|
|
96
|
+
else if (a.startsWith('--task=')) opts.task = a.slice('--task='.length);
|
|
97
|
+
else if (a === '--since') opts.since = next() ?? opts.since;
|
|
98
|
+
else if (a.startsWith('--since=')) opts.since = a.slice('--since='.length);
|
|
99
|
+
else if (a === '--router') opts.router = next() ?? opts.router;
|
|
100
|
+
else if (a.startsWith('--router=')) opts.router = a.slice('--router='.length);
|
|
101
|
+
else if (a === '--audit-dir') opts.auditDir = next() ?? opts.auditDir;
|
|
102
|
+
else if (a.startsWith('--audit-dir=')) opts.auditDir = a.slice('--audit-dir='.length);
|
|
103
|
+
else if (a === '--engagement') opts.engagement = next() ?? opts.engagement;
|
|
104
|
+
else if (a.startsWith('--engagement=')) opts.engagement = a.slice('--engagement='.length);
|
|
105
|
+
else if (a === '--hygiene') opts.hygiene = next() ?? opts.hygiene;
|
|
106
|
+
else if (a.startsWith('--hygiene=')) opts.hygiene = a.slice('--hygiene='.length);
|
|
107
|
+
else if (a === '--output') opts.output = next() ?? opts.output;
|
|
108
|
+
else if (a.startsWith('--output=')) opts.output = a.slice('--output='.length);
|
|
109
|
+
else {
|
|
110
|
+
process.stderr.write(`explain_run: unrecognized argument: ${a}\n`);
|
|
111
|
+
process.exit(2);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return opts;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Parse `--since` into an epoch-ms cutoff; exits 2 on an unparsable value. */
|
|
118
|
+
function parseSince(since: string | null): number | null {
|
|
119
|
+
if (since === null) return null;
|
|
120
|
+
const t = Date.parse(since);
|
|
121
|
+
if (!Number.isFinite(t)) {
|
|
122
|
+
process.stderr.write(`explain_run: --since is not a parsable date: ${since}\n`);
|
|
123
|
+
process.exit(2);
|
|
124
|
+
}
|
|
125
|
+
return t;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Router (rule) reading ───────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
export interface TriggerSpec {
|
|
131
|
+
keyword?: string;
|
|
132
|
+
phrase?: string;
|
|
133
|
+
intent?: string;
|
|
134
|
+
file_pattern?: string;
|
|
135
|
+
path_prefix?: string;
|
|
136
|
+
command?: string;
|
|
137
|
+
reason?: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface RouterRuleEntry {
|
|
141
|
+
id: string;
|
|
142
|
+
triggers: TriggerSpec[];
|
|
143
|
+
routes_to: string[];
|
|
144
|
+
workspaces: string[];
|
|
145
|
+
packs: string[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface RouterFile {
|
|
149
|
+
schema_version: number;
|
|
150
|
+
kernel: string[];
|
|
151
|
+
tier_1: RouterRuleEntry[];
|
|
152
|
+
tier_2: RouterRuleEntry[];
|
|
153
|
+
profiles?: Record<string, string[]>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Read + parse `dist/router.json`. Returns null on missing/malformed file. */
|
|
157
|
+
export function readRouter(routerPath: string): RouterFile | null {
|
|
158
|
+
if (!fs.existsSync(routerPath)) return null;
|
|
159
|
+
try {
|
|
160
|
+
const raw = JSON.parse(fs.readFileSync(routerPath, 'utf8')) as RouterFile;
|
|
161
|
+
if (!raw || !Array.isArray(raw.kernel) || !Array.isArray(raw.tier_1) || !Array.isArray(raw.tier_2)) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return raw;
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Render one trigger spec compactly, e.g. `keyword:commit`. */
|
|
171
|
+
function formatTrigger(t: TriggerSpec): string {
|
|
172
|
+
const entry = Object.entries(t).find(([k]) => k !== 'reason');
|
|
173
|
+
if (!entry) return '(empty trigger)';
|
|
174
|
+
return `${entry[0]}:${entry[1]}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function formatTriggers(triggers: TriggerSpec[]): string {
|
|
178
|
+
if (triggers.length === 0) return '(none)';
|
|
179
|
+
return triggers.map(formatTrigger).join(', ');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Audit-log-v1 reading (orchestration + rules_applied) ────────────────
|
|
183
|
+
|
|
184
|
+
export interface ExplainOrchestrationRecord {
|
|
185
|
+
spawn_count?: number;
|
|
186
|
+
tiers?: string[];
|
|
187
|
+
token_delta?: number;
|
|
188
|
+
token_delta_provenance?: string;
|
|
189
|
+
dispatch_mode?: string | null;
|
|
190
|
+
tier_chosen?: string | null;
|
|
191
|
+
task_class?: string | null;
|
|
192
|
+
first_pass_success?: boolean | null;
|
|
193
|
+
escalated?: boolean | null;
|
|
194
|
+
outcome?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface ExplainAuditLine {
|
|
198
|
+
id?: string;
|
|
199
|
+
ts?: string;
|
|
200
|
+
work_id?: string;
|
|
201
|
+
phase?: string;
|
|
202
|
+
outcome?: string;
|
|
203
|
+
rules_applied?: string[];
|
|
204
|
+
input_kind?: string;
|
|
205
|
+
orchestration?: ExplainOrchestrationRecord;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function matchesWindow(ts: string | undefined, task: string | null, taskField: string | undefined, since: number | null): boolean {
|
|
209
|
+
if (task !== null && (taskField === undefined || !taskField.includes(task))) return false;
|
|
210
|
+
if (since !== null) {
|
|
211
|
+
if (ts === undefined) return false;
|
|
212
|
+
const t = Date.parse(ts);
|
|
213
|
+
if (!Number.isFinite(t) || t < since) return false;
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Filter audit-log-v1 lines to those matching --task (on `work_id`) and/or --since. */
|
|
219
|
+
export function filterAuditLines(lines: ExplainAuditLine[], task: string | null, since: number | null): ExplainAuditLine[] {
|
|
220
|
+
return lines.filter((l) => matchesWindow(l.ts, task, l.work_id, since));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Tally `rules_applied` across matching lines: rule id → number of phase-lines it appeared in. */
|
|
224
|
+
export function tallyRulesApplied(lines: ExplainAuditLine[]): Map<string, number> {
|
|
225
|
+
const tally = new Map<string, number>();
|
|
226
|
+
for (const line of lines) {
|
|
227
|
+
for (const ruleId of line.rules_applied ?? []) {
|
|
228
|
+
tally.set(ruleId, (tally.get(ruleId) ?? 0) + 1);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return tally;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Lines that carry a real dispatch (`orchestration.spawn_count > 0`). */
|
|
235
|
+
export function dispatchLines(lines: ExplainAuditLine[]): ExplainAuditLine[] {
|
|
236
|
+
return lines.filter((l) => (l.orchestration?.spawn_count ?? 0) > 0);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Engagement JSONL reading ─────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
export interface EngagementReadResult {
|
|
242
|
+
exists: boolean;
|
|
243
|
+
totalLines: number;
|
|
244
|
+
skippedLines: number;
|
|
245
|
+
events: EngagementEvent[];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Read + parse an engagement JSONL log. Malformed lines are counted, never thrown. */
|
|
249
|
+
export function readEngagementEvents(logPath: string): EngagementReadResult {
|
|
250
|
+
if (!fs.existsSync(logPath)) {
|
|
251
|
+
return { exists: false, totalLines: 0, skippedLines: 0, events: [] };
|
|
252
|
+
}
|
|
253
|
+
const text = fs.readFileSync(logPath, 'utf8');
|
|
254
|
+
const lines = text.split('\n');
|
|
255
|
+
if (lines[lines.length - 1] === '') lines.pop();
|
|
256
|
+
let totalLines = 0;
|
|
257
|
+
let skippedLines = 0;
|
|
258
|
+
const events: EngagementEvent[] = [];
|
|
259
|
+
for (const raw of lines) {
|
|
260
|
+
const line = raw.trim();
|
|
261
|
+
if (!line) continue;
|
|
262
|
+
totalLines += 1;
|
|
263
|
+
try {
|
|
264
|
+
events.push(parse_event(`${line}\n`));
|
|
265
|
+
} catch (exc) {
|
|
266
|
+
if (exc instanceof EngagementSchemaError) {
|
|
267
|
+
skippedLines += 1;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
throw exc;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return { exists: true, totalLines, skippedLines, events };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function filterEngagement(events: EngagementEvent[], task: string | null, since: number | null): EngagementEvent[] {
|
|
277
|
+
return events.filter((e) => matchesWindow(e.ts, task, e.task_id, since));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const ENGAGEMENT_KINDS = ['skills', 'rules', 'commands', 'guidelines', 'personas'] as const;
|
|
281
|
+
|
|
282
|
+
export interface KindSummary {
|
|
283
|
+
consulted: number;
|
|
284
|
+
applied: number;
|
|
285
|
+
consultedIds: Set<string>;
|
|
286
|
+
appliedIds: Set<string>;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Per-kind consulted/applied counts + distinct ids across the filtered events. */
|
|
290
|
+
export function summarizeEngagement(events: EngagementEvent[]): Record<string, KindSummary> {
|
|
291
|
+
const summary: Record<string, KindSummary> = {};
|
|
292
|
+
for (const kind of ENGAGEMENT_KINDS) {
|
|
293
|
+
summary[kind] = { consulted: 0, applied: 0, consultedIds: new Set(), appliedIds: new Set() };
|
|
294
|
+
}
|
|
295
|
+
for (const event of events) {
|
|
296
|
+
for (const [kind, ids] of Object.entries(event.consulted)) {
|
|
297
|
+
const s = summary[kind];
|
|
298
|
+
if (!s) continue;
|
|
299
|
+
for (const id of ids) {
|
|
300
|
+
s.consulted += 1;
|
|
301
|
+
s.consultedIds.add(id);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
for (const [kind, ids] of Object.entries(event.applied)) {
|
|
305
|
+
const s = summary[kind];
|
|
306
|
+
if (!s) continue;
|
|
307
|
+
for (const id of ids) {
|
|
308
|
+
s.applied += 1;
|
|
309
|
+
s.appliedIds.add(id);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return summary;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── Context-hygiene state reading ────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
export interface HygieneReadResult {
|
|
319
|
+
path: string | null;
|
|
320
|
+
checkedPaths: string[];
|
|
321
|
+
state: Record<string, unknown> | null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Try each candidate path in order; first one that parses as a JSON object wins. */
|
|
325
|
+
export function readHygieneState(candidates: string[]): HygieneReadResult {
|
|
326
|
+
for (const p of candidates) {
|
|
327
|
+
if (!fs.existsSync(p)) continue;
|
|
328
|
+
try {
|
|
329
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf8')) as unknown;
|
|
330
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
331
|
+
return { path: p, checkedPaths: candidates, state: parsed as Record<string, unknown> };
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
// fall through to next candidate
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return { path: null, checkedPaths: candidates, state: null };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Rendering helpers ─────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
/** One plain-language line per section, for a reader who has never seen the internals. */
|
|
343
|
+
function renderSummarySection(
|
|
344
|
+
router: RouterFile | null,
|
|
345
|
+
engagementRead: EngagementReadResult,
|
|
346
|
+
filteredEngagement: EngagementEvent[],
|
|
347
|
+
engagementSummary: Record<string, KindSummary> | null,
|
|
348
|
+
dispatches: ExplainAuditLine[],
|
|
349
|
+
hygiene: HygieneReadResult,
|
|
350
|
+
): string[] {
|
|
351
|
+
const out: string[] = [];
|
|
352
|
+
out.push('## Summary');
|
|
353
|
+
out.push('');
|
|
354
|
+
if (router === null) {
|
|
355
|
+
out.push('- Rules: no rule data (router file absent or malformed).');
|
|
356
|
+
} else {
|
|
357
|
+
const triggerRouted = router.tier_1.length + router.tier_2.length;
|
|
358
|
+
out.push(`- Rules: ${router.kernel.length} always-on (kernel), ${triggerRouted} available on triggers.`);
|
|
359
|
+
}
|
|
360
|
+
if (!engagementRead.exists || filteredEngagement.length === 0 || engagementSummary === null) {
|
|
361
|
+
out.push('- Skill usage: no engagement data recorded in this window (telemetry off or no boundaries logged).');
|
|
362
|
+
} else {
|
|
363
|
+
let consulted = 0;
|
|
364
|
+
let applied = 0;
|
|
365
|
+
for (const s of Object.values(engagementSummary)) {
|
|
366
|
+
consulted += s.consulted;
|
|
367
|
+
applied += s.applied;
|
|
368
|
+
}
|
|
369
|
+
out.push(`- Skill usage: ${consulted} artifact(s) consulted, ${applied} applied across ${filteredEngagement.length} task boundary(ies).`);
|
|
370
|
+
}
|
|
371
|
+
if (dispatches.length === 0) {
|
|
372
|
+
out.push('- Subagent dispatches: none in window.');
|
|
373
|
+
} else {
|
|
374
|
+
const totalDelta = dispatches.reduce((sum, l) => sum + (l.orchestration?.token_delta ?? 0), 0);
|
|
375
|
+
out.push(`- Subagent dispatches: ${dispatches.length} dispatch(es), total token delta ${totalDelta}.`);
|
|
376
|
+
}
|
|
377
|
+
if (hygiene.state === null) {
|
|
378
|
+
out.push('- Session health: no state recorded.');
|
|
379
|
+
} else {
|
|
380
|
+
const s = hygiene.state;
|
|
381
|
+
out.push(`- Session health: ${String(s['tool_calls'] ?? '(unset)')} tool call(s) recorded, loop detected: ${String(s['loop_detected'] ?? '(unset)')}.`);
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function capList(ids: Iterable<string>): string {
|
|
387
|
+
const arr = [...ids].sort();
|
|
388
|
+
if (arr.length === 0) return '(none)';
|
|
389
|
+
if (arr.length <= ID_CAP) return arr.join(', ');
|
|
390
|
+
return `${arr.slice(0, ID_CAP).join(', ')}, …+${arr.length - ID_CAP} more`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function windowLabel(opts: Options): string {
|
|
394
|
+
const task = opts.task ?? 'any';
|
|
395
|
+
const since = opts.since ?? 'any';
|
|
396
|
+
return `task=${task} · since=${since}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function renderRulesSection(router: RouterFile | null, opts: Options, rulesTally: Map<string, number>, auditDirExists: boolean): string[] {
|
|
400
|
+
const out: string[] = [];
|
|
401
|
+
out.push('## Resolved rule set');
|
|
402
|
+
out.push(`_Source: \`${opts.router}\`_`);
|
|
403
|
+
out.push('');
|
|
404
|
+
if (router === null) {
|
|
405
|
+
out.push(`no data — \`${opts.router}\` absent or malformed`);
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
out.push(`- Kernel (always-on, no triggers, ${router.kernel.length} rule(s)): ${router.kernel.slice().sort().join(', ') || '(none)'}`);
|
|
409
|
+
out.push('');
|
|
410
|
+
for (const [label, entries] of [['Tier-1', router.tier_1], ['Tier-2', router.tier_2]] as const) {
|
|
411
|
+
out.push(`### ${label} (trigger-routed, ${entries.length} rule(s))`);
|
|
412
|
+
out.push('');
|
|
413
|
+
out.push('| rule | triggers |');
|
|
414
|
+
out.push('|---|---|');
|
|
415
|
+
for (const entry of [...entries].sort((a, b) => (a.id < b.id ? -1 : 1))) {
|
|
416
|
+
out.push(`| ${entry.id} | ${formatTriggers(entry.triggers)} |`);
|
|
417
|
+
}
|
|
418
|
+
out.push('');
|
|
419
|
+
}
|
|
420
|
+
out.push('### Observed firing in window (audit-log-v1 `rules_applied`)');
|
|
421
|
+
out.push(`_Source: \`${DEFAULT_AUDIT_DIR}/*.jsonl\` → \`rules_applied\` field — emitted only at /work and /implement-ticket phase boundaries, not for ad hoc chat turns_`);
|
|
422
|
+
out.push('');
|
|
423
|
+
if (!auditDirExists) {
|
|
424
|
+
out.push(`no data — audit directory absent (no /work or /implement-ticket run has written telemetry yet)`);
|
|
425
|
+
} else if (rulesTally.size === 0) {
|
|
426
|
+
out.push('no data — no audit-log-v1 line in window carries `rules_applied`');
|
|
427
|
+
} else {
|
|
428
|
+
const rows = [...rulesTally.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
|
|
429
|
+
for (const [ruleId, count] of rows) {
|
|
430
|
+
out.push(`- ${ruleId} (${count} phase-line(s))`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return out;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function renderEngagementSection(opts: Options, read: EngagementReadResult, filtered: EngagementEvent[], summary: Record<string, KindSummary> | null): string[] {
|
|
437
|
+
const out: string[] = [];
|
|
438
|
+
out.push('## Artefact engagement (consulted vs applied)');
|
|
439
|
+
out.push(`_Source: \`${opts.engagement}\` (telemetry.artifact_engagement)_`);
|
|
440
|
+
out.push('');
|
|
441
|
+
if (!read.exists) {
|
|
442
|
+
out.push(`no data — \`${opts.engagement}\` absent (telemetry.artifact_engagement is likely disabled, or nothing has been recorded yet)`);
|
|
443
|
+
return out;
|
|
444
|
+
}
|
|
445
|
+
if (filtered.length === 0 || summary === null) {
|
|
446
|
+
out.push(`no data — 0 event(s) match ${windowLabel(opts)} (${read.totalLines} total line(s) in file, ${read.skippedLines} skipped as malformed)`);
|
|
447
|
+
return out;
|
|
448
|
+
}
|
|
449
|
+
out.push(`${filtered.length} event(s) match ${windowLabel(opts)} (of ${read.totalLines} total line(s), ${read.skippedLines} skipped as malformed).`);
|
|
450
|
+
out.push('');
|
|
451
|
+
out.push('| kind | consulted | applied | consulted ids |');
|
|
452
|
+
out.push('|---|---|---|---|');
|
|
453
|
+
for (const kind of ENGAGEMENT_KINDS) {
|
|
454
|
+
const s = summary[kind];
|
|
455
|
+
if (!s || (s.consulted === 0 && s.applied === 0)) continue;
|
|
456
|
+
out.push(`| ${kind} | ${s.consulted} | ${s.applied} | ${capList(s.consultedIds)} |`);
|
|
457
|
+
}
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function renderOrchestrationSection(opts: Options, auditDirExists: boolean, dispatches: ExplainAuditLine[]): string[] {
|
|
462
|
+
const out: string[] = [];
|
|
463
|
+
out.push('## Subagent dispatches');
|
|
464
|
+
out.push(`_Source: \`${opts.auditDir}/*.jsonl\` (\`orchestration\` sub-object, audit-log-v1)_`);
|
|
465
|
+
out.push('');
|
|
466
|
+
if (!auditDirExists) {
|
|
467
|
+
out.push(`no data — \`${opts.auditDir}\` absent (no orchestration telemetry recorded yet)`);
|
|
468
|
+
return out;
|
|
469
|
+
}
|
|
470
|
+
if (dispatches.length === 0) {
|
|
471
|
+
out.push(`no data — 0 dispatch(es) match ${windowLabel(opts)}`);
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
out.push(`${dispatches.length} dispatch(es) match ${windowLabel(opts)}.`);
|
|
475
|
+
out.push('');
|
|
476
|
+
out.push('| ts | work_id | mode | tiers | token_delta (provenance) | first_pass_success | escalated |');
|
|
477
|
+
out.push('|---|---|---|---|---|---|---|');
|
|
478
|
+
for (const line of dispatches) {
|
|
479
|
+
const o = line.orchestration ?? {};
|
|
480
|
+
const mode = o.dispatch_mode ?? '(unset)';
|
|
481
|
+
const tiers = (o.tiers ?? []).join(',') || '(unset)';
|
|
482
|
+
const delta = `${o.token_delta ?? 0} (${o.token_delta_provenance ?? 'estimated'})`;
|
|
483
|
+
const fps = o.first_pass_success === null || o.first_pass_success === undefined ? 'n/a' : String(o.first_pass_success);
|
|
484
|
+
const esc = o.escalated === null || o.escalated === undefined ? 'n/a' : String(o.escalated);
|
|
485
|
+
out.push(`| ${line.ts ?? '(unset)'} | ${line.work_id ?? '(unset)'} | ${mode} | ${tiers} | ${delta} | ${fps} | ${esc} |`);
|
|
486
|
+
}
|
|
487
|
+
return out;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function renderHygieneSection(hygiene: HygieneReadResult): string[] {
|
|
491
|
+
const out: string[] = [];
|
|
492
|
+
out.push('## Hook / loop state (context-hygiene)');
|
|
493
|
+
out.push(`_Source: tried ${hygiene.checkedPaths.map((p) => `\`${p}\``).join(', then ')}_`);
|
|
494
|
+
out.push('');
|
|
495
|
+
if (hygiene.state === null) {
|
|
496
|
+
out.push('no data — none of the candidate paths exist yet (no PostToolUse hook has run this session, or this host has no hook surface for it)');
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
const s = hygiene.state;
|
|
500
|
+
out.push(`Found at \`${hygiene.path}\`.`);
|
|
501
|
+
out.push('');
|
|
502
|
+
out.push(`- tool_calls: ${String(s['tool_calls'] ?? '(unset)')}`);
|
|
503
|
+
out.push(`- consecutive_same_tool: ${String(s['consecutive_same_tool'] ?? '(unset)')}`);
|
|
504
|
+
out.push(`- last_tool: ${String(s['last_tool'] ?? '(unset)')}`);
|
|
505
|
+
out.push(`- loop_detected: ${String(s['loop_detected'] ?? '(unset)')}`);
|
|
506
|
+
out.push(`- freshness_threshold: ${String(s['freshness_threshold'] ?? '(unset)')}`);
|
|
507
|
+
out.push(`- checked_at: ${String(s['checked_at'] ?? '(unset)')}`);
|
|
508
|
+
return out;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function renderParkedSection(): string[] {
|
|
512
|
+
return [
|
|
513
|
+
'## Not answerable today (parked — revisit-if new capture ships)',
|
|
514
|
+
'',
|
|
515
|
+
'1. **Which specific trigger phrase matched, for a given chat turn** —',
|
|
516
|
+
' `rules_applied` (above) records THAT a rule fired, never WHICH',
|
|
517
|
+
' trigger fired it, and is only emitted at /work · /implement-ticket',
|
|
518
|
+
' phase boundaries — never for ad hoc chat turns. Revisit-if a',
|
|
519
|
+
' per-turn trigger-match log ships.',
|
|
520
|
+
'2. **What content was trimmed/omitted from context due to a token or',
|
|
521
|
+
' thin-projector budget decision** — no on-disk record of "artefact',
|
|
522
|
+
' considered but dropped for budget" exists today. Revisit-if such a',
|
|
523
|
+
' record ships.',
|
|
524
|
+
'3. **Which memory entry id influenced a specific answer**, when the run',
|
|
525
|
+
' did NOT opt into `decision_engine.surface_traces` — the',
|
|
526
|
+
' `🧠 Memory: n/m · ids=[...]` line is printed to stdout only, never',
|
|
527
|
+
' persisted. The durable form (`memory.ids` in',
|
|
528
|
+
' `agents/runtime/state/work/<id>/decision-trace-<phase>.json`) exists',
|
|
529
|
+
' ONLY when `decision_engine.surface_traces: true` (off by default) and',
|
|
530
|
+
' the run went through /work or /implement-ticket — see `explain last`',
|
|
531
|
+
' for that narrower, per-run case; not duplicated here.',
|
|
532
|
+
'4. **Why a given subagent primitive was/wasn\'t available on this host**',
|
|
533
|
+
' — the host-capability manifest is resolved in-session by the agent',
|
|
534
|
+
' from its own knowledge (+ an optional settings override) and is never',
|
|
535
|
+
' written to disk. Nothing for this script to read.',
|
|
536
|
+
];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ── Report assembly ──────────────────────────────────────────────────────
|
|
540
|
+
|
|
541
|
+
export function buildReport(opts: Options): string {
|
|
542
|
+
const since = parseSince(opts.since);
|
|
543
|
+
const hygieneCandidates = opts.hygiene !== null ? [opts.hygiene] : DEFAULT_HYGIENE_CANDIDATES;
|
|
544
|
+
|
|
545
|
+
const router = readRouter(opts.router);
|
|
546
|
+
|
|
547
|
+
const auditDirExists = fs.existsSync(opts.auditDir);
|
|
548
|
+
const rawAuditLines = (auditDirExists ? readAuditLines(opts.auditDir) : []) as unknown as ExplainAuditLine[];
|
|
549
|
+
const matchedAuditLines = filterAuditLines(rawAuditLines, opts.task, since);
|
|
550
|
+
const rulesTally = tallyRulesApplied(matchedAuditLines);
|
|
551
|
+
const dispatches = dispatchLines(matchedAuditLines);
|
|
552
|
+
|
|
553
|
+
const engagementRead = readEngagementEvents(opts.engagement);
|
|
554
|
+
const filteredEngagement = filterEngagement(engagementRead.events, opts.task, since);
|
|
555
|
+
const engagementSummary = filteredEngagement.length > 0 ? summarizeEngagement(filteredEngagement) : null;
|
|
556
|
+
|
|
557
|
+
const hygiene = readHygieneState(hygieneCandidates);
|
|
558
|
+
|
|
559
|
+
const out: string[] = [];
|
|
560
|
+
out.push('# explain-run');
|
|
561
|
+
out.push('');
|
|
562
|
+
out.push(`Window: ${windowLabel(opts)} · generated_at: ${new Date().toISOString()}`);
|
|
563
|
+
out.push('');
|
|
564
|
+
out.push(...renderSummarySection(router, engagementRead, filteredEngagement, engagementSummary, dispatches, hygiene));
|
|
565
|
+
out.push('');
|
|
566
|
+
out.push(...renderRulesSection(router, opts, rulesTally, auditDirExists));
|
|
567
|
+
out.push('');
|
|
568
|
+
out.push(...renderEngagementSection(opts, engagementRead, filteredEngagement, engagementSummary));
|
|
569
|
+
out.push('');
|
|
570
|
+
out.push(...renderOrchestrationSection(opts, auditDirExists, dispatches));
|
|
571
|
+
out.push('');
|
|
572
|
+
out.push(...renderHygieneSection(hygiene));
|
|
573
|
+
out.push('');
|
|
574
|
+
out.push(...renderParkedSection());
|
|
575
|
+
return `${out.join('\n')}\n`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
579
|
+
const opts = parseArgs(argv);
|
|
580
|
+
const report = buildReport(opts);
|
|
581
|
+
if (opts.output) {
|
|
582
|
+
fs.mkdirSync(path.dirname(opts.output) || '.', { recursive: true });
|
|
583
|
+
fs.writeFileSync(opts.output, report, 'utf8');
|
|
584
|
+
} else {
|
|
585
|
+
process.stdout.write(report);
|
|
586
|
+
}
|
|
587
|
+
return 0;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (process.argv[1] !== undefined) {
|
|
591
|
+
const invokedUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
592
|
+
if (import.meta.url === invokedUrl) process.exit(main());
|
|
593
|
+
}
|