@event4u/agent-config 9.27.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 +13 -12
- package/CHANGELOG.md +165 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/analyze/conformance.md +24 -0
- 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 +28 -3
- 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/artifact-engagement-recording.md +4 -1
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/brand-source-of-truth.md +33 -6
- package/dist/agent-src/rules/context-hygiene.md +3 -1
- package/dist/agent-src/rules/council-availability.md +93 -0
- package/dist/agent-src/rules/cross-source-consistency.md +3 -0
- package/dist/agent-src/rules/delegation-policy.md +38 -10
- package/dist/agent-src/rules/design-fidelity.md +3 -0
- package/dist/agent-src/rules/evaluator-independence.md +2 -1
- package/dist/agent-src/rules/external-code-graph-interop.md +2 -1
- package/dist/agent-src/rules/finance-safety-floor.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +1 -1
- package/dist/agent-src/rules/history-discipline.md +3 -1
- package/dist/agent-src/rules/legal-safety-floor.md +1 -1
- package/dist/agent-src/rules/no-pr-progress-comments.md +3 -1
- package/dist/agent-src/rules/onboarding-gate.md +3 -1
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +4 -1
- package/dist/agent-src/rules/role-mode-adherence.md +1 -1
- package/dist/agent-src/rules/scale-discipline.md +3 -1
- package/dist/agent-src/rules/self-repair-loop.md +103 -0
- package/dist/agent-src/rules/session-canary.md +2 -1
- package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
- package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
- package/dist/agent-src/rules/token-budget-discipline.md +4 -2
- package/dist/agent-src/rules/tool-safety.md +5 -1
- package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
- package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.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/performance-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
- package/dist/agent-src/skills/premortem/SKILL.md +30 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
- package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -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/skills/universal-project-analysis/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +8 -12
- 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 +7 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-66OVWGB5.js +270 -0
- package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-LCFJIQ4J.js} +89 -7
- 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-WR6GTRHJ.js → chunk-MP3JT3IU.js} +3 -3
- package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-NZ5N2XQL.js} +467 -305
- package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-OJM3MBJM.js} +1 -1
- package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-RCIA67XN.js} +117 -96
- package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +134 -0
- package/dist/cli-delegate/cmd_conformance.js +10 -47
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +10 -6
- package/dist/cli-delegate/cmd_explain.js +80 -330
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- package/dist/cli-delegate/cmd_mcp_available.js +433 -0
- package/dist/cli-delegate/cmd_migrate.js +3 -3
- package/dist/cli-delegate/cmd_packs_active.js +153 -0
- package/dist/cli-delegate/cmd_preflight.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_route_audit.js +8 -8
- package/dist/cli-delegate/cmd_route_explain.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +266 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +8 -2968
- 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 +4 -4
- package/dist/cli-delegate/cmd_upgrade.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/cli-delegate/cmd_versions.js +4 -4
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +164 -55
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +44 -5
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +16 -6
- package/dist/hooks/dispatch.js +9341 -6131
- package/dist/install/install.mjs +739 -524
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +777 -281
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +134 -15
- 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 +68 -17
- package/docs/guidelines/agent-infra/memory-access.md +14 -3
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +8 -12
- package/src/config/agent-settings.template.yml +52 -66
- package/src/config/budgets.yml +29 -0
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +26 -0
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/scripts/_cli/cmd_brand_status.ts +174 -0
- package/src/scripts/_cli/cmd_conformance.ts +14 -42
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
- package/src/scripts/_cli/cmd_packs_active.ts +225 -0
- package/src/scripts/_cli/cmd_settings_get.ts +324 -0
- package/src/scripts/_dispatch.bash +57 -0
- package/src/scripts/_lib/agent_settings.ts +140 -16
- 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/carrier_divergence.ts +135 -0
- package/src/scripts/_lib/host_capability.ts +144 -4
- 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/prompt_shape.ts +88 -0
- package/src/scripts/_lib/review_skipped_record.ts +102 -0
- package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
- package/src/scripts/_lib/self_repair.ts +489 -0
- package/src/scripts/_lib/self_repair_store.ts +128 -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/transcript_entry.ts +80 -0
- 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_branch_freshness.ts +255 -0
- package/src/scripts/check_council_config_location.ts +125 -1
- package/src/scripts/check_detector_corpus.ts +202 -0
- package/src/scripts/check_hook_bundle_freshness.ts +4 -1
- package/src/scripts/check_standing_rule_delivery.ts +314 -0
- package/src/scripts/condense.ts +162 -2
- package/src/scripts/config/profiles.ts +13 -3
- package/src/scripts/conformance_scan.ts +272 -24
- package/src/scripts/council_availability_hook.ts +188 -0
- package/src/scripts/council_cli.ts +527 -32
- package/src/scripts/forensics_report.ts +333 -0
- package/src/scripts/hook_manifest.yaml +107 -26
- package/src/scripts/hooks/block_no_verify.ts +39 -2
- package/src/scripts/hooks/block_unauthorized_git.ts +156 -7
- package/src/scripts/hooks/concern_registry.ts +10 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +484 -0
- package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
- package/src/scripts/install.ts +151 -0
- package/src/scripts/language_mirror_hook.ts +30 -37
- 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/measure_scope_dedup.ts +19 -13
- package/src/scripts/onboarding_gate_hook.ts +40 -1
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/preamble_byte_census.ts +9 -20
- package/src/scripts/rebuild_hook_bundle.ts +177 -0
- package/src/scripts/release.ts +74 -2
- package/src/scripts/report_carrier_divergence.ts +364 -0
- package/src/scripts/report_skill_activation.ts +257 -0
- package/src/scripts/report_skill_obligation_violations.ts +579 -0
- package/src/scripts/routing_doctor.ts +95 -39
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +470 -0
- package/src/scripts/self_repair_hook.ts +241 -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 +10 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- package/src/shared/settingsClasses.ts +11 -9
|
@@ -37,6 +37,14 @@
|
|
|
37
37
|
* (ADR-104); any project-tree path is drift. Corrective mentions ("is
|
|
38
38
|
* ignored", "never read", "no project-local", "superseded") pass on the
|
|
39
39
|
* same line. (PATH_CHECK_GLOBS)
|
|
40
|
+
* 4. NO always-loaded carrier for the user-global fact. §1–§3 keep the tree
|
|
41
|
+
* honest but say nothing about REACH: before this check the fact lived
|
|
42
|
+
* only in `src/skills/ai-council/SKILL.md`, a skill body that reaches
|
|
43
|
+
* context only on activation, so consumer sessions repeatedly inferred
|
|
44
|
+
* "council not configured" from a missing `.agent-settings.yml` while the
|
|
45
|
+
* council was configured all along. §4 requires one rule under
|
|
46
|
+
* `src/rules/` to carry the fact and to project at least as widely as the
|
|
47
|
+
* kernel reference. (RULES_GLOB)
|
|
40
48
|
*
|
|
41
49
|
* Escape hatch: a line carrying `<!-- council-config-allowed -->` is exempt
|
|
42
50
|
* (for a legitimate non-council `.agent-settings.yml` reference, e.g.
|
|
@@ -64,6 +72,7 @@ const SCAN_GLOBS = [
|
|
|
64
72
|
'src/domains/meta/council/**/*.md',
|
|
65
73
|
'src/domains/product-basic/roadmap/ai-council/**/*.md',
|
|
66
74
|
'src/skills/ai-council/**/*.md',
|
|
75
|
+
'src/rules/council-availability.md',
|
|
67
76
|
'docs/contracts/ai-council-config.md',
|
|
68
77
|
] as const;
|
|
69
78
|
|
|
@@ -105,6 +114,35 @@ const PROJECT_PATH_COUNCIL_RE =
|
|
|
105
114
|
const PATH_NEGATION_RE =
|
|
106
115
|
/\b(not|never|removed|no\s+longer|neither|instead|ignored|superseded|no\s+project-local|not\s+read|does\s+not\s+read)\b/i;
|
|
107
116
|
|
|
117
|
+
// ── §4 always-loaded carrier check ─────────────────────────────────
|
|
118
|
+
// The user-global fact is useless where the agent cannot see it. Before this
|
|
119
|
+
// check the fact lived only in `src/skills/ai-council/SKILL.md` — a skill body
|
|
120
|
+
// that only reaches context on activation — so consumer sessions repeatedly
|
|
121
|
+
// inferred "council not configured" from a missing `.agent-settings.yml` and
|
|
122
|
+
// substituted a subagent fan-out while the council was configured all along.
|
|
123
|
+
// §4 pins the fix: at least one rule under `src/rules/` must carry the fact,
|
|
124
|
+
// declare a council trigger, and project at least as widely as the kernel.
|
|
125
|
+
const RULES_GLOB = 'src/rules/**/*.md';
|
|
126
|
+
// Reference for the reach requirement: a carrier scoped more narrowly than an
|
|
127
|
+
// always-loaded kernel rule would not reach the consumer repos that failed.
|
|
128
|
+
const REACH_REFERENCE_RULE = 'src/rules/direct-answers.md';
|
|
129
|
+
const WORKSPACES_RE = /^workspaces:\s*\[([^\]]*)\]/m;
|
|
130
|
+
// The three markers a carrier must show, all in the same file.
|
|
131
|
+
const CARRIER_MARKERS = [
|
|
132
|
+
{
|
|
133
|
+
re: /^\s*-\s*keyword:\s*"council"\s*$/m,
|
|
134
|
+
what: 'a `- keyword: "council"` trigger',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
re: /~\/\.event4u\/agent-config\/settings\/\.ai-council\.yml/,
|
|
138
|
+
what: 'the one real user-global config path',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
re: /NEVER\s+INFER\s+"NOT\s+CONFIGURED"/i,
|
|
142
|
+
what: 'the never-infer-from-project-files obligation',
|
|
143
|
+
},
|
|
144
|
+
] as const;
|
|
145
|
+
|
|
108
146
|
/**
|
|
109
147
|
* Mirror Python `str.lstrip()` — strips all leading Unicode whitespace.
|
|
110
148
|
* Python's `str.lstrip()` (no args) strips the Unicode whitespace class;
|
|
@@ -318,6 +356,84 @@ function find_path_violations(root: string): string[] {
|
|
|
318
356
|
return findings;
|
|
319
357
|
}
|
|
320
358
|
|
|
359
|
+
/** Parse a rule's `workspaces: [a, b, c]` frontmatter line into a set. */
|
|
360
|
+
function _workspaces(text: string): Set<string> {
|
|
361
|
+
const m = WORKSPACES_RE.exec(text);
|
|
362
|
+
if (m === null) {
|
|
363
|
+
return new Set<string>();
|
|
364
|
+
}
|
|
365
|
+
return new Set(
|
|
366
|
+
m[1]!
|
|
367
|
+
.split(',')
|
|
368
|
+
.map((s) => s.trim().replace(/^["']|["']$/g, ''))
|
|
369
|
+
.filter((s) => s.length > 0),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* §4 — the user-global fact must have an always-loaded carrier (reach check).
|
|
375
|
+
*
|
|
376
|
+
* A correct resolver, a correct skill, and a green §1–§3 still left consumer
|
|
377
|
+
* sessions claiming "council not configured" — because the fact reached no
|
|
378
|
+
* always-loaded surface. This check fails when no rule under `src/rules/`
|
|
379
|
+
* carries it, and when every carrier that does is scoped more narrowly than
|
|
380
|
+
* the kernel reference (i.e. would not reach the consumer repos that failed).
|
|
381
|
+
*/
|
|
382
|
+
function find_carrier_violations(root: string): string[] {
|
|
383
|
+
// Scope guard: only a tree that HAS a rule layer can carry the fact. A
|
|
384
|
+
// consumer checkout or a tmp fixture without `src/rules/` is out of scope,
|
|
385
|
+
// exactly as a missing `src/skills/ai-council/` is for §1. The real-repo
|
|
386
|
+
// test asserts this scope is live here, so the skip cannot go unnoticed.
|
|
387
|
+
if (!_isDir(path.join(root, 'src', 'rules'))) {
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
390
|
+
const rules = [...iter_files(root, [RULES_GLOB])];
|
|
391
|
+
if (rules.length === 0) {
|
|
392
|
+
return [
|
|
393
|
+
`${RULES_GLOB}: \`src/rules/\` exists but resolved to no files — ` +
|
|
394
|
+
'the council user-global fact is unpinned.',
|
|
395
|
+
];
|
|
396
|
+
}
|
|
397
|
+
const carriers = rules.filter((p) => {
|
|
398
|
+
const text = fs.readFileSync(p, 'utf-8');
|
|
399
|
+
return CARRIER_MARKERS.every((m) => m.re.test(text));
|
|
400
|
+
});
|
|
401
|
+
if (carriers.length === 0) {
|
|
402
|
+
return [
|
|
403
|
+
'src/rules/: no always-loaded rule carries the council user-global ' +
|
|
404
|
+
'fact. One rule must show all of — ' +
|
|
405
|
+
CARRIER_MARKERS.map((m) => m.what).join('; ') +
|
|
406
|
+
'. Without it the fact lives only in ' +
|
|
407
|
+
'`src/skills/ai-council/SKILL.md`, whose body reaches context ' +
|
|
408
|
+
'only on skill activation.',
|
|
409
|
+
];
|
|
410
|
+
}
|
|
411
|
+
const refPath = path.join(root, REACH_REFERENCE_RULE);
|
|
412
|
+
if (!_isFile(refPath)) {
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
const refWs = _workspaces(fs.readFileSync(refPath, 'utf-8'));
|
|
416
|
+
if (refWs.size === 0) {
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
const findings: string[] = [];
|
|
420
|
+
for (const p of carriers) {
|
|
421
|
+
const missing = [...refWs].filter(
|
|
422
|
+
(w) => !_workspaces(fs.readFileSync(p, 'utf-8')).has(w),
|
|
423
|
+
);
|
|
424
|
+
if (missing.length > 0) {
|
|
425
|
+
findings.push(
|
|
426
|
+
`${_relPosix(root, p)}: council carrier is scoped more narrowly ` +
|
|
427
|
+
`than \`${REACH_REFERENCE_RULE}\` — missing workspace(s): ` +
|
|
428
|
+
`${missing.join(', ')}. A carrier the consumer never receives ` +
|
|
429
|
+
'is exactly the gap this check exists to close.',
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// One carrier with full reach is enough; only report when none has it.
|
|
434
|
+
return findings.length === carriers.length ? findings : [];
|
|
435
|
+
}
|
|
436
|
+
|
|
321
437
|
function main(): number {
|
|
322
438
|
const root = process.cwd();
|
|
323
439
|
// PATH_CHECK_GLOBS is the superset of both passes, so its resolution is the
|
|
@@ -338,7 +454,11 @@ function main(): number {
|
|
|
338
454
|
}
|
|
339
455
|
throw exc;
|
|
340
456
|
}
|
|
341
|
-
const findings = [
|
|
457
|
+
const findings = [
|
|
458
|
+
...find_violations(root),
|
|
459
|
+
...find_path_violations(root),
|
|
460
|
+
...find_carrier_violations(root),
|
|
461
|
+
];
|
|
342
462
|
if (findings.length) {
|
|
343
463
|
process.stdout.write('❌ Council config-location violations:\n\n');
|
|
344
464
|
for (const f of findings) {
|
|
@@ -396,5 +516,9 @@ export {
|
|
|
396
516
|
iter_files,
|
|
397
517
|
find_violations,
|
|
398
518
|
find_path_violations,
|
|
519
|
+
find_carrier_violations,
|
|
520
|
+
RULES_GLOB,
|
|
521
|
+
REACH_REFERENCE_RULE,
|
|
522
|
+
CARRIER_MARKERS,
|
|
399
523
|
main,
|
|
400
524
|
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Detector corpus gate (P4.3 of road-to-rule-delivery-integrity).
|
|
4
|
+
*
|
|
5
|
+
* Every self-repair detector must carry all THREE fixture classes, and every
|
|
6
|
+
* fixture must behave as its class says:
|
|
7
|
+
*
|
|
8
|
+
* fire a recorded real failure the detector must catch
|
|
9
|
+
* near-miss-fire a real failure phrased unlike the canonical one, which must
|
|
10
|
+
* still be caught — recall at the boundary
|
|
11
|
+
* must-not-fire text that superficially matches and must stay silent
|
|
12
|
+
*
|
|
13
|
+
* The three-class rule is the point of the gate rather than a stylistic one.
|
|
14
|
+
* Precision and recall fail in opposite directions and a single corpus hides
|
|
15
|
+
* that: a detector with `fire` cases only looks perfect while opening records on
|
|
16
|
+
* praise, which is exactly what `detectUserReport` did until 2026-08-09 (it
|
|
17
|
+
* fired on "das ist fine, du hast nichts falsch gemacht"). A missing class is
|
|
18
|
+
* therefore a gate failure, not a warning — the absent half is where the defects
|
|
19
|
+
* live.
|
|
20
|
+
*
|
|
21
|
+
* Registration is explicit: a detector added to `self_repair.ts` without a
|
|
22
|
+
* matching corpus entry fails here, which is the coupling that keeps the corpus
|
|
23
|
+
* from silently lagging the code.
|
|
24
|
+
*/
|
|
25
|
+
import * as fs from 'node:fs';
|
|
26
|
+
import * as path from 'node:path';
|
|
27
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
28
|
+
|
|
29
|
+
import * as YAML from 'yaml';
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
type DefectFinding,
|
|
33
|
+
type TurnSnapshot,
|
|
34
|
+
detectCouncilClaim,
|
|
35
|
+
detectLanguageMirror,
|
|
36
|
+
detectUserReport,
|
|
37
|
+
} from './_lib/self_repair.js';
|
|
38
|
+
import { assertScanned } from './_lib/scan_scope.js';
|
|
39
|
+
|
|
40
|
+
export const CORPUS_REL = path.join('tests', 'fixtures', 'self-repair-detector-corpus.yml');
|
|
41
|
+
|
|
42
|
+
const GATE = 'check_detector_corpus';
|
|
43
|
+
const CLASSES = ['fire', 'near-miss-fire', 'must-not-fire'] as const;
|
|
44
|
+
type FixtureClass = (typeof CLASSES)[number];
|
|
45
|
+
|
|
46
|
+
/** One corpus entry, in the union of shapes the three detectors need. */
|
|
47
|
+
export interface Fixture {
|
|
48
|
+
text?: string;
|
|
49
|
+
reply?: string;
|
|
50
|
+
pinned_language?: 'de' | 'en';
|
|
51
|
+
tool_commands?: string[];
|
|
52
|
+
note?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type Corpus = Record<string, Partial<Record<FixtureClass, Fixture[]>>>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Every detector that must appear in the corpus, with how to run it.
|
|
59
|
+
*
|
|
60
|
+
* Keyed by the `defect_class` the detector produces, so the corpus reads as a
|
|
61
|
+
* list of defect classes rather than of function names.
|
|
62
|
+
*/
|
|
63
|
+
export const DETECTORS: Record<string, (f: Fixture) => DefectFinding | null> = {
|
|
64
|
+
'user-reported': (f) => detectUserReport(f.text ?? f.reply ?? ''),
|
|
65
|
+
'council-availability-claim': (f) => detectCouncilClaim(asTurn(f)),
|
|
66
|
+
'language-mirror': (f) => detectLanguageMirror(asTurn(f)),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function asTurn(f: Fixture): TurnSnapshot {
|
|
70
|
+
return {
|
|
71
|
+
prompt: f.text ?? '',
|
|
72
|
+
reply: f.reply ?? '',
|
|
73
|
+
toolCommands: f.tool_commands ?? [],
|
|
74
|
+
pinnedLanguage: f.pinned_language ?? null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface Finding {
|
|
79
|
+
detector: string;
|
|
80
|
+
kind: 'missing-class' | 'empty-class' | 'wrong-outcome' | 'unregistered';
|
|
81
|
+
message: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Pure: audit a parsed corpus against the registered detectors. */
|
|
85
|
+
export function auditCorpus(corpus: Corpus): { findings: Finding[]; fixtures: number } {
|
|
86
|
+
const findings: Finding[] = [];
|
|
87
|
+
let fixtures = 0;
|
|
88
|
+
|
|
89
|
+
for (const name of Object.keys(corpus)) {
|
|
90
|
+
if (!(name in DETECTORS)) {
|
|
91
|
+
findings.push({
|
|
92
|
+
detector: name,
|
|
93
|
+
kind: 'unregistered',
|
|
94
|
+
message: `corpus names "${name}", which is not a registered detector`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const [name, run] of Object.entries(DETECTORS)) {
|
|
100
|
+
const entry = corpus[name];
|
|
101
|
+
if (entry === undefined) {
|
|
102
|
+
findings.push({
|
|
103
|
+
detector: name,
|
|
104
|
+
kind: 'missing-class',
|
|
105
|
+
message: `no corpus entry at all — needs all of: ${CLASSES.join(', ')}`,
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
for (const cls of CLASSES) {
|
|
110
|
+
const cases = entry[cls];
|
|
111
|
+
if (cases === undefined) {
|
|
112
|
+
findings.push({
|
|
113
|
+
detector: name,
|
|
114
|
+
kind: 'missing-class',
|
|
115
|
+
message: `missing the \`${cls}\` class`,
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (cases.length === 0) {
|
|
120
|
+
// An empty class is the same hole as a missing one wearing a key.
|
|
121
|
+
findings.push({
|
|
122
|
+
detector: name,
|
|
123
|
+
kind: 'empty-class',
|
|
124
|
+
message: `\`${cls}\` is present but empty`,
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
for (const f of cases) {
|
|
129
|
+
fixtures += 1;
|
|
130
|
+
const fired = run(f) !== null;
|
|
131
|
+
const shouldFire = cls !== 'must-not-fire';
|
|
132
|
+
if (fired !== shouldFire) {
|
|
133
|
+
const what = f.text ?? f.reply ?? '(empty)';
|
|
134
|
+
findings.push({
|
|
135
|
+
detector: name,
|
|
136
|
+
kind: 'wrong-outcome',
|
|
137
|
+
message:
|
|
138
|
+
`\`${cls}\` fixture ${shouldFire ? 'did NOT fire' : 'FIRED'}: ` +
|
|
139
|
+
JSON.stringify(what.slice(0, 90)),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { findings, fixtures };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function loadCorpus(repoRoot: string): Corpus {
|
|
149
|
+
const p = path.join(repoRoot, CORPUS_REL);
|
|
150
|
+
const parsed = YAML.parse(fs.readFileSync(p, 'utf-8')) as { detectors?: Corpus };
|
|
151
|
+
return parsed.detectors ?? {};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function main(repoRoot: string = process.cwd()): number {
|
|
155
|
+
const corpus = loadCorpus(repoRoot);
|
|
156
|
+
const { findings, fixtures } = auditCorpus(corpus);
|
|
157
|
+
|
|
158
|
+
// A corpus that parsed to nothing is the dead-scope case, not a pass.
|
|
159
|
+
assertScanned({
|
|
160
|
+
gate: GATE,
|
|
161
|
+
scanned: fixtures,
|
|
162
|
+
units: 'detector fixtures',
|
|
163
|
+
roots: [CORPUS_REL],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (findings.length > 0) {
|
|
167
|
+
process.stderr.write(`${GATE}: ${String(findings.length)} finding(s)\n\n`);
|
|
168
|
+
for (const f of findings) {
|
|
169
|
+
process.stderr.write(` ${f.detector}: ${f.message}\n`);
|
|
170
|
+
}
|
|
171
|
+
process.stderr.write(
|
|
172
|
+
`\nEvery detector needs all three classes (${CLASSES.join(', ')}) and every\n` +
|
|
173
|
+
'fixture must behave as its class says. Precision and recall fail in\n' +
|
|
174
|
+
'opposite directions; a corpus missing one half hides the other.\n',
|
|
175
|
+
);
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
process.stdout.write(
|
|
180
|
+
`✅ ${GATE}: ${String(Object.keys(DETECTORS).length)} detector(s) × ` +
|
|
181
|
+
`${String(CLASSES.length)} classes, ${String(fixtures)} fixtures, all behaving\n`,
|
|
182
|
+
);
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isCliEntry(): boolean {
|
|
187
|
+
const argv1 = process.argv[1];
|
|
188
|
+
if (argv1 === undefined) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
return (
|
|
193
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(argv1))
|
|
194
|
+
);
|
|
195
|
+
} catch {
|
|
196
|
+
return pathToFileURL(argv1).href === import.meta.url;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (isCliEntry()) {
|
|
201
|
+
process.exit(main());
|
|
202
|
+
}
|
|
@@ -31,7 +31,10 @@
|
|
|
31
31
|
* WHAT IT DOES NOT DO: it never builds. Building into a live hook path is a
|
|
32
32
|
* documented hazard in this repo (an esbuild run that overwrites the
|
|
33
33
|
* dispatcher a running hook is executing can wedge the tool loop), so the
|
|
34
|
-
* remedy is printed for a human to run deliberately.
|
|
34
|
+
* remedy is printed for a human to run deliberately. The SAFE rebuild
|
|
35
|
+
* (build to .new → probe → atomic rename) lives in `rebuild_hook_bundle.ts`,
|
|
36
|
+
* which preflight runs right before this gate — so a red here means the heal
|
|
37
|
+
* itself failed or was bypassed, not merely that a merge landed.
|
|
35
38
|
*
|
|
36
39
|
* Exit codes: 0 = fresh, or no self-hosted bundle to check · 1 = stale.
|
|
37
40
|
*/
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Gate — how many rule tokens does a session actually RECEIVE?
|
|
4
|
+
*
|
|
5
|
+
* P1.2 of `road-to-rule-delivery-integrity`. Claude Code loads
|
|
6
|
+
* `~/.claude/rules/` and `<project>/.claude/rules/` **both**, user layer first,
|
|
7
|
+
* with no dedup between them (host contract, quoted with citations in
|
|
8
|
+
* `agents/evidence/analysis/claude-code-rules-dir-contract.md`). So the number
|
|
9
|
+
* that matters is not what one tree contains — it is the sum of what arrives.
|
|
10
|
+
*
|
|
11
|
+
* Measured 2026-08-08 on a maintainer machine, exact BPE: **176,354 tokens
|
|
12
|
+
* standing, 74,137 of them (42%) the same 91 rules twice.** Full six-topology
|
|
13
|
+
* accounting: `agents/evidence/analysis/standing-rule-delivery-topologies.md`.
|
|
14
|
+
*
|
|
15
|
+
* Three deliberate properties:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Measure what arrived, not what was projected.** Where an
|
|
18
|
+
* `InstructionsLoaded` record exists (the host logs which instruction files
|
|
19
|
+
* loaded, when, and why) the gate reads it and the filesystem sum becomes a
|
|
20
|
+
* cross-check. Where it does not, the filesystem sum stands alone. The gate
|
|
21
|
+
* always NAMES which input it used — a filesystem-only sum cannot see a rule
|
|
22
|
+
* that is projected but never loaded, nor one that loads from a layer nobody
|
|
23
|
+
* projected, and the second of those is the defect itself.
|
|
24
|
+
* 2. **`assertScanned` posture.** Zero rule files is exit 3, not green. This
|
|
25
|
+
* repo has shipped gates that scanned an emptied tree and certified coverage
|
|
26
|
+
* that did not exist; the posture is the recorded fix, not boilerplate.
|
|
27
|
+
* 3. **The ceiling cannot be raised silently.** `cap_raise_reason` in
|
|
28
|
+
* `src/config/budgets.yml` must be a real sentence naming which rule added
|
|
29
|
+
* the tokens. A placeholder fails the gate.
|
|
30
|
+
*
|
|
31
|
+
* Exit codes: `0` under cap · `1` over cap or an unstated cap raise · `2` usage
|
|
32
|
+
* error · `3` dead scan scope.
|
|
33
|
+
*
|
|
34
|
+
* ## This is deliberately NOT a CI gate
|
|
35
|
+
*
|
|
36
|
+
* Both inputs are machine-local by construction. `.claude/rules/` is gitignored
|
|
37
|
+
* (`.gitignore:123` — it is generated by `task sync` / `task generate-tools`),
|
|
38
|
+
* and no CI workflow performs a user-scope install, so on a fresh runner
|
|
39
|
+
* **neither** layer exists. Wired into `ci` it would either red on a dead scope
|
|
40
|
+
* or, with the empty case allowed, sit permanently green while certifying a
|
|
41
|
+
* property it cannot observe — the exact failure `src/config/gate-coverage.yml`
|
|
42
|
+
* was written to end. It is therefore registered as a local / doctor-surface
|
|
43
|
+
* check and is absent from `gate-coverage.yml`, whose contract is CI-identical
|
|
44
|
+
* invocation.
|
|
45
|
+
*
|
|
46
|
+
* What that costs, stated plainly: the duplicate-delivery defect is not
|
|
47
|
+
* mechanically caught for anyone who never runs this locally. The enforced layer
|
|
48
|
+
* is the installer gate (`install.ts::_gate_rule_layer_overlap`), which refuses
|
|
49
|
+
* to CREATE the doubled state; this script measures a state that already exists.
|
|
50
|
+
* Making the measurement CI-observable needs the `InstructionsLoaded` record
|
|
51
|
+
* committed from a real session, which is a different piece of work.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import * as fs from 'node:fs';
|
|
55
|
+
import * as os from 'node:os';
|
|
56
|
+
import * as path from 'node:path';
|
|
57
|
+
import process from 'node:process';
|
|
58
|
+
import { fileURLToPath } from 'node:url';
|
|
59
|
+
|
|
60
|
+
import { assertScanned, DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
61
|
+
import { measure, method_note } from './_lib/token_count.js';
|
|
62
|
+
import * as rule_layer_overlap from './_lib/rule_layer_overlap.js';
|
|
63
|
+
|
|
64
|
+
const PROG = 'check_standing_rule_delivery';
|
|
65
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
66
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
67
|
+
const BUDGETS = path.join(REPO_ROOT, 'src', 'config', 'budgets.yml');
|
|
68
|
+
|
|
69
|
+
/** Placeholder reasons that do not count as a stated cause for a cap raise. */
|
|
70
|
+
const PLACEHOLDER_REASONS: readonly string[] = ['', '-', 'tbd', 'todo', 'n/a', 'na', 'none', 'bump'];
|
|
71
|
+
|
|
72
|
+
interface Budget {
|
|
73
|
+
readonly total_cap_tokens: number;
|
|
74
|
+
readonly warn_threshold: number;
|
|
75
|
+
readonly cap_raise_reason: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read the `standing_rule_delivery` block from `budgets.yml`.
|
|
80
|
+
*
|
|
81
|
+
* Deliberately a narrow line scan rather than a YAML dependency: this gate must
|
|
82
|
+
* run in a consumer tree where `yaml` may not be installed, and the three keys
|
|
83
|
+
* it needs are flat scalars. An absent block is a configuration error (exit 2),
|
|
84
|
+
* never a permissive default — a budget gate that invents its own ceiling is
|
|
85
|
+
* the same class of failure as one that scans nothing.
|
|
86
|
+
*/
|
|
87
|
+
export function readBudget(text: string): Budget | null {
|
|
88
|
+
const lines = text.split('\n');
|
|
89
|
+
const start = lines.findIndex((l) => l.trimEnd() === 'standing_rule_delivery:');
|
|
90
|
+
if (start === -1) return null;
|
|
91
|
+
let cap: number | null = null;
|
|
92
|
+
let warn: number | null = null;
|
|
93
|
+
let reason: string | null = null;
|
|
94
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
95
|
+
const line = lines[i] ?? '';
|
|
96
|
+
if (/^\S/.test(line)) break; // next top-level key ends the block
|
|
97
|
+
const m = /^\s+([a-z_]+):\s*(.*)$/.exec(line);
|
|
98
|
+
if (m === null) continue;
|
|
99
|
+
const [, key, rawValue] = m;
|
|
100
|
+
const value = (rawValue ?? '').trim().replace(/^['"]|['"]$/g, '');
|
|
101
|
+
if (key === 'total_cap_tokens') cap = Number.parseInt(value, 10);
|
|
102
|
+
if (key === 'warn_threshold') warn = Number.parseFloat(value);
|
|
103
|
+
if (key === 'cap_raise_reason') reason = value;
|
|
104
|
+
}
|
|
105
|
+
if (cap === null || Number.isNaN(cap) || warn === null || Number.isNaN(warn) || reason === null) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return { total_cap_tokens: cap, warn_threshold: warn, cap_raise_reason: reason };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Is `reason` a real stated cause, or a placeholder? */
|
|
112
|
+
export function reasonIsStated(reason: string): boolean {
|
|
113
|
+
return !PLACEHOLDER_REASONS.includes(reason.trim().toLowerCase());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** One measured layer. */
|
|
117
|
+
interface LayerMeasure {
|
|
118
|
+
readonly label: string;
|
|
119
|
+
readonly dir: string;
|
|
120
|
+
readonly files: number;
|
|
121
|
+
readonly tokens: number;
|
|
122
|
+
readonly exact: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function measureLayer(label: string, dir: string): LayerMeasure | null {
|
|
126
|
+
const layer = rule_layer_overlap.readRuleLayer(dir);
|
|
127
|
+
if (layer === null) return null;
|
|
128
|
+
const joined = [...layer.files.values()].join('\n');
|
|
129
|
+
const m = measure(joined);
|
|
130
|
+
return {
|
|
131
|
+
label,
|
|
132
|
+
dir,
|
|
133
|
+
files: layer.files.size,
|
|
134
|
+
tokens: m.tokens_gpt,
|
|
135
|
+
exact: m.tokens_gpt_exact,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Where the loaded-instruction record would live if the host wrote one.
|
|
141
|
+
*
|
|
142
|
+
* Returning a path that does not exist is the expected case today: the
|
|
143
|
+
* `InstructionsLoaded` hook is a host feature this suite does not yet bind, so
|
|
144
|
+
* the gate reports `filesystem` as its input and says so. The probe exists now
|
|
145
|
+
* so that the day the hook is bound, the gate reads the better input without a
|
|
146
|
+
* second change — and so that "which input did this number come from" is
|
|
147
|
+
* answerable rather than assumed.
|
|
148
|
+
*/
|
|
149
|
+
export function instructionsLoadedRecord(repo_root: string): string {
|
|
150
|
+
return path.join(repo_root, 'agents', 'runtime', 'metrics', 'instructions-loaded.jsonl');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
154
|
+
const unknown = argv.filter((a) => a !== '--quiet');
|
|
155
|
+
if (unknown.length > 0) {
|
|
156
|
+
process.stderr.write(`usage: ${PROG} [--quiet]\n`);
|
|
157
|
+
process.stderr.write(`${PROG}: error: unrecognized arguments: ${unknown.join(' ')}\n`);
|
|
158
|
+
return 2;
|
|
159
|
+
}
|
|
160
|
+
const quiet = argv.includes('--quiet');
|
|
161
|
+
const out = (s: string): void => {
|
|
162
|
+
if (!quiet) process.stdout.write(s);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
let budget: Budget | null;
|
|
166
|
+
try {
|
|
167
|
+
budget = readBudget(fs.readFileSync(BUDGETS, 'utf-8'));
|
|
168
|
+
} catch {
|
|
169
|
+
process.stderr.write(`❌ ${PROG}: cannot read ${path.relative(REPO_ROOT, BUDGETS)}\n`);
|
|
170
|
+
return 2;
|
|
171
|
+
}
|
|
172
|
+
if (budget === null) {
|
|
173
|
+
process.stderr.write(
|
|
174
|
+
`❌ ${PROG}: no complete \`standing_rule_delivery\` block in `
|
|
175
|
+
+ `${path.relative(REPO_ROOT, BUDGETS)} — needs total_cap_tokens, `
|
|
176
|
+
+ 'warn_threshold and cap_raise_reason. A budget gate does not invent its own ceiling.\n',
|
|
177
|
+
);
|
|
178
|
+
return 2;
|
|
179
|
+
}
|
|
180
|
+
if (!reasonIsStated(budget.cap_raise_reason)) {
|
|
181
|
+
process.stderr.write(
|
|
182
|
+
`❌ ${PROG}: cap_raise_reason is a placeholder (${JSON.stringify(budget.cap_raise_reason)}). `
|
|
183
|
+
+ 'Every ceiling value needs a stated cause naming which rule added the tokens and why.\n',
|
|
184
|
+
);
|
|
185
|
+
return 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const layers = [
|
|
189
|
+
measureLayer('global', path.join(os.homedir(), '.claude', 'rules')),
|
|
190
|
+
measureLayer('project', path.join(REPO_ROOT, '.claude', 'rules')),
|
|
191
|
+
].filter((l): l is LayerMeasure => l !== null);
|
|
192
|
+
|
|
193
|
+
const scanned = layers.reduce((n, l) => n + l.files, 0);
|
|
194
|
+
// Two zeroes that mean opposite things, and conflating them is how this gate
|
|
195
|
+
// would become the thing it guards against:
|
|
196
|
+
//
|
|
197
|
+
// - **No layer directory at all** — a machine that has not installed the
|
|
198
|
+
// suite. Zero rule tokens are delivered, which IS the success state, so
|
|
199
|
+
// `allowEmpty` is honest here and passes scan_scope's own operational test
|
|
200
|
+
// ("if the scan root were deleted, would the reason still make sense?" —
|
|
201
|
+
// yes: nothing installed, nothing loaded).
|
|
202
|
+
// - **A layer directory that exists and holds no `.md`** — a projection that
|
|
203
|
+
// ran and produced nothing. That is blindness, not cleanliness, and it
|
|
204
|
+
// stays exit 3.
|
|
205
|
+
const no_layer_present = layers.length === 0;
|
|
206
|
+
try {
|
|
207
|
+
assertScanned({
|
|
208
|
+
gate: PROG,
|
|
209
|
+
scanned,
|
|
210
|
+
units: 'rule file(s) across the layers a session loads',
|
|
211
|
+
roots: ['~/.claude/rules', '.claude/rules'],
|
|
212
|
+
...(no_layer_present
|
|
213
|
+
? {
|
|
214
|
+
allowEmpty:
|
|
215
|
+
'EMPTY_VALID: neither ~/.claude/rules nor .claude/rules exists on this '
|
|
216
|
+
+ 'machine, so zero rule tokens are delivered — the success state, not a dead scope.',
|
|
217
|
+
}
|
|
218
|
+
: {}),
|
|
219
|
+
});
|
|
220
|
+
} catch (exc) {
|
|
221
|
+
if (exc instanceof DeadScopeError) {
|
|
222
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
223
|
+
return 3;
|
|
224
|
+
}
|
|
225
|
+
throw exc;
|
|
226
|
+
}
|
|
227
|
+
if (no_layer_present) {
|
|
228
|
+
out(
|
|
229
|
+
`${PROG}: no rule layer on this machine (neither ~/.claude/rules nor .claude/rules) — `
|
|
230
|
+
+ 'nothing is delivered, nothing to cap.\n',
|
|
231
|
+
);
|
|
232
|
+
reportScanned({
|
|
233
|
+
gate: PROG,
|
|
234
|
+
scanned: 0,
|
|
235
|
+
units: 'rule file(s)',
|
|
236
|
+
roots: ['~/.claude/rules', '.claude/rules'],
|
|
237
|
+
allowEmpty: 'EMPTY_VALID: no layer installed on this machine.',
|
|
238
|
+
});
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const record = instructionsLoadedRecord(REPO_ROOT);
|
|
243
|
+
const input = fs.existsSync(record) ? 'instructions-loaded' : 'filesystem';
|
|
244
|
+
const total = layers.reduce((n, l) => n + l.tokens, 0);
|
|
245
|
+
const exact = layers.every((l) => l.exact);
|
|
246
|
+
|
|
247
|
+
out(`${PROG} · input: ${input} · ${method_note()}\n`);
|
|
248
|
+
if (input === 'filesystem') {
|
|
249
|
+
out(
|
|
250
|
+
' note: no InstructionsLoaded record present, so this is what the filesystem\n'
|
|
251
|
+
+ ' projects, not what the host confirmed loading. A rule projected but never\n'
|
|
252
|
+
+ ' loaded, or loaded from an unprojected layer, is invisible to this input.\n',
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
for (const l of layers) {
|
|
256
|
+
out(` ${l.label.padEnd(8)} ${String(l.files).padStart(4)} file(s) ${String(l.tokens).padStart(8)} tok\n`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (layers.length === 2) {
|
|
260
|
+
const g = rule_layer_overlap.readRuleLayer(layers[0]!.dir);
|
|
261
|
+
const p = rule_layer_overlap.readRuleLayer(layers[1]!.dir);
|
|
262
|
+
if (g !== null && p !== null) {
|
|
263
|
+
const r = rule_layer_overlap.compareLayers(g.files, p.files);
|
|
264
|
+
if (r.overlap.length > 0) {
|
|
265
|
+
out(
|
|
266
|
+
` overlap ${String(r.overlap.length).padStart(4)} rule(s) in both layers `
|
|
267
|
+
+ `(${r.duplicate.length} duplicate, ${r.divergent.length} divergent)\n`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const pct = total / budget.total_cap_tokens;
|
|
274
|
+
out(
|
|
275
|
+
` TOTAL ${String(total).padStart(13)} tok / ${budget.total_cap_tokens} cap `
|
|
276
|
+
+ `(${(pct * 100).toFixed(1)}%)${exact ? '' : ' — proxy, not exact'}\n`,
|
|
277
|
+
);
|
|
278
|
+
reportScanned({
|
|
279
|
+
gate: PROG,
|
|
280
|
+
scanned,
|
|
281
|
+
units: 'rule file(s)',
|
|
282
|
+
roots: ['~/.claude/rules', '.claude/rules'],
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (pct > 1) {
|
|
286
|
+
process.stderr.write(
|
|
287
|
+
`❌ ${PROG}: ${total} tok of standing rule prose exceeds the ${budget.total_cap_tokens} cap. `
|
|
288
|
+
+ 'If two layers overlap, `agent-config install --layer=<global|project>` suppresses one '
|
|
289
|
+
+ 'without deleting a file; otherwise scope rules with `paths:` or move bodies into the digest.\n',
|
|
290
|
+
);
|
|
291
|
+
return 1;
|
|
292
|
+
}
|
|
293
|
+
if (pct >= budget.warn_threshold) {
|
|
294
|
+
process.stderr.write(
|
|
295
|
+
`⚠️ ${PROG}: ${(pct * 100).toFixed(1)}% of the standing rule cap used `
|
|
296
|
+
+ `(warn at ${(budget.warn_threshold * 100).toFixed(0)}%).\n`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
out(`✅ ${PROG}: standing rule delivery within cap\n`);
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function _isCliEntry(): boolean {
|
|
304
|
+
try {
|
|
305
|
+
if (process.argv[1] === undefined) return false;
|
|
306
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
307
|
+
} catch {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (_isCliEntry()) {
|
|
313
|
+
process.exit(main());
|
|
314
|
+
}
|