@event4u/agent-config 9.26.0 → 9.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +12 -11
  3. package/CHANGELOG.md +134 -207
  4. package/README.md +1 -1
  5. package/dist/agent-src/commands/analyze/conformance.md +43 -0
  6. package/dist/agent-src/commands/roadmap/next.md +65 -3
  7. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +10 -3
  8. package/dist/agent-src/rules/active-remediation.md +2 -0
  9. package/dist/agent-src/rules/analysis-skill-routing.md +2 -0
  10. package/dist/agent-src/rules/architecture.md +2 -0
  11. package/dist/agent-src/rules/artifact-drafting-protocol.md +2 -0
  12. package/dist/agent-src/rules/artifact-engagement-recording.md +6 -1
  13. package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
  14. package/dist/agent-src/rules/autonomous-execution.md +3 -1
  15. package/dist/agent-src/rules/brand-consistency.md +2 -0
  16. package/dist/agent-src/rules/brand-source-of-truth.md +35 -6
  17. package/dist/agent-src/rules/broken-access-control.md +2 -0
  18. package/dist/agent-src/rules/cli-output-handling.md +2 -0
  19. package/dist/agent-src/rules/code-comment-discipline.md +2 -0
  20. package/dist/agent-src/rules/code-provenance.md +2 -0
  21. package/dist/agent-src/rules/command-suggestion-policy.md +2 -0
  22. package/dist/agent-src/rules/commit-conventions.md +2 -0
  23. package/dist/agent-src/rules/communication-through-line.md +2 -0
  24. package/dist/agent-src/rules/content-quoting-floor.md +2 -0
  25. package/dist/agent-src/rules/context-hygiene.md +13 -1
  26. package/dist/agent-src/rules/copilot-routing.md +2 -0
  27. package/dist/agent-src/rules/council-availability.md +93 -0
  28. package/dist/agent-src/rules/cross-source-consistency.md +5 -0
  29. package/dist/agent-src/rules/decision-revisit-gate.md +2 -0
  30. package/dist/agent-src/rules/delegation-policy.md +31 -5
  31. package/dist/agent-src/rules/design-fidelity.md +5 -0
  32. package/dist/agent-src/rules/design-review-after-ui-write.md +4 -0
  33. package/dist/agent-src/rules/devcontainer-routing.md +2 -0
  34. package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
  35. package/dist/agent-src/rules/docker-commands.md +2 -0
  36. package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
  37. package/dist/agent-src/rules/domain-safety-disclaimer.md +2 -0
  38. package/dist/agent-src/rules/domain-safety-pii.md +2 -0
  39. package/dist/agent-src/rules/domain-safety-retention.md +2 -0
  40. package/dist/agent-src/rules/downstream-changes.md +2 -0
  41. package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
  42. package/dist/agent-src/rules/evaluator-independence.md +14 -3
  43. package/dist/agent-src/rules/external-code-graph-interop.md +4 -1
  44. package/dist/agent-src/rules/external-reference-deep-dive.md +2 -0
  45. package/dist/agent-src/rules/fast-path-marker-visibility.md +2 -0
  46. package/dist/agent-src/rules/finance-safety-floor.md +3 -1
  47. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
  48. package/dist/agent-src/rules/git-history-discipline.md +3 -1
  49. package/dist/agent-src/rules/guidelines.md +2 -0
  50. package/dist/agent-src/rules/history-discipline.md +5 -1
  51. package/dist/agent-src/rules/icon-consistency.md +2 -0
  52. package/dist/agent-src/rules/image-likeness-and-rights.md +2 -0
  53. package/dist/agent-src/rules/improve-before-implement.md +2 -0
  54. package/dist/agent-src/rules/invite-challenge.md +2 -0
  55. package/dist/agent-src/rules/laravel-routing.md +2 -0
  56. package/dist/agent-src/rules/laravel-translations.md +2 -0
  57. package/dist/agent-src/rules/legal-safety-floor.md +3 -1
  58. package/dist/agent-src/rules/lethal-trifecta-guard.md +2 -0
  59. package/dist/agent-src/rules/linked-projects-onboarding-gate.md +2 -0
  60. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +2 -0
  61. package/dist/agent-src/rules/markdown-safe-codeblocks.md +2 -0
  62. package/dist/agent-src/rules/media-governance-routing.md +2 -0
  63. package/dist/agent-src/rules/media-sync-ground-truth.md +2 -0
  64. package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
  65. package/dist/agent-src/rules/missing-tool-handling.md +2 -0
  66. package/dist/agent-src/rules/model-recommendation.md +2 -0
  67. package/dist/agent-src/rules/no-attribution-footers.md +2 -0
  68. package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +2 -0
  69. package/dist/agent-src/rules/no-pr-progress-comments.md +5 -1
  70. package/dist/agent-src/rules/no-roadmap-references.md +2 -0
  71. package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
  72. package/dist/agent-src/rules/onboarding-gate.md +5 -1
  73. package/dist/agent-src/rules/output-discipline.md +2 -0
  74. package/dist/agent-src/rules/package-ci-checks.md +2 -0
  75. package/dist/agent-src/rules/persona-governance.md +2 -0
  76. package/dist/agent-src/rules/php-coding.md +2 -0
  77. package/dist/agent-src/rules/prefer-enums-over-literals.md +2 -0
  78. package/dist/agent-src/rules/preservation-guard.md +2 -0
  79. package/dist/agent-src/rules/provider-lifecycle-discipline.md +2 -0
  80. package/dist/agent-src/rules/question-not-instruction.md +2 -0
  81. package/dist/agent-src/rules/reviewer-awareness.md +2 -0
  82. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +6 -1
  83. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  84. package/dist/agent-src/rules/role-mode-adherence.md +3 -1
  85. package/dist/agent-src/rules/rule-type-governance.md +2 -0
  86. package/dist/agent-src/rules/runtime-safety.md +2 -0
  87. package/dist/agent-src/rules/scale-discipline.md +5 -1
  88. package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
  89. package/dist/agent-src/rules/security-sensitive-stop.md +2 -0
  90. package/dist/agent-src/rules/self-repair-loop.md +103 -0
  91. package/dist/agent-src/rules/senior-engineering-discipline.md +2 -0
  92. package/dist/agent-src/rules/session-canary.md +62 -19
  93. package/dist/agent-src/rules/settings-ask-protocol.md +4 -0
  94. package/dist/agent-src/rules/size-enforcement.md +2 -0
  95. package/dist/agent-src/rules/skill-improvement-trigger.md +2 -0
  96. package/dist/agent-src/rules/skill-quality.md +2 -0
  97. package/dist/agent-src/rules/slash-command-routing-policy.md +2 -0
  98. package/dist/agent-src/rules/source-confidentiality.md +2 -0
  99. package/dist/agent-src/rules/source-discovery-gate.md +2 -0
  100. package/dist/agent-src/rules/source-of-truth.md +2 -0
  101. package/dist/agent-src/rules/spreadsheet-source-quality.md +2 -0
  102. package/dist/agent-src/rules/strategy-safety-floor.md +3 -1
  103. package/dist/agent-src/rules/symfony-routing.md +2 -0
  104. package/dist/agent-src/rules/think-before-action.md +2 -0
  105. package/dist/agent-src/rules/token-budget-discipline.md +5 -1
  106. package/dist/agent-src/rules/token-efficiency.md +2 -0
  107. package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
  108. package/dist/agent-src/rules/tool-safety.md +7 -1
  109. package/dist/agent-src/rules/ui-audit-gate.md +4 -0
  110. package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
  111. package/dist/agent-src/rules/upstream-proposal.md +2 -0
  112. package/dist/agent-src/rules/user-interaction.md +2 -0
  113. package/dist/agent-src/rules/user-interrupt-priority.md +2 -0
  114. package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
  115. package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
  116. package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
  117. package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
  118. package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
  119. package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
  120. package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
  121. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  122. package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
  123. package/dist/agent-src/templates/AGENTS.md +6 -10
  124. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  125. package/dist/cli/registry.js +9 -0
  126. package/dist/cli/registry.js.map +1 -1
  127. package/dist/cli-delegate/chunk-22GCIJ2O.js +270 -0
  128. package/dist/cli-delegate/{chunk-BSLUHV2F.js → chunk-2BE5I2MN.js} +156 -141
  129. package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-7OMQCH4V.js} +1 -1
  130. package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-GB2FSUEE.js} +57 -7
  131. package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-JXARCGVN.js} +3 -3
  132. package/dist/cli-delegate/{chunk-SJQMJI7M.js → chunk-PQSAQKHW.js} +467 -305
  133. package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
  134. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  135. package/dist/cli-delegate/cmd_brand_status.js +134 -0
  136. package/dist/cli-delegate/cmd_conformance.js +7 -7
  137. package/dist/cli-delegate/cmd_converge.js +3 -3
  138. package/dist/cli-delegate/cmd_doctor.js +4 -4
  139. package/dist/cli-delegate/cmd_explain.js +80 -330
  140. package/dist/cli-delegate/cmd_export.js +5 -5
  141. package/dist/cli-delegate/cmd_fleet.js +3 -3
  142. package/dist/cli-delegate/cmd_handoff.js +2 -2
  143. package/dist/cli-delegate/cmd_mcp_available.js +433 -0
  144. package/dist/cli-delegate/cmd_migrate.js +3 -3
  145. package/dist/cli-delegate/cmd_packs_active.js +153 -0
  146. package/dist/cli-delegate/cmd_preflight.js +1 -1
  147. package/dist/cli-delegate/cmd_prune.js +5 -5
  148. package/dist/cli-delegate/cmd_route_audit.js +8 -8
  149. package/dist/cli-delegate/cmd_route_explain.js +4 -4
  150. package/dist/cli-delegate/cmd_settings_get.js +272 -0
  151. package/dist/cli-delegate/cmd_settings_migrate.js +5 -5
  152. package/dist/cli-delegate/cmd_settings_set.js +7 -2967
  153. package/dist/cli-delegate/cmd_sync.js +5 -5
  154. package/dist/cli-delegate/cmd_uninstall.js +5 -5
  155. package/dist/cli-delegate/cmd_update.js +4 -4
  156. package/dist/cli-delegate/cmd_upgrade.js +4 -4
  157. package/dist/cli-delegate/cmd_validate.js +5 -5
  158. package/dist/cli-delegate/cmd_versions.js +4 -4
  159. package/dist/discovery/deprecation-report.md +1 -1
  160. package/dist/discovery/discovery-manifest.json +184 -124
  161. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  162. package/dist/discovery/discovery-manifest.summary.md +3 -3
  163. package/dist/discovery/orphan-report.md +1 -1
  164. package/dist/discovery/packs.json +7 -5
  165. package/dist/discovery/trust-report.md +11 -11
  166. package/dist/discovery/workspaces.json +6 -4
  167. package/dist/hooks/dispatch.js +9080 -5701
  168. package/dist/install/install.mjs +698 -508
  169. package/dist/mcp/registry-manifest.json +3 -3
  170. package/dist/mcp/server.json +2 -2
  171. package/dist/mcp/server.mjs +698 -437
  172. package/dist/router.json +1 -1
  173. package/dist/scripts/_lib/agent_settings.js +80 -15
  174. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  175. package/dist/scripts/condense.js +33 -2
  176. package/dist/scripts/condense.js.map +1 -1
  177. package/dist/server/io/yamlIO.js +85 -0
  178. package/dist/server/io/yamlIO.js.map +1 -1
  179. package/dist/server/routes/settings.js +1 -12
  180. package/dist/server/routes/settings.js.map +1 -1
  181. package/dist/server/routes/wizard.js +52 -2
  182. package/dist/server/routes/wizard.js.map +1 -1
  183. package/dist/shared/settingsCarveOut.js +118 -0
  184. package/dist/shared/settingsCarveOut.js.map +1 -0
  185. package/dist/shared/settingsSurface.js +7 -1
  186. package/dist/shared/settingsSurface.js.map +1 -1
  187. package/dist/ui/assets/index-DmR0mbk4.js.map +1 -1
  188. package/docs/contracts/settings-classes.md +29 -8
  189. package/docs/guidelines/agent-infra/memory-access.md +14 -3
  190. package/package.json +1 -1
  191. package/src/agent-src/templates/AGENTS.md +6 -10
  192. package/src/config/budgets.yml +29 -0
  193. package/src/config/evaluator-budgets.json +4 -4
  194. package/src/config/gate-coverage.yml +26 -0
  195. package/src/scripts/_cli/cmd_brand_status.ts +174 -0
  196. package/src/scripts/_cli/cmd_doctor.ts +6 -21
  197. package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
  198. package/src/scripts/_cli/cmd_packs_active.ts +225 -0
  199. package/src/scripts/_cli/cmd_settings_get.ts +324 -0
  200. package/src/scripts/_dispatch.bash +75 -0
  201. package/src/scripts/_lib/agent_settings.ts +82 -16
  202. package/src/scripts/_lib/carrier_divergence.ts +135 -0
  203. package/src/scripts/_lib/git_common_dir.ts +128 -0
  204. package/src/scripts/_lib/host_capability.ts +84 -1
  205. package/src/scripts/_lib/obligation_frequency.ts +391 -0
  206. package/src/scripts/_lib/prompt_shape.ts +88 -0
  207. package/src/scripts/_lib/review_skipped_record.ts +102 -0
  208. package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
  209. package/src/scripts/_lib/self_repair.ts +342 -0
  210. package/src/scripts/_lib/self_repair_store.ts +100 -0
  211. package/src/scripts/_lib/session_register.ts +361 -0
  212. package/src/scripts/_lib/transcript_entry.ts +80 -0
  213. package/src/scripts/check_branch_freshness.ts +255 -0
  214. package/src/scripts/check_council_config_location.ts +125 -1
  215. package/src/scripts/check_enforcement_coverage.ts +192 -7
  216. package/src/scripts/check_rule_projection_integrity.ts +364 -0
  217. package/src/scripts/check_standing_rule_delivery.ts +314 -0
  218. package/src/scripts/condense.ts +36 -2
  219. package/src/scripts/config/profiles.ts +13 -3
  220. package/src/scripts/conformance_scan.ts +307 -24
  221. package/src/scripts/council_availability_hook.ts +188 -0
  222. package/src/scripts/council_cli.ts +126 -7
  223. package/src/scripts/generate_settings_reference.ts +232 -0
  224. package/src/scripts/hook_manifest.yaml +155 -33
  225. package/src/scripts/hooks/block_no_verify.ts +95 -3
  226. package/src/scripts/hooks/block_unauthorized_git.ts +217 -2
  227. package/src/scripts/hooks/concern_registry.ts +12 -0
  228. package/src/scripts/hooks/delegation_nudge_hook.ts +450 -0
  229. package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
  230. package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
  231. package/src/scripts/install.ts +151 -0
  232. package/src/scripts/language_mirror_hook.ts +49 -6
  233. package/src/scripts/measure_scope_dedup.ts +19 -13
  234. package/src/scripts/onboarding_gate_hook.ts +40 -1
  235. package/src/scripts/orchestration_backfill.ts +354 -0
  236. package/src/scripts/orchestration_record.ts +4 -1
  237. package/src/scripts/preamble_byte_census.ts +9 -20
  238. package/src/scripts/release.ts +142 -36
  239. package/src/scripts/report_carrier_divergence.ts +364 -0
  240. package/src/scripts/report_skill_activation.ts +257 -0
  241. package/src/scripts/report_skill_obligation_violations.ts +579 -0
  242. package/src/scripts/routing_doctor.ts +77 -6
  243. package/src/scripts/schemas/rule.schema.json +5 -0
  244. package/src/scripts/self_repair_cli.ts +240 -0
  245. package/src/scripts/self_repair_hook.ts +241 -0
  246. package/src/scripts/session_canary_hook.ts +35 -3
  247. package/src/scripts/session_register_hook.ts +288 -0
  248. package/src/scripts/sessions_cli.ts +140 -0
  249. package/src/scripts/validate_frontmatter.ts +68 -0
  250. package/src/server/io/yamlIO.ts +87 -0
  251. package/src/shared/settingsCarveOut.ts +143 -0
  252. package/src/shared/settingsSurface.ts +7 -1
