@event4u/agent-config 9.28.0 → 9.30.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 +172 -5
- package/README.md +2 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/analyze/inbox.md +22 -1
- package/dist/agent-src/commands/cost/report.md +2 -2
- package/dist/agent-src/commands/explain-run.md +31 -1
- package/dist/agent-src/commands/team/adversarial.md +8 -7
- package/dist/agent-src/commands/team/delegate.md +14 -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 +22 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +17 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +65 -1
- 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/preservation-guard.md +9 -0
- 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/scripts/roadmap_gates.ts +312 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +72 -4
- 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 +17 -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/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
- package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-BTJNRUX3.js} +22 -35
- package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-DDUNNFYX.js} +24 -17
- package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-EQY4FLJB.js} +3 -3
- package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-IYTGIG2Y.js} +155 -91
- 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-MMJL4TIX.js +18 -0
- package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
- package/dist/cli-delegate/chunk-O3CO5BFB.js +161 -0
- package/dist/cli-delegate/{chunk-REKCMYOU.js → chunk-PFBV6XRT.js} +36 -20
- package/dist/cli-delegate/chunk-RVGBH4XK.js +142 -0
- 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 +2803 -137
- package/dist/cli-delegate/cmd_doctor.js +10 -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 +88 -8
- 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 +3 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +177 -0
- package/dist/cli-delegate/cmd_settings_get.js +2 -8
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +2 -2
- 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 +72 -23
- 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 +2278 -611
- package/dist/install/install.mjs +56 -17
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +4122 -699
- 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 +12 -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-B5qGYOJo.js +26 -0
- package/dist/ui/assets/index-B5qGYOJo.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +58 -12
- package/package.json +1 -1
- package/src/agent-src/scripts/roadmap_gates.ts +312 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +72 -4
- package/src/agent-src/templates/AGENTS.md +4 -4
- package/src/config/agent-settings.template.yml +88 -66
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/dispatch-economy-metrics.json +61 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/hook-token-budget.json +91 -0
- package/src/config/recycle-threshold-budget.json +12 -0
- package/src/config/rule-enforcement-baseline.json +86 -0
- package/src/scripts/_cli/cmd_conformance.ts +31 -44
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- package/src/scripts/_cli/cmd_session_recycle.ts +226 -0
- package/src/scripts/_dispatch.bash +23 -0
- 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 +634 -0
- package/src/scripts/_lib/orchestration_gate.ts +8 -0
- package/src/scripts/_lib/orchestration_record.ts +78 -10
- package/src/scripts/_lib/orchestration_savings.ts +1 -1
- package/src/scripts/_lib/recycle_envelope_paths.ts +35 -0
- package/src/scripts/_lib/self_repair.ts +354 -13
- package/src/scripts/_lib/self_repair_store.ts +28 -0
- package/src/scripts/_lib/session_eol.ts +230 -0
- package/src/scripts/_lib/session_role.ts +42 -0
- package/src/scripts/_lib/subagent_capsule.ts +341 -0
- package/src/scripts/_lib/subagent_response.ts +94 -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 +126 -55
- 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/ask_transport.ts +221 -0
- package/src/scripts/bench_critic_protocol.ts +396 -0
- package/src/scripts/bench_hook_injection.ts +234 -0
- package/src/scripts/check_adr_frontmatter.ts +84 -40
- package/src/scripts/check_branch_freshness.ts +1 -0
- package/src/scripts/check_corpus_staleness.ts +18 -4
- package/src/scripts/check_detector_corpus.ts +219 -0
- package/src/scripts/check_gitignore_freshness.ts +42 -8
- package/src/scripts/check_hook_bundle_freshness.ts +24 -1
- package/src/scripts/check_kernel_prefix_stability.ts +1 -0
- package/src/scripts/check_standing_rule_delivery.ts +73 -32
- package/src/scripts/check_static_layer_stability.ts +154 -0
- package/src/scripts/check_suppression_hygiene.ts +48 -0
- package/src/scripts/condense.ts +162 -2
- package/src/scripts/council_cli.ts +401 -25
- package/src/scripts/dispatch_economy_report.ts +448 -0
- package/src/scripts/dispatch_r2_reviewer.ts +15 -2
- package/src/scripts/explain_run.ts +206 -4
- package/src/scripts/forensics_report.ts +333 -0
- package/src/scripts/generate_settings_reference.ts +1 -0
- package/src/scripts/handoff_context_hook.ts +112 -4
- package/src/scripts/hook_manifest.yaml +117 -10
- package/src/scripts/hooks/concern_registry.ts +8 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +106 -35
- package/src/scripts/hooks/dispatch_hook.ts +39 -2
- package/src/scripts/hooks/edit_shape_hook.ts +303 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +19 -1
- package/src/scripts/hooks/reread_guard_hook.ts +314 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +133 -14
- package/src/scripts/hooks/session_eol_hook.ts +268 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +507 -0
- package/src/scripts/install-hooks.sh +1 -1
- package/src/scripts/language_mirror_hook.ts +89 -0
- package/src/scripts/lint_decision_impact.ts +14 -1
- package/src/scripts/lint_discovery_vocabulary.ts +2 -0
- package/src/scripts/lint_hook_manifest.ts +82 -0
- package/src/scripts/lint_never_silent.ts +276 -0
- package/src/scripts/lint_no_activation_gates.ts +271 -0
- package/src/scripts/lint_readme_serial_comma.ts +1 -0
- package/src/scripts/lint_rule_enforcement_declaration.ts +347 -0
- package/src/scripts/measure_turn_end_gate.ts +194 -0
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/rebuild_hook_bundle.ts +178 -0
- package/src/scripts/release.ts +74 -2
- package/src/scripts/report_conformance_funnel.ts +284 -0
- package/src/scripts/report_imperative_density.ts +1 -0
- package/src/scripts/routing_doctor.ts +118 -36
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +337 -59
- package/src/scripts/session_eol_report.ts +261 -0
- 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 +24 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- package/src/shared/settingsClasses.ts +11 -9
- package/src/templates/compact-instructions.md +40 -0
- package/dist/ui/assets/index-DmR0mbk4.js +0 -26
- package/dist/ui/assets/index-DmR0mbk4.js.map +0 -1
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
* 4. Subagent dispatches in window — mode, tiers, token deltas,
|
|
20
20
|
* first_pass_success/escalated. Source: same audit-log-v1 files,
|
|
21
21
|
* `orchestration` sub-object.
|
|
22
|
+
* 4b. (opt-in via `--decision "<task text>"`) Dispatch-decision trace —
|
|
23
|
+
* the judgment-ladder rung taken, every rung rejected with its
|
|
24
|
+
* detector's own reason (why-not-team / why-not-council /
|
|
25
|
+
* why-no-spawn), and the token/cost estimate from the most recent
|
|
26
|
+
* matching orchestration-telemetry record. No record in window → an
|
|
27
|
+
* honest "no telemetry record" line, never a fabricated estimate.
|
|
28
|
+
* Sources: `_lib/judgment_ladder.ts::explainLadder` + the same
|
|
29
|
+
* audit-log-v1 files as §4.
|
|
22
30
|
* 5. Hook / loop / freshness state snapshot. Source:
|
|
23
31
|
* `agents/state/context-hygiene.json` (the code path the hook
|
|
24
32
|
* actually writes — see `context_hygiene_hook.ts::STATE_FILE`) or
|
|
@@ -39,7 +47,10 @@
|
|
|
39
47
|
* ./scripts-run src/scripts/explain_run \
|
|
40
48
|
* [--task <id>] [--since <ISO-8601>] \
|
|
41
49
|
* [--router <path>] [--audit-dir <path>] [--engagement <path>] \
|
|
42
|
-
* [--hygiene <path>] [--output <path>]
|
|
50
|
+
* [--hygiene <path>] [--output <path>] \
|
|
51
|
+
* [--decision "<task text>"] [--size-estimate <n>] [--slices <n>] \
|
|
52
|
+
* [--ordered-plan] [--agent-teams] [--halted] [--no-spawn-primitive] \
|
|
53
|
+
* [--inside-subagent] [--approval-required]
|
|
43
54
|
*
|
|
44
55
|
* Defaults: --router dist/router.json · --audit-dir agents/runtime/state/audit
|
|
45
56
|
* · --engagement .agent-engagement.jsonl · --hygiene tries
|
|
@@ -51,6 +62,7 @@ import * as path from 'node:path';
|
|
|
51
62
|
import { pathToFileURL } from 'node:url';
|
|
52
63
|
|
|
53
64
|
import { EngagementSchemaError, parse_event, type EngagementEvent } from '../agent-src/templates/scripts/telemetry/engagement.js';
|
|
65
|
+
import { explainLadder, type LadderExplanation, type LadderInputs } from './_lib/judgment_ladder.js';
|
|
54
66
|
import { readAuditLines } from './orchestration_savings_report.js';
|
|
55
67
|
|
|
56
68
|
// ── CLI ──────────────────────────────────────────────────────────────────
|
|
@@ -76,7 +88,48 @@ export interface Options {
|
|
|
76
88
|
engagement: string;
|
|
77
89
|
hygiene: string | null;
|
|
78
90
|
output: string | null;
|
|
79
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Dispatch-decision task text (§4b); null/absent → the section is not
|
|
93
|
+
* rendered. Optional (with the flags below) so pre-existing callers
|
|
94
|
+
* constructing `Options` literals stay source-compatible.
|
|
95
|
+
*/
|
|
96
|
+
decision?: string | null;
|
|
97
|
+
/** Ladder signal flags — map 1:1 onto `LadderInputs`; nothing is inferred. */
|
|
98
|
+
sizeEstimate?: number;
|
|
99
|
+
slices?: number;
|
|
100
|
+
orderedPlan?: boolean;
|
|
101
|
+
agentTeams?: boolean;
|
|
102
|
+
halted?: boolean;
|
|
103
|
+
noSpawnPrimitive?: boolean;
|
|
104
|
+
insideSubagent?: boolean;
|
|
105
|
+
approvalRequired?: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const USAGE = `usage: explain_run [--task <id>] [--since <ISO-8601>] [--output <path>]
|
|
109
|
+
[--decision "<task text>"] [--size-estimate <int>] [--slices <int>]
|
|
110
|
+
[--ordered-plan] [--agent-teams] [--halted] [--no-spawn-primitive]
|
|
111
|
+
[--inside-subagent] [--approval-required]
|
|
112
|
+
|
|
113
|
+
Window:
|
|
114
|
+
--task <id> restrict to one task id in the audit log
|
|
115
|
+
--since <ISO-8601> cutoff timestamp / date
|
|
116
|
+
--output <path> write the report to a file instead of stdout
|
|
117
|
+
|
|
118
|
+
Dispatch decision (all optional; absent --decision omits the section):
|
|
119
|
+
--decision <text> classify this task text through the judgment ladder and
|
|
120
|
+
render the rung taken, the rungs rejected with their
|
|
121
|
+
detector's reason, and the rungs never reached
|
|
122
|
+
--size-estimate <int> size signal; at or below the floor nothing delegates
|
|
123
|
+
--slices <int> number of independent slices
|
|
124
|
+
--ordered-plan the slices are ordered, not independent
|
|
125
|
+
--agent-teams the host reports the agent_teams capability
|
|
126
|
+
--halted emergency.orchestration_halt is set
|
|
127
|
+
--no-spawn-primitive the host has no subagent_spawn primitive
|
|
128
|
+
--inside-subagent classification runs inside a subagent (recursive guard)
|
|
129
|
+
--approval-required a human decision is pending; resolves before every rung
|
|
130
|
+
|
|
131
|
+
Exit: 0 report rendered · 2 bad argument.
|
|
132
|
+
`;
|
|
80
133
|
|
|
81
134
|
function parseArgs(argv: string[]): Options {
|
|
82
135
|
const opts: Options = {
|
|
@@ -87,6 +140,23 @@ function parseArgs(argv: string[]): Options {
|
|
|
87
140
|
engagement: DEFAULT_ENGAGEMENT,
|
|
88
141
|
hygiene: null,
|
|
89
142
|
output: null,
|
|
143
|
+
decision: null,
|
|
144
|
+
sizeEstimate: 0,
|
|
145
|
+
slices: 0,
|
|
146
|
+
orderedPlan: false,
|
|
147
|
+
agentTeams: false,
|
|
148
|
+
halted: false,
|
|
149
|
+
noSpawnPrimitive: false,
|
|
150
|
+
insideSubagent: false,
|
|
151
|
+
approvalRequired: false,
|
|
152
|
+
};
|
|
153
|
+
const intValue = (flag: string, raw: string | undefined): number => {
|
|
154
|
+
const n = Number(raw);
|
|
155
|
+
if (raw === undefined || raw === '' || !Number.isInteger(n)) {
|
|
156
|
+
process.stderr.write(`explain_run: ${flag} requires an integer, got: ${raw ?? '(missing)'}\n`);
|
|
157
|
+
process.exit(2);
|
|
158
|
+
}
|
|
159
|
+
return n;
|
|
90
160
|
};
|
|
91
161
|
for (let i = 0; i < argv.length; i++) {
|
|
92
162
|
const a = argv[i];
|
|
@@ -106,8 +176,38 @@ function parseArgs(argv: string[]): Options {
|
|
|
106
176
|
else if (a.startsWith('--hygiene=')) opts.hygiene = a.slice('--hygiene='.length);
|
|
107
177
|
else if (a === '--output') opts.output = next() ?? opts.output;
|
|
108
178
|
else if (a.startsWith('--output=')) opts.output = a.slice('--output='.length);
|
|
109
|
-
else {
|
|
110
|
-
|
|
179
|
+
else if (a === '--decision') {
|
|
180
|
+
const v = next();
|
|
181
|
+
// Exit rather than no-op: silently dropping the value produced a full
|
|
182
|
+
// report with the dispatch section absent and exit 0, so a typo read
|
|
183
|
+
// as "this run had no dispatch decision". `--size-estimate` already
|
|
184
|
+
// exits 2 on the same mistake; the flags now behave alike.
|
|
185
|
+
if (v === undefined || v === '') {
|
|
186
|
+
process.stderr.write('explain_run: --decision requires a value (the task text to classify)\n');
|
|
187
|
+
process.exit(2);
|
|
188
|
+
}
|
|
189
|
+
opts.decision = v;
|
|
190
|
+
} else if (a.startsWith('--decision=')) opts.decision = a.slice('--decision='.length);
|
|
191
|
+
else if (a === '--size-estimate') opts.sizeEstimate = intValue('--size-estimate', next());
|
|
192
|
+
else if (a.startsWith('--size-estimate=')) opts.sizeEstimate = intValue('--size-estimate', a.slice('--size-estimate='.length));
|
|
193
|
+
else if (a === '--slices') opts.slices = intValue('--slices', next());
|
|
194
|
+
else if (a.startsWith('--slices=')) opts.slices = intValue('--slices', a.slice('--slices='.length));
|
|
195
|
+
else if (a === '--ordered-plan') opts.orderedPlan = true;
|
|
196
|
+
else if (a === '--agent-teams') opts.agentTeams = true;
|
|
197
|
+
else if (a === '--halted') opts.halted = true;
|
|
198
|
+
else if (a === '--no-spawn-primitive') opts.noSpawnPrimitive = true;
|
|
199
|
+
else if (a === '--inside-subagent') opts.insideSubagent = true;
|
|
200
|
+
else if (a === '--approval-required') opts.approvalRequired = true;
|
|
201
|
+
else if (a === '--help' || a === '-h') {
|
|
202
|
+
process.stdout.write(USAGE);
|
|
203
|
+
process.exit(0);
|
|
204
|
+
} else {
|
|
205
|
+
// The usage text goes to stderr with the error, so a typo shows the
|
|
206
|
+
// whole surface rather than only the rejected token. Nine of these
|
|
207
|
+
// flags landed at once (the dispatch-decision trace); a script whose
|
|
208
|
+
// only discovery path is reading its source is a script whose flags
|
|
209
|
+
// do not exist for the person at the terminal.
|
|
210
|
+
process.stderr.write(`explain_run: unrecognized argument: ${a}\n\n${USAGE}`);
|
|
111
211
|
process.exit(2);
|
|
112
212
|
}
|
|
113
213
|
}
|
|
@@ -486,6 +586,104 @@ function renderOrchestrationSection(opts: Options, auditDirExists: boolean, disp
|
|
|
486
586
|
return out;
|
|
487
587
|
}
|
|
488
588
|
|
|
589
|
+
// ── Dispatch-decision trace (§4b — judgment ladder + telemetry estimate) ─
|
|
590
|
+
|
|
591
|
+
/** Map the CLI signal flags 1:1 onto `LadderInputs` — nothing inferred. */
|
|
592
|
+
export function ladderInputsFromOptions(opts: Options): LadderInputs {
|
|
593
|
+
return {
|
|
594
|
+
taskText: opts.decision ?? '',
|
|
595
|
+
signals: {
|
|
596
|
+
size_estimate: opts.sizeEstimate ?? 0,
|
|
597
|
+
independent_slices: opts.slices ?? 0,
|
|
598
|
+
ordered_plan: opts.orderedPlan ?? false,
|
|
599
|
+
},
|
|
600
|
+
activation: { halted: opts.halted ?? false, subagent_spawn: !(opts.noSpawnPrimitive ?? false) },
|
|
601
|
+
agentTeams: opts.agentTeams ?? false,
|
|
602
|
+
interactiveApprovalRequired: opts.approvalRequired ?? false,
|
|
603
|
+
insideSubagentSession: opts.insideSubagent ?? false,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Most recent dispatch line by `ts` (undefined ts sorts oldest). */
|
|
608
|
+
export function latestDispatch(dispatches: ExplainAuditLine[]): ExplainAuditLine | null {
|
|
609
|
+
let latest: ExplainAuditLine | null = null;
|
|
610
|
+
let latestTs = Number.NEGATIVE_INFINITY;
|
|
611
|
+
for (const line of dispatches) {
|
|
612
|
+
const t = line.ts === undefined ? Number.NEGATIVE_INFINITY : Date.parse(line.ts);
|
|
613
|
+
const ts = Number.isFinite(t) ? t : Number.NEGATIVE_INFINITY;
|
|
614
|
+
if (latest === null || ts >= latestTs) {
|
|
615
|
+
latest = line;
|
|
616
|
+
latestTs = ts;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return latest;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function renderDecisionSection(
|
|
623
|
+
opts: Options,
|
|
624
|
+
explanation: LadderExplanation,
|
|
625
|
+
auditDirExists: boolean,
|
|
626
|
+
dispatches: ExplainAuditLine[],
|
|
627
|
+
): string[] {
|
|
628
|
+
const out: string[] = [];
|
|
629
|
+
out.push('## Dispatch decision (judgment ladder)');
|
|
630
|
+
out.push(
|
|
631
|
+
`_Source: \`src/scripts/_lib/judgment_ladder.ts\` (\`explainLadder\`) over the \`--decision\` text + signal flags; token/cost estimate from \`${opts.auditDir}/*.jsonl\` orchestration telemetry_`,
|
|
632
|
+
);
|
|
633
|
+
out.push('');
|
|
634
|
+
out.push(`Decision text: ${JSON.stringify(opts.decision ?? '')}`);
|
|
635
|
+
const r = explanation.result;
|
|
636
|
+
const rungLabel = r.rung === null ? '∅ (never spawns)' : String(r.rung);
|
|
637
|
+
const modeSuffix = r.mode !== undefined && r.mode !== null ? ` (mode ${r.mode})` : '';
|
|
638
|
+
const degradedSuffix = r.degraded_from !== undefined ? ` [degraded from rung ${r.degraded_from}]` : '';
|
|
639
|
+
out.push(`Resolved: rung ${rungLabel} — verdict \`${r.verdict}\`${modeSuffix}${degradedSuffix} — ${r.reason}`);
|
|
640
|
+
out.push('');
|
|
641
|
+
out.push('### Ladder trail (priority order 0 → 4 → 3 → 2 → 1)');
|
|
642
|
+
out.push('');
|
|
643
|
+
out.push('| rung | resolves to | status | reason |');
|
|
644
|
+
out.push('|---|---|---|---|');
|
|
645
|
+
for (const step of explanation.trail) {
|
|
646
|
+
out.push(`| ${step.rung} | ${step.resolves_to} | ${step.status} | ${step.reason} |`);
|
|
647
|
+
}
|
|
648
|
+
if (explanation.no_spawn_reason !== undefined) {
|
|
649
|
+
out.push('');
|
|
650
|
+
out.push(`Why no spawn: ${explanation.no_spawn_reason}`);
|
|
651
|
+
}
|
|
652
|
+
out.push('');
|
|
653
|
+
out.push('### Token/cost estimate');
|
|
654
|
+
if (!auditDirExists) {
|
|
655
|
+
out.push(`no telemetry record — \`${opts.auditDir}\` absent; no token/cost estimate (a fabricated number would be worse than none)`);
|
|
656
|
+
return out;
|
|
657
|
+
}
|
|
658
|
+
const latest = latestDispatch(dispatches);
|
|
659
|
+
if (latest === null) {
|
|
660
|
+
out.push(`no telemetry record matches ${windowLabel(opts)}; no token/cost estimate (a fabricated number would be worse than none)`);
|
|
661
|
+
return out;
|
|
662
|
+
}
|
|
663
|
+
const o = latest.orchestration ?? {};
|
|
664
|
+
// NOT matched to this decision, and the label must say so. The record shape
|
|
665
|
+
// carries no key that ties a line to a decision text — `latestDispatch` sorts
|
|
666
|
+
// by `ts` alone — so calling it "matching" made an unrelated run's cost read
|
|
667
|
+
// as this decision's estimate, in a section whose own header promises never
|
|
668
|
+
// to fabricate one. Printing it is still useful (it is the freshest real
|
|
669
|
+
// measurement on this host); claiming correspondence was the defect.
|
|
670
|
+
out.push(
|
|
671
|
+
`most recent dispatch in ${windowLabel(opts)} — NOT matched to this decision ` +
|
|
672
|
+
`(the record carries no decision key; sorted by ts alone): ` +
|
|
673
|
+
`ts ${latest.ts ?? '(unset)'}, work_id ${latest.work_id ?? '(unset)'}, ` +
|
|
674
|
+
`token_delta ${o.token_delta ?? 0} (${o.token_delta_provenance ?? 'estimated'}), ` +
|
|
675
|
+
`mode ${o.dispatch_mode ?? '(unset)'}, tiers ${(o.tiers ?? []).join(',') || '(unset)'}`,
|
|
676
|
+
);
|
|
677
|
+
const resolved = explanation.result.verdict;
|
|
678
|
+
const recordMode = o.dispatch_mode;
|
|
679
|
+
if (recordMode !== undefined && resolved === 'in-session') {
|
|
680
|
+
out.push(
|
|
681
|
+
` ⚠️ this decision resolves to \`${resolved}\` (no dispatch), so the mode above describes a different run.`,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
return out;
|
|
685
|
+
}
|
|
686
|
+
|
|
489
687
|
function renderHygieneSection(hygiene: HygieneReadResult): string[] {
|
|
490
688
|
const out: string[] = [];
|
|
491
689
|
out.push('## Hook / loop state (context-hygiene)');
|
|
@@ -568,6 +766,10 @@ export function buildReport(opts: Options): string {
|
|
|
568
766
|
out.push('');
|
|
569
767
|
out.push(...renderOrchestrationSection(opts, auditDirExists, dispatches));
|
|
570
768
|
out.push('');
|
|
769
|
+
if (opts.decision !== null && opts.decision !== undefined) {
|
|
770
|
+
out.push(...renderDecisionSection(opts, explainLadder(ladderInputsFromOptions(opts)), auditDirExists, dispatches));
|
|
771
|
+
out.push('');
|
|
772
|
+
}
|
|
571
773
|
out.push(...renderHygieneSection(hygiene));
|
|
572
774
|
out.push('');
|
|
573
775
|
out.push(...renderParkedSection());
|
|
@@ -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 };
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
} from '../shared/settingsClasses.js';
|
|
44
44
|
import { flattenSurface, type SurfaceEntry } from '../shared/settingsSurface.js';
|
|
45
45
|
|
|
46
|
+
// ledger-exempt: single-artifact generator — renders ONE derived page from the zod schema and byte-compares it against the committed copy in --check mode; the verdict is one aggregate drift comparison over the whole page, so per-target findings do not exist.
|
|
46
47
|
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
47
48
|
export const ROOT = path.resolve(_HERE, '..', '..');
|
|
48
49
|
|