@event4u/agent-config 14.12.0 → 14.13.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 (466) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +234 -0
  5. package/CONTRIBUTING.md +38 -0
  6. package/CREDITS.md +135 -0
  7. package/NOTICE +13 -0
  8. package/README.md +8 -6
  9. package/dist/agent-src/commands/analyze/inbox.md +109 -2
  10. package/dist/agent-src/commands/feature/dev.md +7 -2
  11. package/dist/agent-src/commands/ghostwriter/write.md +5 -1
  12. package/dist/agent-src/commands/implement-ticket.md +1 -1
  13. package/dist/agent-src/commands/mission/upgrade.md +1 -1
  14. package/dist/agent-src/commands/orchestrate.md +2 -2
  15. package/dist/agent-src/commands/post-as/me.md +5 -1
  16. package/dist/agent-src/commands/pr/merge.md +19 -6
  17. package/dist/agent-src/commands/roadmap/process-full.md +19 -5
  18. package/dist/agent-src/commands/tdd/red.md +12 -2
  19. package/dist/agent-src/commands/work.md +1 -1
  20. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +40 -0
  21. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -2
  22. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +21 -0
  23. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +1 -0
  24. package/dist/agent-src/contexts/execution/subagent-response-contract.md +40 -0
  25. package/dist/agent-src/contexts/execution/subagent-steering.md +13 -1
  26. package/dist/agent-src/contexts/execution/terminal-states.md +96 -0
  27. package/dist/agent-src/rules/artifact-drafting-protocol.md +1 -1
  28. package/dist/agent-src/rules/broken-access-control.md +4 -0
  29. package/dist/agent-src/rules/code-provenance.md +4 -0
  30. package/dist/agent-src/rules/design-fidelity.md +1 -1
  31. package/dist/agent-src/rules/domain-safety-pii.md +4 -0
  32. package/dist/agent-src/rules/domain-safety-retention.md +4 -0
  33. package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
  34. package/dist/agent-src/rules/external-code-graph-interop.md +14 -12
  35. package/dist/agent-src/rules/improve-before-implement.md +6 -8
  36. package/dist/agent-src/rules/laravel-routing.md +1 -1
  37. package/dist/agent-src/rules/lethal-trifecta-guard.md +4 -0
  38. package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
  39. package/dist/agent-src/rules/security-sensitive-stop.md +2 -0
  40. package/dist/agent-src/rules/source-confidentiality.md +12 -34
  41. package/dist/agent-src/rules/symfony-routing.md +1 -1
  42. package/dist/agent-src/rules/token-efficiency.md +14 -0
  43. package/dist/agent-src/rules/tool-safety.md +4 -0
  44. package/dist/agent-src/rules/untrusted-input-defense.md +4 -0
  45. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +247 -3
  46. package/dist/agent-src/scripts/guarded_baseline.ts +328 -0
  47. package/dist/agent-src/scripts/roadmap_set_step.ts +253 -0
  48. package/dist/agent-src/scripts/stub_queue.ts +82 -0
  49. package/dist/agent-src/scripts/stubs_due.ts +400 -0
  50. package/dist/agent-src/scripts/ticket_bundles.ts +84 -0
  51. package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -4
  52. package/dist/agent-src/skills/brand-asset-generation/SKILL.md +17 -4
  53. package/dist/agent-src/skills/brand-audit/SKILL.md +33 -0
  54. package/dist/agent-src/skills/code-intelligence/SKILL.md +57 -0
  55. package/dist/agent-src/skills/database/SKILL.md +8 -0
  56. package/dist/agent-src/skills/dependency-upgrade/SKILL.md +27 -0
  57. package/dist/agent-src/skills/design-review/SKILL.md +1 -1
  58. package/dist/agent-src/skills/doc-coauthoring/SKILL.md +1 -1
  59. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +17 -1
  60. package/dist/agent-src/skills/feature-planning/SKILL.md +9 -2
  61. package/dist/agent-src/skills/frontend-render-security/SKILL.md +44 -0
  62. package/dist/agent-src/skills/git-workflow/SKILL.md +1 -1
  63. package/dist/agent-src/skills/js-library-packaging/scripts/check_package_surface.ts +38 -4
  64. package/dist/agent-src/skills/judge-artifact-completeness/rubrics/ticket-quality-score.json +2 -2
  65. package/dist/agent-src/skills/laravel/SKILL.md +35 -0
  66. package/dist/agent-src/skills/laravel-migration/SKILL.md +57 -3
  67. package/dist/agent-src/skills/monorepo-workspace/SKILL.md +26 -0
  68. package/dist/agent-src/skills/playbook-authoring/SKILL.md +28 -1
  69. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  70. package/dist/agent-src/skills/skill-writing/references/procedure.md +5 -5
  71. package/dist/agent-src/skills/sql-writing/SKILL.md +7 -1
  72. package/dist/agent-src/skills/standards-from-config/SKILL.md +27 -4
  73. package/dist/agent-src/skills/supply-chain-intake/SKILL.md +11 -4
  74. package/dist/agent-src/skills/symfony-workflow/SKILL.md +35 -0
  75. package/dist/agent-src/skills/test-case-discovery/SKILL.md +85 -0
  76. package/dist/agent-src/skills/test-driven-development/SKILL.md +33 -9
  77. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +1 -1
  78. package/dist/agent-src/skills/testing-anti-patterns/process-anti-patterns.md +37 -0
  79. package/dist/agent-src/skills/ui-component-architect/SKILL.md +45 -2
  80. package/dist/agent-src/skills/upstream-contribute/SKILL.md +38 -1
  81. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +9 -4
  82. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  83. package/dist/agent-src/templates/roadmaps.md +97 -3
  84. package/dist/agent-src/templates/scripts/check_memory.ts +258 -17
  85. package/dist/agent-src/templates/scripts/check_memory_proposal.ts +9 -3
  86. package/dist/agent-src/templates/scripts/memory_lookup.ts +86 -0
  87. package/dist/agent-src/templates/scripts/memory_signal.ts +44 -0
  88. package/dist/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +90 -0
  89. package/dist/agent-src/templates/scripts/work_engine/directives/backend/index.ts +11 -0
  90. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +23 -1
  91. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +10 -1
  92. package/dist/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +5 -1
  93. package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +102 -14
  94. package/dist/cli/commands/uiAudit.js +124 -5
  95. package/dist/cli/registry.js +2 -0
  96. package/dist/cli-delegate/{chunk-HYWSRNUX.js → chunk-2DK2VJWG.js} +3 -3
  97. package/dist/cli-delegate/{chunk-JCW7ZLHK.js → chunk-4FS4WRXP.js} +5 -0
  98. package/dist/cli-delegate/{chunk-OMIVVCG2.js → chunk-A4QJ6ENT.js} +24 -0
  99. package/dist/cli-delegate/{chunk-JJA2QEWA.js → chunk-AKJGUQNO.js} +466 -285
  100. package/dist/cli-delegate/{chunk-E46KPN6K.js → chunk-BTWUYDGN.js} +424 -363
  101. package/dist/cli-delegate/{chunk-K4FFODTS.js → chunk-ILMUPLGW.js} +1227 -316
  102. package/dist/cli-delegate/{chunk-FF7WAFMR.js → chunk-KPBWFOPZ.js} +5 -1
  103. package/dist/cli-delegate/cmd_adr_effective.js +102 -0
  104. package/dist/cli-delegate/cmd_conformance.js +42 -10
  105. package/dist/cli-delegate/cmd_doctor.js +3 -3
  106. package/dist/cli-delegate/cmd_export.js +3 -2
  107. package/dist/cli-delegate/cmd_fleet.js +2 -2
  108. package/dist/cli-delegate/cmd_handoff.js +1 -1
  109. package/dist/cli-delegate/cmd_preflight.js +2 -2
  110. package/dist/cli-delegate/cmd_prune.js +3 -2
  111. package/dist/cli-delegate/cmd_settings_get.js +1 -1
  112. package/dist/cli-delegate/cmd_settings_migrate.js +3 -2
  113. package/dist/cli-delegate/cmd_settings_set.js +2 -2
  114. package/dist/cli-delegate/cmd_sync.js +3 -2
  115. package/dist/cli-delegate/cmd_uninstall.js +3 -2
  116. package/dist/cli-delegate/cmd_validate.js +3 -2
  117. package/dist/discovery/deprecation-report.md +1 -1
  118. package/dist/discovery/discovery-manifest.json +57 -57
  119. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  120. package/dist/discovery/discovery-manifest.summary.md +1 -1
  121. package/dist/discovery/dormancy-report.md +1 -1
  122. package/dist/discovery/orphan-report.md +1 -1
  123. package/dist/discovery/packs.json +2 -2
  124. package/dist/discovery/trust-report.md +1 -1
  125. package/dist/discovery/workspaces.json +2 -2
  126. package/dist/hooks/dispatch.js +1836 -712
  127. package/dist/install/claudeRuleRewrite.js +254 -0
  128. package/dist/install/detect.js +29 -16
  129. package/dist/install/detect_php_shape.js +159 -0
  130. package/dist/install/install.mjs +634 -428
  131. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  132. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
  133. package/dist/mcp/registry-manifest.json +3 -3
  134. package/dist/mcp/server.json +3 -3
  135. package/dist/mcp/server.mjs +4818 -2645
  136. package/dist/server/schemas/settings.js +3 -0
  137. package/dist/ui/assets/index-D0jaCRPM.js +29 -0
  138. package/dist/ui/index.html +1 -1
  139. package/docs/MIGRATION.md +1 -1
  140. package/docs/THIRD-PARTY-NOTICES.md +25 -0
  141. package/docs/contracts/provider-lifecycle.md +1 -1
  142. package/docs/contracts/settings-classes.md +10 -4
  143. package/docs/guidelines/agent-infra/5w2h-analysis.md +3 -3
  144. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -0
  145. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +58 -3
  146. package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +2 -2
  147. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +3 -3
  148. package/docs/guidelines/agent-infra/break-glass-usage.md +7 -7
  149. package/docs/guidelines/agent-infra/carve-out-predicates.md +3 -3
  150. package/docs/guidelines/agent-infra/comparison-matrix.md +1 -1
  151. package/docs/guidelines/agent-infra/critical-thinking.md +4 -4
  152. package/docs/guidelines/agent-infra/direct-answers-demos.md +2 -2
  153. package/docs/guidelines/agent-infra/domain-eval-anti-pattern.md +1 -1
  154. package/docs/guidelines/agent-infra/engineering-memory-data-format.md +3 -3
  155. package/docs/guidelines/agent-infra/existence-question-verdicts.md +80 -0
  156. package/docs/guidelines/agent-infra/first-principles.md +2 -2
  157. package/docs/guidelines/agent-infra/framework-neutrality-patterns.md +2 -2
  158. package/docs/guidelines/agent-infra/guarded-baseline.md +39 -0
  159. package/docs/guidelines/agent-infra/inversion-thinking.md +5 -5
  160. package/docs/guidelines/agent-infra/language-and-tone-examples.md +1 -1
  161. package/docs/guidelines/agent-infra/layered-settings.md +10 -10
  162. package/docs/guidelines/agent-infra/memory-access.md +1 -1
  163. package/docs/guidelines/agent-infra/mental-models.md +3 -3
  164. package/docs/guidelines/agent-infra/reuse-verdict-mechanics.md +71 -0
  165. package/docs/guidelines/agent-infra/review-routing-data-format.md +7 -7
  166. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +13 -7
  167. package/docs/guidelines/agent-infra/role-contracts.md +2 -2
  168. package/docs/guidelines/agent-infra/role-mode-router.md +3 -3
  169. package/docs/guidelines/agent-infra/rule-type-governance.md +2 -2
  170. package/docs/guidelines/agent-infra/scqa-framework.md +5 -5
  171. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +6 -6
  172. package/docs/guidelines/agent-infra/simplicity-and-goal-demos.md +2 -2
  173. package/docs/guidelines/agent-infra/six-hats.md +3 -3
  174. package/docs/guidelines/agent-infra/skill-quality-checklist.md +6 -6
  175. package/docs/guidelines/agent-infra/source-confidentiality-mechanics.md +109 -0
  176. package/docs/guidelines/agent-infra/systems-thinking.md +1 -1
  177. package/docs/guidelines/agent-infra/verify-before-complete-demos.md +2 -2
  178. package/docs/guidelines/augment-portability-patterns.md +6 -4
  179. package/docs/guidelines/component-oriented-and-oop-development.md +3 -2
  180. package/docs/guidelines/cross-role-handoff.md +2 -2
  181. package/docs/guidelines/design-fidelity-mechanics.md +1 -1
  182. package/docs/guidelines/docs/readme-size-and-splitting.md +3 -3
  183. package/docs/guidelines/php/database.md +90 -7
  184. package/docs/guidelines/prompt-templates.md +7 -7
  185. package/llms.txt +2 -2
  186. package/package.json +5 -2
  187. package/src/agent-src/scripts/archive_completed_roadmaps.ts +247 -3
  188. package/src/agent-src/scripts/guarded_baseline.ts +328 -0
  189. package/src/agent-src/scripts/roadmap_set_step.ts +253 -0
  190. package/src/agent-src/scripts/stub_queue.ts +82 -0
  191. package/src/agent-src/scripts/stubs_due.ts +400 -0
  192. package/src/agent-src/scripts/ticket_bundles.ts +84 -0
  193. package/src/agent-src/scripts/update_roadmap_progress.ts +8 -4
  194. package/src/agent-src/templates/scripts/check_memory.ts +258 -17
  195. package/src/agent-src/templates/scripts/check_memory_proposal.ts +9 -3
  196. package/src/agent-src/templates/scripts/memory_lookup.ts +86 -0
  197. package/src/agent-src/templates/scripts/memory_signal.ts +44 -0
  198. package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +90 -0
  199. package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +11 -0
  200. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +23 -1
  201. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +10 -1
  202. package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +5 -1
  203. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +102 -14
  204. package/src/config/agent-settings.template.yml +32 -3
  205. package/src/config/assurance-capability-registry.json +21 -9
  206. package/src/config/canonical-terms.yml +119 -0
  207. package/src/config/capability-native-outcome-bars.json +81 -0
  208. package/src/config/ci-local-parity.yml +80 -0
  209. package/src/config/drafting-channels.yml +43 -0
  210. package/src/config/estate-count-budget.json +38 -36
  211. package/src/config/evaluator-budgets.json +14 -14
  212. package/src/config/gate-coverage.yml +579 -63
  213. package/src/config/gate-reachability-exemptions.json +21 -0
  214. package/src/config/gate-violation-baselines.json +37 -19
  215. package/src/config/harness-evolution-budget.json +25 -0
  216. package/src/config/lapsed-beta-baseline.json +96 -0
  217. package/src/config/memory-twin-verdicts.yml +129 -0
  218. package/src/config/metric-registry.yml +124 -0
  219. package/src/config/pack-size-budget.json +16 -15
  220. package/src/config/preamble-payload-budget.json +8 -8
  221. package/src/config/profiles/balanced.ini +27 -0
  222. package/src/config/profiles/full.ini +27 -0
  223. package/src/config/profiles/minimal.ini +27 -0
  224. package/src/config/publish-surface.json +9 -2
  225. package/src/config/routing-coverage-seed.json +16 -2
  226. package/src/config/rule-stub-ceilings.json +126 -112
  227. package/src/config/turnaround-budget.json +66 -0
  228. package/src/install/claudeRuleRewrite.ts +288 -0
  229. package/src/install/detect.ts +60 -17
  230. package/src/install/detect_php_shape.ts +191 -0
  231. package/src/scripts/_cli/cmd_doctor.ts +27 -112
  232. package/src/scripts/_dispatch.bash +27 -0
  233. package/src/scripts/_lib/activation_ladder.ts +211 -0
  234. package/src/scripts/_lib/activation_states.ts +88 -0
  235. package/src/scripts/_lib/adherence_detectors.ts +143 -0
  236. package/src/scripts/_lib/arith_claims.ts +181 -0
  237. package/src/scripts/_lib/arm_ranking.ts +62 -0
  238. package/src/scripts/_lib/asset_delivery_ledger.ts +303 -0
  239. package/src/scripts/_lib/audit_field_provenance.ts +54 -0
  240. package/src/scripts/_lib/auto_dispatch.ts +32 -3
  241. package/src/scripts/_lib/bench_ab_size_claim.ts +98 -4
  242. package/src/scripts/_lib/benchmark_cache_fields.ts +158 -0
  243. package/src/scripts/_lib/candidate_proposer.ts +402 -0
  244. package/src/scripts/_lib/candidate_record.ts +601 -0
  245. package/src/scripts/_lib/capture_rate.ts +319 -0
  246. package/src/scripts/_lib/collector_denominator.ts +771 -0
  247. package/src/scripts/_lib/collector_record.ts +340 -0
  248. package/src/scripts/_lib/collector_store.ts +664 -0
  249. package/src/scripts/_lib/collector_supervision.ts +827 -0
  250. package/src/scripts/_lib/concern_estate.ts +77 -0
  251. package/src/scripts/_lib/config_chain.ts +477 -0
  252. package/src/scripts/_lib/config_cost.ts +209 -0
  253. package/src/scripts/_lib/council_settings_block.ts +6 -0
  254. package/src/scripts/_lib/curator_ops.ts +172 -0
  255. package/src/scripts/_lib/delivery_arm_experiment.ts +266 -0
  256. package/src/scripts/_lib/delivery_criticality.ts +126 -0
  257. package/src/scripts/_lib/doctor_runtime_checks.ts +70 -0
  258. package/src/scripts/_lib/empty_cycles.ts +88 -0
  259. package/src/scripts/_lib/eval_publication.ts +354 -0
  260. package/src/scripts/_lib/evaluation_vector.ts +283 -0
  261. package/src/scripts/_lib/evaluator_promotion.ts +225 -0
  262. package/src/scripts/_lib/evidence_basis.ts +118 -0
  263. package/src/scripts/_lib/exec_evidence.ts +8 -0
  264. package/src/scripts/_lib/experience_card.ts +272 -0
  265. package/src/scripts/_lib/experience_report.ts +151 -0
  266. package/src/scripts/_lib/experiment_freeze.ts +112 -0
  267. package/src/scripts/_lib/generated_by.ts +66 -0
  268. package/src/scripts/_lib/harness_evolution_guards.ts +229 -0
  269. package/src/scripts/_lib/hook_effect_probe.ts +141 -0
  270. package/src/scripts/_lib/host_denominator.ts +687 -0
  271. package/src/scripts/_lib/host_projection_reach.ts +157 -0
  272. package/src/scripts/_lib/ignored_blocker.ts +148 -0
  273. package/src/scripts/_lib/install_reach_checks.ts +495 -0
  274. package/src/scripts/_lib/json_duplicate_keys.ts +150 -0
  275. package/src/scripts/_lib/judge_hygiene.ts +228 -0
  276. package/src/scripts/_lib/legacy_boundary_map.ts +228 -0
  277. package/src/scripts/_lib/loop_guards.ts +299 -0
  278. package/src/scripts/_lib/loss_class.ts +129 -0
  279. package/src/scripts/_lib/machine_wake.ts +51 -0
  280. package/src/scripts/_lib/md_prose_lines.ts +177 -0
  281. package/src/scripts/_lib/minimality_tiebreak.ts +168 -0
  282. package/src/scripts/_lib/one_resolver_invariant.ts +449 -0
  283. package/src/scripts/_lib/orchestration_record.ts +253 -5
  284. package/src/scripts/_lib/outcome_envelope.ts +227 -0
  285. package/src/scripts/_lib/outcome_vocabularies.ts +125 -0
  286. package/src/scripts/_lib/paired_stats.ts +245 -0
  287. package/src/scripts/_lib/paired_verdict.ts +226 -0
  288. package/src/scripts/_lib/prefix_stable_surfaces.ts +123 -0
  289. package/src/scripts/_lib/privacy_class.ts +47 -0
  290. package/src/scripts/_lib/regression_neighbourhood.ts +259 -0
  291. package/src/scripts/_lib/repeated_failure.ts +105 -0
  292. package/src/scripts/_lib/repo_root.ts +111 -0
  293. package/src/scripts/_lib/review_skipped_record.ts +37 -0
  294. package/src/scripts/_lib/role_split.ts +338 -0
  295. package/src/scripts/_lib/runtime_journal.ts +1499 -0
  296. package/src/scripts/_lib/runtime_wiring_checks.ts +430 -0
  297. package/src/scripts/_lib/skill_catalogue.ts +11 -1
  298. package/src/scripts/_lib/source_digest.ts +200 -0
  299. package/src/scripts/_lib/source_redact.ts +260 -0
  300. package/src/scripts/_lib/source_shape.ts +502 -0
  301. package/src/scripts/_lib/source_snapshot_dedup.ts +199 -0
  302. package/src/scripts/_lib/strip_comments.d.mts +8 -0
  303. package/src/scripts/_lib/strip_comments.mjs +65 -0
  304. package/src/scripts/_lib/test_red_state.ts +154 -0
  305. package/src/scripts/_lib/trigger_shift.ts +144 -0
  306. package/src/scripts/_lib/untrusted_content.ts +82 -7
  307. package/src/scripts/adoption_report.ts +1 -1
  308. package/src/scripts/adr/evidence_census.ts +1 -1
  309. package/src/scripts/adr/regenerate_index.ts +26 -2
  310. package/src/scripts/adr_cite_check.ts +164 -1
  311. package/src/scripts/ai_council/blind_review.ts +11 -1
  312. package/src/scripts/ai_council/clients.ts +4 -0
  313. package/src/scripts/ai_council/config.ts +3 -0
  314. package/src/scripts/ai_council/consensus.ts +28 -8
  315. package/src/scripts/ai_council/consensus_round.ts +226 -0
  316. package/src/scripts/ai_council/disagreement_signal.ts +338 -0
  317. package/src/scripts/ai_council/inline_findings.ts +340 -0
  318. package/src/scripts/ai_council/judge_position_bias.ts +197 -0
  319. package/src/scripts/ai_council/orchestrator.ts +16 -203
  320. package/src/scripts/ai_council/orchestrator_results.ts +23 -3
  321. package/src/scripts/ai_council/pricing.ts +106 -0
  322. package/src/scripts/ai_council/prompts.ts +74 -7
  323. package/src/scripts/ai_council/provider_leakage_bench.ts +327 -0
  324. package/src/scripts/ai_council/py_format.ts +14 -3
  325. package/src/scripts/ai_council/recouncil_guard.ts +293 -0
  326. package/src/scripts/ai_council/response_record.ts +50 -0
  327. package/src/scripts/ai_council/topology_bench_manifest.ts +860 -0
  328. package/src/scripts/ai_council/topology_vocabulary.ts +175 -0
  329. package/src/scripts/asset_delivery_ledger.ts +157 -0
  330. package/src/scripts/attest_artifact.ts +193 -0
  331. package/src/scripts/audit_auto_rules.ts +2 -1
  332. package/src/scripts/audit_command_surface.ts +1 -1
  333. package/src/scripts/audit_mcp_tools.ts +2 -2
  334. package/src/scripts/audit_skill_overlap_allowlist.json +6 -1
  335. package/src/scripts/audit_user_type_axis.ts +2 -1
  336. package/src/scripts/bench_ab_clone.ts +215 -10
  337. package/src/scripts/bench_ab_integrity.ts +119 -5
  338. package/src/scripts/bench_ab_v2_stats.ts +40 -213
  339. package/src/scripts/build_archive_index.ts +1 -1
  340. package/src/scripts/build_linear_digest.ts +1 -1
  341. package/src/scripts/build_proof.ts +11 -4
  342. package/src/scripts/build_rule_trigger_matrix.ts +1 -1
  343. package/src/scripts/build_source_digests.ts +157 -0
  344. package/src/scripts/cache_realization_report.ts +103 -0
  345. package/src/scripts/check_adr_frontmatter.ts +126 -0
  346. package/src/scripts/check_benchmark_report_fields.ts +207 -0
  347. package/src/scripts/check_beta_review_markers.ts +235 -6
  348. package/src/scripts/check_bundle_path_leakage.ts +34 -1
  349. package/src/scripts/check_claims.ts +576 -6
  350. package/src/scripts/check_composite_arming.ts +35 -0
  351. package/src/scripts/check_concern_admissions.ts +365 -0
  352. package/src/scripts/check_enforcement_coverage.ts +28 -0
  353. package/src/scripts/check_estate_count.ts +142 -1
  354. package/src/scripts/check_gate_reachability.ts +383 -0
  355. package/src/scripts/check_host_loadability.ts +104 -0
  356. package/src/scripts/check_loss_class_declared.ts +308 -0
  357. package/src/scripts/check_md_language.ts +15 -98
  358. package/src/scripts/check_memory_proposal.ts +18 -11
  359. package/src/scripts/check_no_currency_in_cost_surfaces.ts +225 -0
  360. package/src/scripts/check_no_external_sources.ts +281 -6
  361. package/src/scripts/check_pack_conformance_fixture.ts +11 -1
  362. package/src/scripts/check_pack_size.ts +59 -2
  363. package/src/scripts/check_preamble_payload_budget.ts +114 -3
  364. package/src/scripts/check_prefix_stable_mutation.ts +372 -0
  365. package/src/scripts/check_publish_surface.ts +10 -1
  366. package/src/scripts/check_references.ts +24 -1
  367. package/src/scripts/check_rule_evidence_declaration.ts +316 -0
  368. package/src/scripts/check_rule_projection_integrity.ts +14 -4
  369. package/src/scripts/check_score_contract.ts +26 -3
  370. package/src/scripts/check_static_parity.ts +285 -0
  371. package/src/scripts/check_supervision_claim_atomicity.ts +498 -0
  372. package/src/scripts/check_suppression_hygiene.ts +15 -0
  373. package/src/scripts/ci_settle.ts +39 -2
  374. package/src/scripts/code_graph/build.ts +60 -3
  375. package/src/scripts/code_graph/extract.ts +44 -0
  376. package/src/scripts/code_graph/types.ts +24 -0
  377. package/src/scripts/collector_daemon.ts +714 -0
  378. package/src/scripts/config_cost_report.ts +302 -0
  379. package/src/scripts/consumer_matrix.ts +74 -1
  380. package/src/scripts/cost/budget.mjs +10 -3
  381. package/src/scripts/cost/track.mjs +10 -3
  382. package/src/scripts/council_cli.ts +37 -54
  383. package/src/scripts/dispatch_r2_reviewer.ts +46 -5
  384. package/src/scripts/evolution_lab.ts +980 -0
  385. package/src/scripts/external_sources_denylist.json +5 -18
  386. package/src/scripts/extract_audit_patterns.ts +21 -0
  387. package/src/scripts/fold_intake.ts +12 -1
  388. package/src/scripts/gen_discovery_baseline.ts +1 -1
  389. package/src/scripts/generate_capabilities_index.ts +12 -5
  390. package/src/scripts/generate_capability_matrix.ts +3 -3
  391. package/src/scripts/generate_command_flows.ts +2 -2
  392. package/src/scripts/generate_cookbook.ts +2 -2
  393. package/src/scripts/generate_index.ts +8 -1
  394. package/src/scripts/generate_knowledge_index.ts +2 -1
  395. package/src/scripts/generate_ownership_matrix.ts +2 -1
  396. package/src/scripts/generate_pack_manifests.ts +10 -2
  397. package/src/scripts/generate_role_experiences_catalog.ts +2 -2
  398. package/src/scripts/generate_settings_reference.ts +1 -1
  399. package/src/scripts/git_authorization_hook.ts +22 -1
  400. package/src/scripts/grade_target_readiness.ts +48 -5
  401. package/src/scripts/hook_effect_doctor.ts +261 -0
  402. package/src/scripts/hook_manifest.json +1 -1
  403. package/src/scripts/hook_manifest.yaml +98 -11
  404. package/src/scripts/hooks/block_speaking_inbox_dir.ts +271 -0
  405. package/src/scripts/hooks/block_unauthorized_git.ts +18 -0
  406. package/src/scripts/hooks/concern_registry.ts +4 -0
  407. package/src/scripts/hooks/dispatch_hook.ts +32 -0
  408. package/src/scripts/hooks/journal_record_hook.ts +345 -0
  409. package/src/scripts/hooks/prepush_metadata_sources.sh +110 -0
  410. package/src/scripts/hooks/run_continuation_hook.ts +44 -9
  411. package/src/scripts/hooks/shims/install.sh +61 -0
  412. package/src/scripts/hooks/shims/php +101 -0
  413. package/src/scripts/hooks/suggestion_capture_hook.ts +10 -0
  414. package/src/scripts/hot_context_hook.ts +16 -0
  415. package/src/scripts/install-hooks.sh +20 -5
  416. package/src/scripts/install.ts +17 -35
  417. package/src/scripts/inventory_frontmatter.ts +2 -1
  418. package/src/scripts/lint_adr_sweep_routing.ts +12 -0
  419. package/src/scripts/lint_canonical_terms.ts +604 -0
  420. package/src/scripts/lint_composition_review.ts +619 -0
  421. package/src/scripts/lint_consolidation_lineage.ts +666 -0
  422. package/src/scripts/lint_eval_specs.ts +593 -0
  423. package/src/scripts/lint_hook_manifest.ts +19 -0
  424. package/src/scripts/lint_host_portability.ts +373 -0
  425. package/src/scripts/lint_judge_prompt_expectation.ts +27 -1
  426. package/src/scripts/lint_memory_twin_parity.ts +335 -0
  427. package/src/scripts/lint_metric_consumers.ts +355 -0
  428. package/src/scripts/lint_skill_descriptions.ts +18 -1
  429. package/src/scripts/lint_skill_frontmatter_safety.ts +58 -0
  430. package/src/scripts/lint_skill_link_reach.ts +252 -0
  431. package/src/scripts/lint_skill_trigger_corpus.ts +426 -23
  432. package/src/scripts/lint_ticket_buildable.ts +17 -1
  433. package/src/scripts/mcp_server/consumer_tool_catalog.json +1 -1
  434. package/src/scripts/mcp_server/tool_catalog_source.ts +1 -1
  435. package/src/scripts/measure_docs_dead_links.ts +348 -0
  436. package/src/scripts/measure_host_capture.ts +461 -0
  437. package/src/scripts/migration_status.ts +3 -3
  438. package/src/scripts/model_rule_injection.ts +60 -0
  439. package/src/scripts/orchestration_record.ts +14 -0
  440. package/src/scripts/preamble_byte_census.ts +139 -10
  441. package/src/scripts/prepack-check.mjs +4 -1
  442. package/src/scripts/probe_turnaround.ts +384 -0
  443. package/src/scripts/probe_unblocked_ask.ts +279 -0
  444. package/src/scripts/render_benchmark_md.ts +1 -1
  445. package/src/scripts/rule_activation_census.ts +70 -9
  446. package/src/scripts/rule_backlinks.ts +1 -1
  447. package/src/scripts/run_lifecycle_suite.ts +208 -0
  448. package/src/scripts/run_skill_evals.ts +40 -0
  449. package/src/scripts/schemas/delivery-manifest.schema.json +43 -0
  450. package/src/scripts/schemas/drafting-channels.schema.json +43 -0
  451. package/src/scripts/schemas/evals.schema.json +24 -1
  452. package/src/scripts/schemas/rule.schema.json +52 -1
  453. package/src/scripts/schemas/skill.schema.json +17 -2
  454. package/src/scripts/skill_overlap.ts +7 -1
  455. package/src/scripts/skill_tools/run_block_d_eval.ts +8 -1
  456. package/src/scripts/skill_tools/score_skill_relevance.ts +84 -3
  457. package/src/scripts/skill_tools/suggest_skill_for_task.ts +8 -1
  458. package/src/scripts/skill_usage_report.ts +2 -1
  459. package/src/scripts/stubs_due.ts +211 -0
  460. package/src/scripts/sweep_source_surfaces.ts +678 -0
  461. package/src/scripts/sync_pr_branch.ts +3 -3
  462. package/src/scripts/trigger_eval_grandfather.json +202 -222
  463. package/src/scripts/utilization_report.ts +22 -2
  464. package/src/server/schemas/settings.ts +5 -0
  465. package/src/templates/marketing-copy.yml +1 -1
  466. package/dist/ui/assets/index-Cxgy63kz.js +0 -26