@@ -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
+ }
@@ -1108,6 +1108,36 @@ function _scoped_rule_basenames(): string[] {
1108
1108
  .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
1109
1109
  }
1110
1110
 
1111
+ /**
1112
+ * The emit plan of {@link generate_rule_symlinks}: active tool dir → the exact
1113
+ * rule basenames it projects there.
1114
+ *
1115
+ * Exported so a completeness gate can assert a host rule tree matches the
1116
+ * generator WITHOUT regenerating it. That distinction is the whole point:
1117
+ * `check_bridge_derivation` regenerates and then diffs, so it can only ever see
1118
+ * a fixpoint, and a five-week-stale tree on a working checkout is invisible to
1119
+ * it. A gate that reads the tree as it stands needs the expected set from
1120
+ * somewhere, and every other candidate source is wrong in a way that
1121
+ * propagates: a hand-maintained array goes obsolete silently, and
1122
+ * `dist/agent-src/rules/` over-counts by the ADR-004 `type: manual` rules these
1123
+ * trees deliberately omit (113 files, 5 manual, 108 projected).
1124
+ *
1125
+ * `generate_rule_symlinks` consumes this too, so there is one emit plan rather
1126
+ * than a gate's reconstruction of one.
1127
+ */
1128
+ export function projected_rule_trees(
1129
+ user_home: string = process.env['HOME'] ?? os.homedir(),
1130
+ ): Record<string, string[]> {
1131
+ const rules = _scoped_rule_basenames();
1132
+ const dedup_on = _read_projection_scope_dedup();
1133
+ const out: Record<string, string[]> = {};
1134
+ for (const tool_dir of Object.keys(_filter_tool_dirs(TOOL_DIRS))) {
1135
+ const skip = dedup_on ? _dedupable_rules(tool_dir, rules, user_home) : new Set<string>();
1136
+ out[tool_dir] = rules.filter((r) => !skip.has(r));
1137
+ }
1138
+ return out;
1139
+ }
1140
+
1111
1141
  export function generate_rule_symlinks(): number {
1112
1142
  const rules = _scoped_rule_basenames();
1113
1143
  const tool_dirs = _filter_tool_dirs(TOOL_DIRS);
@@ -1130,14 +1160,18 @@ export function generate_rule_symlinks(): number {
1130
1160
  );
1131
1161
  }
