@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
@@ -23,35 +23,67 @@
23
23
  * the manifest is empty or unreadable, and it REPORTS `pending` gates (listed but
24
24
  * not yet emitting a count) instead of skipping them silently.
25
25
  *
26
+ * MUTATION CANARY (`--canary`, road-to-gates-that-can-fail Phase 7). Coverage
27
+ * proves a gate READ something; it cannot prove the gate can still FAIL. The
28
+ * canary plants one declared violation per gate, runs the gate, and records
29
+ * red/green — a gate that stays green over a real planted defect is dead by
30
+ * definition. It runs the same contract as the review-side canary in
31
+ * `docs/contracts/adversarial-review-protocol.md` § 6 (rotating class, sealed
32
+ * record, NEVER SHIPS) and is deliberately kept OFF the default CI path: it
33
+ * mutates the tree, so it is an operator-invoked biannual experiment, not a
34
+ * per-PR gate. Every plant is reverted in a `finally`.
35
+ *
26
36
  * CLI:
27
37
  * ./scripts-run src/scripts/check_gate_coverage # enforce
28
38
  * ./scripts-run src/scripts/check_gate_coverage --list # show the manifest
29
39
  * ./scripts-run src/scripts/check_gate_coverage --format json
40
+ * ./scripts-run src/scripts/check_gate_coverage --canary [--ledger <file>]
30
41
  *
31
42
  * Exit codes: 0 all enforced gates cleared their floor · 1 a gate is blind,
32
- * collapsed, or silent · 2 the manifest itself is missing/empty/malformed.
43
+ * collapsed, or silent (in `--canary`: a gate stayed green, or the ledger
44
+ * disagrees with the census) · 2 the manifest itself is missing/empty/malformed.
33
45
  */
34
46
  import * as fs from 'node:fs';
35
47
  import * as path from 'node:path';
36
48
  import process from 'node:process';
37
49
  import { fileURLToPath, pathToFileURL } from 'node:url';
38
50
 
39
- import yaml from 'js-yaml';
51
+ import * as yaml from 'js-yaml';
40
52
  import { runCountedProbe } from './_lib/counted_probe.js';
41
53
 
42
54
  const _HERE = fileURLToPath(import.meta.url);
43
55
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
44
56
  const MANIFEST = path.join(REPO_ROOT, 'src/config/gate-coverage.yml');
57
+ const CENSUS = path.join(REPO_ROOT, 'agents/evidence/reports/gate-scope-census.md');
45
58
 
46
59
  /** The one line a gate must emit. Deliberately narrow: no format guessing. */
47
60
  const SCANNED_RE = /^\s*scanned:\s*(\d+)\s*$/m;
48
61
 