@@ -219,17 +219,71 @@ export function payloadClassCounts(
219
219
  }));
220
220
  }
221
221
 
222
+ /**
223
+ * The recorded BUILT-surface figure, if the budget file carries one.
224
+ *
225
+ * Read by key SHAPE (`built_surface_measurement_<date>`) rather than by the one
226
+ * dated key that exists today, so re-measuring the built surface is an added
227
+ * record rather than an edit to this function. The newest key wins.
228
+ */
229
+ export function recordedBuiltPackedMb(budget: PackSizeBudget): number | null {
230
+ const keys = Object.keys(budget)
231
+ .filter((k) => /^built_surface_measurement_/.test(k))
232
+ .sort();
233
+ const newest = keys[keys.length - 1];
234
+ if (newest === undefined) return null;
235
+ const block = (budget as unknown as Record<string, unknown>)[newest];
236
+ if (block === null || typeof block !== 'object') return null;
237
+ const built = (block as { built?: unknown }).built;
238
+ if (built === null || typeof built !== 'object') return null;
239
+ const value = (built as { packed_mb?: unknown }).packed_mb;
240
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
241
+ }
242
+
222
243
  export function evaluate(budget: PackSizeBudget, pack: PackResult): string[] {
223
244
  const errors: string[] = [];
224
245
 
225
246
  const packedMb = pack.size / 1e6;
226
247
  const entry = budget.budgets['packed_size_mb'];
248
+ const built = payloadIsBuilt(pack.files);
249
+ const pct = budget.regression_pct;
227
250
  if (entry === undefined) {
228
251
  errors.push('packed_size_mb: missing from the budget file — gate would pass vacuously');
252
+ } else if (built) {
253
+ // A BUILT payload is not the surface `max` describes, and comparing
254
+ // them is the category error `pack-size-budget.json` warns about in its
255
+ // own `measurement_conditions`: every cap in that file, `max: 9.1`
256
+ // included, was measured with `--ignore-scripts` on a tree with no
257
+ // `dist/cli/**`. The built surface it records separately is ~2 MB
258
+ // larger *by construction*, so a job that happens to build first turns
259
+ // a green branch red for a reason that has nothing to do with its diff.
260
+ // Measured 2026-08-30: main 8.985 (2715 entries, unbuilt) against a
261
+ // branch adding six source files at 9.922 (2827 entries) — the 112-file
262
+ // difference is `dist/cli` + `dist/cli-delegate`, not the diff.
263
+ //
264
+ // So the built payload is judged against the recorded BUILT figure,
265
+ // with the same creep percentage. That keeps teeth on the surface a
266
+ // consumer installs instead of pretending the unbuilt cap covers it.
267
+ const baseline = recordedBuiltPackedMb(budget);
268
+ if (baseline === null) {
269
+ errors.push(
270
+ `packed_size_mb: measured ${packedMb.toFixed(3)} on a BUILT payload, and the budget `
271
+ + 'file records no built-surface measurement to compare it against — the unbuilt '
272
+ + 'cap does not describe this tree. Record one, or pack with --ignore-scripts.',
273
+ );
274
+ } else {
275
+ const ceiling = baseline * (1 + pct / 100);
276
+ if (packedMb > ceiling) {
277
+ errors.push(
278
+ `packed_size_mb (BUILT surface): measured ${packedMb.toFixed(3)} regressed `
279
+ + `>${String(pct)}% vs the recorded built figure ${String(baseline)} `
280
+ + `(ceiling ${ceiling.toFixed(3)})`,
281
+ );
282
+ }
283
+ }
229
284
  } else if (packedMb > entry.max) {
230
285
  errors.push(`packed_size_mb: measured ${packedMb.toFixed(3)} exceeds budget ${entry.max}`);
231
286
  } else {
232
- const pct = budget.regression_pct;
233
287
  const ceiling = entry.last_measured * (1 + pct / 100);
234
288
  if (entry.last_measured > 0 && packedMb > ceiling) {
235
289
  errors.push(
@@ -436,7 +490,10 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
436
490
  return 1;
437
491
  }
438
492
  process.stdout.write(
439
- `✅ pack size within budget — ${(pack.size / 1e6).toFixed(3)} MB packed, ` +
493
+ // The SURFACE is named on the green path too. A figure with no surface
494
+ // label is how two numbers for one lever start circulating, which is
495
+ // the confusion this budget file opens by warning about.
496
+ `✅ pack size within budget (${payloadIsBuilt(pack.files) ? 'BUILT surface, vs the recorded built figure' : 'UNBUILT surface, vs max'}) — ${(pack.size / 1e6).toFixed(3)} MB packed, ` +
440
497
  `${Object.keys(perSkill).length} skills, largest share ` +
441
498
  `${Math.max(...Object.values(perSkill).map((b) => (b / total) * 100)).toFixed(2)}%\n`,
442
499
  );
@@ -20,11 +20,15 @@
20
20
  *
21
21
  * Exit codes: 0 within budget · 1 over budget · 2 misuse / unreadable budget.
22
22
  */
23
+ import { execFileSync } from 'node:child_process';
23
24
  import * as fs from 'node:fs';
25
+ import * as os from 'node:os';
24
26
  import * as path from 'node:path';
25
27
 
26
28
  import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
27
29
  import { censusClaudeMdHierarchy, censusRuleDir, censusSkillsCatalog } from './preamble_byte_census.js';
30
+ import { PREFIX_STABLE_SURFACES, prefixStableRoots } from './_lib/prefix_stable_surfaces.js';
31
+ import { attributeGrowth, buildLedger, renderAttribution } from './_lib/asset_delivery_ledger.js';
28
32
 
29
33
  const HERE = path.dirname(new URL(import.meta.url).pathname);
30
34
  const REPO_ROOT = path.resolve(HERE, '..', '..');
@@ -63,12 +67,30 @@ export function readBudget(file: string = BUDGET_FILE): Budget {
63
67
  };
64
68
  }
65
69
 
70
+ /** Look one surface root up by id. Throws on an unknown id — a renamed surface
71
+ * must fail loudly here rather than degrade this gate to measuring nothing. */
72
+ function surfaceRoot(id: string): string {
73
+ const s = PREFIX_STABLE_SURFACES.find((x) => x.id === id);
74
+ if (s === undefined) {
75
+ throw new Error(
76
+ `prefix-stable surface '${id}' is not declared in _lib/prefix_stable_surfaces.ts — ` +
77
+ `the payload census cannot measure a bucket whose root it cannot resolve`,
78
+ );
79
+ }
80
+ return s.root;
81
+ }
82
+
66
83
  /** Measure only what the repo tree determines — no `~`, no transcripts, no network. */
67
84
  export function measureDeterministicPayload(
68
85
  repoRoot: string = REPO_ROOT,
69
86
  ): Array<{ name: string; tokens: number; files: number }> {
70
- const projectRules = censusRuleDir(path.join(repoRoot, 'dist', 'agent-src', 'rules'));
71
- const skills = censusSkillsCatalog(path.join(repoRoot, 'dist', 'agent-src', 'skills'));
87
+ // Roots come from the canonical prefix-stable surface registry, never from a
88
+ // literal here: `check_prefix_stable_mutation` guards the same boundary, and
89
+ // two independent lists of one boundary is the drift shape this repository
90
+ // has already paid for. `surfaceRoot` throws rather than silently measuring
91
+ // nothing if an id is renamed out from under it.
92
+ const projectRules = censusRuleDir(path.join(repoRoot, surfaceRoot('project-scope-rules')));
93
+ const skills = censusSkillsCatalog(path.join(repoRoot, surfaceRoot('preloaded-skills-catalog')));
72
94
  // Only the PROJECT half of the CLAUDE.md hierarchy is deterministic — the
73
95
  // user file and its @-imports live on whatever machine runs this.
74
96
  const claudeMd = censusClaudeMdHierarchy(repoRoot, path.join(repoRoot, '.no-such-home'));
@@ -120,6 +142,67 @@ export function evaluate(repoRoot?: string, budgetFile?: string, overrideCeiling
120
142
  return { measured, baseline: budget.baseline_tokens, ceiling, withinBudget: measured <= ceiling, buckets };
121
143
  }
122
144
 
145
+ /**
146
+ * Per-asset attribution for a refusal, against the merge-base tree.
147
+ *
148
+ * Reads the base tree through `git worktree`-free plumbing: the ledger is built
149
+ * over a temporary checkout of `git merge-base HEAD origin/main`. Every failure
150
+ * mode returns an empty list — the caller treats attribution as an explanation,
151
+ * never as a precondition for refusing.
152
+ */
153
+ function attributeGrowthAgainstBase(): string[] | null {
154
+ try {
155
+ const base = execFileSync('git', ['merge-base', 'HEAD', 'origin/main'], {
156
+ cwd: REPO_ROOT,
157
+ encoding: 'utf-8',
158
+ stdio: ['ignore', 'pipe', 'ignore'],
159
+ }).trim();
160
+ if (base.length === 0) return null;
161
+
162
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'payload-base-'));
163
+ try {
164
+ for (const rel of prefixStableRoots()) {
165
+ // `git archive` of one subtree, extracted into the scratch root.
166
+ // A root absent at the base ref simply yields nothing.
167
+ try {
168
+ const tar = execFileSync('git', ['archive', base, '--', rel], {
169
+ cwd: REPO_ROOT,
170
+ maxBuffer: 256 * 1024 * 1024,
171
+ stdio: ['ignore', 'pipe', 'ignore'],
172
+ });
173
+ // `-f -` is not optional: without it BSD tar reads its
174
+ // default device rather than stdin, and the extraction
175
+ // silently produces nothing — which reads downstream as
176
+ // "the base tree was unavailable" rather than as a bug.
177
+ execFileSync('tar', ['-x', '-f', '-', '-C', tmp], {
178
+ input: tar,
179
+ stdio: ['pipe', 'ignore', 'ignore'],
180
+ });
181
+ } catch {
182
+ /* root absent at base — nothing to extract */
183
+ }
184
+ }
185
+ const [rulesRel, skillsRel] = prefixStableRoots();
186
+ const before = buildLedger(
187
+ path.join(tmp, rulesRel ?? ''),
188
+ path.join(tmp, skillsRel ?? ''),
189
+ tmp,
190
+ );
191
+ const after = buildLedger(
192
+ path.join(REPO_ROOT, rulesRel ?? ''),
193
+ path.join(REPO_ROOT, skillsRel ?? ''),
194
+ REPO_ROOT,
195
+ );
196
+ if (before.rows.length === 0) return null;
197
+ return renderAttribution(attributeGrowth(before.rows, after.rows));
198
+ } finally {
199
+ fs.rmSync(tmp, { recursive: true, force: true });
200
+ }
201
+ } catch {
202
+ return null;
203
+ }
204
+ }
205
+
123
206
  export function main(argv: string[] = process.argv.slice(2)): number {
124
207
  const json = argv.includes('--format=json') || argv.includes('--json');
125
208
  // `--ceiling <n>`: the grace ceiling the CI step reads out of
@@ -145,7 +228,7 @@ export function main(argv: string[] = process.argv.slice(2)): number {
145
228
  gate: 'check_preamble_payload_budget',
146
229
  scanned: verdict.buckets.reduce((n, b) => n + b.files, 0),
147
230
  units: 'payload source file(s)',
148
- roots: ['dist/agent-src/rules', 'dist/agent-src/skills', 'CLAUDE.md'],
231
+ roots: prefixStableRoots(),
149
232
  });
150
233
  } catch (err) {
151
234
  if (err instanceof DeadScopeError) {
@@ -170,12 +253,40 @@ export function main(argv: string[] = process.argv.slice(2)): number {
170
253
  `(baseline ${verdict.baseline}, ${sign}${delta}; ceiling ${verdict.ceiling})\n`,
171
254
  );
172
255
  if (!verdict.withinBudget) {
256
+ // road-to-delivered-cost-truth 2.2 — a gate names its own "no". The
257
+ // ceiling message alone states a fact and leaves the reader to find the
258
+ // cause; naming the assets and their token deltas makes the refusal
259
+ // actionable, which is the difference between a refusal that gets fixed
260
+ // and one that gets suppressed.
261
+ //
262
+ // Attribution is BEST-EFFORT and never changes the verdict: the
263
+ // comparison needs the merge-base tree, and a shallow clone, a detached
264
+ // build or a first commit legitimately has none. A gate that failed to
265
+ // refuse because it could not explain itself would be strictly worse
266
+ // than one that refuses without the explanation.
267
+ const attribution = attributeGrowthAgainstBase();
173
268
  process.stderr.write(
174
269
  `❌ per-spawn preamble payload grew past the ratchet: ${verdict.measured} > ${verdict.ceiling} tok.\n` +
175
270
  ` Every rule and skill description here is re-written on EVERY subagent spawn, so growth\n` +
176
271
  ` is paid per spawn, not once. Either shrink the addition, or raise baseline_tokens in\n` +
177
272
  ` src/config/preamble-payload-budget.json with the reason in the same commit.\n`,
178
273
  );
274
+ // Three distinct states, and conflating the last two is a diagnostic
275
+ // defect rather than a cosmetic one: "I could not look" and "I looked
276
+ // and nothing changed" send a reader to completely different places.
277
+ if (attribution === null) {
278
+ process.stderr.write(
279
+ '\n (no per-asset attribution: the merge-base tree could not be read here, so the\n' +
280
+ ' growth cannot be traced to specific assets. The refusal stands regardless.)\n',
281
+ );
282
+ } else if (attribution.length === 0) {
283
+ process.stderr.write(
284
+ '\n Per-asset attribution: NO standing asset changed against the merge base — this\n' +
285
+ ' diff did not cause the overage, it inherited it.\n',
286
+ );
287
+ } else {
288
+ process.stderr.write('\n' + attribution.join('\n') + '\n');
289
+ }
179
290
  return 1;
180
291
  }
181
292
  process.stdout.write('✅ per-spawn preamble payload within the ratchet.\n');
@@ -0,0 +1,372 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * A mid-session mutation of a declared prefix-stable surface is a violation.
4
+ *
5
+ * WHY
6
+ * ---
7
+ * `check_kernel_prefix_stability` guards the kernel prefix at AUTHORING time —
8
+ * it compares a committed snapshot and re-anchors in the same PR. `payload_hash_drift`
9
+ * measures prefix churn AFTER the fact, from an audit ledger. Neither can see the
10
+ * case this gate exists for: a hook, a script, or (once one exists) a resident
11
+ * process rewriting a prefix-stable surface DURING a session, which invalidates
12
+ * the prompt cache for the rest of it and makes the next call pay the cache-WRITE
13
+ * rate over the whole prefix.
14
+ *
15
+ * That is the runtime half, and until this gate it was prose.
16
+ *
17
+ * WHAT IT CHECKS
18
+ * --------------
19
+ * For every concern in `src/scripts/hook_manifest.yaml` bound on at least one
20
+ * MID-SESSION slot — any slot that is not a declared re-arm event — the gate reads
21
+ * the concern's script and looks for evidence that it writes inside a surface
22
+ * declared in `_lib/prefix_stable_surfaces.ts`. Evidence is a violation unless the
23
+ * concern declares `re_arm: <event>`, naming the boundary at which a rebuilt
24
+ * prefix is expected and paid for once.
25
+ *
26
+ * THE LIST IS LOADED, NEVER RESTATED. Both this gate and
27
+ * `check_preamble_payload_budget` import `PREFIX_STABLE_SURFACES`. Two lists
28
+ * describing one boundary is the drift shape this repository has already paid for.
29
+ *
30
+ * FAIL-CLOSED ON UNDECIDABLE (council 2026-08-28, 2/2)
31
+ * ----------------------------------------------------
32
+ * A write whose target is a runtime variable cannot be classified by reading the
33
+ * source. Classifying that as "fine" turns every dynamic path into an accidental
34
+ * exemption. So: a dynamic write target in a script that ALSO carries a literal
35
+ * resolving into a declared surface is reported as `undecidable` and fails. A
36
+ * dynamic write in a script with no such literal is not reported — the gate does
37
+ * not claim to know what it cannot see, and reporting every dynamic write in every
38
+ * hook would be a gate nobody can keep green.
39
+ *
40
+ * Exit codes: 0 clean · 1 violation(s) · 2 misuse / unreadable manifest.
41
+ *
42
+ * Usage:
43
+ * ./scripts-run src/scripts/check_prefix_stable_mutation [--root <dir>] [--json] [--quiet]
44
+ * ./scripts-run src/scripts/check_prefix_stable_mutation --self-test
45
+ */
46
+ import { spawnSync } from 'node:child_process';
47
+ import * as fs from 'node:fs';
48
+ import * as os from 'node:os';
49
+ import * as path from 'node:path';
50
+
51
+ import { GateLedger } from './_lib/gate_ledger.js';
52
+ import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
53
+ import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
54
+ import { PREFIX_STABLE_SURFACES, RE_ARM_EVENTS, prefixStableRoots, surfaceFor } from './_lib/prefix_stable_surfaces.js';
55
+
56
+ const HERE = path.dirname(new URL(import.meta.url).pathname);
57
+ const REPO_ROOT = path.resolve(HERE, '..', '..');
58
+
59
+ /** Write-shaped calls. A read never invalidates a cache; only a write does. */
60
+ const WRITE_CALLS = [
61
+ 'writeFileSync',
62
+ 'appendFileSync',
63
+ 'createWriteStream',
64
+ 'renameSync',
65
+ 'unlinkSync',
66
+ 'rmSync',
67
+ 'rmdirSync',
68
+ 'cpSync',
69
+ 'copyFileSync',
70
+ 'truncateSync',
71
+ 'writeFile',
72
+ 'appendFile',
73
+ 'outputFileSync',
74
+ ] as const;
75
+
76
+ export interface Finding {
77
+ concern: string;
78
+ script: string;
79
+ slots: string[];
80
+ kind: 'literal' | 'undecidable';
81
+ surface: string;
82
+ evidence: string;
83
+ }
84
+
85
+ export interface Verdict {
86
+ scanned: number;
87
+ findings: Finding[];
88
+ midSessionConcerns: number;
89
+ }
90
+
91
+ interface ConcernSpec {
92
+ script?: unknown;
93
+ re_arm?: unknown;
94
+ }
95
+
96
+ /** Yaml is already a dependency of the manifest linter; reuse the same reader shape. */
97
+ function loadManifest(root: string): { concerns: Record<string, ConcernSpec>; platforms: Record<string, Record<string, string[]>> } {
98
+ const file = path.join(root, 'src', 'scripts', 'hook_manifest.yaml');
99
+ const raw = fs.readFileSync(file, 'utf-8');
100
+ // Minimal, dependency-free parse of the two shapes this gate needs. The full
101
+ // schema is `lint_hook_manifest`'s business; duplicating a YAML dependency
102
+ // here would add a second parser to disagree with.
103
+ const out = spawnSync(
104
+ process.execPath,
105
+ ['-e', 'const y=require("js-yaml");let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const d=y.load(s);process.stdout.write(JSON.stringify({concerns:d.concerns||{},platforms:d.platforms||{}}))})'],
106
+ { input: raw, encoding: 'utf-8', cwd: REPO_ROOT, maxBuffer: 64 * 1024 * 1024 },
107
+ );
108
+ if (out.status !== 0 || !out.stdout) {
109
+ throw new Error(`hook_manifest.yaml could not be parsed: ${out.stderr || 'no output'}`);
110
+ }
111
+ return JSON.parse(out.stdout) as { concerns: Record<string, ConcernSpec>; platforms: Record<string, Record<string, string[]>> };
112
+ }
113
+
114
+ /** Every slot each concern is bound on, across every platform. */
115
+ export function slotsByConcern(platforms: Record<string, Record<string, string[]>>): Map<string, Set<string>> {
116
+ const m = new Map<string, Set<string>>();
117
+ for (const slots of Object.values(platforms)) {
118
+ if (slots === null || typeof slots !== 'object') continue;
119
+ for (const [slot, names] of Object.entries(slots)) {
120
+ if (!Array.isArray(names)) continue;
121
+ for (const n of names) {
122
+ const set = m.get(n) ?? new Set<string>();
123
+ set.add(slot);
124
+ m.set(n, set);
125
+ }
126
+ }
127
+ }
128
+ return m;
129
+ }
130
+
131
+ /** Slots that are NOT re-arm boundaries — i.e. fire inside a live session. */
132
+ export function midSessionSlots(slots: Iterable<string>): string[] {
133
+ return [...slots].filter((s) => !RE_ARM_EVENTS.includes(s)).sort();
134
+ }
135
+
136
+ /** All string literals in a source fragment, in order. */
137
+ function literals(src: string): string[] {
138
+ const out: string[] = [];
139
+ const re = /'([^'\\\n]*)'|"([^"\\\n]*)"|`([^`$\\\n]*)`/g;
140
+ let m: RegExpExecArray | null;
141
+ while ((m = re.exec(src)) !== null) out.push(m[1] ?? m[2] ?? m[3] ?? '');
142
+ return out;
143
+ }
144
+
145
+ /** The first argument expression of each write-shaped call in `src`. */
146
+ export function writeTargets(src: string): string[] {
147
+ const out: string[] = [];
148
+ for (const call of WRITE_CALLS) {
149
+ const re = new RegExp(`\\b${call}\\s*\\(`, 'g');
150
+ let m: RegExpExecArray | null;
151
+ while ((m = re.exec(src)) !== null) {
152
+ let depth = 1;
153
+ let i = m.index + m[0].length;
154
+ const start = i;
155
+ while (i < src.length && depth > 0) {
156
+ const ch = src[i];
157
+ if (ch === '(' || ch === '[' || ch === '{') depth += 1;
158
+ else if (ch === ')' || ch === ']' || ch === '}') depth -= 1;
159
+ else if (ch === ',' && depth === 1) break;
160
+ i += 1;
161
+ }
162
+ out.push(src.slice(start, i));
163
+ }
164
+ }
165
+ return out;
166
+ }
167
+
168
+ /**
169
+ * Classify one script. Returns findings for this concern.
170
+ *
171
+ * `literal` — a write whose own argument carries a literal resolving into a
172
+ * declared surface. Unambiguous.
173
+ * `undecidable` — a write with no classifiable literal, in a file that carries a
174
+ * surface literal somewhere. Fails closed.
175
+ */
176
+ export function classifyScript(source: string): Array<{ kind: 'literal' | 'undecidable'; surface: string; evidence: string }> {
177
+ const found: Array<{ kind: 'literal' | 'undecidable'; surface: string; evidence: string }> = [];
178
+ const targets = writeTargets(source);
179
+ if (targets.length === 0) return found;
180
+
181
+ // A file-wide literal that resolves into a surface — the fail-closed trigger.
182
+ const fileSurfaces = literals(source)
183
+ .map((l) => ({ lit: l, s: surfaceFor(l) }))
184
+ .filter((x) => x.s !== null);
185
+
186
+ let sawDynamic = false;
187
+ for (const t of targets) {
188
+ const lits = literals(t);
189
+ // `path.join('dist','agent-src','rules', …)` — join the literal segments too.
190
+ const joined = lits.join('/');
191
+ const hit = lits.map((l) => surfaceFor(l)).find((s) => s !== null) ?? surfaceFor(joined);
192
+ if (hit) {
193
+ found.push({ kind: 'literal', surface: hit.id, evidence: t.trim().slice(0, 120) });
194
+ } else if (lits.length === 0) {
195
+ sawDynamic = true;
196
+ }
197
+ }
198
+
199
+ const firstSurface = fileSurfaces[0];
200
+ if (found.length === 0 && sawDynamic && firstSurface !== undefined && firstSurface.s !== null) {
201
+ found.push({
202
+ kind: 'undecidable',
203
+ surface: firstSurface.s.id,
204
+ evidence: `a write target is computed at runtime and this file carries the literal '${firstSurface.lit}'`,
205
+ });
206
+ }
207
+ return found;
208
+ }
209
+
210
+ export function evaluate(root: string = REPO_ROOT, ledger?: GateLedger): Verdict {
211
+ const { concerns, platforms } = loadManifest(root);
212
+ const bySlot = slotsByConcern(platforms);
213
+ const findings: Finding[] = [];
214
+ let scanned = 0;
215
+ let midSessionConcerns = 0;
216
+
217
+ ledger?.plan(Object.keys(concerns));
218
+
219
+ for (const [name, spec] of Object.entries(concerns)) {
220
+ const slots = midSessionSlots(bySlot.get(name) ?? []);
221
+ if (slots.length === 0) {
222
+ ledger?.outOfScope(name, 'not_applicable_kind');
223
+ continue;
224
+ }
225
+ midSessionConcerns += 1;
226
+
227
+ const rel = typeof spec?.script === 'string' ? spec.script : null;
228
+ if (rel === null) {
229
+ ledger?.skip(name, 'manifest_absent');
230
+ continue;
231
+ }
232
+ const abs = path.join(root, rel);
233
+ if (!fs.existsSync(abs)) {
234
+ ledger?.skip(name, 'no_applicable_files');
235
+ continue;
236
+ }
237
+ scanned += 1;
238
+
239
+ const declaredReArm = typeof spec?.re_arm === 'string' ? spec.re_arm : null;
240
+ if (declaredReArm !== null && RE_ARM_EVENTS.includes(declaredReArm)) {
241
+ ledger?.skip(name, 'declared_exemption');
242
+ continue;
243
+ }
244
+
245
+ const hits = classifyScript(fs.readFileSync(abs, 'utf-8'));
246
+ for (const hit of hits) {
247
+ findings.push({ concern: name, script: rel, slots, kind: hit.kind, surface: hit.surface, evidence: hit.evidence });
248
+ }
249
+ if (hits.length > 0) ledger?.fail(name, `${String(hits.length)} mid-session write(s) into a prefix-stable surface`);
250
+ else ledger?.complete(name);
251
+ }
252
+ return { scanned, findings, midSessionConcerns };
253
+ }
254
+
255
+ // ---------------------------------------------------------------- self-test
256
+
257
+ function plant(dir: string, reArm: string | null, body: string): void {
258
+ fs.mkdirSync(path.join(dir, 'src', 'scripts'), { recursive: true });
259
+ fs.writeFileSync(path.join(dir, 'src', 'scripts', 'fixture_hook.ts'), body);
260
+ const arm = reArm === null ? '' : `\n re_arm: ${reArm}`;
261
+ fs.writeFileSync(
262
+ path.join(dir, 'src', 'scripts', 'hook_manifest.yaml'),
263
+ `schema_version: 1\nconcerns:\n fixture:\n script: src/scripts/fixture_hook.ts${arm}\nplatforms:\n claude:\n post_tool_use:\n - fixture\n`,
264
+ );
265
+ }
266
+
267
+ const WRITES_SURFACE = `import * as fs from 'node:fs';\nfs.writeFileSync('dist/agent-src/rules/x.md', 'hi');\n`;
268
+ const WRITES_ELSEWHERE = `import * as fs from 'node:fs';\nfs.writeFileSync('agents/runtime/state/x.json', 'hi');\n`;
269
+ const DYNAMIC_WITH_SURFACE_LITERAL = `import * as fs from 'node:fs';\nconst root = 'dist/agent-src/rules';\nfs.writeFileSync(target, 'hi');\n`;
270
+ const DYNAMIC_NO_SURFACE = `import * as fs from 'node:fs';\nfs.writeFileSync(target, 'hi');\n`;
271
+ const READS_ONLY = `import * as fs from 'node:fs';\nfs.readFileSync('dist/agent-src/rules/x.md', 'utf-8');\n`;
272
+
273
+ function selfTestCases(): SelfTestCase[] {
274
+ const mk = (name: string, expect: 'reject' | 'accept', reArm: string | null, body: string): SelfTestCase => ({
275
+ name,
276
+ expect,
277
+ run: () => {
278
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'psm-'));
279
+ try {
280
+ plant(dir, reArm, body);
281
+ return runGateCli(REPO_ROOT, 'src/scripts/check_prefix_stable_mutation.ts', ['--root', dir, '--quiet'], REPO_ROOT);
282
+ } finally {
283
+ fs.rmSync(dir, { recursive: true, force: true });
284
+ }
285
+ },
286
+ });
287
+ return [
288
+ mk('mid-session write to a declared surface, no re-arm → reject', 'reject', null, WRITES_SURFACE),
289
+ mk('same write with re_arm: pre_compact → accept', 'accept', 'pre_compact', WRITES_SURFACE),
290
+ mk('same write with re_arm: session_start → accept', 'accept', 'session_start', WRITES_SURFACE),
291
+ mk('write outside every declared surface → accept', 'accept', null, WRITES_ELSEWHERE),
292
+ mk('dynamic target + surface literal in file → reject (fail-closed)', 'reject', null, DYNAMIC_WITH_SURFACE_LITERAL),
293
+ mk('dynamic target, no surface literal → accept (not claimed)', 'accept', null, DYNAMIC_NO_SURFACE),
294
+ mk('reads a surface but never writes → accept', 'accept', null, READS_ONLY),
295
+ ];
296
+ }
297
+
298
+ // ---------------------------------------------------------------------- CLI
299
+
300
+ export function main(argv: string[] = process.argv.slice(2)): number {
301
+ if (argv.includes('--self-test')) {
302
+ return runSelfTest({ gate: 'check_prefix_stable_mutation', cases: selfTestCases(), minCases: 6, minRejectCases: 2 });
303
+ }
304
+ const quiet = argv.includes('--quiet');
305
+ const json = argv.includes('--json');
306
+ const ri = argv.indexOf('--root');
307
+ const rootArg = ri !== -1 ? argv[ri + 1] : undefined;
308
+ const root = rootArg !== undefined ? path.resolve(rootArg) : REPO_ROOT;
309
+
310
+ const ledger = new GateLedger('check_prefix_stable_mutation');
311
+ let verdict: Verdict;
312
+ try {
313
+ verdict = evaluate(root, ledger);
314
+ } catch (err) {
315
+ process.stderr.write(`❌ check_prefix_stable_mutation: ${(err as Error).message}\n`);
316
+ return 2;
317
+ }
318
+
319
+ try {
320
+ reportScanned({
321
+ gate: 'check_prefix_stable_mutation',
322
+ scanned: verdict.scanned,
323
+ units: 'mid-session hook script(s)',
324
+ roots: ['src/scripts/hook_manifest.yaml', ...prefixStableRoots()],
325
+ });
326
+ } catch (err) {
327
+ if (err instanceof DeadScopeError) {
328
+ process.stderr.write(`❌ check_prefix_stable_mutation: ${err.message}\n`);
329
+ return 2;
330
+ }
331
+ throw err;
332
+ }
333
+
334
+ ledger.report();
335
+
336
+ if (json) {
337
+ process.stdout.write(JSON.stringify(verdict, null, 2) + '\n');
338
+ return verdict.findings.length === 0 ? 0 : 1;
339
+ }
340
+
341
+ if (verdict.findings.length === 0) {
342
+ if (!quiet) {
343
+ process.stdout.write(
344
+ `✅ no mid-session mutation of a prefix-stable surface ` +
345
+ `(${String(verdict.scanned)} script(s) across ${String(verdict.midSessionConcerns)} mid-session concern(s), ` +
346
+ `${String(PREFIX_STABLE_SURFACES.length)} declared surface(s)).\n`,
347
+ );
348
+ }
349
+ return 0;
350
+ }
351
+
352
+ for (const f of verdict.findings) {
353
+ process.stderr.write(
354
+ `❌ ${f.concern} (${f.script}) fires on ${f.slots.join(', ')} and ${f.kind === 'literal' ? 'writes' : 'may write'} ` +
355
+ `inside prefix-stable surface '${f.surface}':\n ${f.evidence}\n`,
356
+ );
357
+ }
358
+ process.stderr.write(
359
+ `\n A mid-session write to a prefix-stable surface invalidates the prompt cache for the\n` +
360
+ ` rest of the session — the next call pays the cache-WRITE rate over the whole prefix.\n` +
361
+ ` Either move the write to a re-arm boundary (${RE_ARM_EVENTS.join(' / ')}), or declare\n` +
362
+ ` 're_arm: <event>' on the concern in src/scripts/hook_manifest.yaml naming the boundary\n` +
363
+ ` at which the rebuilt prefix is expected and paid for once.\n`,
364
+ );
365
+ return 1;
366
+ }
367
+
368
+ if (process.env['GATE_SELF_TEST_CHILD'] !== '1' || process.argv.includes('--root')) {
369
+ if (process.argv[1] !== undefined && import.meta.url === `file://${process.argv[1]}`) {
370
+ process.exit(main());
371
+ }
372
+ }
@@ -127,7 +127,7 @@ export function buildSurface(root: string, payload: readonly string[]): PublishS
127
127
  const shadowed = npmignore.filter((pat) => payload.some((p) => npmignoreMatches(pat, p)));
