@event4u/agent-config 9.28.0 → 9.29.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 (147) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +6 -6
  3. package/CHANGELOG.md +66 -0
  4. package/README.md +1 -1
  5. package/agents/templates/.ai-council.yml.example +55 -24
  6. package/dist/agent-src/commands/cost/report.md +2 -2
  7. package/dist/agent-src/commands/team/adversarial.md +8 -7
  8. package/dist/agent-src/commands/team/delegate.md +13 -12
  9. package/dist/agent-src/commands/team/review.md +9 -8
  10. package/dist/agent-src/commands/team/status.md +9 -8
  11. package/dist/agent-src/commands/team.md +29 -19
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
  13. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
  14. package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
  15. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +18 -0
  16. package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
  17. package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
  18. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
  19. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
  20. package/dist/agent-src/rules/delegation-policy.md +11 -7
  21. package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
  22. package/dist/agent-src/rules/token-budget-discipline.md +1 -1
  23. package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
  24. package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
  25. package/dist/agent-src/skills/premortem/SKILL.md +30 -1
  26. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
  27. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
  28. package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
  29. package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
  30. package/dist/agent-src/templates/AGENTS.md +4 -4
  31. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  32. package/dist/agent-src/templates/roadmaps.md +23 -0
  33. package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
  34. package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
  35. package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
  36. package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
  37. package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
  38. package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-NZ5N2XQL.js} +3 -3
  39. package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-OJM3MBJM.js} +1 -1
  40. package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-RCIA67XN.js} +111 -90
  41. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  42. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  43. package/dist/cli-delegate/cmd_conformance.js +6 -43
  44. package/dist/cli-delegate/cmd_doctor.js +9 -5
  45. package/dist/cli-delegate/cmd_explain.js +2 -2
  46. package/dist/cli-delegate/cmd_export.js +4 -4
  47. package/dist/cli-delegate/cmd_handoff.js +3 -3
  48. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  49. package/dist/cli-delegate/cmd_migrate.js +2 -2
  50. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  51. package/dist/cli-delegate/cmd_prune.js +4 -4
  52. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  53. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  54. package/dist/cli-delegate/cmd_settings_get.js +1 -7
  55. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  56. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  57. package/dist/cli-delegate/cmd_sync.js +4 -4
  58. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  59. package/dist/cli-delegate/cmd_update.js +1 -1
  60. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  61. package/dist/cli-delegate/cmd_validate.js +4 -4
  62. package/dist/cli-delegate/cmd_versions.js +1 -1
  63. package/dist/discovery/deprecation-report.md +1 -1
  64. package/dist/discovery/discovery-manifest.json +69 -20
  65. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  66. package/dist/discovery/discovery-manifest.summary.md +3 -2
  67. package/dist/discovery/orphan-report.md +1 -1
  68. package/dist/discovery/packs.json +39 -2
  69. package/dist/discovery/trust-report.md +2 -2
  70. package/dist/discovery/workspaces.json +12 -4
  71. package/dist/hooks/dispatch.js +373 -35
  72. package/dist/install/install.mjs +42 -17
  73. package/dist/mcp/registry-manifest.json +2 -2
  74. package/dist/mcp/server.json +2 -2
  75. package/dist/mcp/server.mjs +651 -262
  76. package/dist/router.json +1 -1
  77. package/dist/scripts/_lib/agent_settings.js +54 -0
  78. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  79. package/dist/scripts/condense.js +147 -2
  80. package/dist/scripts/condense.js.map +1 -1
  81. package/dist/server/schemas/settings.js +6 -7
  82. package/dist/server/schemas/settings.js.map +1 -1
  83. package/dist/shared/settingsCarveOut.js +0 -9
  84. package/dist/shared/settingsCarveOut.js.map +1 -1
  85. package/dist/shared/settingsClasses.js +11 -9
  86. package/dist/shared/settingsClasses.js.map +1 -1
  87. package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
  88. package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
  89. package/dist/ui/index.html +1 -1
  90. package/docs/contracts/settings-classes.md +39 -9
  91. package/package.json +1 -1
  92. package/src/agent-src/templates/AGENTS.md +4 -4
  93. package/src/config/agent-settings.template.yml +52 -66
  94. package/src/config/discovery/packs.yml +11 -0
  95. package/src/config/discovery/workspaces.yml +1 -1
  96. package/src/config/gate-violation-baselines.json +3 -3
  97. package/src/scripts/_cli/cmd_conformance.ts +14 -42
  98. package/src/scripts/_cli/cmd_doctor.ts +65 -48
  99. package/src/scripts/_lib/agent_settings.ts +58 -0
  100. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
  101. package/src/scripts/_lib/auto_dispatch.ts +23 -18
  102. package/src/scripts/_lib/capsule_trigger.ts +124 -0
  103. package/src/scripts/_lib/host_capability.ts +63 -6
  104. package/src/scripts/_lib/judgment_ladder.ts +382 -0
  105. package/src/scripts/_lib/orchestration_gate.ts +8 -0
  106. package/src/scripts/_lib/orchestration_record.ts +37 -0
  107. package/src/scripts/_lib/orchestration_savings.ts +1 -1
  108. package/src/scripts/_lib/self_repair.ts +160 -13
  109. package/src/scripts/_lib/self_repair_store.ts +28 -0
  110. package/src/scripts/_lib/subagent_capsule.ts +182 -0
  111. package/src/scripts/_lib/subagent_response.ts +19 -0
  112. package/src/scripts/_lib/subagent_spawn.ts +13 -0
  113. package/src/scripts/_lib/subagent_steering.ts +9 -4
  114. package/src/scripts/_lib/tier_budget_routing.ts +6 -1
  115. package/src/scripts/_lib/worker_budget.ts +37 -2
  116. package/src/scripts/ai_council/clients.ts +76 -54
  117. package/src/scripts/ai_council/config.ts +94 -6
  118. package/src/scripts/ai_council/handoff.ts +113 -0
  119. package/src/scripts/ai_council/modes.ts +19 -4
  120. package/src/scripts/ai_council/orchestrator.ts +105 -0
  121. package/src/scripts/ai_council/quorum.ts +84 -0
  122. package/src/scripts/ai_council/session.ts +44 -0
  123. package/src/scripts/ai_council/transport_resolver.ts +127 -1
  124. package/src/scripts/ai_team/availability.ts +110 -0
  125. package/src/scripts/ai_team/config.ts +34 -14
  126. package/src/scripts/ai_team/team_dispatch.ts +71 -29
  127. package/src/scripts/bench_critic_protocol.ts +396 -0
  128. package/src/scripts/check_detector_corpus.ts +202 -0
  129. package/src/scripts/check_hook_bundle_freshness.ts +4 -1
  130. package/src/scripts/condense.ts +162 -2
  131. package/src/scripts/council_cli.ts +401 -25
  132. package/src/scripts/forensics_report.ts +333 -0
  133. package/src/scripts/hooks/delegation_nudge_hook.ts +69 -35
  134. package/src/scripts/lint_discovery_vocabulary.ts +2 -0
  135. package/src/scripts/lint_never_silent.ts +276 -0
  136. package/src/scripts/lint_no_activation_gates.ts +271 -0
  137. package/src/scripts/orchestration_record.ts +8 -0
  138. package/src/scripts/rebuild_hook_bundle.ts +177 -0
  139. package/src/scripts/release.ts +74 -2
  140. package/src/scripts/routing_doctor.ts +21 -36
  141. package/src/scripts/score_skill_selection.ts +19 -5
  142. package/src/scripts/self_repair_cli.ts +287 -57
  143. package/src/scripts/team_events_spike.ts +271 -0
  144. package/src/scripts/team_review_gate_hook.ts +31 -9
  145. package/src/server/schemas/settings.ts +10 -17
  146. package/src/shared/settingsCarveOut.ts +0 -10
  147. package/src/shared/settingsClasses.ts +11 -9
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Forensic analyzers — hotspot risk + change coupling from `git log` alone
3
+ * (road-to-judgment-and-forensic-evidence Phase 3; pack `forensics`,
4
+ * default-off, read-only, advisory).
5
+ *
6
+ * Two deterministic analyzers over a PINNED revision range:
7
+ *
8
+ * hotspot(file) = norm(change_frequency) × norm(complexity)
9
+ * complexity = LOC + total indentation units (tabs = 1,
10
+ * 4 spaces = 1) of the file in the working tree — a cheap,
11
+ * language-agnostic indentation proxy, documented rather
12
+ * than clever. A hotspot is a place to LOOK, never a code
13
+ * quality verdict (both inputs can be correct for a file).
14
+ *
15
+ * coupling(A,B) = co_changes(A,B) / min(changes(A), changes(B))
16
+ * over commits with ≤ max_commit_files files (bulk sweeps
17
+ * poison coupling; skipped commits are counted in the
18
+ * report, never silently dropped).
19
+ *
20
+ * boundary_contradictions — coupling pairs ABOVE the threshold whose files
21
+ * live in different top-level modules. Recorded as a finding CLASS (the
22
+ * intended boundaries and the actual change pattern disagree), not as a
23
+ * failure — per the roadmap's step 3.6.
24
+ *
25
+ * Determinism bar (step 3.3): byte-stable output on a frozen fixture — same
26
+ * inputs, same bytes. Inputs are the git log for the pinned range plus file
27
+ * metrics; `--log-file` + `--metrics-file` inject both for fixture tests, so
28
+ * the analyzer's whole pipeline is exercised without a live repo. All maps
29
+ * are sorted before emission; scores are rounded to 4 decimals.
30
+ *
31
+ * Advisory wiring (step 3.4): `--findings-out` emits
32
+ * review-findings.schema.json-shaped items with kind `correctness` and
33
+ * severity `low|medium` — non-blocking by construction in
34
+ * `check_finding_dispositions.isBlocking` — for ingestion into the existing
35
+ * release findings ledger (`check_finding_dispositions --ingest`).
36
+ *
37
+ * Gaming risk: an empty scan (dead range, wrong cwd) yielding a green "no
38
+ * hotspots" report — mitigated by the shared scan-scope helper: zero scanned
39
+ * commits throws (DeadScopeError), it never publishes an empty finding set
40
+ * (step 3.2). Residual: a range that is real but trivially small still
41
+ * produces low-signal output; the report publishes commit + file counts so a
42
+ * reader can judge the denominator.
43
+ */
44
+ // ledger-exempt: on-demand advisory analyzer, not a CI gate — the report JSON
45
+ // itself carries the full denominator (scanned commits/files) per run.
46
+ import * as fs from 'node:fs';
47
+ import { createHash } from 'node:crypto';
48
+ import { execFileSync } from 'node:child_process';
49
+
50
+ import { reportScanned } from './_lib/scan_scope.js';
51
+
52
+ interface Params {
53
+ max_commit_files: number;
54
+ coupling_threshold: number;
55
+ min_cochanges: number;
56
+ top_hotspots: number;
57
+ }
58
+ const DEFAULTS: Params = {
59
+ max_commit_files: 50,
60
+ coupling_threshold: 0.5,
61
+ min_cochanges: 3,
62
+ top_hotspots: 25,
63
+ };
64
+
65
+ interface Commit {
66
+ hash: string;
67
+ files: string[];
68
+ }
69
+
70
+ /** Parse `git log --pretty=format:'C %H' --name-only` output. */
71
+ export function parseLog(text: string): Commit[] {
72
+ const commits: Commit[] = [];
73
+ let current: Commit | null = null;
74
+ for (const raw of text.split('\n')) {
75
+ const line = raw.trimEnd();
76
+ if (line.startsWith('C ')) {
77
+ if (current) commits.push(current);
78
+ current = { hash: line.slice(2).trim(), files: [] };
79
+ } else if (line.length > 0 && current) {
80
+ current.files.push(line);
81
+ }
82
+ }
83
+ if (current) commits.push(current);
84
+ return commits;
85
+ }
86
+
87
+ function gitLog(range: string): string {
88
+ return execFileSync('git', ['log', '--pretty=format:C %H', '--name-only', range], {
89
+ encoding: 'utf8',
90
+ maxBuffer: 256 * 1024 * 1024,
91
+ });
92
+ }
93
+
94
+ /** LOC + indentation units — the documented complexity proxy. */
95
+ export function complexityOf(content: string): number {
96
+ const lines = content.split('\n');
97
+ let indent = 0;
98
+ for (const l of lines) {
99
+ const m = /^[\t ]*/.exec(l);
100
+ const ws = m ? m[0] : '';
101
+ let units = 0;
102
+ let spaces = 0;
103
+ for (const ch of ws) {
104
+ if (ch === '\t') units += 1;
105
+ else spaces += 1;
106
+ }
107
+ units += Math.floor(spaces / 4);
108
+ indent += units;
109
+ }
110
+ return lines.length + indent;
111
+ }
112
+
113
+ function liveComplexity(file: string): number | null {
114
+ try {
115
+ const st = fs.statSync(file);
116
+ if (!st.isFile()) return null;
117
+ const buf = fs.readFileSync(file);
118
+ if (buf.includes(0)) return null; // binary
119
+ return complexityOf(buf.toString('utf8'));
120
+ } catch {
121
+ return null; // deleted / renamed away — frequency without complexity
122
+ }
123
+ }
124
+
125
+ function round4(n: number): number {
126
+ return Math.round(n * 10000) / 10000;
127
+ }
128
+
129
+ /** First path segment — the module boundary the router treats as independent. */
130
+ export function moduleOf(file: string): string {
131
+ const parts = file.split('/');
132
+ if (parts.length >= 2 && (parts[0] === 'src' || parts[0] === 'tests' || parts[0] === 'docs')) {
133
+ return `${parts[0]}/${parts[1] ?? ''}`;
134
+ }
135
+ return parts[0] ?? file;
136
+ }
137
+
138
+ interface Hotspot {
139
+ file: string;
140
+ changes: number;
141
+ complexity: number | null;
142
+ score: number;
143
+ }
144
+ interface CouplingPair {
145
+ a: string;
146
+ b: string;
147
+ co_changes: number;
148
+ changes_a: number;
149
+ changes_b: number;
150
+ coupling: number;
151
+ cross_module: boolean;
152
+ }
153
+
154
+ export interface Report {
155
+ mode: string;
156
+ range: string;
157
+ params: Params;
158
+ scanned: { commits: number; commits_skipped_bulk: number; files: number };
159
+ hotspots: Hotspot[];
160
+ coupling: CouplingPair[];
161
+ boundary_contradictions: CouplingPair[];
162
+ }
163
+
164
+ export function analyze(
165
+ commits: Commit[],
166
+ params: Params,
167
+ complexity: (file: string) => number | null,
168
+ range: string,
169
+ mode: string,
170
+ ): Report {
171
+ const changes = new Map<string, number>();
172
+ const co = new Map<string, number>();
173
+ let skipped = 0;
174
+ for (const c of commits) {
175
+ const files = [...new Set(c.files)].sort();
176
+ if (files.length > params.max_commit_files) {
177
+ skipped++;
178
+ continue;
179
+ }
180
+ for (const f of files) changes.set(f, (changes.get(f) ?? 0) + 1);
181
+ for (let i = 0; i < files.length; i++) {
182
+ for (let j = i + 1; j < files.length; j++) {
183
+ const key = `${files[i]}\u0000${files[j]}`;
184
+ co.set(key, (co.get(key) ?? 0) + 1);
185
+ }
186
+ }
187
+ }
188
+
189
+ const files = [...changes.keys()].sort();
190
+ const complexities = new Map<string, number | null>();
191
+ for (const f of files) complexities.set(f, complexity(f));
192
+ let maxChanges = 0;
193
+ for (const v of changes.values()) if (v > maxChanges) maxChanges = v;
194
+ let maxComplexity = 0;
195
+ for (const v of complexities.values()) if (v !== null && v > maxComplexity) maxComplexity = v;
196
+
197
+ const hotspots: Hotspot[] = files
198
+ .map((f) => {
199
+ const ch = changes.get(f) ?? 0;
200
+ const cx = complexities.get(f) ?? null;
201
+ const score =
202
+ cx === null || maxChanges === 0 || maxComplexity === 0
203
+ ? 0
204
+ : round4((ch / maxChanges) * (cx / maxComplexity));
205
+ return { file: f, changes: ch, complexity: cx, score };
206
+ })
207
+ .filter((h) => h.score > 0)
208
+ .sort((a, b) => b.score - a.score || a.file.localeCompare(b.file))
209
+ .slice(0, params.top_hotspots);
210
+
211
+ const coupling: CouplingPair[] = [];
212
+ for (const [key, n] of [...co.entries()].sort()) {
213
+ if (n < params.min_cochanges) continue;
214
+ const [a, b] = key.split('\u0000') as [string, string];
215
+ const ca = changes.get(a) ?? 0;
216
+ const cb = changes.get(b) ?? 0;
217
+ const denom = Math.min(ca, cb);
218
+ if (denom === 0) continue;
219
+ const strength = round4(n / denom);
220
+ if (strength < params.coupling_threshold) continue;
221
+ coupling.push({
222
+ a,
223
+ b,
224
+ co_changes: n,
225
+ changes_a: ca,
226
+ changes_b: cb,
227
+ coupling: strength,
228
+ cross_module: moduleOf(a) !== moduleOf(b),
229
+ });
230
+ }
231
+ coupling.sort((x, y) => y.coupling - x.coupling || x.a.localeCompare(y.a) || x.b.localeCompare(y.b));
232
+
233
+ return {
234
+ mode,
235
+ range,
236
+ params,
237
+ scanned: {
238
+ commits: commits.length - skipped,
239
+ commits_skipped_bulk: skipped,
240
+ files: files.length,
241
+ },
242
+ hotspots,
243
+ coupling,
244
+ boundary_contradictions: coupling.filter((p) => p.cross_module),
245
+ };
246
+ }
247
+
248
+ /** Same id convention as self_review_gate.findingId — the schema pins ^[0-9a-f]{12}$. */
249
+ function forensicsFindingId(kind: string, title: string, file: string): string {
250
+ return createHash('sha256').update(`${kind}|${title}|${file}`).digest('hex').slice(0, 12);
251
+ }
252
+
253
+ /** Findings-schema-shaped advisory items — non-blocking by construction. */
254
+ export function toFindings(report: Report): Record<string, unknown> {
255
+ const findings: Record<string, unknown>[] = [];
256
+ for (const h of report.hotspots.slice(0, 10)) {
257
+ const title = `hotspot: ${h.file} (score ${String(h.score)})`;
258
+ findings.push({
259
+ finding_id: forensicsFindingId('correctness', title, h.file),
260
+ severity: 'low',
261
+ kind: 'correctness',
262
+ title,
263
+ detail: `${String(h.changes)} changes in range × complexity ${String(h.complexity ?? 'n/a')} — a place to look, not a quality verdict.`,
264
+ file: h.file,
265
+ });
266
+ }
267
+ for (const p of report.boundary_contradictions) {
268
+ const title = `boundary contradiction: ${p.a} <-> ${p.b} (coupling ${String(p.coupling)})`;
269
+ findings.push({
270
+ finding_id: forensicsFindingId('correctness', title, p.a),
271
+ severity: 'medium',
272
+ kind: 'correctness',
273
+ title,
274
+ detail: `Cross-module co-change ${String(p.co_changes)}/${String(Math.min(p.changes_a, p.changes_b))} above threshold ${String(report.params.coupling_threshold)} — the intended module boundary and the actual change pattern disagree. A finding class, not a failure.`,
275
+ file: p.a,
276
+ });
277
+ }
278
+ return { schema_version: 1, findings };
279
+ }
280
+
281
+ function stringifyReport(report: Report): string {
282
+ return JSON.stringify(report, null, 2) + '\n';
283
+ }
284
+
285
+ function argValue(argv: string[], flag: string): string | null {
286
+ const i = argv.indexOf(flag);
287
+ return i >= 0 ? (argv[i + 1] ?? null) : null;
288
+ }
289
+
290
+ function main(argv: string[]): number {
291
+ const range = argValue(argv, '--range');
292
+ const logFile = argValue(argv, '--log-file');
293
+ const metricsFile = argValue(argv, '--metrics-file');
294
+ const outFile = argValue(argv, '--out');
295
+ const findingsOut = argValue(argv, '--findings-out');
296
+ if (!range && !logFile) {
297
+ process.stderr.write(
298
+ 'usage: forensics_report (--range <rev-range> | --log-file <fixture>) [--metrics-file <json>] [--out <file>] [--findings-out <file>]\n',
299
+ );
300
+ return 2;
301
+ }
302
+ const params: Params = { ...DEFAULTS };
303
+ const text = logFile ? fs.readFileSync(logFile, 'utf8') : gitLog(range as string);
304
+ const commits = parseLog(text);
305
+ let complexity: (file: string) => number | null = liveComplexity;
306
+ if (metricsFile) {
307
+ const metrics = JSON.parse(fs.readFileSync(metricsFile, 'utf8')) as Record<string, number>;
308
+ complexity = (f) => (Object.prototype.hasOwnProperty.call(metrics, f) ? metrics[f]! : null);
309
+ }
310
+ const report = analyze(commits, params, complexity, range ?? `log-file:${logFile ?? ''}`, 'run');
311
+
312
+ // Loud-fail on an empty scan BEFORE anything is written — a report over
313
+ // nothing is not a report, and it must not exist on disk either (throws
314
+ // DeadScopeError when zero commits were analyzed).
315
+ reportScanned({
316
+ gate: 'forensics_report',
317
+ scanned: report.scanned.commits,
318
+ units: 'commit(s)',
319
+ roots: [range ?? (logFile as string)],
320
+ });
321
+
322
+ const json = stringifyReport(report);
323
+ if (outFile) fs.writeFileSync(outFile, json);
324
+ process.stdout.write(json);
325
+ if (findingsOut) fs.writeFileSync(findingsOut, JSON.stringify(toFindings(report), null, 2) + '\n');
326
+ return 0;
327
+ }
328
+
329
+ if (import.meta.url === `file://${process.argv[1]}`) {
330
+ process.exit(main(process.argv.slice(2)));
331
+ }
332
+
333
+ export { main };
@@ -10,12 +10,15 @@
10
10
  * prompt. This concern is the missing signal producer: it extracts cheap,