62
+ /**
63
+ * A declared, revertible violation for one gate (Phase 7).
64
+ *
65
+ * Only ONE operation shape exists on purpose: create a file that does not exist,
66
+ * delete it afterwards. An in-place edit would need byte-exact restoration of a
67
+ * tracked file, and a canary that can corrupt the tree is worse than no canary —
68
+ * the contract's "never ships" rule binds this implementation too.
69
+ */
70
+ export interface CanarySpec {
71
+ /** Rotating-class label, per adversarial-review-protocol § 6. */
72
+ class: string;
73
+ /** Repo-relative path to create. MUST NOT already exist. */
74
+ path: string;
75
+ /** File body planted at that path. */
76
+ content: string;
77
+ }
78
+
49
79
  export interface GateSpec {
50
80
  id: string;
51
81
  argv: string[];
52
82
  min_scanned: number;
53
83
  corpus: string;
54
84
  status: 'enforced' | 'pending';
85
+ /** Absent = this gate has no canary recipe; reported, never hidden. */
86
+ canary?: CanarySpec;
55
87
  /**
56
88
  * Exit code meaning "my prerequisite is absent, so I inspected nothing" — e.g.
57
89
  * check_site_links exits 2 when the site is unbuilt or stale. Without this the
@@ -117,10 +149,27 @@ export function load_manifest(file = MANIFEST): GateSpec[] {
117
149
  ? {}
118
150
  : { unavailable_exit: _require_int(e['unavailable_exit'], id, i) }),
119
151
  ...(e['note'] === undefined ? {} : { note: String(e['note']) }),
152
+ ...(e['canary'] === undefined ? {} : { canary: _require_canary(e['canary'], id, i) }),
120
153
  };
121
154
  });
122
155
  }
123
156
 
157
+ function _require_canary(v: unknown, id: string, i: number): CanarySpec {
158
+ const c = v as Record<string, unknown>;
159
+ const cls = String(c?.['class'] ?? '');
160
+ const p = String(c?.['path'] ?? '');
161
+ const content = String(c?.['content'] ?? '');
162
+ // A half-declared recipe would plant nothing and read as a passing canary —
163
+ // the exact false green this whole file exists to prevent.
164
+ if (cls === '' || p === '' || content === '') {
165
+ throw new Error(`gates[${String(i)}] (${id}): canary needs class, path and content`);
166
+ }
167
+ if (path.isAbsolute(p) || p.split('/').includes('..')) {
168
+ throw new Error(`gates[${String(i)}] (${id}): canary path must be repo-relative and contain no '..'`);
169
+ }
170
+ return { class: cls, path: p, content };
171
+ }
172
+
124
173
  /** Extract the contract line. Returns null when the gate emitted no count. */
125
174
  export function parse_scanned(output: string): number | null {
126
175
  const m = SCANNED_RE.exec(output);
@@ -215,6 +264,10 @@ export function main(argv: readonly string[]): number {
215
264
  return 2;
216
265
  }
217
266
 
267
+ if (argv.includes('--canary')) {
268
+ return run_canary_mode(specs, argv);
269
+ }
270
+
218
271
  if (listOnly) {
219
272
  for (const s of specs) {
220
273
  process.stdout.write(
@@ -283,6 +336,289 @@ function probe_pending(s: GateSpec): { scanned: number | null; crashed: boolean
283
336
  return run_gate(s);
284
337
  }
285
338
 
339
+ // ── Mutation canary (Phase 7) ──────────────────────────────────────────────
340
+
341
+ export type CanaryVerdict = 'red' | 'green' | 'no_recipe' | 'crashed';
342
+
343
+ export interface CanaryResult {
344
+ id: string;
345
+ verdict: CanaryVerdict;
346
+ class: string | null;
347
+ exit_code: number | null;
348
+ message: string;
349
+ }
350
+
351
+ /**
352
+ * Plant one declared violation, run the gate, revert.
353
+ *
354
+ * The revert is in a `finally` and deletes exactly the file this function
355
+ * created — never a pre-existing one, which is why an occupied path aborts
356
+ * instead of overwriting.
357
+ */
358
+ export function run_canary(spec: GateSpec, repoRoot = REPO_ROOT): CanaryResult {
359
+ const c = spec.canary;
360
+ if (c === undefined) {
361
+ return {
362
+ id: spec.id,
363
+ verdict: 'no_recipe',
364
+ class: null,
365
+ exit_code: null,
366
+ message: 'no canary recipe declared — this gate is UNPROVEN, not proven working',
367
+ };
368
+ }
369
+ const abs = path.join(repoRoot, c.path);
370
+ if (fs.existsSync(abs)) {
371
+ return {
372
+ id: spec.id,
373
+ verdict: 'crashed',
374
+ class: c.class,
375
+ exit_code: null,
376
+ message: `canary path ${c.path} already exists — refusing to overwrite a real file`,
377
+ };
378
+ }
379
+ let planted = false;
380
+ // Directories the plant had to create. An empty `src/skills/<x>/` left behind
381
+ // is invisible to `git status` but is still residue, and a skill directory
382
+ // with no SKILL.md is itself a defect the next gate run would report.
383
+ const madeDirs: string[] = [];
384
+ for (let d = path.dirname(abs); !fs.existsSync(d) && d.startsWith(repoRoot); d = path.dirname(d)) {
385
+ madeDirs.push(d);
386
+ }
387
+ try {
388
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
389
+ fs.writeFileSync(abs, c.content, 'utf8');
390
+ planted = true;
391
+ const runner = path.join(repoRoot, 'scripts-run');
392
+ const r = runCountedProbe(runner, [`src/scripts/${spec.id}`, ...spec.argv], { cwd: repoRoot });
393
+ if (r.failure !== null && r.status === null) {
394
+ return {
395
+ id: spec.id,
396
+ verdict: 'crashed',
397
+ class: c.class,
398
+ exit_code: null,
399
+ message: `gate could not be executed under the plant: ${r.failure}`,
400
+ };
401
+ }
402
+ const code = r.status ?? 0;
403
+ return code !== 0
404
+ ? {
405
+ id: spec.id,
406
+ verdict: 'red',
407
+ class: c.class,
408
+ exit_code: code,
409
+ message: `caught the planted ${c.class} defect (exit ${String(code)})`,
410
+ }
411
+ : {
412
+ id: spec.id,
413
+ verdict: 'green',
414
+ class: c.class,
415
+ exit_code: 0,
416
+ message:
417
+ `stayed GREEN over a planted ${c.class} defect at ${c.path} — ` +
418
+ 'the gate is dead by definition. This row is a defect ticket',
419
+ };
420
+ } finally {
421
+ // NEVER SHIPS (adversarial-review-protocol § 6): the plant is removed even
422
+ // when the gate crashes or the process is interrupted mid-run.
423
+ if (planted) {
424
+ try {
425
+ fs.rmSync(abs, { force: true });
426
+ // Innermost first — rmdir only succeeds while the directory is empty,
427
+ // so a dir that gained other content is left alone.
428
+ for (const d of madeDirs) {
429
+ try {
430
+ fs.rmdirSync(d);
431
+ } catch {
432
+ break;
433
+ }
434
+ }
435
+ } catch {
436
+ process.stderr.write(`❌ canary could not remove its plant at ${c.path} — REMOVE IT BY HAND\n`);
437
+ }
438
+ }
439
+ }
440
+ }
441
+
442
+ export interface Disagreement {
443
+ gate: string;
444
+ kind: 'dead_gate' | 'census_stale';
445
+ detail: string;
446
+ }
447
+
448
+ /**
449
+ * Read the scan-scope census into `gate -> units`.
450
+ *
451
+ * Column-position-agnostic: it locates the gate column and the units column by
452
+ * header text, so a census regenerated with a different column set still
453
+ * cross-checks. `null` = the census recorded no measurable count.
454
+ */
455
+ export function parse_census(md: string): Map<string, number | null> {
456
+ const out = new Map<string, number | null>();
457
+ const rows = md.split('\n').filter((l) => l.trim().startsWith('|'));
458
+ const cells = (l: string): string[] =>
459
+ l.split('|').slice(1, -1).map((c) => c.trim());
460
+ let gateCol = -1;
461
+ let unitCol = -1;
462
+ for (const line of rows) {
463
+ const cs = cells(line);
464
+ const head = cs.map((c) => c.toLowerCase());
465
+ const g = head.findIndex((c) => c === 'gate');
466
+ const u = head.findIndex((c) => c.startsWith('unit'));
467
+ if (g >= 0 && u >= 0) {
468
+ gateCol = g;
469
+ unitCol = u;
470
+ continue;
471
+ }
472
+ if (gateCol < 0 || cs.length <= Math.max(gateCol, unitCol)) continue;
473
+ const idm = /`([A-Za-z0-9_.-]+)`/.exec(cs[gateCol] as string);
474
+ if (idm === null) continue;
475
+ const raw = cs[unitCol] as string;
476
+ const num = /(\d[\d,]*)/.exec(raw.replace(/\*/g, ''));
477
+ out.set(idm[1] as string, num === null ? null : Number((num[1] as string).replace(/,/g, '')));
478
+ }
479
+ return out;
480
+ }
481
+
482
+ /**
483
+ * Make the two artefacts disagree loudly (Phase 7 step 2).
484
+ *
485
+ * The census says what a gate READS; the canary says whether anything it reads
486
+ * can make it FAIL. Each alone is satisfiable by a broken gate; together they
487
+ * are not.
488
+ */
489
+ export function cross_check(
490
+ results: readonly CanaryResult[],
491
+ census: ReadonlyMap<string, number | null>,
492
+ ): Disagreement[] {
493
+ const out: Disagreement[] = [];
494
+ for (const r of results) {
495
+ const units = census.get(r.id);
496
+ if (units === undefined) continue; // not censused — nothing to disagree with
497
+ if (r.verdict === 'green' && units !== null && units > 0) {
498
+ out.push({
499
+ gate: r.id,
500
+ kind: 'dead_gate',
501
+ detail:
502
+ `census records ${String(units)} unit(s) read, but the canary could not make it fail — ` +
503
+ 'a gate that reads a live corpus and cannot go red is dead',
504
+ });
505
+ }
506
+ if (r.verdict === 'red' && (units === null || units === 0)) {
507
+ out.push({
508
+ gate: r.id,
509
+ kind: 'census_stale',
510
+ detail:
511
+ 'the canary made it fail, so it reads a live corpus, but the census records no units — ' +
512
+ 're-run the census; its row is stale',
513
+ });
514
+ }
515
+ }
516
+ return out;
517
+ }
518
+
519
+ const CANARY_ICON: Record<CanaryVerdict, string> = {
520
+ red: '✅',
521
+ green: '❌',
522
+ no_recipe: '⚠️',
523
+ crashed: '❌',
524
+ };
525
+
526
+ export function render_ledger(
527
+ results: readonly CanaryResult[],
528
+ disagreements: readonly Disagreement[],
529
+ totalGateScripts: number,
530
+ cycleId: string,
531
+ ): string {
532
+ const recipes = results.filter((r) => r.verdict !== 'no_recipe');
533
+ const red = results.filter((r) => r.verdict === 'red');
534
+ const green = results.filter((r) => r.verdict === 'green');
535
+ const lines = [
536
+ `# Gate-surface canary ledger — ${cycleId}`,
537
+ '',
538
+ 'Produced by `./scripts-run src/scripts/check_gate_coverage --canary`. Governed by',
539
+ '[`adversarial-review-protocol`](../../../../docs/contracts/adversarial-review-protocol.md)',
540
+ '§ 6 — biannual cadence, rotating class, sealed record, never ships.',
541
+ '',
542
+ '## Coverage (stated, not implied)',
543
+ '',
544
+ `- Gate scripts in \`src/scripts/\`: **${String(totalGateScripts)}**`,
545
+ `- Listed in \`src/config/gate-coverage.yml\`: **${String(results.length)}**`,
546
+ `- Carrying a canary recipe: **${String(recipes.length)}**`,
547
+ `- RED (caught the plant): **${String(red.length)}** · GREEN (dead): **${String(green.length)}**`,
548
+ '',
549
+ 'Every gate outside the recipe count is UNPROVEN by this experiment. That is a',
550
+ 'gap, not a pass.',
551
+ '',
552
+ '## Per-gate ledger',
553
+ '',
554
+ '| Gate | Class | Verdict | Exit | Detail |',
555
+ '|---|---|---|---:|---|',
556
+ ];
557
+ for (const r of results) {
558
+ lines.push(
559
+ `| \`${r.id}\` | ${r.class ?? '—'} | ${CANARY_ICON[r.verdict]} ${r.verdict.toUpperCase()} ` +
560
+ `| ${r.exit_code === null ? '—' : String(r.exit_code)} | ${r.message} |`,
561
+ );
562
+ }
563
+ lines.push('', '## Cross-check against the scan-scope census', '');
564
+ if (disagreements.length === 0) {
565
+ lines.push('No disagreement: every censused gate with a recipe went red, and every red gate');
566
+ lines.push('has a non-zero census count.');
567
+ } else {
568
+ lines.push('| Gate | Kind | Detail |', '|---|---|---|');
569
+ for (const d of disagreements) lines.push(`| \`${d.gate}\` | **${d.kind}** | ${d.detail} |`);
570
+ }
571
+ lines.push('');
572
+ return lines.join('\n');
573
+ }
574
+
575
+ /** Count of gate-shaped scripts, for the honest denominator in the ledger. */
576
+ export function count_gate_scripts(dir = path.join(REPO_ROOT, 'src/scripts')): number {
577
+ try {
578
+ return fs
579
+ .readdirSync(dir)
580
+ .filter((f) => /^(lint|check|audit)_.*\.ts$/.test(f) && !f.endsWith('.d.ts')).length;
581
+ } catch {
582
+ return 0;
583
+ }
584
+ }
585
+
586
+ function run_canary_mode(specs: readonly GateSpec[], argv: readonly string[]): number {
587
+ const results = specs.map((s) => run_canary(s));
588
+ const censusMd = fs.existsSync(CENSUS) ? fs.readFileSync(CENSUS, 'utf8') : '';
589
+ const disagreements = cross_check(results, parse_census(censusMd));
590
+
591
+ for (const r of results) {
592
+ process.stdout.write(` ${CANARY_ICON[r.verdict]} ${r.id}: ${r.message}\n`);
593
+ }
594
+ for (const d of disagreements) {
595
+ process.stdout.write(` ❌ DISAGREEMENT [${d.kind}] ${d.gate}: ${d.detail}\n`);
596
+ }
597
+
598
+ const cycleId = `gate-surface-${new Date().toISOString().slice(0, 10)}`;
599
+ const li = argv.indexOf('--ledger');
600
+ if (li >= 0 && argv[li + 1] !== undefined) {
601
+ const out = path.resolve(REPO_ROOT, argv[li + 1] as string);
602
+ fs.mkdirSync(path.dirname(out), { recursive: true });
603
+ fs.writeFileSync(out, render_ledger(results, disagreements, count_gate_scripts(), cycleId), 'utf8');
604
+ process.stdout.write(`\nledger: ${path.relative(REPO_ROOT, out)}\n`);
605
+ }
606
+
607
+ const recipes = results.filter((r) => r.verdict !== 'no_recipe').length;
608
+ const green = results.filter((r) => r.verdict === 'green' || r.verdict === 'crashed').length;
609
+ process.stdout.write(
610
+ `\nscanned: ${String(results.length)}\n` +
611
+ ` recipes ${String(recipes)} · unproven ${String(results.length - recipes)} · ` +
612
+ `green(dead) ${String(green)} · disagreements ${String(disagreements.length)}\n`,
613
+ );
614
+ if (green > 0 || disagreements.length > 0) {
615
+ process.stdout.write('❌ canary found dead gate(s) or a census disagreement — each row is a defect ticket.\n');
616
+ return 1;
617
+ }
618
+ process.stdout.write('✅ every gate with a recipe went red over its planted defect.\n');
619
+ return 0;
620
+ }
621
+
286
622
  function _isCliEntry(): boolean {
287
623
  if (process.argv[1] === undefined) return false;
288
624
  const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
@@ -35,7 +35,8 @@ const GENERATOR_OUTPUT_ROOTS = [
35
35
  { root: '.claude/rules', generator: 'generate_rule_symlinks', tool: 'claude-code' },
36
36
  { root: '.claude/personas', generator: 'generate_persona_symlinks', tool: 'claude-code' },
37
37
  { root: '.claude/user-types', generator: 'generate_user_type_symlinks', tool: 'claude-code' },
38
- { root: '.claude/skills', generator: 'generate_claude_skills + generate_plugin_command_skills', tool: 'claude-code' },
38
+ { root: '.claude/skills', generator: 'generate_claude_skills + generate_plugin_command_skills + generate_claude_commands', tool: 'claude-code' },
39
+ { root: '.claude/commands', generator: 'generate_claude_project_commands', tool: 'claude-code' },
39
40
  { root: '.claude/agents', generator: 'generate_claude_subagents', tool: 'claude-code' },
40
41
  { root: '.augment', generator: 'sync_augment (condense main)', tool: 'augment' },
41
42
  { root: '.cursor/rules', generator: 'generate_cursor_mdc_rules', tool: 'cursor' },
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Ported from the retired Python `src/scripts/check_iron_law_prominence.py` (ADR-200,
7
7
  * Phase 4 / Wave 4c). The CLI contract is pinned — positional
8
- * paths (default `.agent-src.uncondensed/rules`), `--format` / `--quiet`
8
+ * paths (default `src/rules`), `--format` / `--quiet`
9
9
  * flags, exit codes (0 clean, 1 violations, 3 internal error),
10
10
  * stdout/stderr split, byte-identical finding messages, and the same
11
11
  * heading parse / code-fence skipping. No behaviour changes — latent bugs
@@ -11,7 +11,7 @@
11
11
  * historical quirks preserved (consumers pin the exact behaviour).
12
12
  *
13
13
  * Fails when a single PR (or commit range) modifies more than one
14
- * kernel rule under `.agent-src.uncondensed/rules/`. Override via the
14
+ * kernel rule under `src/rules/`. Override via the
15
15
  * PR label `bundled-always-rules-acknowledged`.
16
16
  *
17
17
  * Kernel set is the locked 9-rule list in
@@ -45,12 +45,20 @@ import * as fs from 'node:fs';
45
45
  import * as path from 'node:path';
46
46
  import { fileURLToPath, pathToFileURL } from 'node:url';
47
47
 
48
+ import { checkRatchet } from './_lib/gate_baseline.js';
49
+
48
50
  const LEGACY = '.agent-src.uncondensed/';
49
51
  const EXEMPT: ReadonlySet<string> = new Set([
50
52
  'src/scripts/_lib/agent_src.py',
51
53
  'src/scripts/check_references.py',
52
54
  'src/scripts/check_condensed_paths.py',
53
55
  'src/scripts/check_no_new_legacy_path.py', // this file documents the literal
56
+ // The `.ts` twin needs its own entry now: the docstring above says the
57
+ // faithful-twin rule covers it, but that rule reads a `.py` sibling from
58
+ // disk and the py2ts final deletion removed every sibling — so since then
59
+ // ANY edit to this guard's own message strings flags the guard. Surfaced
60
+ // by the Phase-5 edit that added the existing-literal pass below.
61
+ 'src/scripts/check_no_new_legacy_path.ts',
54
62
  'src/scripts/check_source_pointer_freshness.ts', // forbidden-substring detector for the legacy tree
55
63
  ]);
56
64
 
@@ -131,6 +139,154 @@ function find_offenders(diffText: string, twinCheck: TwinCheck = _is_faithful_tw
131
139
  return offenders;
132
140
  }
133
141
 
142
+ // ---------------------------------------------------------------------------
143
+ // Pass 2 — EXISTING hardcoded legacy scan roots (ratcheted).
144
+ //
145
+ // The diff pass above stops the debt from growing. It is structurally blind to
146
+ // the debt that is already there — and every one of the 14 dead gates found in
147
+ // 2026-07 was pre-existing, so a new-violations-only check could never have
148
+ // seen them (road-to-gates-that-can-fail Phase 5).
149
+ //
150
+ // WHY THIS IS NOT THE FULL-TREE LINT THAT WAS ALREADY REJECTED. The note in
151
+ // `find_offenders` records a measured rejection: a full-tree lint over `tests/`
152
+ // hits 44 files / 213 lines, most of them legitimate (the legacy detectors'
153
+ // own test data, `validator_ignore` substrings, synthetic tmpdir fixtures), so
154
+ // the signal drowns. That measurement stands and this pass does not repeat it.
155
+ // Three things make this scope different:
156
+ //
157
+ // 1. It reads EXECUTABLE code only — `src/scripts/**/*.{ts,mts,mjs}`. No
158
+ // prose, no docs, no tests, no fixtures.
159
+ // 2. It counts only lines that CONSTRUCT A PATH — the literal must reach
160
+ // `path.join(...)` inside a string. A detector holding the literal as
161
+ // data to match, strip, or print is not a scan root and is not counted.
162
+ // Measured on 2026-08-02: 236 raw mentions in src/scripts → 147 string
163
+ // literals → 70 path-constructing lines. The narrowing is the point.
164
+ // 3. It is a RATCHET, not a hard fail. The current count is committed to
165
+ // `src/config/gate-violation-baselines.json`; the gate goes red only when
166
+ // the count RISES. Every Phase-1 repair lowers it, and the baseline's
167
+ // 56-day expiry (see `_lib/gate_baseline.ts`) stops a stalled number from
168
+ // hardening into configuration.
169
+ // ---------------------------------------------------------------------------
170
+
171
+ /** Gate key in `src/config/gate-violation-baselines.json`. */
172
+ export const HARDCODED_ROOT_GATE = 'check_no_new_legacy_path:hardcoded-scan-roots';
173
+
174
+ /** The executable-code root this pass reads. */
175
+ export const HARDCODED_ROOT_SCAN_DIR = 'src/scripts';
176
+
177
+ /** Bare directory name — the form that appears inside a `path.join(...)` call. */
178
+ const LEGACY_DIRNAME = LEGACY.replace(/\/+$/, '');
179
+
180
+ /**
181
+ * Files allowed to construct a legacy path forever.
182
+ *
183
+ * `_lib/agent_src.ts` is the shared resolver — owning the constant is its whole
184
+ * job, and every gate that routes through it survived the ADR-051 move. Exempting
185
+ * it is what makes the count mean "gates that bypassed the resolver".
186
+ */
187
+ const HARDCODED_ROOT_EXEMPT: ReadonlySet<string> = new Set(['src/scripts/_lib/agent_src.ts']);
188
+
189
+ const _STRING_LITERAL_LEGACY = new RegExp(
190
+ `['"\`][^'"\`]*${LEGACY_DIRNAME.replace(/\./g, '\\.')}`,
191
+ );
192
+
193
+ function _isCommentLine(trimmed: string): boolean {
194
+ return (
195
+ trimmed.startsWith('//') ||
196
+ trimmed.startsWith('*') ||
197
+ trimmed.startsWith('/*') ||
198
+ trimmed.startsWith('#')
199
+ );
200
+ }
201
+
202
+ function* _walkScripts(dir: string): Generator<string> {
203
+ let entries: fs.Dirent[];
204
+ try {
205
+ entries = fs.readdirSync(dir, { withFileTypes: true });
206
+ } catch {
207
+ return;
208
+ }
209
+ for (const e of entries) {
210
+ const full = path.join(dir, e.name);
211
+ if (e.isDirectory()) {
212
+ yield* _walkScripts(full);
213
+ } else if (/\.(ts|mts|mjs)$/.test(e.name) && !/\.d\.m?ts$/.test(e.name)) {
214
+ yield full;
215
+ }
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Lines under `src/scripts/` that build a filesystem path against the retired
221
+ * tree. Returns `file:line: <source>` strings, sorted by path.
222
+ *
223
+ * `repoRoot` defaults to cwd (CI runs the guard at the repo root) and is
224
+ * injectable so tests can point at a fixture tree.
225
+ */
226
+ export function find_hardcoded_scan_roots(
227
+ repoRoot: string = process.cwd(),
228
+ twinCheck: TwinCheck = _is_faithful_twin,
229
+ ): string[] {
230
+ const root = path.join(repoRoot, HARDCODED_ROOT_SCAN_DIR);
231
+ const findings: string[] = [];
232
+ for (const abs of _walkScripts(root)) {
233
+ const rel = path.relative(repoRoot, abs).split(path.sep).join('/');
234
+ if (EXEMPT.has(rel) || HARDCODED_ROOT_EXEMPT.has(rel) || twinCheck(rel)) {
235
+ continue;
236
+ }
237
+ let text: string;
238
+ try {
239
+ text = fs.readFileSync(abs, 'utf-8');
240
+ } catch {
241
+ continue;
242
+ }
243
+ if (!text.includes(LEGACY_DIRNAME)) {
244
+ continue;
245
+ }
246
+ text.split('\n').forEach((line, i) => {
247
+ if (!line.includes(LEGACY_DIRNAME)) return;
248
+ const trimmed = line.trim();
249
+ if (_isCommentLine(trimmed)) return;
250
+ if (!/path\.join\s*\(/.test(line)) return;
251
+ if (!_STRING_LITERAL_LEGACY.test(line)) return;
252
+ findings.push(`${rel}:${i + 1}: ${trimmed.slice(0, 100)}`);
253
+ });
254
+ }
255
+ findings.sort();
256
+ return findings;
257
+ }
258
+
259
+ /**
260
+ * Run the existing-literal pass and judge it against the committed baseline.
261
+ * Returns 0 when the count is at or below the baseline (and the baseline is
262
+ * fresh), 1 otherwise. Prints either way — a passing ratchet still reports the
263
+ * live number, which is the whole point of committing it.
264
+ */
265
+ export function checkHardcodedScanRoots(repoRoot: string = process.cwd()): number {
266
+ const findings = find_hardcoded_scan_roots(repoRoot);
267
+ const verdict = checkRatchet({
268
+ gate: HARDCODED_ROOT_GATE,
269
+ actual: findings.length,
270
+ repoRoot,
271
+ });
272
+ if (verdict.ok) {
273
+ process.stdout.write(`✅ ${verdict.message}\n`);
274
+ return 0;
275
+ }
276
+ process.stdout.write(
277
+ `❌ Hardcoded \`${LEGACY}\` scan root(s) under ${HARDCODED_ROOT_SCAN_DIR}/ ` +
278
+ 'above the recorded baseline:\n',
279
+ );
280
+ for (const f of findings.slice(0, 20)) {
281
+ process.stdout.write(` 🔴 ${f}\n`);
282
+ }
283
+ if (findings.length > 20) {
284
+ process.stdout.write(` … and ${findings.length - 20} more\n`);
285
+ }
286
+ process.stdout.write(`\n${verdict.message}\n`);
287
+ return 1;
288
+ }
289
+
134
290
  function main(): number {
135
291
  // --stdin: read a unified diff from stdin (CI pipes `gh pr diff` — auth-safe,
136
292
  // no `git fetch <base>` extraheader race on shallow PR-merge checkouts, the
@@ -170,6 +326,7 @@ function main(): number {
170
326
  offenders = find_offenders((proc.stdout as string | null) ?? '');
171
327
  }
172
328
 
329
+ let rc = 0;
173
330
  if (offenders.length) {
174
331
  process.stdout.write(
175
332
  '❌ New `.agent-src.uncondensed/` reference(s) added under src/ ' +
@@ -182,11 +339,18 @@ function main(): number {
182
339
  '\nFix: reference the real `src/` target. Existing stale prose is ' +
183
340
  'migrated opportunistically; do not ADD new dead-path references.\n',
184
341
  );
185
- return 1;
342
+ rc = 1;
343
+ } else {
344
+ process.stdout.write('✅ No new `.agent-src.uncondensed/` references added under src/.\n');
186
345
  }
187
346
 
188
- process.stdout.write('✅ No new `.agent-src.uncondensed/` references added under src/.\n');
189
- return 0;
347
+ // Pass 2 — the pre-existing hardcoded scan roots the diff pass cannot see.
348
+ // Skippable for the golden-parity harness, which compares this guard's
349
+ // diff behaviour against a pinned reference implementation.
350
+ if (!process.argv.includes('--no-existing')) {
351
+ rc = Math.max(rc, checkHardcodedScanRoots());
352
+ }
353
+ return rc;
190
354
  }
191
355
 
192
356
  const _HERE = fileURLToPath(import.meta.url);
@@ -55,12 +55,26 @@ export interface PackSizeBudget {
55
55
  /**
56
56
  * Parse `npm pack --json` stdout. npm runs the `prepare` lifecycle script even
57
57
  * under `--ignore-scripts` on some versions, and this repo's `prepare` prints a
58
- * banner — straight into the stream we parse. Slice from the first `[` so a
59
- * lifecycle banner is tolerated instead of becoming a SyntaxError.
58
+ * banner — straight into the stream we parse. Slice from a `[` so a lifecycle
59
+ * banner is tolerated instead of becoming a SyntaxError.
60
+ *
61
+ * Not the FIRST `[`: npm echoes the lifecycle command line, and this repo's own
62
+ * `prepare` is `[ -d .git ] && bash src/scripts/install-hooks.sh || true` — its
63
+ * `[` precedes the payload's. Try each candidate offset in order and keep the
64
+ * first that actually parses (road-to-gates-that-can-fail Phase 6.2).
60
65
  */
61
66
  export function parsePackJson(stdout: string): PackResult {
62
- const start = stdout.indexOf('[');
63
- const parsed = JSON.parse(start >= 0 ? stdout.slice(start) : stdout) as PackResult[];
67
+ let parsed: PackResult[] | null = null;
68
+ for (let i = stdout.indexOf('['); i >= 0; i = stdout.indexOf('[', i + 1)) {
69
+ try {
70
+ parsed = JSON.parse(stdout.slice(i)) as PackResult[];
71
+ break;
72
+ } catch {
73
+ // Not the payload — a banner bracket. Keep scanning.
74
+ }
75
+ }
76
+ // No candidate parsed: reproduce the original SyntaxError from the raw text.
77
+ parsed ??= JSON.parse(stdout) as PackResult[];
64
78
  const first = parsed[0];
65
79
  if (first === undefined) throw new Error('npm pack --json returned an empty array');
66
80
  return first;