@event4u/agent-config 9.14.0 → 9.16.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 (287) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +118 -99
  3. package/README.md +8 -0
  4. package/dist/agent-src/commands/agents/audit.md +7 -7
  5. package/dist/agent-src/commands/agents/optimize.md +1 -1
  6. package/dist/agent-src/commands/analyze/reference-repo.md +1 -1
  7. package/dist/agent-src/commands/chat-history.md +1 -0
  8. package/dist/agent-src/commands/context.md +1 -0
  9. package/dist/agent-src/commands/council.md +1 -1
  10. package/dist/agent-src/commands/ghostwriter.md +1 -1
  11. package/dist/agent-src/commands/memory.md +2 -1
  12. package/dist/agent-src/commands/module.md +1 -0
  13. package/dist/agent-src/commands/optimize/agents-dir.md +1 -1
  14. package/dist/agent-src/commands/optimize/deep.md +5 -2
  15. package/dist/agent-src/commands/optimize/skills.md +5 -5
  16. package/dist/agent-src/commands/override.md +1 -0
  17. package/dist/agent-src/commands/prediction-pool.md +4 -4
  18. package/dist/agent-src/commands/research/deep.md +1 -1
  19. package/dist/agent-src/commands/roadmap.md +2 -1
  20. package/dist/agent-src/commands/rule-compliance-audit.md +1 -1
  21. package/dist/agent-src/commands/tdd.md +1 -0
  22. package/dist/agent-src/commands/tests.md +1 -0
  23. package/dist/agent-src/commands/upstream-contribute.md +5 -5
  24. package/dist/agent-src/commands/video/from-script.md +1 -1
  25. package/dist/agent-src/commands/video/from-song.md +1 -1
  26. package/dist/agent-src/commands/video/scene.md +1 -1
  27. package/dist/agent-src/commands/video/stitch.md +1 -1
  28. package/dist/agent-src/commands/video/storyboard.md +1 -1
  29. package/dist/agent-src/commands/worktree/create.md +2 -1
  30. package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
  31. package/dist/agent-src/contexts/contracts/artifact-engagement-flow.md +5 -5
  32. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +3 -3
  33. package/dist/agent-src/contexts/contracts/emergency-triage-block.md +2 -2
  34. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +4 -3
  35. package/dist/agent-src/contexts/execution/subagent-routing.md +19 -0
  36. package/dist/agent-src/contexts/override-system.md +1 -1
  37. package/dist/agent-src/ghostwriter/fictional-fixture-v1.md +1 -1
  38. package/dist/agent-src/rules/augment-edit-discipline.md +1 -1
  39. package/dist/agent-src/rules/autonomous-execution.md +2 -1
  40. package/dist/agent-src/rules/brand-consistency.md +1 -0
  41. package/dist/agent-src/rules/brand-source-of-truth.md +1 -0
  42. package/dist/agent-src/rules/code-comment-discipline.md +2 -0
  43. package/dist/agent-src/rules/communication-through-line.md +1 -0
  44. package/dist/agent-src/rules/content-quoting-floor.md +1 -0
  45. package/dist/agent-src/rules/cross-source-consistency.md +0 -1
  46. package/dist/agent-src/rules/downstream-changes.md +1 -0
  47. package/dist/agent-src/rules/external-code-graph-interop.md +1 -0
  48. package/dist/agent-src/rules/external-reference-deep-dive.md +1 -0
  49. package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
  50. package/dist/agent-src/rules/finance-safety-floor.md +0 -1
  51. package/dist/agent-src/rules/icon-consistency.md +1 -0
  52. package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
  53. package/dist/agent-src/rules/improve-before-implement.md +1 -1
  54. package/dist/agent-src/rules/invite-challenge.md +1 -0
  55. package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
  56. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  57. package/dist/agent-src/rules/markdown-safe-codeblocks.md +1 -0
  58. package/dist/agent-src/rules/media-governance-routing.md +4 -3
  59. package/dist/agent-src/rules/media-sync-ground-truth.md +1 -0
  60. package/dist/agent-src/rules/no-attribution-footers.md +1 -0
  61. package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +1 -0
  62. package/dist/agent-src/rules/no-pr-progress-comments.md +1 -0
  63. package/dist/agent-src/rules/onboarding-gate.md +1 -0
  64. package/dist/agent-src/rules/output-discipline.md +1 -0
  65. package/dist/agent-src/rules/prefer-enums-over-literals.md +1 -0
  66. package/dist/agent-src/rules/preservation-guard.md +1 -0
  67. package/dist/agent-src/rules/question-not-instruction.md +1 -0
  68. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +1 -0
  69. package/dist/agent-src/rules/role-mode-adherence.md +1 -0
  70. package/dist/agent-src/rules/runtime-safety.md +1 -0
  71. package/dist/agent-src/rules/security-sensitive-stop.md +1 -0
  72. package/dist/agent-src/rules/session-canary.md +9 -2
  73. package/dist/agent-src/rules/source-confidentiality.md +1 -0
  74. package/dist/agent-src/rules/source-of-truth.md +1 -0
  75. package/dist/agent-src/rules/spreadsheet-source-quality.md +1 -0
  76. package/dist/agent-src/rules/think-before-action.md +1 -0
  77. package/dist/agent-src/rules/token-budget-discipline.md +1 -0
  78. package/dist/agent-src/rules/token-efficiency.md +1 -0
  79. package/dist/agent-src/rules/token-optimizer-maintenance.md +5 -5
  80. package/dist/agent-src/rules/tool-safety.md +1 -0
  81. package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
  82. package/dist/agent-src/rules/user-interaction.md +1 -0
  83. package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
  84. package/dist/agent-src/skills/adr-create/SKILL.md +1 -1
  85. package/dist/agent-src/skills/check-refs/SKILL.md +1 -1
  86. package/dist/agent-src/skills/competitive-positioning/SKILL.md +1 -1
  87. package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
  88. package/dist/agent-src/skills/context-authoring/SKILL.md +1 -1
  89. package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +1 -1
  90. package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
  91. package/dist/agent-src/skills/github-ci/SKILL.md +1 -0
  92. package/dist/agent-src/skills/guideline-writing/SKILL.md +1 -1
  93. package/dist/agent-src/skills/lint-skills/SKILL.md +1 -1
  94. package/dist/agent-src/skills/readme-reviewer/SKILL.md +1 -1
  95. package/dist/agent-src/skills/roadmap-management/SKILL.md +8 -1
  96. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  97. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  98. package/dist/agent-src/skills/skill-writing/SKILL.md +4 -4
  99. package/dist/agent-src/skills/token-optimizer/SKILL.md +4 -4
  100. package/dist/agent-src/skills/upstream-contribute/SKILL.md +6 -6
  101. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -0
  102. package/dist/agent-src/templates/AGENTS.md +1 -1
  103. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  104. package/dist/agent-src/templates/command.md +1 -1
  105. package/dist/agent-src/templates/copilot-instructions.md +3 -3
  106. package/dist/agent-src/templates/rule.md +2 -2
  107. package/dist/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
  108. package/dist/agent-src/templates/skill-archive-note.md +5 -5
  109. package/dist/agent-src/templates/skill.md +1 -1
  110. package/dist/cli/agent-config.js +66 -19
  111. package/dist/cli/agent-config.js.map +1 -1
  112. package/dist/cli/main.js +9 -4
  113. package/dist/cli/main.js.map +1 -1
  114. package/dist/cli/mcp/content.js +1 -1
  115. package/dist/cli/mcp/content.js.map +1 -1
  116. package/dist/cli/python/workspace_drive.js +2 -0
  117. package/dist/cli/python/workspace_drive.js.map +1 -1
  118. package/dist/cli/python/workspace_drive_health.js +2 -0
  119. package/dist/cli/python/workspace_drive_health.js.map +1 -1
  120. package/dist/cli/python/workspace_hosts.js +2 -0
  121. package/dist/cli/python/workspace_hosts.js.map +1 -1
  122. package/dist/cli/python/workspace_sessions.js +2 -0
  123. package/dist/cli/python/workspace_sessions.js.map +1 -1
  124. package/dist/cli/registry.js +1 -0
  125. package/dist/cli/registry.js.map +1 -1
  126. package/dist/cli-delegate/{chunk-A3HJ6NTI.js → chunk-63WSP6YZ.js} +2 -1
  127. package/dist/cli-delegate/{chunk-EJYIGZ3T.js → chunk-BKO2AZ7Z.js} +1 -1
  128. package/dist/cli-delegate/{chunk-GCVFLBP2.js → chunk-H574KOUH.js} +338 -494
  129. package/dist/cli-delegate/chunk-NXFWFAYV.js +339 -0
  130. package/dist/cli-delegate/{chunk-TEGJU2JX.js → chunk-ZXXJLL5K.js} +2 -2
  131. package/dist/cli-delegate/cmd_conformance.js +29 -23
  132. package/dist/cli-delegate/cmd_doctor.js +3 -3
  133. package/dist/cli-delegate/cmd_export.js +4 -4
  134. package/dist/cli-delegate/cmd_preflight.js +2 -2
  135. package/dist/cli-delegate/cmd_prune.js +4 -4
  136. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  137. package/dist/cli-delegate/cmd_sync.js +4 -4
  138. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  139. package/dist/cli-delegate/cmd_validate.js +4 -4
  140. package/dist/discovery/deprecation-report.md +1 -1
  141. package/dist/discovery/discovery-manifest.json +127 -95
  142. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  143. package/dist/discovery/discovery-manifest.summary.md +1 -1
  144. package/dist/discovery/orphan-report.md +1 -1
  145. package/dist/discovery/packs.json +2 -2
  146. package/dist/discovery/trust-report.md +1 -1
  147. package/dist/discovery/workspaces.json +2 -2
  148. package/dist/hooks/dispatch.js +729 -430
  149. package/dist/install/install.mjs +52 -11
  150. package/dist/install/rule_scope.js +18 -2
  151. package/dist/install/rule_scope.js.map +1 -1
  152. package/dist/mcp/registry-manifest.json +2 -2
  153. package/dist/mcp/server.json +2 -2
  154. package/dist/mcp/server.mjs +518 -411
  155. package/dist/router.json +1 -1
  156. package/dist/scripts/_lib/claude_settings_hooks.js +30 -5
  157. package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
  158. package/dist/scripts/_lib/scoped_projection.js +223 -0
  159. package/dist/scripts/_lib/scoped_projection.js.map +1 -0
  160. package/dist/scripts/_lib/surface_tiers.js +125 -0
  161. package/dist/scripts/_lib/surface_tiers.js.map +1 -0
  162. package/dist/scripts/_lib/token_count.js +166 -0
  163. package/dist/scripts/_lib/token_count.js.map +1 -0
  164. package/dist/scripts/_lib/trigger_eval_floors.js +8 -0
  165. package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -1
  166. package/dist/scripts/condense.js +139 -9
  167. package/dist/scripts/condense.js.map +1 -1
  168. package/dist/scripts/project_thin_rules.js +450 -0
  169. package/dist/scripts/project_thin_rules.js.map +1 -0
  170. package/dist/server/routes/workspace.js +3 -1
  171. package/dist/server/routes/workspace.js.map +1 -1
  172. package/dist/server/schemas/settings.js +9 -3
  173. package/dist/server/schemas/settings.js.map +1 -1
  174. package/dist/shared/userMd/utils.js +1 -1
  175. package/dist/shared/userMd/utils.js.map +1 -1
  176. package/dist/ui/assets/index-DmR0mbk4.js +26 -0
  177. package/dist/ui/assets/index-DmR0mbk4.js.map +1 -0
  178. package/dist/ui/index.html +1 -1
  179. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  180. package/package.json +14 -14
  181. package/src/agent-src/templates/AGENTS.md +1 -1
  182. package/src/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
  183. package/src/config/agent-settings.template.yml +61 -11
  184. package/src/config/budget-routing.json +7 -0
  185. package/src/config/ci-local-parity.yml +8 -0
  186. package/src/config/discovery/unassigned-artefacts.yml +23 -25
  187. package/src/config/evaluator-budgets.json +35 -5
  188. package/src/config/gate-coverage.yml +147 -0
  189. package/src/config/gate-violation-baselines.json +25 -0
  190. package/src/install/rule_scope.ts +27 -2
  191. package/src/scripts/_cli/cmd_conformance.ts +13 -24
  192. package/src/scripts/_dispatch.bash +10 -0
  193. package/src/scripts/_lib/claude_settings_hooks.ts +30 -4
  194. package/src/scripts/_lib/gate_baseline.ts +213 -0
  195. package/src/scripts/_lib/scoped_projection.ts +50 -0
  196. package/src/scripts/_lib/tier_budget_routing.ts +360 -0
  197. package/src/scripts/_lib/tool_adapter_registry.ts +8 -1
  198. package/src/scripts/_lib/trigger_eval_floors.ts +9 -0
  199. package/src/scripts/_lib/trigger_routers.ts +17 -5
  200. package/src/scripts/adr/regenerate_index.ts +4 -3
  201. package/src/scripts/ai-video/lib/parse-blueprint.sh +1 -1
  202. package/src/scripts/ai_council/compile_corpus.ts +1 -1
  203. package/src/scripts/anchor_scoring_run.ts +1 -1
  204. package/src/scripts/audit_cloud_compatibility.ts +37 -2
  205. package/src/scripts/audit_initial_context.ts +53 -19
  206. package/src/scripts/audit_likelihood.ts +31 -2
  207. package/src/scripts/audit_user_type_axis.ts +58 -11
  208. package/src/scripts/bench_hook_latency.ts +172 -41
  209. package/src/scripts/bench_persona_placebo.ts +1 -1
  210. package/src/scripts/bench_quality_run.ts +1 -1
  211. package/src/scripts/build_mcp_catalog.ts +12 -1
  212. package/src/scripts/check_always_budget.ts +122 -11
  213. package/src/scripts/check_augment_description_cap.ts +24 -3
  214. package/src/scripts/check_ci_strict_superset.ts +150 -0
  215. package/src/scripts/check_cli_registry_budget_sync.ts +168 -0
  216. package/src/scripts/check_cluster_patterns.ts +100 -0
  217. package/src/scripts/check_consumer_scope_flip.ts +26 -4
  218. package/src/scripts/check_evaluator_budgets.ts +198 -19
  219. package/src/scripts/check_gate_coverage.ts +338 -2
  220. package/src/scripts/check_generator_output_coverage.ts +2 -1
  221. package/src/scripts/check_iron_law_prominence.ts +1 -1
  222. package/src/scripts/check_kernel_rule_bundle.ts +1 -1
  223. package/src/scripts/check_no_new_legacy_path.ts +167 -3
  224. package/src/scripts/check_pack_size.ts +18 -4
  225. package/src/scripts/check_portability.ts +37 -7
  226. package/src/scripts/check_references.ts +19 -3
  227. package/src/scripts/check_release_pr_shape.ts +13 -0
  228. package/src/scripts/check_token_quality_golden.ts +1 -1
  229. package/src/scripts/check_token_regression.ts +9 -2
  230. package/src/scripts/condense.ts +155 -12
  231. package/src/scripts/cost/budget.mjs +77 -2
  232. package/src/scripts/evaluator_umbrella.sh +34 -1
  233. package/src/scripts/generate_capability_matrix.ts +8 -0
  234. package/src/scripts/generate_router_coverage_from_matrix.ts +155 -0
  235. package/src/scripts/hook_manifest.yaml +17 -3
  236. package/src/scripts/hooks/block_config_weakening.ts +337 -0
  237. package/src/scripts/hooks/concern_registry.ts +2 -0
  238. package/src/scripts/hooks/probe_throttle.ts +75 -0
  239. package/src/scripts/install-hooks.sh +24 -2
  240. package/src/scripts/install.ts +2 -2
  241. package/src/scripts/lint_agents_md.ts +9 -3
  242. package/src/scripts/lint_archived_skills.ts +1 -1
  243. package/src/scripts/lint_artefact_frontmatter.ts +185 -37
  244. package/src/scripts/lint_command_routing.ts +21 -52
  245. package/src/scripts/lint_command_verbs.ts +47 -28
  246. package/src/scripts/lint_context_spine_usage.ts +38 -12
  247. package/src/scripts/lint_framework_leakage.ts +25 -6
  248. package/src/scripts/lint_handoffs.ts +49 -2
  249. package/src/scripts/lint_media_policy_linkage.ts +62 -30
  250. package/src/scripts/lint_namespace.ts +92 -7
  251. package/src/scripts/lint_new_skill_gate.ts +39 -31
  252. package/src/scripts/lint_originality_shingles.ts +7 -1
  253. package/src/scripts/lint_override_kernel_guard.ts +21 -3
  254. package/src/scripts/lint_pack_boundaries.ts +461 -91
  255. package/src/scripts/lint_pack_dependencies.ts +24 -57
  256. package/src/scripts/lint_persona_governance.ts +1 -1
  257. package/src/scripts/lint_role_experiences.ts +34 -5
  258. package/src/scripts/lint_trigger_precision.ts +142 -0
  259. package/src/scripts/mcp_server/consumer_tool_catalog.json +12 -201
  260. package/src/scripts/mcp_server/tools.ts +1 -1
  261. package/src/scripts/prepack-check.mjs +40 -1
  262. package/src/scripts/prepack_router_targets.d.mts +7 -0
  263. package/src/scripts/prepack_router_targets.mjs +93 -0
  264. package/src/scripts/print_required_checks.ts +72 -18
  265. package/src/scripts/profile_staleness_hook.ts +12 -0
  266. package/src/scripts/record_evaluator_measurements.mjs +105 -0
  267. package/src/scripts/recruit_preflight.sh +1 -1
  268. package/src/scripts/release.ts +214 -7
  269. package/src/scripts/router_target_paths.d.mts +4 -0
  270. package/src/scripts/router_target_paths.mjs +69 -0
  271. package/src/scripts/router_telemetry.ts +52 -1
  272. package/src/scripts/routing_doctor.ts +560 -0
  273. package/src/scripts/rule_trigger_eval.ts +403 -0
  274. package/src/scripts/schemas/rule.schema.json +5 -1
  275. package/src/scripts/session_canary_hook.ts +96 -5
  276. package/src/scripts/skill_linter.ts +9 -28
  277. package/src/scripts/smoke/skills.sh +1 -1
  278. package/src/scripts/sweep_dead_scan_roots.ts +356 -10
  279. package/src/scripts/trigger_coverage.ts +13 -2
  280. package/src/scripts/update_counts.ts +1 -1
  281. package/src/scripts/validate_telegraph_carveouts.ts +1 -1
  282. package/src/scripts/wrapper_freshness_hook.ts +14 -0
  283. package/src/server/schemas/settings.ts +19 -5
  284. package/src/shared/userMd/utils.ts +1 -1
  285. package/dist/cli-delegate/chunk-TJIAM5FW.js +0 -133
  286. package/dist/ui/assets/index-BpSQt5vm.js +0 -40
  287. package/dist/ui/assets/index-BpSQt5vm.js.map +0 -1
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Derive the routing-matrix router-coverage corpus (road-to-tested-routing
4
+ * Phase 2, PREREG P2 corpus expansion).
5
+ *
6
+ * Reads every per-rule routing matrix under `tests/eval/routing-matrix/`
7
+ * and emits ONE generated corpus file,
8
+ * `internal/bench/corpora/router-coverage/routing-matrix-derived.yaml`,
9
+ * with one `router-coverage` prompt per matrix POSITIVE, labelled
10
+ * `intended_triggers: [<rule>]`. Near-misses are deliberately NOT emitted —
11
+ * the corpus measures intended activation; the matrices' negative half is
12
+ * enforced by `tests/scripts/routing_matrix.test.ts`.
13
+ *
14
+ * Ownership (two-gate pattern): this generator is the only writer;
15
+ * `--check` re-derives in memory and exits 1 on drift, so the derived file
16
+ * can never be hand-edited into disagreement with its source matrices.
17
+ */
18
+ import * as fs from "node:fs";
19
+ import * as path from "node:path";
20
+ import { fileURLToPath, pathToFileURL } from "node:url";
21
+ import { parse as parseYaml } from "yaml";
22
+
23
+ const REPO_ROOT = path.resolve(
24
+ path.dirname(fileURLToPath(import.meta.url)),
25
+ "..",
26
+ "..",
27
+ );
28
+ const MATRIX_DIR = path.join(REPO_ROOT, "tests", "eval", "routing-matrix");
29
+ export const OUT_FILE = path.join(
30
+ REPO_ROOT,
31
+ "internal",
32
+ "bench",
33
+ "corpora",
34
+ "router-coverage",
35
+ "routing-matrix-derived.yaml",
36
+ );
37
+
38
+ interface MatrixCase {
39
+ prompt: string;
40
+ open_files?: string[];
41
+ command?: string;
42
+ }
43
+ interface MatrixFile {
44
+ rule: string;
45
+ positives: MatrixCase[];
46
+ near_misses: MatrixCase[];
47
+ }
48
+
49
+ /** Cheap language tag for the corpus `language:` field. */
50
+ export function detect_language(prompt: string): "de" | "en" {
51
+ if (/[äöüßÄÖÜ]/.test(prompt)) {
52
+ return "de";
53
+ }
54
+ return /(^|\s)(der|die|das|und|nicht|bitte|keine?|mach|prüfe|erstelle|für)(\s|$)/i.test(prompt)
55
+ ? "de"
56
+ : "en";
57
+ }
58
+
59
+ function yamlQuote(s: string): string {
60
+ return JSON.stringify(s);
61
+ }
62
+
63
+ export function derive(): string {
64
+ const files = fs
65
+ .readdirSync(MATRIX_DIR)
66
+ .filter((f) => f.endsWith(".yaml"))
67
+ .sort();
68
+ const lines: string[] = [];
69
+ lines.push("# Router-coverage corpus — DERIVED FROM the per-rule routing matrices.");
70
+ lines.push("#");
71
+ lines.push("# GENERATED FILE — do not hand-edit. Source of truth:");
72
+ lines.push("# tests/eval/routing-matrix/*.yaml");
73
+ lines.push("# Regenerate: npx tsx src/scripts/generate_router_coverage_from_matrix.ts");
74
+ lines.push("# Drift gate: npx tsx src/scripts/generate_router_coverage_from_matrix.ts --check");
75
+ lines.push("");
76
+ lines.push("version: 1");
77
+ lines.push("corpus_id: router-coverage-routing-matrix-derived");
78
+ lines.push("selection_accuracy_target: 0.0");
79
+ lines.push("");
80
+ lines.push("prompts:");
81
+ for (const f of files) {
82
+ const parsed = parseYaml(fs.readFileSync(path.join(MATRIX_DIR, f), "utf-8")) as MatrixFile;
83
+ parsed.positives.forEach((c, i) => {
84
+ const id = `rm-${parsed.rule}-${String(i + 1).padStart(2, "0")}`;
85
+ lines.push(` - id: ${id}`);
86
+ lines.push(" category: router-coverage");
87
+ lines.push(` language: ${detect_language(c.prompt)}`);
88
+ lines.push(` prompt: ${yamlQuote(c.prompt)}`);
89
+ lines.push(" expected_skills: []");
90
+ lines.push(` intended_triggers: [${parsed.rule}]`);
91
+ if (c.open_files && c.open_files.length > 0) {
92
+ lines.push(` open_files: [${c.open_files.map(yamlQuote).join(", ")}]`);
93
+ }
94
+ lines.push("");
95
+ });
96
+ }
97
+ return `${lines.join("\n").trimEnd()}\n`;
98
+ }
99
+
100
+ export function main(argv?: string[]): number {
101
+ const args = argv ?? process.argv.slice(2);
102
+ const check = args.includes("--check");
103
+ const derived = derive();
104
+ if (check) {
105
+ let current = "";
106
+ try {
107
+ current = fs.readFileSync(OUT_FILE, "utf-8");
108
+ } catch {
109
+ process.stderr.write(`missing derived corpus: ${OUT_FILE} — run the generator\n`);
110
+ return 1;
111
+ }
112
+ if (current !== derived) {
113
+ process.stderr.write(
114
+ "routing-matrix-derived.yaml is stale — regenerate via generate_router_coverage_from_matrix\n",
115
+ );
116
+ return 1;
117
+ }
118
+ process.stdout.write("routing-matrix-derived corpus: fresh\n");
119
+ return 0;
120
+ }
121
+ fs.writeFileSync(OUT_FILE, derived, "utf-8");
122
+ const promptCount = (derived.match(/^ {2}- id: /gm) ?? []).length;
123
+ const ruleCount = new Set(
124
+ [...derived.matchAll(/intended_triggers: \[([^\]]+)\]/g)].map((m) => m[1]),
125
+ ).size;
126
+ process.stdout.write(
127
+ `wrote ${path.relative(REPO_ROOT, OUT_FILE)} — ${promptCount} prompts, ${ruleCount} distinct rule ids\n`,
128
+ );
129
+ return 0;
130
+ }
131
+
132
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
133
+ function _isCliEntry(): boolean {
134
+ if (typeof __AGENT_CONFIG_BUNDLE__ !== "undefined" && __AGENT_CONFIG_BUNDLE__) {
135
+ return false;
136
+ }
137
+ if (process.argv[1] === undefined) {
138
+ return false;
139
+ }
140
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
141
+ if (import.meta.url === argvUrl) {
142
+ return true;
143
+ }
144
+ try {
145
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
146
+ const argv1 = fs.realpathSync(path.resolve(process.argv[1]));
147
+ return here === argv1;
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ if (_isCliEntry()) {
154
+ process.exit(main());
155
+ }
@@ -91,6 +91,20 @@ concerns:
91
91
  script: src/scripts/hooks/block_kernel_rule_writes.ts
92
92
  args: []
93
93
  fail_closed: true
94
+ # road-to-renewal-leverage Phase 2 — "fix the code, not the config".
95
+ # PreToolUse guard over the recorded allowlist-growth antipattern
96
+ # (autonomous-execution § Antipattern): warns as an allowlist grows within a
97
+ # session and BLOCKS past the cap that rule already states in words (>20
98
+ # entries in one session = the linter is wrong). Baselines and budget
99
+ # thresholds warn only — whether a rising count is weakening or a legitimate
100
+ # ratchet reset is not decidable from the edit alone. Council 2026-08-02
101
+ # rejected inventing a "fix-loop is active" predicate; the SESSION is the
102
+ # window the recorded rule already names. fail_closed: false — a malformed
103
+ # envelope must never block an unrelated edit.
104
+ block-config-weakening:
105
+ script: src/scripts/hooks/block_config_weakening.ts
106
+ args: []
107
+ fail_closed: false
94
108
  # token-saving Phase 3 — PreToolUse RTK-wrap nudge. DETERMINISTIC: keys off a
95
109
  # live `which rtk` probe (not the self-reported flag). When rtk is installed
96
110
  # and the agent is about to run a single verbose CLI command that is NOT
@@ -211,7 +225,7 @@ platforms:
211
225
  session_start: [chat-history, hot-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
212
226
  session_end: [chat-history, memory-learn]
213
227
  stop: [chat-history, hot-context, verify-before-complete]
214
- pre_tool_use: [block-no-verify, block-kernel-rule-writes, rtk-wrap, design-slop, code-graph-nudge]
228
+ pre_tool_use: [block-no-verify, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
215
229
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
216
230
 
217
231
  claude:
@@ -219,7 +233,7 @@ platforms:
219
233
  session_end: [chat-history, memory-learn]
220
234
  stop: [chat-history, hot-context, verify-before-complete, team-review-gate]
221
235
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
222
- pre_tool_use: [block-no-verify, block-kernel-rule-writes, rtk-wrap, design-slop, code-graph-nudge]
236
+ pre_tool_use: [block-no-verify, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
223
237
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
224
238
 
225
239
  # Cowork — the Claude desktop app's local-agent-mode runtime, built
@@ -241,7 +255,7 @@ platforms:
241
255
  session_end: [chat-history, memory-learn]
242
256
  stop: [chat-history, hot-context, verify-before-complete]
243
257
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
244
- pre_tool_use: [block-no-verify, block-kernel-rule-writes, rtk-wrap, design-slop, code-graph-nudge]
258
+ pre_tool_use: [block-no-verify, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
245
259
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
246
260
 
247
261
  # Phase 7.5 — Cursor. `.cursor/hooks.json` (project) is read by the
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env node
2
+ // MIGRATE: precompiled-hook-layer — carry this hook when road-to-credible-install
3
+ // Phase 1 precompiles the hook path (touch-once preserved at migration).
4
+ /**
5
+ * PreToolUse guard: "fix the code, not the config."
6
+ *
7
+ * The recorded antipattern (`autonomous-execution` § Antipattern —
8
+ * allowlist-growth as silent budget bypass): during a fix loop the agent
9
+ * weakens the gate instead of the code, growing a lint allowlist entry by
10
+ * entry until the linter no longer objects. The rule already states the
11
+ * threshold in words —
12
+ *
13
+ * ALLOWLIST > 20 ENTRIES IN ONE SESSION = THE LINTER IS WRONG.
14
+ * STOP. PROPOSE LINTER REDESIGN OR REMOVAL.
15
+ *
16
+ * — and two linters cite it in their own failure messages. Nothing enforced
17
+ * it: by the time CI sees the diff the session has already spent its budget
18
+ * on the wrong fix. This hook moves that stated threshold to tool-call time.
19
+ *
20
+ * Deliberately NOT built (council 2026-08-02, anthropic/claude-sonnet-4-5 +
21
+ * openai/gpt-4o):
22
+ *
23
+ * - **No invented "fix-loop is active" predicate.** Both members rejected it
24
+ * as speculative — no such state exists in the repo and defining entry/exit
25
+ * invites false positives in both directions. The SESSION is the window the
26
+ * recorded rule already names, so the session id off the hook envelope is
27
+ * the predicate, and it is not an invention.
28
+ * - **No blocking on baselines or budgets.** Whether a violation-baseline
29
+ * count rising is weakening or a legitimate ratchet reset after a refactor
30
+ * is not mechanically decidable from the edit alone. Those surfaces warn
31
+ * (exit 2) and stay a review decision; only allowlist growth — mechanical,
32
+ * countable, and already carrying a stated numeric threshold — blocks.
33
+ *
34
+ * Cumulative within a session, per allowlist file. Small additions warn from
35
+ * the first one so the count is visible while it is still cheap to change
36
+ * course; crossing the recorded cap blocks.
37
+ *
38
+ * Exit codes (docs/contracts/hook-architecture-v1.md):
39
+ * 0 — allow
40
+ * 1 — block
41
+ * 2 — warn
42
+ */
43
+
44
+ import * as fs from 'node:fs';
45
+ import * as path from 'node:path';
46
+ import { fileURLToPath, pathToFileURL } from 'node:url';
47
+
48
+ import { EDIT_TOOLS } from '../minimal_safe_diff_hook.js';
49
+ import { readHookStdin } from './hook_stdin.js';
50
+
51
+ const _HERE = fileURLToPath(import.meta.url);
52
+
53
+ const EXIT_ALLOW = 0;
54
+ const EXIT_BLOCK = 1;
55
+ const EXIT_WARN = 2;
56
+
57
+ /**
58
+ * The stated cap from `autonomous-execution` § Antipattern. Crossing it in one
59
+ * session is defined there as the third validation-target failure — the point
60
+ * at which the linter, not the content, is wrong.
61
+ */
62
+ export const SESSION_ENTRY_CAP = 20;
63
+
64
+ /** Warn from this many cumulative added entries so the count stays visible. */
65
+ export const SESSION_WARN_AT = 5;
66
+
67
+ type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
68
+ type JsonObject = { [k: string]: JsonValue };
69
+
70
+ function _isObject(v: JsonValue | undefined): v is JsonObject {
71
+ return v !== null && v !== undefined && typeof v === 'object' && !Array.isArray(v);
72
+ }
73
+
74
+ const _PATH_KEYS: readonly string[] = ['file_path', 'path', 'target_file', 'filePath', 'notebook_path'];
75
+
76
+ /**
77
+ * Config surfaces this guard recognises.
78
+ *
79
+ * `allowlist` is the blocking class — an entry count is a plain number and the
80
+ * recorded rule already fixes its threshold. `advisory` covers the surfaces
81
+ * where "weakening" needs context a tool call does not carry (a baseline count
82
+ * may legitimately reset after a refactor); those warn and never block.
83
+ */
84
+ export type ConfigKind = 'allowlist' | 'advisory' | null;
85
+
86
+ /** Classify a target path into a config surface, or null for everything else. */
87
+ export function classify_target(p: string): ConfigKind {
88
+ const posix = p.split(path.sep).join('/');
89
+ const base = posix.split('/').pop() ?? '';
90
+ if (/_allowlist\.(json|txt)$/.test(base) || /^allowlist[-_.]/.test(base)) {
91
+ return 'allowlist';
92
+ }
93
+ if (/(^|-)baselines?\.json$/.test(base)) {
94
+ return 'advisory';
95
+ }
96
+ if (/-budget(s)?\.(json|ya?ml)$/.test(base) || base === 'budgets.yml') {
97
+ return 'advisory';
98
+ }
99
+ return null;
100
+ }
101
+
102
+ /**
103
+ * Count allowlist entries in a blob.
104
+ *
105
+ * JSON: every string leaf in the structure (arrays of paths, and
106
+ * `{"rule": ["path", …]}` maps alike) — parsing beats a line count because a
107
+ * reformat would otherwise read as growth. Falls back to non-blank,
108
+ * non-comment lines for `.txt` allowlists and for JSON fragments that do not
109
+ * parse on their own (an `Edit` payload is rarely a whole document).
110
+ */
111
+ export function count_entries(text: string): number {
112
+ const trimmed = text.trim();
113
+ if (trimmed) {
114
+ try {
115
+ let n = 0;
116
+ const walk = (v: unknown): void => {
117
+ if (typeof v === 'string') {
118
+ n += 1;
119
+ } else if (Array.isArray(v)) {
120
+ v.forEach(walk);
121
+ } else if (v !== null && typeof v === 'object') {
122
+ Object.values(v as Record<string, unknown>).forEach(walk);
123
+ }
124
+ };
125
+ walk(JSON.parse(trimmed));
126
+ return n;
127
+ } catch {
128
+ /* not a standalone JSON document — fall through */
129
+ }
130
+ }
131
+ return text
132
+ .split(/\r?\n/)
133
+ .map((l) => l.trim())
134
+ .filter((l) => l.length > 0 && !l.startsWith('#') && !l.startsWith('//')).length;
135
+ }
136
+
137
+ /**
138
+ * Entries this single tool call adds to an allowlist.
139
+ *
140
+ * `Write` carries the whole new document, so the delta is measured against
141
+ * what is on disk. `Edit` carries a replacement pair, so the delta is the pair
142
+ * itself. Negative results (a shrinking allowlist — the direction this guard
143
+ * wants) collapse to 0: removals must never bank credit against a later
144
+ * addition.
145
+ */
146
+ export function added_entries(
147
+ tool_input: JsonObject,
148
+ on_disk: string | null,
149
+ ): number {
150
+ const content = tool_input['content'];
151
+ if (typeof content === 'string') {
152
+ const before = on_disk === null ? 0 : count_entries(on_disk);
153
+ return Math.max(0, count_entries(content) - before);
154
+ }
155
+ const oldS = tool_input['old_string'] ?? tool_input['oldStr'] ?? tool_input['old_str'];
156
+ const newS = tool_input['new_string'] ?? tool_input['newStr'] ?? tool_input['new_str'];
157
+ if (typeof newS === 'string') {
158
+ const before = typeof oldS === 'string' ? count_entries(oldS) : 0;
159
+ return Math.max(0, count_entries(newS) - before);
160
+ }
161
+ return 0;
162
+ }
163
+
164
+ export interface Decision {
165
+ action: 'allow' | 'warn' | 'block';
166
+ reason: string;
167
+ }
168
+
169
+ /** Decide from the cumulative session total for one allowlist file. */
170
+ export function decide(kind: ConfigKind, rel_path: string, session_total: number, added: number): Decision {
171
+ if (kind === null || added <= 0) {
172
+ return { action: 'allow', reason: '' };
173
+ }
174
+ if (kind === 'advisory') {
175
+ return {
176
+ action: 'warn',
177
+ reason:
178
+ `${rel_path}: this edit loosens a gate threshold. Whether that is a ` +
179
+ 'legitimate ratchet reset or the config being bent around a failing ' +
180
+ 'check is a review decision — state which in the diff.',
181
+ };
182
+ }
183
+ if (session_total > SESSION_ENTRY_CAP) {
184
+ return {
185
+ action: 'block',
186
+ reason:
187
+ `${rel_path}: ${session_total} allowlist entries added this session ` +
188
+ `(cap ${SESSION_ENTRY_CAP}). Per autonomous-execution § Antipattern — ` +
189
+ 'allowlist-growth as silent budget bypass, crossing this cap means the ' +
190
+ 'LINTER is wrong, not the content. Fix the code or redesign the check; ' +
191
+ 'do not expand the allowlist further.\n' +
192
+ ' Legitimate override is a human action outside this session: land the ' +
193
+ 'linter redesign, or remove the `block-config-weakening` entry in ' +
194
+ 'src/scripts/hook_manifest.yaml.',
195
+ };
196
+ }
197
+ if (session_total >= SESSION_WARN_AT) {
198
+ return {
199
+ action: 'warn',
200
+ reason:
201
+ `${rel_path}: ${session_total} allowlist entries added this session ` +
202
+ `(cap ${SESSION_ENTRY_CAP}). Fix the code, not the config.`,
203
+ };
204
+ }
205
+ return { action: 'allow', reason: '' };
206
+ }
207
+
208
+ // --- session state -------------------------------------------------------
209
+
210
+ function _state_file(root: string): string {
211
+ return path.join(root, 'agents', 'runtime', 'state', 'config-weakening.json');
212
+ }
213
+
214
+ /** Read the cumulative count, add `added`, persist, and return the new total. */
215
+ export function bump_session(root: string, session: string, key: string, added: number): number {
216
+ const f = _state_file(root);
217
+ let data: Record<string, Record<string, number>> = {};
218
+ try {
219
+ data = JSON.parse(fs.readFileSync(f, 'utf-8')) as Record<string, Record<string, number>>;
220
+ } catch {
221
+ data = {};
222
+ }
223
+ const bucket = data[session] ?? {};
224
+ const total = (bucket[key] ?? 0) + added;
225
+ bucket[key] = total;
226
+ data[session] = bucket;
227
+ try {
228
+ fs.mkdirSync(path.dirname(f), { recursive: true });
229
+ fs.writeFileSync(f, `${JSON.stringify(data, null, 2)}\n`, 'utf-8');
230
+ } catch {
231
+ /* state is an optimisation, never a gate — a read-only tree still decides */
232
+ }
233
+ return total;
234
+ }
235
+
236
+ // --- entry ---------------------------------------------------------------
237
+
238
+ function _extract(envelope: JsonObject): { tool: string; paths: string[]; ti: JsonObject | null } {
239
+ const payload = _isObject(envelope['payload']) ? envelope['payload'] : envelope;
240
+ const nameVal =
241
+ payload['tool_name'] ?? payload['toolName'] ?? payload['tool'] ?? envelope['tool_name'] ?? envelope['tool'];
242
+ const tool = typeof nameVal === 'string' ? nameVal : '';
243
+ const ti = _isObject(payload['tool_input'])
244
+ ? payload['tool_input']
245
+ : _isObject(envelope['tool_input'])
246
+ ? envelope['tool_input']
247
+ : null;
248
+ const paths: string[] = [];
249
+ if (ti !== null) {
250
+ for (const key of _PATH_KEYS) {
251
+ const v = ti[key];
252
+ if (typeof v === 'string' && v) {
253
+ paths.push(v);
254
+ }
255
+ }
256
+ }
257
+ return { tool, paths, ti };
258
+ }
259
+
260
+ export function main(): number {
261
+ let envelope: JsonValue;
262
+ try {
263
+ const raw = readHookStdin();
264
+ envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
265
+ } catch {
266
+ return EXIT_ALLOW;
267
+ }
268
+ if (!_isObject(envelope)) {
269
+ return EXIT_ALLOW;
270
+ }
271
+
272
+ const { tool, paths, ti } = _extract(envelope);
273
+ if (!tool || !EDIT_TOOLS.has(tool) || ti === null) {
274
+ return EXIT_ALLOW;
275
+ }
276
+
277
+ const cwd = envelope['cwd'];
278
+ const pr = envelope['workspace_root'] ?? envelope['project_root'];
279
+ const root = typeof cwd === 'string' && cwd ? cwd : typeof pr === 'string' && pr ? pr : '.';
280
+ const sidRaw = envelope['session_id'] ?? envelope['sessionId'];
281
+ const session = typeof sidRaw === 'string' && sidRaw ? sidRaw : 'default';
282
+
283
+ for (const p of paths) {
284
+ const kind = classify_target(p);
285
+ if (kind === null) {
286
+ continue;
287
+ }
288
+ const abs = path.isAbsolute(p) ? p : path.join(root, p);
289
+ let on_disk: string | null = null;
290
+ try {
291
+ on_disk = fs.readFileSync(abs, 'utf-8');
292
+ } catch {
293
+ on_disk = null;
294
+ }
295
+ const added = added_entries(ti, on_disk);
296
+ if (added <= 0) {
297
+ continue;
298
+ }
299
+ const rel = path.relative(root, abs) || p;
300
+ const total = kind === 'allowlist' ? bump_session(root, session, rel, added) : added;
301
+ const d = decide(kind, rel, total, added);
302
+ if (d.action === 'block') {
303
+ process.stderr.write(`block-config-weakening: BLOCKED — ${d.reason}\n`);
304
+ return EXIT_BLOCK;
305
+ }
306
+ if (d.action === 'warn') {
307
+ process.stdout.write(`${JSON.stringify({ decision: 'warn', reason: d.reason })}\n`);
308
+ return EXIT_WARN;
309
+ }
310
+ }
311
+ return EXIT_ALLOW;
312
+ }
313
+
314
+ // Bundle-safety: never auto-run when inlined into an esbuild bundle, where
315
+ // every module shares the bundle's `import.meta.url`.
316
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
317
+ function _isCliEntry(): boolean {
318
+ if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
319
+ return false;
320
+ }
321
+ if (process.argv[1] === undefined) {
322
+ return false;
323
+ }
324
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
325
+ if (import.meta.url === argvUrl) {
326
+ return true;
327
+ }
328
+ try {
329
+ return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
330
+ } catch {
331
+ return false;
332
+ }
333
+ }
334
+
335
+ if (_isCliEntry() || process.argv[1] === _HERE) {
336
+ process.exit(main());
337
+ }
@@ -42,6 +42,7 @@ import { main as prUrlReminderMain } from '../pr_url_reminder_hook.js';
42
42
  import { main as sessionCanaryMain } from '../session_canary_hook.js';
43
43
  import { main as blockNoVerifyMain } from './block_no_verify.js';
44
44
  import { main as blockKernelRuleWritesMain } from './block_kernel_rule_writes.js';
45
+ import { main as blockConfigWeakeningMain } from './block_config_weakening.js';
45
46
  import { main as rtkWrapMain } from './rtk_wrap_hook.js';
46
47
  import { main as designSlopMain } from './design_slop_hook.js';
47
48
  import { main as codeGraphNudgeMain } from './code_graph_nudge_hook.js';
@@ -69,6 +70,7 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
69
70
  'src/scripts/session_canary_hook.ts': sessionCanaryMain as ConcernMain,
70
71
  'src/scripts/hooks/block_no_verify.ts': blockNoVerifyMain as ConcernMain,
71
72
  'src/scripts/hooks/block_kernel_rule_writes.ts': blockKernelRuleWritesMain as ConcernMain,
73
+ 'src/scripts/hooks/block_config_weakening.ts': blockConfigWeakeningMain as ConcernMain,
72
74
  'src/scripts/hooks/rtk_wrap_hook.ts': rtkWrapMain as ConcernMain,
73
75
  'src/scripts/hooks/design_slop_hook.ts': designSlopMain as ConcernMain,
74
76
  'src/scripts/hooks/code_graph_nudge_hook.ts': codeGraphNudgeMain as ConcernMain,
@@ -0,0 +1,75 @@
1
+ /**
2
+ * probe_throttle — daily throttle for session_start probe concerns
3
+ * (road-to-hook-latency-repair Phase 2, SessionStart fan-out lever).
4
+ *
5
+ * `profile-staleness`, `wrapper-freshness`, and `surface-probe` are
6
+ * defense-in-depth probes whose findings do not change within a day; running
7
+ * them on EVERY session_start pays their cost per session for no new
8
+ * information. Each probe checks a per-concern stamp file under
9
+ * `agents/runtime/state/probe-throttle/` and skips when the stamp's mtime is
10
+ * from the same LOCAL calendar day (a skipped probe re-runs tomorrow).
11
+ *
12
+ * Deliberate simplicity (roadmap lock — "an mtime file, not a daemon"):
13
+ * - The stamp carries NO semantics beyond its mtime. Deleting the file or
14
+ * the directory only changes WHEN a probe re-runs, never what it answers.
15
+ * - Same-day equality is skew-safe: a stamp dated in the future or the past
16
+ * is simply "not today", so the probe runs and re-stamps — there is no
17
+ * lockout state (council 2026-08-03).
18
+ * - Fail-open both ways: an unreadable stamp means "run the probe"; an
19
+ * unwritable state dir means the probe simply runs again next session.
20
+ * - Replay runs (AGENT_CONFIG_REPLAY=1) bypass the throttle and never
21
+ * write stamps — fixture re-execution and the latency bench must be
22
+ * deterministic, not dependent on yesterday's stamps (state_io replay
23
+ * contract).
24
+ */
25
+
26
+ import fs from 'node:fs';
27
+ import path from 'node:path';
28
+
29
+ import { is_replay_mode } from './state_io.js';
30
+
31
+ const STATE_SUBDIR = ['agents', 'runtime', 'state', 'probe-throttle'] as const;
32
+
33
+ function _stampPath(projectRoot: string, concern: string): string {
34
+ return path.join(projectRoot, ...STATE_SUBDIR, `${concern}.stamp`);
35
+ }
36
+
37
+ /** Local-calendar-day key (YYYY-MM-DD in local time, not UTC). */
38
+ function _localDay(d: Date): string {
39
+ const y = d.getFullYear();
40
+ const m = String(d.getMonth() + 1).padStart(2, '0');
41
+ const day = String(d.getDate()).padStart(2, '0');
42
+ return `${y}-${m}-${day}`;
43
+ }
44
+
45
+ /**
46
+ * True when the probe already ran today (same LOCAL calendar day as the
47
+ * stamp's mtime) and may be skipped. Replay mode and any read problem
48
+ * return false — the probe runs.
49
+ */
50
+ export function probeRanToday(projectRoot: string, concern: string): boolean {
51
+ if (is_replay_mode()) return false;
52
+ try {
53
+ const st = fs.statSync(_stampPath(projectRoot, concern));
54
+ return _localDay(st.mtime) === _localDay(new Date());
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Record that the probe ran now. Fail-silent — an unwritable state dir just
62
+ * means the probe runs again next session. No-op in replay mode.
63
+ */
64
+ export function stampProbeRun(projectRoot: string, concern: string): void {
65
+ if (is_replay_mode()) return;
66
+ try {
67
+ const p = _stampPath(projectRoot, concern);
68
+ fs.mkdirSync(path.dirname(p), { recursive: true });
69
+ // Content is a human-readable timestamp for debugging only — the
70
+ // mtime is the sole semantic carrier.
71
+ fs.writeFileSync(p, `${new Date().toISOString()}\n`);
72
+ } catch {
73
+ /* fail-silent */
74
+ }
75
+ }
@@ -266,10 +266,20 @@ write_chat_history_hook "post-rewrite" "git:post-rewrite"
266
266
  # `.augment/`, etc. projections until they remember to run `task sync`.
267
267
  # These hooks bridge that gap: fast idempotent re-projection.
268
268
  #
269
+ # The same staleness bites the compiled CLI: the Claude/agent hooks run
270
+ # `./agent-config` → `dist/cli/` (gitignored tsc output). A pull that adds
271
+ # or moves a TS module leaves `dist/` stale and EVERY subsequent tool-use
272
+ # hook fails with ERR_MODULE_NOT_FOUND until someone rebuilds. So when the
273
+ # range touches TS sources, rebuild via `npm run build:cli` — tsc only,
274
+ # deliberately NOT the full `npm run build`: the install-bundle step
275
+ # rewrites the committed `dist/install/install.mjs` and has produced
276
+ # spurious tracked diffs from worktrees before.
277
+ #
269
278
  # Bypass: `git pull --no-verify` does not exist, but devs can disable the
270
279
  # hooks per-command via `git -c core.hooksPath=/dev/null ...` or by
271
280
  # editing the file. Runtime ~200 ms when nothing relevant changed
272
- # (path-diff check exits early); ~2 s on full re-projection.
281
+ # (path-diff check exits early); ~2 s on full re-projection; ~2 s extra
282
+ # for the tsc rebuild when TS sources changed.
273
283
 
274
284
  append_auto_sync_block() {
275
285
  local name="$1"
@@ -294,13 +304,25 @@ elif [ "$name" = "post-checkout" ]; then
294
304
  fi
295
305
 
296
306
  if [ -n "\$prev" ] && [ -n "\$new" ] && [ "\$prev" != "\$new" ]; then
297
- if git diff --name-only "\$prev" "\$new" 2>/dev/null | \\
307
+ changed="\$(git diff --name-only "\$prev" "\$new" 2>/dev/null)"
308
+ if printf '%s\n' "\$changed" | \\
298
309
  grep -qE '^(dist/agent-src/|\\.agent-src\\.uncondensed/|src/scripts/condense\\.ts|\\.agent-tools\\.yml|Taskfile\\.yml)'; then
299
310
  if command -v task >/dev/null 2>&1; then
300
311
  task sync >/dev/null 2>&1 || true
301
312
  task generate-tools >/dev/null 2>&1 || true
302
313
  fi
303
314
  fi
315
+ # TS sources changed → the gitignored tsc output (dist/cli, dist/scripts,
316
+ # …) is stale and the ./agent-config hook chain breaks with
317
+ # ERR_MODULE_NOT_FOUND. Rebuild tsc output only (build:cli), never the
318
+ # full build (install-bundle rewrites committed dist/install/install.mjs).
319
+ # Skip silently without node_modules (fresh clone / bare worktree).
320
+ if printf '%s\n' "\$changed" | \\
321
+ grep -qE '^(src/(cli|server|shared|install|scripts)/|tsconfig\\.json|package\\.json)'; then
322
+ if command -v npm >/dev/null 2>&1 && [ -d node_modules ]; then
323
+ npm run build:cli >/dev/null 2>&1 || true
324
+ fi
325
+ fi
304
326
  fi
305
327
  EOF
306
328
  }