11
11
  * regex-only structural signals from the SUBMITTED prompt text (enumerated
12
12
  * file lists, "for each"/"alle …" shapes, explicit slice counts, ordered-plan
13
- * markers, multi-deliverable conjunctions), feeds them to `classifyTask`
14
- * together with the REAL activation gate (settings + host-capability
15
- * manifest, resolved via `resolveHostCapabilities(hostId, override)` —
16
- * the F5 committed registry, with the `subagents.host_capabilities`
17
- * settings override winning outright when present — the same order
18
- * `routing_doctor.collect_orchestration` uses), and — only on a positive
13
+ * markers, multi-deliverable conjunctions), feeds them to the judgment
14
+ * ladder (`_lib/judgment_ladder.ts`, which wraps `classifyTask` — see the
15
+ * JUDGMENT-LADDER CITATION note below) together with the REAL activation
16
+ * gate (the emergency incident switch +
17
+ * the host-capability manifest, resolved via `probeHostCapabilities(hostId)`
18
+ * — the F5 committed registry merged with observable environment facts;
19
+ * always-on orchestration removed the `subagents.host_capabilities` settings
20
+ * override — the same source `routing_doctor.collect_orchestration` uses),
21
+ * and — only on a positive
19
22
  * `do-in-parallel` / `do-in-steps` verdict — injects
