@event4u/agent-config 14.7.0 → 14.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +175 -390
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +34 -4
- package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
- package/dist/agent-src/commands/evals/roadmap.json +17 -1
- package/dist/agent-src/commands/pr/create.md +14 -2
- package/dist/agent-src/commands/pr/merge.md +383 -0
- package/dist/agent-src/commands/review/changes.md +95 -14
- package/dist/agent-src/commands/roadmap/process-full.md +157 -3
- package/dist/agent-src/commands/roadmap.md +1 -1
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +77 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +100 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +154 -2
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +80 -0
- package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
- package/dist/agent-src/rules/decision-revisit-gate.md +65 -33
- package/dist/agent-src/rules/recurring-criticism.md +1 -1
- package/dist/agent-src/rules/roadmap-progress-sync.md +19 -0
- package/dist/agent-src/rules/source-confidentiality.md +1 -1
- package/dist/agent-src/rules/untrusted-input-defense.md +9 -3
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
- package/dist/agent-src/skills/code-review/evals/evals.json +87 -46
- package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
- package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
- package/dist/agent-src/skills/humanizer/SKILL.md +39 -0
- package/dist/agent-src/skills/humanizer/evals/strip_fixtures.json +57 -0
- package/dist/agent-src/skills/humanizer/references/fixtures.md +54 -0
- package/dist/agent-src/skills/judge-spec-compliance/SKILL.md +154 -0
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +45 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +18 -18
- package/dist/agent-src/skills/security/SKILL.md +8 -0
- package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +28 -2
- package/dist/cli/registry.js +3 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-77GR3VD4.js} +68 -43
- package/dist/cli-delegate/{chunk-JZ4NR2TW.js → chunk-7PRE6ZV2.js} +1 -1
- package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-CYH5SP6M.js} +4 -23
- package/dist/cli-delegate/{chunk-Q26HT4QT.js → chunk-E46KPN6K.js} +251 -150
- package/dist/cli-delegate/{chunk-74E3TKWV.js → chunk-H4VIVD4C.js} +5 -5
- package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
- package/dist/cli-delegate/{chunk-4VR4ORQ6.js → chunk-LPPWO5CE.js} +4 -3
- package/dist/cli-delegate/{chunk-7F3VYSM2.js → chunk-PLSG6UL5.js} +14 -14
- package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
- package/dist/cli-delegate/cmd_conformance.js +101 -25
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +7 -7
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_handoff.js +7 -6
- package/dist/cli-delegate/cmd_memory_get.js +2 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +5 -5
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_route_audit.js +6 -5
- package/dist/cli-delegate/cmd_route_explain.js +3 -3
- package/dist/cli-delegate/cmd_session_recycle.js +3 -3
- package/dist/cli-delegate/cmd_settings_get.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- package/dist/cli-delegate/cmd_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +83 -30
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +11 -9
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +9 -7
- package/dist/hooks/dispatch.js +1295 -812
- package/dist/install/globalRuleLayers.js +169 -0
- package/dist/install/globalRuleLayers.js.map +1 -0
- package/dist/install/partitionEligibility.js +156 -0
- package/dist/install/partitionEligibility.js.map +1 -1
- package/dist/install/ruleLayerPartition.js +157 -0
- package/dist/install/ruleLayerPartition.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1358 -805
- package/dist/scripts/_lib/claude_plugin.js +162 -0
- package/dist/scripts/_lib/claude_plugin.js.map +1 -0
- package/dist/scripts/_lib/environment_detector.js +397 -0
- package/dist/scripts/_lib/environment_detector.js.map +1 -0
- package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
- package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
- package/dist/scripts/_lib/spawn_env.js +129 -0
- package/dist/scripts/_lib/spawn_env.js.map +1 -0
- package/dist/scripts/ai_council/modes.js +199 -0
- package/dist/scripts/ai_council/modes.js.map +1 -0
- package/dist/scripts/ai_council/transport_resolver.js +418 -0
- package/dist/scripts/ai_council/transport_resolver.js.map +1 -0
- package/dist/scripts/condense.js +52 -89
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/routes/wizard.js +3 -2
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/ui/assets/index-Cxgy63kz.js +26 -0
- package/dist/ui/assets/index-Cxgy63kz.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
- package/docs/guidelines/agent-infra/false-green.md +47 -1
- package/docs/guidelines/agent-infra/traceability-field-mechanics.md +50 -0
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +17 -0
- package/package.json +7 -3
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/src/agent-src/scripts/dashboard_mode.ts +212 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/src/config/ci-local-parity.yml +21 -0
- package/src/config/estate-count-budget.json +13 -471
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +458 -12
- package/src/config/gate-violation-baselines.json +18 -8
- package/src/config/gitignore-block.txt +15 -2
- package/src/config/hook-latency-budget.json +5 -0
- package/src/config/publish-surface.json +60 -0
- package/src/config/requirements-trace-budget.json +32 -0
- package/src/config/rule-stub-ceilings.json +25 -4
- package/src/install/globalRuleLayers.ts +197 -0
- package/src/install/partitionEligibility.ts +177 -0
- package/src/install/ruleLayerPartition.ts +212 -0
- package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
- package/src/scripts/_dispatch.bash +18 -0
- package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
- package/src/scripts/_lib/adr_frontmatter.ts +459 -0
- package/src/scripts/_lib/base_tree.ts +175 -0
- package/src/scripts/_lib/billing_grant.ts +165 -0
- package/src/scripts/_lib/billing_grant_cli.ts +123 -0
- package/src/scripts/_lib/council_fallback_posture.ts +53 -6
- package/src/scripts/_lib/council_fallback_wiring.ts +61 -1
- package/src/scripts/_lib/envelope_grounding.ts +11 -1
- package/src/scripts/_lib/host_capability.ts +59 -5
- package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
- package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
- package/src/scripts/_lib/quota_parked.ts +133 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +62 -0
- package/src/scripts/_lib/review_telemetry.ts +196 -0
- package/src/scripts/_lib/subagent_response.ts +92 -0
- package/src/scripts/adr/evidence_census.ts +899 -0
- package/src/scripts/adr/regenerate_index.ts +25 -38
- package/src/scripts/adr_cite_check.ts +540 -39
- package/src/scripts/ai_council/clients.ts +12 -1
- package/src/scripts/ai_council/config.ts +72 -115
- package/src/scripts/ai_council/fallback_config.ts +19 -7
- package/src/scripts/ai_council/mid_flight_fallback.ts +88 -7
- package/src/scripts/ai_council/orchestrator.ts +2 -2
- package/src/scripts/ai_council/py_format.ts +124 -0
- package/src/scripts/ai_council/transport_resolver.ts +65 -2
- package/src/scripts/audit_adr_coverage.ts +134 -21
- package/src/scripts/bench_hook_latency.ts +204 -0
- package/src/scripts/build_archive_index.ts +34 -0
- package/src/scripts/check_adr_frontmatter.ts +574 -39
- package/src/scripts/check_council_pin_staleness.ts +300 -0
- package/src/scripts/check_estate_count.ts +420 -293
- package/src/scripts/check_gate_coverage.ts +91 -0
- package/src/scripts/check_hook_bundle_content.ts +219 -0
- package/src/scripts/check_new_adr_evidence.ts +529 -0
- package/src/scripts/check_no_stub_inventory_table.ts +292 -0
- package/src/scripts/check_pack_size.ts +234 -1
- package/src/scripts/check_publish_surface.ts +297 -0
- package/src/scripts/check_references.ts +18 -0
- package/src/scripts/check_requirements_trace.ts +378 -0
- package/src/scripts/check_rule_layer_partition.ts +531 -0
- package/src/scripts/check_secret_leak.ts +166 -12
- package/src/scripts/check_single_delivery.ts +137 -4
- package/src/scripts/check_standing_rule_delivery.ts +8 -2
- package/src/scripts/code_graph/cli.ts +44 -5
- package/src/scripts/code_graph/extract.ts +87 -0
- package/src/scripts/condense.ts +61 -91
- package/src/scripts/council_cli.ts +12 -16
- package/src/scripts/detect_ai_tells.ts +87 -0
- package/src/scripts/dispatch_r2_reviewer.ts +42 -0
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/generate_capabilities_index.ts +41 -0
- package/src/scripts/git_authorization_hook.ts +54 -4
- package/src/scripts/hooks/block_unauthorized_git.ts +41 -3
- package/src/scripts/hooks/dispatch_hook.ts +154 -187
- package/src/scripts/hooks/dispatch_issues.ts +38 -19
- package/src/scripts/hooks/fallback_yaml.ts +66 -0
- package/src/scripts/hooks/host_semantics.ts +21 -0
- package/src/scripts/hooks/injection_budget.ts +13 -3
- package/src/scripts/hooks/py_json_dumps.ts +103 -0
- package/src/scripts/hooks/run_continuation_hook.ts +3 -1
- package/src/scripts/hooks/state_io.ts +88 -1
- package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +76 -11
- package/src/scripts/injection_scan_hook.ts +257 -15
- package/src/scripts/lint_adr_sweep_routing.ts +365 -0
- package/src/scripts/lint_hook_manifest.ts +49 -0
- package/src/scripts/lint_provenance_vocabulary.ts +472 -4
- package/src/scripts/lint_workflow_security.ts +53 -10
- package/src/scripts/lint_workflow_security_allowlist.json +2 -18
- package/src/scripts/report_envelope_rate.ts +166 -0
- package/src/scripts/review_axis_report.ts +218 -0
- package/src/scripts/routing_doctor.ts +5 -1
- package/src/scripts/run_supervise.ts +46 -1
- package/src/scripts/schemas/evals.schema.json +5 -0
- package/src/scripts/sync_pr_branch.ts +170 -20
- package/dist/cli/commands/commands.candidates.test.js +0 -257
- package/dist/cli/commands/commands.candidates.test.js.map +0 -1
- package/dist/cli/commands/profiles.test.js +0 -88
- package/dist/cli/commands/profiles.test.js.map +0 -1
- package/dist/cli/commands/recordTriggerEval.test.js +0 -113
- package/dist/cli/commands/recordTriggerEval.test.js.map +0 -1
- package/dist/cli/configRoot.test.js +0 -97
- package/dist/cli/configRoot.test.js.map +0 -1
- package/dist/cli/mcp/content.test.js +0 -54
- package/dist/cli/mcp/content.test.js.map +0 -1
- package/dist/cli/mcp/dispatch.test.js +0 -108
- package/dist/cli/mcp/dispatch.test.js.map +0 -1
- package/dist/server/writeRoot.test.js +0 -91
- package/dist/server/writeRoot.test.js.map +0 -1
- package/dist/shared/capabilities.test.js +0 -39
- package/dist/shared/capabilities.test.js.map +0 -1
- package/dist/ui/assets/index-C1pBRm1f.js +0 -26
- package/dist/ui/assets/index-C1pBRm1f.js.map +0 -1
- package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
- package/dist/cli-delegate/{chunk-6EOCNQAM.js → chunk-E4XIZDIB.js} +0 -0
- package/dist/cli-delegate/{chunk-GJ5BVHXM.js → chunk-PMEKGDED.js} +3 -3
|
@@ -89,6 +89,19 @@ export interface GateSpec {
|
|
|
89
89
|
status: 'enforced' | 'pending';
|
|
90
90
|
/** Absent = this gate has no canary recipe; reported, never hidden. */
|
|
91
91
|
canary?: CanarySpec;
|
|
92
|
+
/**
|
|
93
|
+
* Why this entry has no `canary:` recipe — REQUIRED when one is absent.
|
|
94
|
+
*
|
|
95
|
+
* The measurement that forced this field: over 44 enforced entries, only
|
|
96
|
+
* 20 of 44 by a wide detector (and 13 by a narrow one) can be invoked over an
|
|
97
|
+
* injected path at all, so a non-mutating in-memory negative-control mode
|
|
98
|
+
* would cover under half the manifest. Rather than build a per-PR step named
|
|
99
|
+
* "negative controls" that exercises a minority, the gap is REPORTED — and a
|
|
100
|
+
* reported gap is only honest if every row carries its reason. A silently
|
|
101
|
+
* absent row reads as coverage, which is the exact failure this whole file
|
|
102
|
+
* exists to prevent, one level up.
|
|
103
|
+
*/
|
|
104
|
+
no_canary_reason?: string;
|
|
92
105
|
/**
|
|
93
106
|
* Exit code meaning "my prerequisite is absent, so I inspected nothing" — e.g.
|
|
94
107
|
* check_site_links exits 2 when the site is unbuilt or stale. Without this the
|
|
@@ -120,6 +133,13 @@ export interface GateResult {
|
|
|
120
133
|
message: string;
|
|
121
134
|
}
|
|
122
135
|
|
|
136
|
+
function _require_str(v: unknown, field: string, id: string, i: number): string {
|
|
137
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
138
|
+
throw new Error(`gates[${String(i)}] (${id}): ${field} must be a non-empty string`);
|
|
139
|
+
}
|
|
140
|
+
return v;
|
|
141
|
+
}
|
|
142
|
+
|
|
123
143
|
function _require_int(v: unknown, id: string, i: number): number {
|
|
124
144
|
const n = Number(v);
|
|
125
145
|
if (!Number.isInteger(n)) {
|
|
@@ -147,6 +167,16 @@ export function load_manifest(file = MANIFEST): GateSpec[] {
|
|
|
147
167
|
if (!Number.isInteger(min) || min < 0) {
|
|
148
168
|
throw new Error(`gates[${i}] (${id}): min_scanned must be a non-negative integer`);
|
|
149
169
|
}
|
|
170
|
+
if (
|
|
171
|
+
String(e['status'] ?? 'enforced') === 'enforced' &&
|
|
172
|
+
e['canary'] === undefined &&
|
|
173
|
+
e['no_canary_reason'] === undefined
|
|
174
|
+
) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`gates[${String(i)}] (${id}): an enforced entry with no 'canary:' recipe must carry ` +
|
|
177
|
+
`'no_canary_reason:' — a silently absent negative control reads as coverage`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
150
180
|
const status = String(e['status'] ?? 'enforced');
|
|
151
181
|
if (status !== 'enforced' && status !== 'pending') {
|
|
152
182
|
throw new Error(`gates[${i}] (${id}): status must be 'enforced' or 'pending'`);
|
|
@@ -162,6 +192,9 @@ export function load_manifest(file = MANIFEST): GateSpec[] {
|
|
|
162
192
|
: { unavailable_exit: _require_int(e['unavailable_exit'], id, i) }),
|
|
163
193
|
...(e['note'] === undefined ? {} : { note: String(e['note']) }),
|
|
164
194
|
...(e['canary'] === undefined ? {} : { canary: _require_canary(e['canary'], id, i) }),
|
|
195
|
+
...(e['no_canary_reason'] === undefined
|
|
196
|
+
? {}
|
|
197
|
+
: { no_canary_reason: _require_str(e['no_canary_reason'], 'no_canary_reason', id, i) }),
|
|
165
198
|
};
|
|
166
199
|
});
|
|
167
200
|
}
|
|
@@ -338,6 +371,39 @@ const ICON: Record<Verdict, string> = {
|
|
|
338
371
|
unavailable: '⚠️',
|
|
339
372
|
};
|
|
340
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Report which enforced gates carry a negative control and which do not.
|
|
376
|
+
*
|
|
377
|
+
* OBSERVABILITY, NEVER PROOF — and the distinction is the whole point of the
|
|
378
|
+
* wording below. A `canary:` recipe DECLARED is not a recipe RUN: the mutating
|
|
379
|
+
* `--canary` path plants a real file and is deliberately off the per-PR
|
|
380
|
+
* workflow, so on a normal PR the "with recipe" number describes what COULD be
|
|
381
|
+
* exercised, not what was. Reporting it as though it were coverage would be the
|
|
382
|
+
* inflation this file exists to prevent, one level up from where it prevents it.
|
|
383
|
+
*
|
|
384
|
+
* The two populations are reported separately on purpose: whether a gate can be
|
|
385
|
+
* invoked over an injected path and whether it has a recipe are different facts,
|
|
386
|
+
* and an earlier draft of this work conflated them.
|
|
387
|
+
*/
|
|
388
|
+
function report_negative_control_inventory(specs: readonly GateSpec[]): void {
|
|
389
|
+
const enforced = specs.filter((s) => s.status === 'enforced');
|
|
390
|
+
const withRecipe = enforced.filter((s) => s.canary !== undefined);
|
|
391
|
+
const without = enforced.filter((s) => s.canary === undefined);
|
|
392
|
+
process.stdout.write(
|
|
393
|
+
`\nnegative controls: ${String(withRecipe.length)} of ${String(enforced.length)} enforced ` +
|
|
394
|
+
`entries carry a \`canary:\` recipe · ${String(without.length)} do not\n`,
|
|
395
|
+
);
|
|
396
|
+
// The arithmetic is the guard against skimming: two numbers that reconcile
|
|
397
|
+
// against the enforced total cannot be read as "all of them checked".
|
|
398
|
+
process.stdout.write(
|
|
399
|
+
` ⚠️ declared, not run — the mutating --canary path is operator-invoked and off the per-PR workflow, ` +
|
|
400
|
+
`so no negative control ran here.\n`,
|
|
401
|
+
);
|
|
402
|
+
for (const s of without) {
|
|
403
|
+
process.stdout.write(` · ${s.id}: ${s.no_canary_reason ?? '(no reason recorded)'}\n`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
341
407
|
export function main(argv: readonly string[]): number {
|
|
342
408
|
const wantJson = argv.includes('--format') && argv[argv.indexOf('--format') + 1] === 'json';
|
|
343
409
|
const listOnly = argv.includes('--list');
|
|
@@ -455,6 +521,7 @@ export function main(argv: readonly string[]): number {
|
|
|
455
521
|
`⚠️ ${String(pending.length)} gate(s) are listed but NOT enforced — this guard's coverage is partial by declaration.\n`,
|
|
456
522
|
);
|
|
457
523
|
}
|
|
524
|
+
report_negative_control_inventory(specs);
|
|
458
525
|
const hardening = report_hardening_ratchet();
|
|
459
526
|
const selfTest = report_self_test_ratchet();
|
|
460
527
|
if (failed.length > 0) {
|
|
@@ -771,6 +838,30 @@ export function parse_census(md: string): Map<string, number | null> {
|
|
|
771
838
|
if (gateCol < 0 || cs.length <= Math.max(gateCol, unitCol)) continue;
|
|
772
839
|
const idm = /`([A-Za-z0-9_.-]+)`/.exec(cs[gateCol] as string);
|
|
773
840
|
if (idm === null) continue;
|
|
841
|
+
// A row the census could not extract a root for is NOT a row that read
|
|
842
|
+
// nothing — the two are indistinguishable in the units column, which prints
|
|
843
|
+
// `**0**` for both, and conflating them produced a false `census_stale`
|
|
844
|
+
// disagreement on every dynamically-scoped gate. `check_secret_leak`
|
|
845
|
+
// resolves its corpus by spawning `git diff` / `npm pack`, and
|
|
846
|
+
// `check_no_roadmap_refs` likewise: a static root extractor finds nothing to
|
|
847
|
+
// count, so the canary correctly reds them while the census correctly says
|
|
848
|
+
// 0, and the comparison then calls a correct pair a defect.
|
|
849
|
+
//
|
|
850
|
+
// `null` already means "no measurable count" here and is already skipped by
|
|
851
|
+
// the disagreement check, so this maps the un-extractable case onto the
|
|
852
|
+
// meaning it always had.
|
|
853
|
+
if (/\(none extracted\)/.test(line)) {
|
|
854
|
+
// Left UNSET, not set to `null`. The disagreement check treats
|
|
855
|
+
// `undefined` as "not censused — nothing to disagree with" and `null` as
|
|
856
|
+
// "censused and read nothing", which is a stale row. An un-extractable
|
|
857
|
+
// root is the first, not the second: nothing was measured, so there is no
|
|
858
|
+
// measurement to disagree with the canary about.
|
|
859
|
+
//
|
|
860
|
+
// Setting `null` here was tried first and made it WORSE — it turned every
|
|
861
|
+
// dynamically-scoped gate into a fresh false `census_stale`, six of them
|
|
862
|
+
// at once. The distinction between the two absences is the whole fix.
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
774
865
|
const raw = cs[unitCol] as string;
|
|
775
866
|
const num = /(\d[\d,]*)/.exec(raw.replace(/\*/g, ''));
|
|
776
867
|
out.set(idm[1] as string, num === null ? null : Number((num[1] as string).replace(/,/g, '')));
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Content-equivalence gate for the self-hosted hook bundle.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS — measured, not hypothetical.
|
|
6
|
+
*
|
|
7
|
+
* `check_hook_bundle_freshness` compares **mtimes**: every bundled hook source
|
|
8
|
+
* must be older than `dist/hooks/dispatch.js`. That establishes ordering, not
|
|
9
|
+
* equivalence. An mtime-preserving edit, a `touch` on the bundle, a restore
|
|
10
|
+
* from an archive, or a filesystem with coarse timestamp resolution all make
|
|
11
|
+
* the inference false while the check stays green.
|
|
12
|
+
*
|
|
13
|
+
* On 2026-08-21 a live run edited `LEDGER_MAX_AGE_MS` in
|
|
14
|
+
* `block_unauthorized_git.ts`, did not rebuild, and the guard kept enforcing
|
|
15
|
+
* the old value; all three copies (source, repo bundle, global install) were
|
|
16
|
+
* inspected by hand before anyone noticed. **The freshness gate did not miss
|
|
17
|
+
* that one — it is pre-push and was never run.** Saying it "stayed green" would
|
|
18
|
+
* be a plausible mechanism standing in for the observed one, which is the
|
|
19
|
+
* failure this repository's own evidence discipline names.
|
|
20
|
+
*
|
|
21
|
+
* What the mtime check genuinely cannot see is narrower and is the reason this
|
|
22
|
+
* gate exists: the mtime check cannot see an edit that does not move the mtime — an
|
|
23
|
+
mtime-preserving write, a `touch` on the bundle, an archive or `cp -p` restore,
|
|
24
|
+
or a filesystem whose timestamp resolution is coarser than the edit interval.
|
|
25
|
+
Demonstrated on this tree rather than argued: an mtime-preserving edit to one
|
|
26
|
+
constant left the bundle at 1 139 302 bytes with an untouched mtime, the
|
|
27
|
+
freshness gate passed, and the digest differed.
|
|
28
|
+
*
|
|
29
|
+
* This gate answers the question directly — are the bytes that execute the
|
|
30
|
+
* bytes this source produces?
|
|
31
|
+
*
|
|
32
|
+
* WHAT IT CANNOT DO, STATED PLAINLY.
|
|
33
|
+
*
|
|
34
|
+
* `dist/hooks/` is **untracked** — `git ls-files dist/hooks/` returns zero.
|
|
35
|
+
* There is no committed artefact to compare against, so this gate can only
|
|
36
|
+
* compare the bundle **present on this machine** against a fresh rebuild from
|
|
37
|
+
* this checkout's sources. In CI a fresh checkout has no bundle at all, so
|
|
38
|
+
* this gate declares a loud no-op there and a green CI says nothing about the
|
|
39
|
+
* bundle a maintainer is actually running. That asymmetry is the finding, not
|
|
40
|
+
* a gap to paper over: the bundle is a local execution artefact and only a
|
|
41
|
+
* local gate can see it.
|
|
42
|
+
*
|
|
43
|
+
* The esbuild invocation is NOT duplicated here — it is read from
|
|
44
|
+
* `package.json`'s `build:hooks` script with only `--outfile=` rewritten, via
|
|
45
|
+
* the same `rewriteOutfile` helper `rebuild_hook_bundle` uses, so the flag set
|
|
46
|
+
* has exactly one home.
|
|
47
|
+
*
|
|
48
|
+
* DETERMINISM — measured, not assumed. Two consecutive rebuilds of an
|
|
49
|
+
* unchanged tree produced the identical sha256 (`616561fb…`, 2026-08-21), and
|
|
50
|
+
* a one-constant edit that left the output byte-count unchanged
|
|
51
|
+
* (1 139 302 bytes both times) still produced a different digest. So the
|
|
52
|
+
* comparison is byte-exact with no canonicalisation, and it is sensitive to
|
|
53
|
+
* the smallest semantic change. Had the rebuild NOT been reproducible, that
|
|
54
|
+
* would have been a release-integrity defect to fix rather than a reason to
|
|
55
|
+
* fall back to timestamps.
|
|
56
|
+
*
|
|
57
|
+
* Exit codes: 0 = equivalent, or no bundle to check · 1 = the executing bundle
|
|
58
|
+
* is not what this source produces · 2 = the rebuild itself failed.
|
|
59
|
+
*/
|
|
60
|
+
// ledger-exempt: single-artefact transaction — one rebuild compared against one
|
|
61
|
+
// bundle, yielding one aggregate verdict. The per-source accounting lives in
|
|
62
|
+
// check_hook_bundle_freshness, which carries the ledger.
|
|
63
|
+
import { spawnSync } from "node:child_process";
|
|
64
|
+
import * as crypto from "node:crypto";
|
|
65
|
+
import * as fs from "node:fs";
|
|
66
|
+
import * as path from "node:path";
|
|
67
|
+
import { fileURLToPath } from "node:url";
|
|
68
|
+
|
|
69
|
+
import { reportScanned } from "./_lib/scan_scope.js";
|
|
70
|
+
import { rewriteOutfile } from "./rebuild_hook_bundle.js";
|
|
71
|
+
|
|
72
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
73
|
+
const REPO_ROOT = path.resolve(HERE, "..", "..");
|
|
74
|
+
const LIVE_REL = path.join("dist", "hooks", "dispatch.js");
|
|
75
|
+
// Suffixed with the pid: two concurrent invocations in one checkout (the
|
|
76
|
+
// pre-push gate plus a manual run) otherwise share one path, the first
|
|
77
|
+
// `finally` deletes the other's output, and the survivor reports "rebuild
|
|
78
|
+
// failed" off a missing file — a false red on a pre-push gate, which is the
|
|
79
|
+
// failure mode the PATH injection below exists to avoid.
|
|
80
|
+
const PROBE_REL = path.join("dist", "hooks", `dispatch.content-check.${String(process.pid)}.js`);
|
|
81
|
+
|
|
82
|
+
/** sha256 of a file's bytes, hex. */
|
|
83
|
+
export function digestOf(file: string): string {
|
|
84
|
+
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read `build:hooks` out of package.json. Returns null when the script is
|
|
89
|
+
* absent — a renamed build script must fail loudly, never be guessed at.
|
|
90
|
+
*/
|
|
91
|
+
export function buildScript(repoRoot: string): string | null {
|
|
92
|
+
const pkgPath = path.join(repoRoot, "package.json");
|
|
93
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
94
|
+
const pkg: unknown = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
95
|
+
const scripts =
|
|
96
|
+
typeof pkg === "object" && pkg !== null ? (pkg as Record<string, unknown>)["scripts"] : null;
|
|
97
|
+
if (typeof scripts !== "object" || scripts === null) return null;
|
|
98
|
+
const s = (scripts as Record<string, unknown>)["build:hooks"];
|
|
99
|
+
return typeof s === "string" ? s : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function main(root: string = REPO_ROOT): number {
|
|
103
|
+
const live = path.join(root, LIVE_REL);
|
|
104
|
+
if (!fs.existsSync(live)) {
|
|
105
|
+
// Loud no-op, matching the sibling gates: a fresh checkout or CI has no
|
|
106
|
+
// bundle, and materialising one here would change what other gates see.
|
|
107
|
+
process.stdout.write(
|
|
108
|
+
`check_hook_bundle_content: no self-hosted ${LIVE_REL} — nothing executes locally, nothing to compare\n`,
|
|
109
|
+
);
|
|
110
|
+
reportScanned({
|
|
111
|
+
gate: "check_hook_bundle_content",
|
|
112
|
+
scanned: 0,
|
|
113
|
+
units: "bundle(s)",
|
|
114
|
+
roots: [LIVE_REL],
|
|
115
|
+
allowEmpty:
|
|
116
|
+
"OPTIONAL_INPUT: dist/hooks/ is UNTRACKED (`git ls-files dist/hooks/` returns zero), so a fresh " +
|
|
117
|
+
"checkout or a CI runner has no bundle by construction. An empty scope here is the " +
|
|
118
|
+
"correct reading of the environment, not a dead scan root — the artefact this gate " +
|
|
119
|
+
"guards is a local build output and exists only where the hooks actually run.",
|
|
120
|
+
});
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const script = buildScript(root);
|
|
125
|
+
if (script === null) {
|
|
126
|
+
process.stderr.write(
|
|
127
|
+
"check_hook_bundle_content: package.json has no `build:hooks` script — refusing to guess the flag set\n",
|
|
128
|
+
);
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
131
|
+
const rewritten = rewriteOutfile(script, PROBE_REL);
|
|
132
|
+
if (rewritten === null) {
|
|
133
|
+
process.stderr.write(
|
|
134
|
+
`check_hook_bundle_content: \`build:hooks\` no longer writes ${LIVE_REL} — refusing to build to a guessed path\n`,
|
|
135
|
+
);
|
|
136
|
+
return 2;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const probe = path.join(root, PROBE_REL);
|
|
140
|
+
try {
|
|
141
|
+
// PATH is injected exactly as `rebuild_hook_bundle` does it, and for the
|
|
142
|
+
// same reason: `build:hooks` starts with a bare `esbuild`, so inheriting an
|
|
143
|
+
// ambient PATH without `node_modules/.bin` makes this gate report "rebuild
|
|
144
|
+
// failed" on a bundle that is in fact correct — a false red on a pre-push
|
|
145
|
+
// gate, which is how a pre-push gate teaches people to skip it.
|
|
146
|
+
const built = spawnSync("sh", ["-c", rewritten], {
|
|
147
|
+
cwd: root,
|
|
148
|
+
encoding: "utf8",
|
|
149
|
+
env: {
|
|
150
|
+
...process.env,
|
|
151
|
+
PATH: `${path.join(root, "node_modules", ".bin")}:${process.env["PATH"] ?? ""}`,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
if (built.status !== 0 || !fs.existsSync(probe)) {
|
|
155
|
+
process.stderr.write(
|
|
156
|
+
`check_hook_bundle_content: rebuild failed (exit ${String(built.status)})\n${built.stderr ?? ""}\n`,
|
|
157
|
+
);
|
|
158
|
+
return 2;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const liveDigest = digestOf(live);
|
|
162
|
+
const probeDigest = digestOf(probe);
|
|
163
|
+
if (liveDigest === probeDigest) {
|
|
164
|
+
process.stdout.write(
|
|
165
|
+
`✅ ${LIVE_REL} is byte-identical to a rebuild from this source (sha256 ${liveDigest.slice(0, 12)})\n`,
|
|
166
|
+
);
|
|
167
|
+
reportScanned({
|
|
168
|
+
gate: "check_hook_bundle_content",
|
|
169
|
+
scanned: 1,
|
|
170
|
+
units: "bundle(s)",
|
|
171
|
+
roots: [LIVE_REL],
|
|
172
|
+
});
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
process.stderr.write(
|
|
177
|
+
`❌ ${LIVE_REL} is NOT what this source produces.\n` +
|
|
178
|
+
` executing: sha256 ${liveDigest.slice(0, 12)} (${String(fs.statSync(live).size)} bytes)\n` +
|
|
179
|
+
` rebuilt: sha256 ${probeDigest.slice(0, 12)} (${String(fs.statSync(probe).size)} bytes)\n` +
|
|
180
|
+
` The mtime check cannot see this — it compares ordering, not bytes.\n` +
|
|
181
|
+
` Heal with: ./scripts-run src/scripts/rebuild_hook_bundle\n`,
|
|
182
|
+
);
|
|
183
|
+
reportScanned({
|
|
184
|
+
gate: "check_hook_bundle_content",
|
|
185
|
+
scanned: 1,
|
|
186
|
+
units: "bundle(s)",
|
|
187
|
+
roots: [LIVE_REL],
|
|
188
|
+
});
|
|
189
|
+
return 1;
|
|
190
|
+
} finally {
|
|
191
|
+
try {
|
|
192
|
+
fs.rmSync(probe, { force: true });
|
|
193
|
+
} catch {
|
|
194
|
+
// Best effort — a leftover probe file is untracked build output.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Resolved on BOTH sides with `realpathSync`, mirroring `rebuild_hook_bundle`.
|
|
200
|
+
// A string compare against `import.meta.url` is false under a symlinked
|
|
201
|
+
// checkout, and under any repo path containing a space or a non-ASCII
|
|
202
|
+
// character, which `import.meta.url` percent-encodes and `path.resolve` does
|
|
203
|
+
// not. The failure is silent: `main()` never runs, the process exits 0, and
|
|
204
|
+
// the gate reports nothing — a green on an invariant it never evaluated,
|
|
205
|
+
// which is the exact class this gate exists to remove.
|
|
206
|
+
const invokedDirectly = ((): boolean => {
|
|
207
|
+
const entry = process.argv[1];
|
|
208
|
+
if (entry === undefined) return false;
|
|
209
|
+
try {
|
|
210
|
+
return fs.realpathSync(path.resolve(entry)) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
211
|
+
} catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
})();
|
|
215
|
+
|
|
216
|
+
if (invokedDirectly) {
|
|
217
|
+
process.exit(main());
|
|
218
|
+
}
|
|
219
|
+
|