@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +6 -6
- package/CHANGELOG.md +66 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/cost/report.md +2 -2
- package/dist/agent-src/commands/team/adversarial.md +8 -7
- package/dist/agent-src/commands/team/delegate.md +13 -12
- package/dist/agent-src/commands/team/review.md +9 -8
- package/dist/agent-src/commands/team/status.md +9 -8
- package/dist/agent-src/commands/team.md +29 -19
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +18 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
- package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
- package/dist/agent-src/rules/delegation-policy.md +11 -7
- package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
- package/dist/agent-src/rules/token-budget-discipline.md +1 -1
- package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
- package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
- package/dist/agent-src/skills/premortem/SKILL.md +30 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +4 -4
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +23 -0
- package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
- package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
- package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
- package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
- package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
- package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-NZ5N2XQL.js} +3 -3
- package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-OJM3MBJM.js} +1 -1
- package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-RCIA67XN.js} +111 -90
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +6 -43
- package/dist/cli-delegate/cmd_doctor.js +9 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_settings_get.js +1 -7
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +69 -20
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -2
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +39 -2
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +12 -4
- package/dist/hooks/dispatch.js +373 -35
- package/dist/install/install.mjs +42 -17
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +651 -262
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +54 -0
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +147 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +6 -7
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/settingsCarveOut.js +0 -9
- package/dist/shared/settingsCarveOut.js.map +1 -1
- package/dist/shared/settingsClasses.js +11 -9
- package/dist/shared/settingsClasses.js.map +1 -1
- package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
- package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +39 -9
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +4 -4
- package/src/config/agent-settings.template.yml +52 -66
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/scripts/_cli/cmd_conformance.ts +14 -42
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- package/src/scripts/_lib/agent_settings.ts +58 -0
- package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
- package/src/scripts/_lib/auto_dispatch.ts +23 -18
- package/src/scripts/_lib/capsule_trigger.ts +124 -0
- package/src/scripts/_lib/host_capability.ts +63 -6
- package/src/scripts/_lib/judgment_ladder.ts +382 -0
- package/src/scripts/_lib/orchestration_gate.ts +8 -0
- package/src/scripts/_lib/orchestration_record.ts +37 -0
- package/src/scripts/_lib/orchestration_savings.ts +1 -1
- package/src/scripts/_lib/self_repair.ts +160 -13
- package/src/scripts/_lib/self_repair_store.ts +28 -0
- package/src/scripts/_lib/subagent_capsule.ts +182 -0
- package/src/scripts/_lib/subagent_response.ts +19 -0
- package/src/scripts/_lib/subagent_spawn.ts +13 -0
- package/src/scripts/_lib/subagent_steering.ts +9 -4
- package/src/scripts/_lib/tier_budget_routing.ts +6 -1
- package/src/scripts/_lib/worker_budget.ts +37 -2
- package/src/scripts/ai_council/clients.ts +76 -54
- package/src/scripts/ai_council/config.ts +94 -6
- package/src/scripts/ai_council/handoff.ts +113 -0
- package/src/scripts/ai_council/modes.ts +19 -4
- package/src/scripts/ai_council/orchestrator.ts +105 -0
- package/src/scripts/ai_council/quorum.ts +84 -0
- package/src/scripts/ai_council/session.ts +44 -0
- package/src/scripts/ai_council/transport_resolver.ts +127 -1
- package/src/scripts/ai_team/availability.ts +110 -0
- package/src/scripts/ai_team/config.ts +34 -14
- package/src/scripts/ai_team/team_dispatch.ts +71 -29
- package/src/scripts/bench_critic_protocol.ts +396 -0
- package/src/scripts/check_detector_corpus.ts +202 -0
- package/src/scripts/check_hook_bundle_freshness.ts +4 -1
- package/src/scripts/condense.ts +162 -2
- package/src/scripts/council_cli.ts +401 -25
- package/src/scripts/forensics_report.ts +333 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +69 -35
- package/src/scripts/lint_discovery_vocabulary.ts +2 -0
- package/src/scripts/lint_never_silent.ts +276 -0
- package/src/scripts/lint_no_activation_gates.ts +271 -0
- package/src/scripts/orchestration_record.ts +8 -0
- package/src/scripts/rebuild_hook_bundle.ts +177 -0
- package/src/scripts/release.ts +74 -2
- package/src/scripts/routing_doctor.ts +21 -36
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +287 -57
- package/src/scripts/team_events_spike.ts +271 -0
- package/src/scripts/team_review_gate_hook.ts +31 -9
- package/src/server/schemas/settings.ts +10 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- 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
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* the
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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 {
|
|
86
|
-
import {
|
|
87
|
-
|
|
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"`) — `
|
|
269
|
-
* committed registry row for it
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
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: {
|
|
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
|
|
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,
|
|
339
|
-
*
|
|
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
|
|
354
|
-
if (
|
|
355
|
-
return null; //
|
|
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
|
-
|
|
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:
|
|
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.
|