1132
1162
 
1133
- const dedup_on = _read_projection_scope_dedup();
1134
1163
  const user_home = process.env['HOME'] ?? os.homedir();
1164
+ // Read the emit plan from the exported function rather than recomputing the
1165
+ // dedup skip here, so the gate that asserts this tree is complete and the
1166
+ // code that writes it cannot disagree.
1167
+ const plan = projected_rule_trees(user_home);
1135
1168
  const skipped_per_tool: Record<string, Set<string>> = {};
1136
1169
 
1137
1170
  let total = 0;
1138
1171
  for (const [tool_dir, rel_prefix] of Object.entries(tool_dirs)) {
1139
1172
  const target_dir = path.join(MODULE_STATE.PROJECT_ROOT, tool_dir);
1140
- const skip = dedup_on ? _dedupable_rules(tool_dir, rules, user_home) : new Set<string>();
1173
+ const emit_here = new Set(plan[tool_dir] ?? rules);
1174
+ const skip = new Set(rules.filter((r) => !emit_here.has(r)));
1141
1175
  skipped_per_tool[tool_dir] = skip;
1142
1176
  _mkdirp(target_dir);
1143
1177
  // Clean stale symlinks — a rule that became de-duplicable this run is
@@ -130,7 +130,17 @@ function _load_yaml(p: string): Dict {
130
130
  return _isPlainDict(data) ? (data as Dict) : {};
131
131
  }
132
132
 
133
- function _profile_file(project_root: string, profile_id: string): string {
133
+ /**
134
+ * The file `resolve_profile` will read for `profile_id`, or the legacy path
135
+ * when no candidate exists.
136
+ *
137
+ * Exported because a probe that reports "which profile is active" must name
138
+ * the file the resolver actually used. Re-deriving that path in the caller is
139
+ * how the two answers drift apart, which is the defect class
140
+ * `road-to-capability-answerability` exists to close — so there is exactly one
141
+ * implementation and both the resolver and `packs:active` call it.
142
+ */
143
+ export function profile_file(project_root: string, profile_id: string): string {
134
144
  const legacy = path.join(project_root, PROFILES_DIRNAME, `${profile_id}.yml`);
135
145
  if (_exists(legacy)) {
136
146
  return legacy;
@@ -230,11 +240,11 @@ export function resolve_profile(params: {
230
240
  }
231
241
  return _build_resolved(
232
242
  DEFAULT_PROFILE_ID,
233
- _load_yaml(_profile_file(params.project_root, DEFAULT_PROFILE_ID)),
243
+ _load_yaml(profile_file(params.project_root, DEFAULT_PROFILE_ID)),
234
244
  { source: SOURCE_DEFAULT },
235
245
  );
236
246
  }
237
- const yaml_path = _profile_file(params.project_root, profile_id);
247
+ const yaml_path = profile_file(params.project_root, profile_id);
238
248
  if (!_exists(yaml_path)) {
239
249
  throw new ProfileError(
240
250
  `profile.id=${_repr(profile_id)} (${source}) but ${yaml_path} not found`,