128
128
  return {
129
129
  _comment:
130
- 'GENERATED by src/scripts/check_publish_surface.ts --write. Do not hand-edit. ' +
130
+ 'GENERATED by check_publish_surface --write. Do not hand-edit. ' +
131
131
  'It records the resolved publish surface so a change to what ships is a diff a reviewer reads. ' +
132
132
  '`files[]` is an ALLOWLIST and it overrides .npmignore for anything under an included root — ' +
133
133
  'measured, after an attempt to strip compiled test artefacts via .npmignore had no effect because ' +
@@ -228,6 +228,15 @@ export function buildSurfaceFrom(npmignore: readonly string[], payload: readonly
228
228
  return npmignore.filter((pat) => payload.some((p) => npmignoreMatches(pat, p)));
229
229
  }
230
230
 
231
+ // ledger-exempt: this gate has ONE target, not a population. The verdict is a
232
+ // single comparison — `driftKey(committed)` against `driftKey(live)` — over the
233
+ // resolved publish surface as a whole. `payload` is the SCANNED corpus (guarded
234
+ // by `assertScanned` and published as `scanned:`), not a set of targets each
235
+ // reaching its own outcome: no file in it is ever individually skipped,
236
+ // completed or failed. A ledger here would plan one target and complete one
237
+ // target, which measures nothing and reads as adoption. The silent-skip class
238
+ // the ledger exists to catch is closed here by a different mechanism: the
239
+ // gate's own `--self-test` proves the comparison discriminates.
231
240
  export function main(argv: readonly string[] = process.argv.slice(2)): number {
232
241
  if (argv.includes('--self-test')) return selfTest();
233
242
  const payload = packPayload(REPO_ROOT);