20
23
  * ONE line naming the mode, the slice count, and a tier recommendation for
21
24
  * the slices (`resolveSubagentRouting`'s first production caller, applied
@@ -27,6 +30,17 @@
27
30
  * canary shape that measured a 24/29 miss rate; this concern only ever speaks
28
31
  * when it has a concrete, falsifiable verdict to report.
29
32
  *
33
+ * JUDGMENT-LADDER CITATION (road-to-always-on-orchestration Phase 2.4). The
34
+ * verdict now comes from `_lib/judgment_ladder.ts::classifyLadder` (which
35
+ * WRAPS `classifyTask` rather than replacing it) instead of calling
36
+ * `classifyTask` directly, and the injected line names the resolved rung
37
+ * (`rung-2: dispatch 6 lite slices …`). This concern stays scoped to the
38
+ * SUBAGENT dispatch rungs (1/2) — a rung-0 script verdict, a rung-3/4
39
+ * team/council escalation, and the silent ∅ (ask/in-session) verdicts all
40
+ * resolve to silence here exactly as before this concern cited the ladder
41
+ * at all; team and council get their own carrier once Phase 3/4 give them
42
+ * one.
43
+ *
30
44
  * DELIVERY PATH — fixed to mirror `language_mirror_hook.ts` (found while
31
45
  * building this concern; the original `{decision:"allow", context:…}` shape
32
46
  * below is superseded). `context_blocks` forwarding in `dispatch_hook.ts` is
@@ -82,13 +96,9 @@ import path from "node:path";
82
96
  import { fileURLToPath, pathToFileURL } from "node:url";
83
97
  import { readHookStdin } from "./hook_stdin.js";
84
98
  import { load_agent_settings } from "../_lib/agent_settings.js";
85
- import { resolveHostCapabilities } from "../_lib/host_capability.js";
86
- import {
87
- classifyTask,
88
- type ActivationInputs,
89
- type Classification,
90
- type TaskSignals,
91
- } from "../_lib/auto_dispatch.js";
99
+ import { probeHostCapabilities } from "../_lib/host_capability.js";
100
+ import type { ActivationInputs, Classification, TaskSignals } from "../_lib/auto_dispatch.js";
101
+ import { classifyLadder, type LadderRung } from "../_lib/judgment_ladder.js";
92
102
  import { resolveSubagentRouting, type Tier } from "../_lib/subagent_routing.js";
93
103
  import { isSyntheticPrompt } from "../_lib/prompt_shape.js";
94
104
 
@@ -265,11 +275,11 @@ export function extractTaskSignals(text: string): ExtractedSignals {
265
275
 
266
276
  /**
267
277
  * `hostId` is the caller-supplied platform identifier (the envelope's own
268
- * `platform` field, e.g. `"claude"`) — `resolveHostCapabilities` resolves the
269
- * committed registry row for it. The settings-level
270
- * `subagents.host_capabilities` override, when present, still wins outright
271
- * over the registry (per `host_capability.ts § Resolution` — an explicit
272
- * consumer override is never second-guessed by a committed default).
278
+ * `platform` field, e.g. `"claude"`) — `probeHostCapabilities` resolves the
279
+ * committed registry row for it, merged with observable environment facts.
280
+ * Always-on orchestration (road-to-always-on-orchestration Phase 1):
281
+ * capability is a fact about the host, never a settings decision, so there
282
+ * is no more `subagents.host_capabilities` override to read here.
273
283
  */
274
284
  export function resolveActivation(
275
285
  workspace_root: string,
@@ -278,19 +288,20 @@ export function resolveActivation(
278
288
  activation: ActivationInputs;
279
289
  downshift: boolean;
280
290
  separate_quota_pool: boolean;
291
+ /** Host-capability manifest's `agent_teams` field — the ladder's rung-3 precondition. */
292
+ agentTeams: boolean;
281
293
  } {
282
294
  const settings = load_agent_settings({ cwd: workspace_root }) as Record<string, unknown>;
295
+ const emergency = (settings["emergency"] ?? {}) as Record<string, unknown>;
296
+ const halted = emergency["orchestration_halt"] === true;
297
+ const host_manifest = probeHostCapabilities(hostId);
283
298
  const sub = (settings["subagents"] ?? {}) as Record<string, unknown>;
284
- const enabled = sub["enabled"] !== false;
285
- const rawAuto = sub["auto"];
286
- const auto: ActivationInputs["auto"] =
287
- rawAuto === "on" || rawAuto === "off" ? rawAuto : "ask";
288
- const host_manifest = resolveHostCapabilities(hostId, sub["host_capabilities"]);
289
299
  const downshift = sub["downshift"] !== false;
290
300
  return {
291
- activation: { enabled, auto, subagent_spawn: host_manifest.subagent_spawn },
301
+ activation: { halted, subagent_spawn: host_manifest.subagent_spawn },
292
302
  downshift,
293
303
  separate_quota_pool: host_manifest.separate_quota_pool,
304
+ agentTeams: host_manifest.agent_teams,
294
305
  };
295
306
  }
296
307
 
@@ -313,14 +324,25 @@ export function recommendSliceTier(downshift: boolean, separate_quota_pool: bool
313
324
  return decision.tier;
314
325
  }
315
326
 
327
+ /**
328
+ * `rung` names the resolved judgment-ladder rung (2.4): `null` renders as
329
+ * `rung-?` — this concern only ever calls it for a `subagent` verdict
330
+ * (rung 1 or 2), so `null` is unreachable in production and exists only so
331
+ * the function stays total for direct unit-test callers.
332
+ */
316
333
  export function buildNudgeLine(
334
+ rung: LadderRung,
317
335
  classification: Classification,
318
336
  sliceCount: number,
319
337
  tier: Tier,
320
338
  ): string {
321
339
  const unit = sliceCount === 1 ? "slice" : "slices";
340
+ const rungLabel = rung === null ? "rung-?" : `rung-${rung}`;
341
+ // Rung 1 (single bounded slice) carries no do-in-steps/do-in-parallel
342
+ // mode of its own — `classification.mode` is `null` for that shape.
343
+ const modeLabel = classification.mode ?? "single-slice";
322
344
  return (
323
- `<delegation-nudge>classifyTask verdict for this prompt: ${classification.mode} ` +
345
+ `<delegation-nudge>${rungLabel}: dispatch ${modeLabel} ` +
324
346
  `(${sliceCount} ${unit}, ${tier} tier recommended). Consider dispatching via ` +
325
347
  `subagent-orchestration instead of doing every slice in-session — ` +
326
348
  `${classification.reason}.</delegation-nudge>`
@@ -331,13 +353,15 @@ export interface NudgeResult {
331
353
  classification: Classification;
332
354
  sliceCount: number;
333
355
  tier: Tier;
356
+ /** The judgment-ladder rung this verdict resolved to (2.4) — 1 or 2 only, this concern's scope. */
357
+ rung: LadderRung;
334
358
  }
335
359
 
336
360
  /**
337
361
  * Full pipeline: prompt text + workspace root + host id → a positive nudge
338
- * verdict, or `null` when nothing should be injected (no signal, activation
339
- * gate closed, or an internal error — all three degrade to the same silent
340
- * outcome).
362
+ * verdict, or `null` when nothing should be injected (no signal, a rung-0/3/4
363
+ * verdict out of this concern's scope, activation gate closed, or an
364
+ * internal error — all degrade to the same silent outcome).
341
365
  */
342
366
  export function classifyPrompt(
343
367
  prompt: string,
@@ -346,16 +370,26 @@ export function classifyPrompt(
346
370
  ): NudgeResult | null {
347
371
  try {
348
372
  const { signals, sliceCountForLine } = extractTaskSignals(prompt);
349
- const { activation, downshift, separate_quota_pool } = resolveActivation(
373
+ const { activation, downshift, separate_quota_pool, agentTeams } = resolveActivation(
350
374
  workspace_root,
351
375
  hostId,
352
376
  );
353
- const classification = classifyTask(signals, activation);
354
- if (classification.mode === null) {
355
- return null; // no enumerated signal, or the activation gate closed first
377
+ const ladder = classifyLadder({ taskText: prompt, signals, activation, agentTeams });
378
+ if (ladder.verdict !== "subagent") {
379
+ return null; // rung-0 script, rung-3/4 team/council, or ∅ ask/in-session — out of scope here
356
380
  }
357
381
  const tier = recommendSliceTier(downshift, separate_quota_pool);
358
- return { classification, sliceCount: sliceCountForLine, tier };
382
+ // Rung 1 (single bounded slice) has no explicit slice count of its own —
383
+ // extractTaskSignals never produces one for that shape — so the line
384
+ // always names it as exactly one slice.
385
+ const sliceCount = ladder.rung === 1 ? 1 : sliceCountForLine;
386
+ const classification: Classification = {
387
+ delegable: true,
388
+ action: "dispatch",
389
+ mode: ladder.mode ?? null,
390
+ reason: ladder.reason,
391
+ };
392
+ return { classification, sliceCount, tier, rung: ladder.rung };
359
393
  } catch {
360
394
  return null; // classifier/settings error → silence, never a crash-to-block
361
395
  }
@@ -415,8 +449,8 @@ export function main(): number {
415
449
  process.stdout.write(
416
450
  `${JSON.stringify({
417
451
  decision: "warn",
418
- reason: `delegation-nudge: ${result.classification.mode} verdict (${result.sliceCount} slices, ${result.tier} tier)`,
419
- additional_context: buildNudgeLine(result.classification, result.sliceCount, result.tier),
452
+ reason: `delegation-nudge: rung-${result.rung} verdict (${result.sliceCount} slices, ${result.tier} tier)`,
453
+ additional_context: buildNudgeLine(result.rung, result.classification, result.sliceCount, result.tier),
420
454
  })}\n`,
421
455
  );
422
456
  return EXIT_WARN;
@@ -50,6 +50,8 @@ const ADR_PACKS: ReadonlySet<string> = new Set<string>([
50
50
  'legal-review-prep',
51
51
  // road-to-scale-and-history-discipline Phase 1/3 (ADR-013 amendment, same PR).
52
52
  'scale-discipline', 'history-discipline',
53
+ // road-to-judgment-and-forensic-evidence Phase 3 (ADR-013 amendment, same PR).
54
+ 'forensics',
53
55
  ]);
54
56
 
55
57
  // ADR-010 non-overlap reservations.