@event4u/agent-config 9.13.0 → 9.15.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 (280) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +113 -67
  3. package/README.md +9 -1
  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 +150 -0
  15. package/dist/agent-src/commands/optimize/skills.md +5 -5
  16. package/dist/agent-src/commands/optimize.md +12 -6
  17. package/dist/agent-src/commands/override.md +1 -0
  18. package/dist/agent-src/commands/prediction-pool.md +4 -4
  19. package/dist/agent-src/commands/research/deep.md +1 -1
  20. package/dist/agent-src/commands/roadmap.md +2 -1
  21. package/dist/agent-src/commands/rule-compliance-audit.md +1 -1
  22. package/dist/agent-src/commands/tdd.md +1 -0
  23. package/dist/agent-src/commands/tests.md +1 -0
  24. package/dist/agent-src/commands/upstream-contribute.md +5 -5
  25. package/dist/agent-src/commands/video/from-script.md +1 -1
  26. package/dist/agent-src/commands/video/from-song.md +1 -1
  27. package/dist/agent-src/commands/video/scene.md +1 -1
  28. package/dist/agent-src/commands/video/stitch.md +1 -1
  29. package/dist/agent-src/commands/video/storyboard.md +1 -1
  30. package/dist/agent-src/commands/worktree/create.md +2 -1
  31. package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
  32. package/dist/agent-src/contexts/contracts/artifact-engagement-flow.md +5 -5
  33. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +3 -3
  34. package/dist/agent-src/contexts/contracts/emergency-triage-block.md +2 -2
  35. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +4 -3
  36. package/dist/agent-src/contexts/execution/subagent-routing.md +19 -0
  37. package/dist/agent-src/contexts/override-system.md +1 -1
  38. package/dist/agent-src/ghostwriter/fictional-fixture-v1.md +1 -1
  39. package/dist/agent-src/rules/augment-edit-discipline.md +1 -1
  40. package/dist/agent-src/rules/autonomous-execution.md +2 -1
  41. package/dist/agent-src/rules/brand-consistency.md +1 -0
  42. package/dist/agent-src/rules/brand-source-of-truth.md +1 -0
  43. package/dist/agent-src/rules/code-comment-discipline.md +2 -0
  44. package/dist/agent-src/rules/communication-through-line.md +1 -0
  45. package/dist/agent-src/rules/content-quoting-floor.md +1 -0
  46. package/dist/agent-src/rules/cross-source-consistency.md +0 -1
  47. package/dist/agent-src/rules/downstream-changes.md +1 -0
  48. package/dist/agent-src/rules/external-code-graph-interop.md +1 -0
  49. package/dist/agent-src/rules/external-reference-deep-dive.md +1 -0
  50. package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
  51. package/dist/agent-src/rules/finance-safety-floor.md +0 -1
  52. package/dist/agent-src/rules/icon-consistency.md +1 -0
  53. package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
  54. package/dist/agent-src/rules/improve-before-implement.md +1 -1
  55. package/dist/agent-src/rules/invite-challenge.md +1 -0
  56. package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
  57. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  58. package/dist/agent-src/rules/markdown-safe-codeblocks.md +1 -0
  59. package/dist/agent-src/rules/media-governance-routing.md +4 -3
  60. package/dist/agent-src/rules/media-sync-ground-truth.md +1 -0
  61. package/dist/agent-src/rules/no-attribution-footers.md +1 -0
  62. package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +1 -0
  63. package/dist/agent-src/rules/no-pr-progress-comments.md +1 -0
  64. package/dist/agent-src/rules/onboarding-gate.md +1 -0
  65. package/dist/agent-src/rules/output-discipline.md +1 -0
  66. package/dist/agent-src/rules/prefer-enums-over-literals.md +1 -0
  67. package/dist/agent-src/rules/preservation-guard.md +1 -0
  68. package/dist/agent-src/rules/question-not-instruction.md +1 -0
  69. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +1 -0
  70. package/dist/agent-src/rules/role-mode-adherence.md +1 -0
  71. package/dist/agent-src/rules/runtime-safety.md +1 -0
  72. package/dist/agent-src/rules/security-sensitive-stop.md +1 -0
  73. package/dist/agent-src/rules/session-canary.md +9 -2
  74. package/dist/agent-src/rules/source-confidentiality.md +1 -0
  75. package/dist/agent-src/rules/source-of-truth.md +1 -0
  76. package/dist/agent-src/rules/spreadsheet-source-quality.md +1 -0
  77. package/dist/agent-src/rules/think-before-action.md +1 -0
  78. package/dist/agent-src/rules/token-budget-discipline.md +1 -0
  79. package/dist/agent-src/rules/token-efficiency.md +1 -0
  80. package/dist/agent-src/rules/token-optimizer-maintenance.md +5 -5
  81. package/dist/agent-src/rules/tool-safety.md +1 -0
  82. package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
  83. package/dist/agent-src/rules/user-interaction.md +1 -0
  84. package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
  85. package/dist/agent-src/skills/adr-create/SKILL.md +1 -1
  86. package/dist/agent-src/skills/check-refs/SKILL.md +1 -1
  87. package/dist/agent-src/skills/competitive-positioning/SKILL.md +1 -1
  88. package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
  89. package/dist/agent-src/skills/context-authoring/SKILL.md +1 -1
  90. package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +1 -1
  91. package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
  92. package/dist/agent-src/skills/github-ci/SKILL.md +1 -0
  93. package/dist/agent-src/skills/guideline-writing/SKILL.md +1 -1
  94. package/dist/agent-src/skills/lint-skills/SKILL.md +1 -1
  95. package/dist/agent-src/skills/readme-reviewer/SKILL.md +1 -1
  96. package/dist/agent-src/skills/roadmap-management/SKILL.md +8 -1
  97. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  98. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  99. package/dist/agent-src/skills/skill-writing/SKILL.md +4 -4
  100. package/dist/agent-src/skills/token-optimizer/SKILL.md +4 -4
  101. package/dist/agent-src/skills/upstream-contribute/SKILL.md +6 -6
  102. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -0
  103. package/dist/agent-src/templates/AGENTS.md +1 -1
  104. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  105. package/dist/agent-src/templates/command.md +1 -1
  106. package/dist/agent-src/templates/copilot-instructions.md +3 -3
  107. package/dist/agent-src/templates/rule.md +2 -2
  108. package/dist/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
  109. package/dist/agent-src/templates/skill-archive-note.md +5 -5
  110. package/dist/agent-src/templates/skill.md +1 -1
  111. package/dist/cli/agent-config.js +66 -19
  112. package/dist/cli/agent-config.js.map +1 -1
  113. package/dist/cli/main.js +9 -4
  114. package/dist/cli/main.js.map +1 -1
  115. package/dist/cli/python/workspace_drive.js +2 -0
  116. package/dist/cli/python/workspace_drive.js.map +1 -1
  117. package/dist/cli/python/workspace_drive_health.js +2 -0
  118. package/dist/cli/python/workspace_drive_health.js.map +1 -1
  119. package/dist/cli/python/workspace_hosts.js +2 -0
  120. package/dist/cli/python/workspace_hosts.js.map +1 -1
  121. package/dist/cli/python/workspace_sessions.js +2 -0
  122. package/dist/cli/python/workspace_sessions.js.map +1 -1
  123. package/dist/cli/registry.js +1 -0
  124. package/dist/cli/registry.js.map +1 -1
  125. package/dist/cli-delegate/{chunk-A3HJ6NTI.js → chunk-63WSP6YZ.js} +2 -1
  126. package/dist/cli-delegate/{chunk-EJYIGZ3T.js → chunk-BKO2AZ7Z.js} +1 -1
  127. package/dist/cli-delegate/{chunk-QFKQLQ7X.js → chunk-H574KOUH.js} +340 -462
  128. package/dist/cli-delegate/chunk-NXFWFAYV.js +339 -0
  129. package/dist/cli-delegate/{chunk-TEGJU2JX.js → chunk-ZXXJLL5K.js} +2 -2
  130. package/dist/cli-delegate/cmd_conformance.js +29 -23
  131. package/dist/cli-delegate/cmd_doctor.js +3 -3
  132. package/dist/cli-delegate/cmd_export.js +4 -4
  133. package/dist/cli-delegate/cmd_preflight.js +2 -2
  134. package/dist/cli-delegate/cmd_prune.js +4 -4
  135. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  136. package/dist/cli-delegate/cmd_sync.js +4 -4
  137. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  138. package/dist/cli-delegate/cmd_validate.js +4 -4
  139. package/dist/discovery/deprecation-report.md +1 -1
  140. package/dist/discovery/discovery-manifest.json +163 -103
  141. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  142. package/dist/discovery/discovery-manifest.summary.md +3 -3
  143. package/dist/discovery/orphan-report.md +1 -1
  144. package/dist/discovery/packs.json +6 -5
  145. package/dist/discovery/trust-report.md +3 -3
  146. package/dist/discovery/workspaces.json +5 -4
  147. package/dist/hooks/dispatch.js +729 -430
  148. package/dist/install/install.mjs +700 -625
  149. package/dist/install/rule_scope.js +18 -2
  150. package/dist/install/rule_scope.js.map +1 -1
  151. package/dist/mcp/registry-manifest.json +3 -3
  152. package/dist/mcp/server.json +2 -2
  153. package/dist/mcp/server.mjs +564 -424
  154. package/dist/router.json +1 -1
  155. package/dist/scripts/_lib/claude_settings_hooks.js +30 -5
  156. package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
  157. package/dist/scripts/_lib/scoped_projection.js +223 -0
  158. package/dist/scripts/_lib/scoped_projection.js.map +1 -0
  159. package/dist/scripts/_lib/surface_tiers.js +125 -0
  160. package/dist/scripts/_lib/surface_tiers.js.map +1 -0
  161. package/dist/scripts/_lib/token_count.js +166 -0
  162. package/dist/scripts/_lib/token_count.js.map +1 -0
  163. package/dist/scripts/_lib/trigger_eval_floors.js +8 -0
  164. package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -1
  165. package/dist/scripts/condense.js +139 -9
  166. package/dist/scripts/condense.js.map +1 -1
  167. package/dist/scripts/project_thin_rules.js +450 -0
  168. package/dist/scripts/project_thin_rules.js.map +1 -0
  169. package/dist/server/routes/workspace.js +2 -0
  170. package/dist/server/routes/workspace.js.map +1 -1
  171. package/dist/server/schemas/settings.js +9 -3
  172. package/dist/server/schemas/settings.js.map +1 -1
  173. package/dist/ui/assets/index-DJY_vQVn.js +40 -0
  174. package/dist/ui/assets/index-DJY_vQVn.js.map +1 -0
  175. package/dist/ui/index.html +1 -1
  176. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  177. package/package.json +11 -11
  178. package/src/agent-src/templates/AGENTS.md +1 -1
  179. package/src/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
  180. package/src/config/agent-settings.template.yml +59 -11
  181. package/src/config/ci-local-parity.yml +8 -0
  182. package/src/config/discovery/unassigned-artefacts.yml +23 -25
  183. package/src/config/evaluator-budgets.json +35 -5
  184. package/src/config/gate-coverage.yml +147 -0
  185. package/src/config/gate-violation-baselines.json +25 -0
  186. package/src/install/rule_scope.ts +27 -2
  187. package/src/scripts/_cli/cmd_conformance.ts +13 -24
  188. package/src/scripts/_dispatch.bash +10 -0
  189. package/src/scripts/_lib/claude_settings_hooks.ts +30 -4
  190. package/src/scripts/_lib/exec_evidence.ts +5 -0
  191. package/src/scripts/_lib/gate_baseline.ts +213 -0
  192. package/src/scripts/_lib/scoped_projection.ts +262 -0
  193. package/src/scripts/_lib/tier_budget_routing.ts +234 -0
  194. package/src/scripts/_lib/tool_adapter_registry.ts +8 -1
  195. package/src/scripts/_lib/trigger_eval_floors.ts +9 -0
  196. package/src/scripts/_lib/trigger_routers.ts +17 -5
  197. package/src/scripts/adr/regenerate_index.ts +4 -3
  198. package/src/scripts/ai-video/lib/parse-blueprint.sh +1 -1
  199. package/src/scripts/ai_council/compile_corpus.ts +1 -1
  200. package/src/scripts/audit_cloud_compatibility.ts +37 -2
  201. package/src/scripts/audit_initial_context.ts +53 -19
  202. package/src/scripts/audit_likelihood.ts +31 -2
  203. package/src/scripts/audit_user_type_axis.ts +58 -11
  204. package/src/scripts/bench_hook_latency.ts +172 -41
  205. package/src/scripts/build_mcp_catalog.ts +12 -1
  206. package/src/scripts/check_always_budget.ts +122 -11
  207. package/src/scripts/check_artefact_count_messaging.ts +128 -15
  208. package/src/scripts/check_augment_description_cap.ts +24 -3
  209. package/src/scripts/check_ci_strict_superset.ts +150 -0
  210. package/src/scripts/check_cluster_patterns.ts +100 -0
  211. package/src/scripts/check_consumer_scope_flip.ts +26 -4
  212. package/src/scripts/check_evaluator_budgets.ts +198 -19
  213. package/src/scripts/check_gate_coverage.ts +337 -1
  214. package/src/scripts/check_generator_output_coverage.ts +2 -1
  215. package/src/scripts/check_iron_law_prominence.ts +1 -1
  216. package/src/scripts/check_kernel_rule_bundle.ts +1 -1
  217. package/src/scripts/check_no_new_legacy_path.ts +167 -3
  218. package/src/scripts/check_pack_size.ts +18 -4
  219. package/src/scripts/check_portability.ts +37 -7
  220. package/src/scripts/check_references.ts +19 -3
  221. package/src/scripts/check_release_pr_shape.ts +13 -0
  222. package/src/scripts/check_token_regression.ts +9 -2
  223. package/src/scripts/condense.ts +155 -12
  224. package/src/scripts/cost/budget.mjs +63 -2
  225. package/src/scripts/count_scoped_projection.ts +104 -0
  226. package/src/scripts/evaluator_umbrella.sh +34 -1
  227. package/src/scripts/generate_capability_matrix.ts +8 -0
  228. package/src/scripts/generate_router_coverage_from_matrix.ts +155 -0
  229. package/src/scripts/hook_manifest.yaml +17 -3
  230. package/src/scripts/hooks/block_config_weakening.ts +337 -0
  231. package/src/scripts/hooks/concern_registry.ts +2 -0
  232. package/src/scripts/hooks/probe_throttle.ts +75 -0
  233. package/src/scripts/install-hooks.sh +24 -2
  234. package/src/scripts/install.ts +15 -92
  235. package/src/scripts/lint_agents_md.ts +9 -3
  236. package/src/scripts/lint_archived_skills.ts +1 -1
  237. package/src/scripts/lint_artefact_frontmatter.ts +185 -37
  238. package/src/scripts/lint_command_routing.ts +21 -52
  239. package/src/scripts/lint_command_verbs.ts +47 -28
  240. package/src/scripts/lint_context_spine_usage.ts +38 -12
  241. package/src/scripts/lint_framework_leakage.ts +25 -6
  242. package/src/scripts/lint_handoffs.ts +49 -2
  243. package/src/scripts/lint_media_policy_linkage.ts +62 -30
  244. package/src/scripts/lint_namespace.ts +92 -7
  245. package/src/scripts/lint_new_skill_gate.ts +39 -31
  246. package/src/scripts/lint_originality_shingles.ts +7 -1
  247. package/src/scripts/lint_override_kernel_guard.ts +21 -3
  248. package/src/scripts/lint_pack_boundaries.ts +461 -91
  249. package/src/scripts/lint_pack_dependencies.ts +24 -57
  250. package/src/scripts/lint_persona_governance.ts +1 -1
  251. package/src/scripts/lint_role_experiences.ts +34 -5
  252. package/src/scripts/lint_trigger_precision.ts +142 -0
  253. package/src/scripts/mcp_server/consumer_tool_catalog.json +12 -201
  254. package/src/scripts/mcp_server/tools.ts +1 -1
  255. package/src/scripts/prepack-check.mjs +40 -1
  256. package/src/scripts/prepack_router_targets.d.mts +7 -0
  257. package/src/scripts/prepack_router_targets.mjs +93 -0
  258. package/src/scripts/print_required_checks.ts +72 -18
  259. package/src/scripts/profile_staleness_hook.ts +12 -0
  260. package/src/scripts/record_evaluator_measurements.mjs +105 -0
  261. package/src/scripts/recruit_preflight.sh +1 -1
  262. package/src/scripts/release.ts +105 -1
  263. package/src/scripts/router_target_paths.d.mts +4 -0
  264. package/src/scripts/router_target_paths.mjs +69 -0
  265. package/src/scripts/router_telemetry.ts +52 -1
  266. package/src/scripts/routing_doctor.ts +485 -0
  267. package/src/scripts/rule_trigger_eval.ts +403 -0
  268. package/src/scripts/schemas/rule.schema.json +5 -1
  269. package/src/scripts/session_canary_hook.ts +96 -5
  270. package/src/scripts/skill_linter.ts +90 -42
  271. package/src/scripts/smoke/skills.sh +1 -1
  272. package/src/scripts/sweep_dead_scan_roots.ts +356 -10
  273. package/src/scripts/trigger_coverage.ts +13 -2
  274. package/src/scripts/update_counts.ts +91 -12
  275. package/src/scripts/validate_telegraph_carveouts.ts +1 -1
  276. package/src/scripts/wrapper_freshness_hook.ts +14 -0
  277. package/src/server/schemas/settings.ts +19 -5
  278. package/dist/cli-delegate/chunk-TJIAM5FW.js +0 -133
  279. package/dist/ui/assets/index-BpSQt5vm.js +0 -40
  280. package/dist/ui/assets/index-BpSQt5vm.js.map +0 -1
@@ -794,30 +794,6 @@ function extractFrontmatterField(frontmatter: string, pattern: RegExp): string |
794
794
 
795
795
  type YamlListItem = string | Record<string, string>;
796
796
 
797
- function _parseTrustLevel(frontmatter: string): string | null {
798
- const lines = splitlines(frontmatter);
799
- let inBlock = false;
800
- for (const line of lines) {
801
- if (!inBlock) {
802
- if (line.startsWith('trust:')) {
803
- const rhs = line.slice('trust:'.length).trim();
804
- if (rhs === '') {
805
- inBlock = true;
806
- }
807
- }
808
- continue;
809
- }
810
- if (line.startsWith(' level:')) {
811
- return stripQuotes(line.slice(' level:'.length).trim());
812
- }
813
- if (line.startsWith(' ')) {
814
- continue;
815
- }
816
- break;
817
- }
818
- return null;
819
- }
820
-
821
797
  function stripQuotes(s: string): string {
822
798
  return s.replace(/^["']/, '').replace(/["']$/, '');
823
799
  }
@@ -932,13 +908,18 @@ function lint_router_frontmatter(
932
908
  }
933
909
 
934
910
  if (routesTo === null) {
935
- const trustLevel = _parseTrustLevel(frontmatter);
936
- if (trustLevel !== 'core') {
911
+ // Self-contained carve-out (ADR-210): a rule whose body IS the
912
+ // constraint (prohibition / gate / format law — no procedure to
913
+ // delegate) certifies itself via `self_contained: true` instead of
914
+ // routing. Neither declared → error: the contract's "routes_to ≥ 1
915
+ // unless self-contained" is unenforceable without a marker.
916
+ const selfContained = /^self_contained:[ \t]*true[ \t]*$/m.test(frontmatter);
917
+ if (!selfContained) {
937
918
  issues.push(
938
919
  new Issue(
939
- 'info',
920
+ 'error',
940
921
  'router_routes_to_missing',
941
- 'Non-kernel rule has no routes_to: — body should migrate to skill / guideline in Phase 4',
922
+ "Non-kernel rule declares neither routes_to: nor self_contained: true — route the body to its skill/guideline, or certify it self-contained per docs/contracts/rule-router.md",
942
923
  ),
943
924
  );
944
925
  }
@@ -2439,6 +2420,16 @@ export function gather_candidate_files_under(srcRoot: string): string[] {
2439
2420
  */
2440
2421
  export let last_changed_scope_reason = '';
2441
2422
 
2423
+ /**
2424
+ * Whether the base-span diff (`base...HEAD`) itself ran cleanly.
2425
+ *
2426
+ * The verified-empty release outcome (see `main`) may only rest on an empty
2427
+ * change set that was actually COMPUTED — a base diff that errored (missing
2428
+ * tag object, corrupt ref) degrades to the worktree fallbacks and would
2429
+ * otherwise be indistinguishable from a genuinely empty span.
2430
+ */
2431
+ export let last_changed_base_diff_ok = false;
2432
+
2442
2433
  /**
2443
2434
  * Candidate files changed against the resolved base.
2444
2435
  *
@@ -2454,6 +2445,7 @@ export let last_changed_scope_reason = '';
2454
2445
  export function gather_changed_candidate_files(root: string, since?: string | null): string[] {
2455
2446
  const scope = resolveContentLintScope({ cwd: root, since: since ?? null });
2456
2447
  last_changed_scope_reason = scope.reason;
2448
+ last_changed_base_diff_ok = false;
2457
2449
  const diffCommands = [
2458
2450
  ['git', 'diff', '--name-only', `${scope.base}...HEAD`],
2459
2451
  ['git', 'diff', '--name-only', '--cached', 'HEAD'],
@@ -2461,10 +2453,15 @@ export function gather_changed_candidate_files(root: string, since?: string | nu
2461
2453
  ];
2462
2454
  try {
2463
2455
  let rawLines: string[] = [];
2456
+ let first = true;
2464
2457
  for (const cmd of diffCommands) {
2465
2458
  // A truncated diff would silently shrink the change set — the same
2466
2459
  // degrade-to-zero shape this roadmap closes elsewhere.
2467
2460
  const result = runCountedProbe(cmd[0] as string, cmd.slice(1), { cwd: root });
2461
+ if (first) {
2462
+ last_changed_base_diff_ok = result.ok;
2463
+ first = false;
2464
+ }
2468
2465
  if (result.ok && result.stdout.trim()) {
2469
2466
  rawLines = splitlines(result.stdout);
2470
2467
  break;
@@ -4202,6 +4199,43 @@ function _release_scope_requires_findings(root: string, since?: string | null):
4202
4199
  return scope.isRelease && scope.previousTag !== null;
4203
4200
  }
4204
4201
 
4202
+ /**
4203
+ * True when the `--changed` corpus filter still matches tracked files at HEAD.
4204
+ *
4205
+ * The discriminator between a DEAD scope and a VERIFIED-EMPTY release span.
4206
+ * A release that touches no skill/rule file is a legitimate outcome — but only
4207
+ * provably so when the exact predicate `gather_changed_candidate_files`
4208
+ * applies to diff lines demonstrably still sees the corpus. Probing
4209
+ * `git ls-files` with that same predicate means a moved projection root
4210
+ * (the ADR-051 blindness class) fails this probe exactly as it would blind
4211
+ * the diff — so "empty because the root moved" cannot masquerade as
4212
+ * "empty because nothing changed".
4213
+ */
4214
+ function _changed_filter_matches_head(root: string): boolean {
4215
+ // Probes the canonical projection root only (ADR-051): a corpus that
4216
+ // exists solely under a legacy path counts as dead, not alive.
4217
+ const result = runCountedProbe('git', ['ls-files', '--', 'dist/agent-src'], { cwd: root });
4218
+ if (!result.ok) {
4219
+ return false;
4220
+ }
4221
+ for (const raw of splitlines(result.stdout)) {
4222
+ const norm = raw.trim().replace(/\\/g, '/');
4223
+ if (!norm || norm.includes('/evals/')) {
4224
+ continue;
4225
+ }
4226
+ if (!norm.startsWith('dist/agent-src/') && !norm.includes('/dist/agent-src/')) {
4227
+ continue;
4228
+ }
4229
+ if (
4230
+ norm.endsWith('/SKILL.md') ||
4231
+ (norm.endsWith('.md') && (norm.includes('/rules/') || norm.includes('/commands/')))
4232
+ ) {
4233
+ return true;
4234
+ }
4235
+ }
4236
+ return false;
4237
+ }
4238
+
4205
4239
  export function main(argv: string[]): number {
4206
4240
  let args: Args;
4207
4241
  try {
@@ -4244,25 +4278,39 @@ export function main(argv: string[]): number {
4244
4278
  process.stdout.write(`${format_json([])}\n`);
4245
4279
  }
4246
4280
  // A release PR that resolved a real tag span and still matched
4247
- // nothing has not passed — it examined nothing. Mirrors the
4248
- // scan-scope regime the other gates already adopted
4281
+ // nothing needs a discriminator: DEAD scope (the diff errored, or
4282
+ // the corpus filter no longer matches anything at HEAD — the
4283
+ // ADR-051 moved-root blindness) fails; VERIFIED-EMPTY (the base
4284
+ // diff ran cleanly AND the filter provably still sees tracked
4285
+ // corpus files) is a legitimate release that simply changed no
4286
+ // skill/rule — failing it would red every scripts-/docs-only
4287
+ // release forever. Mirrors the scan-scope regime
4249
4288
  // (`_lib/scan_scope.ts`; exit 2, as `check_iron_law_prominence`).
4250
4289
  // Ordinary PRs keep the historical exit 0: a change set with no
4251
4290
  // skill or rule in it is a legitimate empty scope, not a dead one.
4252
4291
  if (args.changed && _release_scope_requires_findings(root, args.since)) {
4253
- try {
4254
- assertScanned({
4255
- gate: 'skill_linter --changed (release PR)',
4256
- scanned: 0,
4257
- units: 'skill/rule files',
4258
- roots: [last_changed_scope_reason],
4259
- });
4260
- } catch (exc) {
4261
- if (exc instanceof DeadScopeError) {
4262
- process.stderr.write(`❌ ${exc.message}\n`);
4263
- return 2;
4292
+ if (last_changed_base_diff_ok && _changed_filter_matches_head(root)) {
4293
+ process.stderr.write(
4294
+ '✅ verified-empty release scope: the base-span diff ran cleanly and ' +
4295
+ 'the corpus filter still matches tracked files at HEAD — this ' +
4296
+ 'release genuinely changes no skill/rule file. The gate is alive, ' +
4297
+ 'not blind.\n',
4298
+ );
4299
+ } else {
4300
+ try {
4301
+ assertScanned({
4302
+ gate: 'skill_linter --changed (release PR)',
4303
+ scanned: 0,
4304
+ units: 'skill/rule files',
4305
+ roots: [last_changed_scope_reason],
4306
+ });
4307
+ } catch (exc) {
4308
+ if (exc instanceof DeadScopeError) {
4309
+ process.stderr.write(`❌ ${exc.message}\n`);
4310
+ return 2;
4311
+ }
4312
+ throw exc;
4264
4313
  }
4265
- throw exc;
4266
4314
  }
4267
4315
  }
4268
4316
  process.stderr.write('No matching skill/rule files found.\n');
@@ -2,7 +2,7 @@
2
2
  # scripts/smoke/skills.sh — skills-tier smoke (step-11 Phase 3 Step 5).
3
3
  #
4
4
  # Picks 5 random skills (deterministic seed = epoch day) from
5
- # .agent-src.uncondensed/skills/*/SKILL.md and asserts:
5
+ # src/skills/*/SKILL.md and asserts:
6
6
  # 1. SKILL.md exists.
7
7
  # 2. Frontmatter parses + validates against scripts/schemas/skill.schema.json.
8
8
  # 3. `name:` field matches the parent directory name.
@@ -3,9 +3,9 @@
3
3
  * sweep_dead_scan_roots — which gate scripts walk a root that does not exist?
4
4
  *
5
5
  * Population expansion for `road-to-gates-that-can-fail` Phase 1. The committed
6
- * census (`agents/evidence/reports/gate-scope-census.md`) covers the 14 gates a
7
- * manual audit confirmed dead; extending it to the full population is that
8
- * step's stated remaining work. This is the deterministic instrument for it.
6
+ * census (`agents/evidence/reports/gate-scope-census.md`) began as a manual audit
7
+ * of 14 gates; since 2026-08-02 this script GENERATES it over the full population
8
+ * via `--census`, so it is re-runnable and a stale row shows up in a diff.
9
9
  *
10
10
  * WHY IT EXISTS: two more dead gates were found OUTSIDE the censused 14 while
11
11
  * doing thematically unrelated work — `audit_skill_overlap` (rooted at a
@@ -73,7 +73,9 @@
73
73
  * ── EXIT CONTRACT (one meaning per code, so red stays informative) ─────────
74
74
  *
75
75
  * 0 clean
76
- * 1 confirmed class-A finding(s) — real dead roots. Nothing else returns 1.
76
+ * 1 class-A finding(s) ABOVE the recorded ratchet baseline
77
+ * (`src/config/gate-violation-baselines.json`). With no baseline entry this
78
+ * is any class-A finding at all. Nothing else returns 1.
77
79
  * 2 self-test failure — the extractor is blind; output is not trustworthy.
78
80
  * 3 ledger hygiene — stale entry or over cap. The sweep ran fine; the LIST
79
81
  * needs work.
@@ -85,7 +87,13 @@
85
87
  *
86
88
  * Usage:
87
89
  * ./scripts-run src/scripts/sweep_dead_scan_roots [--quiet] [--json <path>]
88
- * [--root <dir>] [--ledger <path>]
90
+ * [--census <path>] [--root <dir>]
91
+ * [--ledger <path>]
92
+ *
93
+ * `--census` writes the full scan-scope census — every gate, every root it
94
+ * reads, and the unit count under that root today. The sweep answers "which
95
+ * roots are dead"; the census answers "what does each gate read, and how much",
96
+ * so the NEXT container move shows up as a count dropping in a diff.
89
97
  */
90
98
 
91
99
  import * as fs from 'node:fs';
@@ -93,6 +101,9 @@ import * as path from 'node:path';
93
101
  import process from 'node:process';
94
102
  import { fileURLToPath } from 'node:url';
95
103
 
104
+ import * as agentSrc from './_lib/agent_src.js';
105
+ import { checkRatchet } from './_lib/gate_baseline.js';
106
+
96
107
  const _HERE = fileURLToPath(import.meta.url);
97
108
  const REPO = path.resolve(path.dirname(_HERE), '..', '..');
98
109
  const DEFAULT_SCRIPTS = path.join(REPO, 'src', 'scripts');
@@ -135,9 +146,24 @@ export interface Finding {
135
146
  evidence: string;
136
147
  }
137
148
 
149
+ /** Every root the extractor saw for one gate, dead or alive — the census row source. */
150
+ export interface RootRef {
151
+ rel: string;
152
+ /** Declaring identifier(s), or `(inline)` / `(array)` / `(spec-table)`. */
153
+ names: string;
154
+ }
155
+
138
156
  export interface Analysis {
139
157
  confirmed: Finding[];
140
158
  unproven: Finding[];
159
+ /**
160
+ * Census input: ALL extracted roots, not only the missing ones the sweep
161
+ * reports as findings. The sweep answers "which roots are dead"; the census
162
+ * answers "what does every gate read, and how much" — a root+count pair per
163
+ * gate is what makes the NEXT container move visible in a diff instead of
164
+ * silently disarming a gate for weeks.
165
+ */
166
+ roots: RootRef[];
141
167
  }
142
168
 
143
169
  export interface Disposition {
@@ -345,9 +371,11 @@ export function analyze(src: string, repoRoot: string): Analysis {
345
371
 
346
372
  const confirmed: Finding[] = [];
347
373
  const unproven: Finding[] = [];
374
+ const roots = new Map<string, string>();
348
375
  const missing = (rel: string): boolean => !fs.existsSync(path.join(repoRoot, rel));
349
376
 
350
377
  for (const [rel, e] of found) {
378
+ roots.set(rel, [...e.names].join(',') || '(inline)');
351
379
  if (!missing(rel)) continue;
352
380
  let evidence: string | null =
353
381
  e.inlineRead ? 'inline-read' : (arrayHits.get(rel) ?? specHits.get(rel) ?? null);
@@ -376,7 +404,30 @@ export function analyze(src: string, repoRoot: string): Analysis {
376
404
  for (const [rel, ev] of arrayHits) push(confirmed, [], rel, '(array)', ev);
377
405
  for (const rel of arrayUnproven) push(unproven, unproven, rel, '(array)', '—');
378
406
 
379
- return { confirmed, unproven };
407
+ // Resolver-reached roots: a gate calling `SRC_SKILLS()` reads `src/skills`
408
+ // just as surely as one joining the literal, and the census must see both.
409
+ for (const [getter, rel] of resolverRoots(repoRoot)) {
410
+ if (new RegExp(`\\b${getter}\\s*\\(\\s*\\)`).test(src) && !roots.has(rel)) {
411
+ roots.set(rel, `${getter}()`);
412
+ }
413
+ }
414
+
415
+ // Census-only permissive pass — see `censusOnlyRoots` for why it may never
416
+ // feed the finding path.
417
+ for (const [rel, name] of censusOnlyRoots(src, repoRoot)) {
418
+ if (!roots.has(rel)) roots.set(rel, name);
419
+ }
420
+
421
+ for (const rel of specHits.keys()) if (!roots.has(rel)) roots.set(rel, '(spec-table)');
422
+ for (const rel of specUnproven) if (!roots.has(rel)) roots.set(rel, '(spec-table)');
423
+ for (const rel of arrayHits.keys()) if (!roots.has(rel)) roots.set(rel, '(array)');
424
+ for (const rel of arrayUnproven) if (!roots.has(rel)) roots.set(rel, '(array)');
425
+
426
+ return {
427
+ confirmed,
428
+ unproven,
429
+ roots: [...roots].sort(([a], [b]) => a.localeCompare(b)).map(([rel, names]) => ({ rel, names })),
430
+ };
380
431
  }
381
432
 
382
433
  /**
@@ -411,19 +462,279 @@ export function loadLedger(file: string): Disposition[] {
411
462
  return Array.isArray(data) ? data : [];
412
463
  }
413
464
 
465
+ /**
466
+ * Roots reached through the shared resolver rather than a path literal.
467
+ *
468
+ * Without this, the census is blind to exactly the gates that did the right
469
+ * thing: a gate repaired from `path.join(ROOT, '.agent-src.uncondensed')` to
470
+ * `SRC_SKILLS()` would VANISH from the census, so adopting the resolver — which
471
+ * the roadmap explicitly prefers over new literals — would look like losing
472
+ * coverage. The map is built by calling the real getters, not by copying their
473
+ * values, so it cannot drift from `_lib/agent_src.ts`.
474
+ */
475
+ function resolverRoots(repoRoot: string): ReadonlyMap<string, string> {
476
+ const getters: Record<string, () => string> = {
477
+ SRC: agentSrc.SRC,
478
+ SRC_SKILLS: agentSrc.SRC_SKILLS,
479
+ SRC_RULES: agentSrc.SRC_RULES,
480
+ SRC_AGENT: agentSrc.SRC_AGENT,
481
+ SRC_DOMAINS: agentSrc.SRC_DOMAINS,
482
+ LEGACY_SRC: agentSrc.LEGACY_SRC,
483
+ PACKAGES: agentSrc.PACKAGES,
484
+ };
485
+ const out = new Map<string, string>();
486
+ for (const [name, fn] of Object.entries(getters)) {
487
+ let abs: string;
488
+ try {
489
+ abs = fn();
490
+ } catch {
491
+ continue;
492
+ }
493
+ const rel = path.relative(repoRoot, abs).split(path.sep).join('/');
494
+ if (rel && !rel.startsWith('..')) out.set(name, rel);
495
+ }
496
+ return out;
497
+ }
498
+
499
+ /**
500
+ * Permissive root extraction — CENSUS ONLY, never the finding path.
501
+ *
502
+ * The finding extractor is deliberately strict: it reports a dead root only
503
+ * with positive read evidence, because a false "this gate is dead" turns CI red
504
+ * on a lie. That precision costs recall — a gate writing
505
+ * `const SOURCE_DIR = 'src'` and later `path.join(REPO, SOURCE_DIR)` is
506
+ * invisible to it, and roughly half the population is shaped that way.
507
+ *
508
+ * The census has the opposite trade: a missed root is a coverage hole, and a
509
+ * spurious row is cheap. So this pass substitutes single-literal string consts
510
+ * into `path.join(...)` calls — and then keeps ONLY roots that EXIST on disk.
511
+ * That filter is what makes it safe: a pass that cannot yield a missing path
512
+ * cannot manufacture a dead-root finding, no matter how loose its parsing.
513
+ * Precision stays where red exits are decided; recall goes where the record is.
514
+ */
515
+ export function censusOnlyRoots(src: string, repoRoot: string): Map<string, string> {
516
+ const consts = new Map<string, string>();
517
+ const cre = /(?:const|let)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*'([^'\n]+)'/g;
518
+ let cm: RegExpExecArray | null;
519
+ while ((cm = cre.exec(src)) !== null) {
520
+ const [, name, value] = cm;
521
+ if (value !== undefined && !value.includes(' ') && value !== '') consts.set(name as string, value);
522
+ }
523
+
524
+ const out = new Map<string, string>();
525
+ const jre = /path\.join\(([^)]*)\)/g;
526
+ let jm: RegExpExecArray | null;
527
+ while ((jm = jre.exec(src)) !== null) {
528
+ const parts = (jm[1] as string).split(',').map((p) => p.trim()).filter((p) => p !== '');
529
+ if (parts.length < 2) continue;
530
+ const base = parts[0] as string;
531
+ if (!BASE_IDS.has(base)) continue;
532
+
533
+ const segs: string[] = [];
534
+ for (const raw of parts.slice(1)) {
535
+ const lit = /^'([^']*)'$/.exec(raw);
536
+ if (lit) {
537
+ segs.push(lit[1] as string);
538
+ continue;
539
+ }
540
+ const viaConst = consts.get(raw);
541
+ if (viaConst !== undefined) {
542
+ segs.push(viaConst);
543
+ continue;
544
+ }
545
+ break; // a segment we cannot resolve ends the path — a prefix is still a real root
546
+ }
547
+ if (segs.length === 0) continue;
548
+ const rel = segs.join('/').replace(/\/+$/, '');
549
+ if (rel === '' || rel.startsWith('..')) continue;
550
+ // `node_modules/.bin/tsx` is a tool path a gate SPAWNS, not a corpus it reads.
551
+ if (rel === 'node_modules' || rel.startsWith('node_modules/')) continue;
552
+ if (!fs.existsSync(path.join(repoRoot, rel))) continue; // the safety filter — see the doc comment
553
+ if (!out.has(rel)) out.set(rel, '(census-only)');
554
+ }
555
+ return out;
556
+ }
557
+
558
+ /** Directories never worth walking for a unit count. */
559
+ const COUNT_SKIP: ReadonlySet<string> = new Set(['node_modules', '.git', '.venv', '__pycache__']);
560
+
561
+ /** Depth cap for the unit count — deep enough for every artefact tree here, cheap enough to re-run. */
562
+ const COUNT_MAX_DEPTH = 12;
563
+
564
+ export interface RootCount {
565
+ /** `dir` · `file` · `absent` — absent is the dead-root case the sweep reports separately. */
566
+ kind: 'dir' | 'file' | 'absent';
567
+ /** Files under the root (recursive), 1 for a file, 0 for absent. */
568
+ units: number;
569
+ }
570
+
571
+ /**
572
+ * Count what a root actually contains, on this tree, right now.
573
+ *
574
+ * The number is deliberately generic — files under the root — not the gate's
575
+ * own notion of a unit (rules, skills, declarers). A gate-specific count would
576
+ * mean reimplementing 200 gates' filters here and would rot the moment one
577
+ * changed. What the census needs is a value that MOVES when the tree moves, so
578
+ * a container migration shows up as a diff instead of as silence.
579
+ */
580
+ export function countUnits(repoRoot: string, rel: string): RootCount {
581
+ const abs = path.join(repoRoot, rel);
582
+ let st: fs.Stats;
583
+ try {
584
+ st = fs.statSync(abs);
585
+ } catch {
586
+ return { kind: 'absent', units: 0 };
587
+ }
588
+ if (st.isFile()) return { kind: 'file', units: 1 };
589
+ if (!st.isDirectory()) return { kind: 'absent', units: 0 };
590
+
591
+ let units = 0;
592
+ const walk = (dir: string, depth: number): void => {
593
+ if (depth > COUNT_MAX_DEPTH) return;
594
+ let entries: fs.Dirent[];
595
+ try {
596
+ entries = fs.readdirSync(dir, { withFileTypes: true });
597
+ } catch {
598
+ return;
599
+ }
600
+ for (const e of entries) {
601
+ if (e.isDirectory()) {
602
+ if (!COUNT_SKIP.has(e.name)) walk(path.join(dir, e.name), depth + 1);
603
+ } else if (e.isFile()) {
604
+ units += 1;
605
+ }
606
+ }
607
+ };
608
+ walk(abs, 0);
609
+ return { kind: 'dir', units };
610
+ }
611
+
612
+ export interface CensusRow {
613
+ gate: string;
614
+ rel: string;
615
+ names: string;
616
+ kind: RootCount['kind'];
617
+ units: number;
618
+ }
619
+
620
+ /**
621
+ * Build the census: one row per (gate, extracted root), plus an explicit row
622
+ * for every gate the extractor found NO root in.
623
+ *
624
+ * The no-root rows are the honest half. A census that silently omits the gates
625
+ * it could not read would claim coverage it does not have — the same
626
+ * green-on-nothing shape this whole sweep exists to end.
627
+ */
628
+ export function buildCensus(population: readonly string[], results: ReadonlyMap<string, Analysis>, repoRoot: string): CensusRow[] {
629
+ const rows: CensusRow[] = [];
630
+ for (const file of population) {
631
+ const gate = file.replace(/\.ts$/, '');
632
+ const analysis = results.get(file);
633
+ const refs = analysis?.roots ?? [];
634
+ if (refs.length === 0) {
635
+ rows.push({ gate, rel: '(no literal root extracted)', names: '—', kind: 'absent', units: 0 });
636
+ continue;
637
+ }
638
+ for (const ref of refs) {
639
+ const c = countUnits(repoRoot, ref.rel);
640
+ rows.push({ gate, rel: ref.rel, names: ref.names, kind: c.kind, units: c.units });
641
+ }
642
+ }
643
+ return rows;
644
+ }
645
+
646
+ export function renderCensus(rows: readonly CensusRow[], population: number, today: string): string {
647
+ const dead = rows.filter((r) => r.kind === 'absent' && r.rel !== '(no literal root extracted)');
648
+ const noRoot = rows.filter((r) => r.rel === '(no literal root extracted)');
649
+ const live = rows.filter((r) => r.kind !== 'absent');
650
+ const gatesWithRoots = new Set(live.map((r) => r.gate)).size;
651
+
652
+ const out: string[] = [];
653
+ out.push('# Gate scan-scope census');
654
+ out.push('');
655
+ out.push(`> Generated by \`./scripts-run src/scripts/sweep_dead_scan_roots --census <path>\` on ${today}.`);
656
+ out.push('> Do not hand-edit — re-run the command. A row that disagrees with a fresh');
657
+ out.push('> run is the signal this file exists to produce.');
658
+ out.push('');
659
+ out.push('## Why');
660
+ out.push('');
661
+ out.push('ADR-051 moved the source container. Gates carrying a hardcoded literal root');
662
+ out.push('kept exiting 0 while scanning zero files, for weeks, because nothing recorded');
663
+ out.push('what each gate was supposed to be reading. This census is that record: every');
664
+ out.push('gate, every root it reads, and how much is under that root **today**. The next');
665
+ out.push('container move shows up here as a count dropping to zero in a diff, instead of');
666
+ out.push('as a green checkmark over an empty directory.');
667
+ out.push('');
668
+ out.push('## Headline');
669
+ out.push('');
670
+ out.push('| Metric | Value |');
671
+ out.push('|---|---|');
672
+ out.push(`| Gate scripts in population | ${population} |`);
673
+ out.push(`| Gates with at least one resolvable root | ${gatesWithRoots} |`);
674
+ out.push(`| Gates with no literal root the extractor can see | ${noRoot.length} |`);
675
+ out.push(`| Roots resolved and counted | ${live.length} |`);
676
+ out.push(`| Roots that do not exist on this tree | ${dead.length} |`);
677
+ out.push('');
678
+ out.push('## Scope — what this census does NOT cover');
679
+ out.push('');
680
+ out.push('- **The unit count is files under the root, not the gate\'s own unit.** A gate');
681
+ out.push(' filtering to `*.md` reports fewer units than the row shows. The number is a');
682
+ out.push(' movement detector, not a gate-internal assertion — reimplementing 200 filters');
683
+ out.push(' here would rot on the first filter change.');
684
+ out.push('- **Roots the static extractor cannot see** are listed as');
685
+ out.push(' `(no literal root extracted)`, not omitted. Known blind spots: roots read from');
686
+ out.push(' a config file, bases outside the accepted repo-root identifier set, glob-library');
687
+ out.push(' walks, and template-literal paths. Those gates are uncovered, and the row says so.');
688
+ out.push('- **Nested helper directories** under `src/scripts/` are outside the population —');
689
+ out.push(' the sweep reads the top level only.');
690
+ out.push('- A dead root here is a *finding*, not a repair. Repairs are the roadmap\'s job;');
691
+ out.push(' this file only makes them visible.');
692
+ out.push('');
693
+ out.push('## Roots that do not exist on this tree');
694
+ out.push('');
695
+ if (dead.length === 0) {
696
+ out.push('None. Every extracted root resolves.');
697
+ } else {
698
+ out.push('| Gate | Declared root | Declared by |');
699
+ out.push('|---|---|---|');
700
+ for (const r of dead) out.push(`| \`${r.gate}\` | \`${r.rel}\` | \`${r.names}\` |`);
701
+ }
702
+ out.push('');
703
+ out.push('## Full census');
704
+ out.push('');
705
+ out.push('| Gate | Root | Kind | Units | Declared by |');
706
+ out.push('|---|---|---|---|---|');
707
+ for (const r of rows) {
708
+ const rootCell = r.rel === '(no literal root extracted)' ? '_(none extracted)_' : `\`${r.rel}\``;
709
+ const units = r.kind === 'absent' ? '**0**' : String(r.units);
710
+ out.push(`| \`${r.gate}\` | ${rootCell} | ${r.kind} | ${units} | \`${r.names}\` |`);
711
+ }
712
+ out.push('');
713
+ out.push('## Reproducing');
714
+ out.push('');
715
+ out.push('```bash');
716
+ out.push('./scripts-run src/scripts/sweep_dead_scan_roots --census agents/evidence/reports/gate-scope-census.md');
717
+ out.push('git diff --stat agents/evidence/reports/gate-scope-census.md # empty = the tree has not moved');
718
+ out.push('```');
719
+ out.push('');
720
+ return `${out.join('\n')}\n`;
721
+ }
722
+
414
723
  interface Args {
415
724
  quiet: boolean;
416
725
  json: string | null;
726
+ census: string | null;
417
727
  scripts: string;
418
728
  ledger: string;
419
729
  }
420
730
 
421
731
  export function parseArgs(argv: string[]): Args {
422
- const a: Args = { quiet: false, json: null, scripts: DEFAULT_SCRIPTS, ledger: DEFAULT_LEDGER };
732
+ const a: Args = { quiet: false, json: null, census: null, scripts: DEFAULT_SCRIPTS, ledger: DEFAULT_LEDGER };
423
733
  for (let i = 0; i < argv.length; i += 1) {
424
734
  const arg = argv[i];
425
735
  if (arg === '--quiet') a.quiet = true;
426
736
  else if (arg === '--json') a.json = argv[(i += 1)] ?? '';
737
+ else if (arg === '--census') a.census = argv[(i += 1)] ?? '';
427
738
  else if (arg === '--root') a.scripts = path.resolve(REPO, argv[(i += 1)] ?? DEFAULT_SCRIPTS);
428
739
  else if (arg === '--ledger') a.ledger = path.resolve(REPO, argv[(i += 1)] ?? DEFAULT_LEDGER);
429
740
  else {
@@ -463,9 +774,14 @@ export function main(argv: string[]): number {
463
774
  const pop = fs.existsSync(args.scripts)
464
775
  ? fs.readdirSync(args.scripts).filter((f) => GATE_RE.test(f)).sort()
465
776
  : [];
777
+ // Census needs every gate's roots; the finding report needs only the ones
778
+ // with hits. Keep both, and filter at the point of use rather than dropping
779
+ // data here — `census` is the superset.
780
+ const census = new Map<string, Analysis>();
466
781
  const results = new Map<string, Analysis>();
467
782
  for (const f of pop) {
468
783
  const r = analyze(fs.readFileSync(path.join(args.scripts, f), 'utf-8'), repoRoot);
784
+ census.set(f, r);
469
785
  if (r.confirmed.length || r.unproven.length) results.set(f, r);
470
786
  }
471
787
 
@@ -514,7 +830,11 @@ export function main(argv: string[]): number {
514
830
  unproven: unprovenCount,
515
831
  class_a: classA,
516
832
  stale: stale.map((s) => `${s.script}:${s.rel}`),
517
- findings: [...results].map(([script, r]) => ({ script, ...r })),
833
+ findings: [...results].map(([script, r]) => ({
834
+ script,
835
+ confirmed: r.confirmed,
836
+ unproven: r.unproven,
837
+ })),
518
838
  },
519
839
  null,
520
840
  2,
@@ -522,6 +842,18 @@ export function main(argv: string[]): number {
522
842
  );
523
843
  }
524
844
 
845
+ if (args.census) {
846
+ const rows = buildCensus(pop, census, repoRoot);
847
+ const today = new Date().toISOString().slice(0, 10);
848
+ fs.mkdirSync(path.dirname(args.census), { recursive: true });
849
+ fs.writeFileSync(args.census, renderCensus(rows, pop.length, today));
850
+ if (!args.quiet) {
851
+ process.stdout.write(
852
+ `sweep_dead_scan_roots: census → ${args.census} (${rows.length} row(s) over ${pop.length} gate(s))\n`,
853
+ );
854
+ }
855
+ }
856
+
525
857
  if (!args.quiet) {
526
858
  process.stdout.write(`sweep_dead_scan_roots: ${pop.length} gate script(s) scanned\n`);
527
859
  for (const l of lines) process.stdout.write(`${l}\n`);
@@ -534,14 +866,28 @@ export function main(argv: string[]): number {
534
866
  );
535
867
  }
536
868
 
537
- // Real findings outrank ledger hygiene — never let bookkeeping mask a dead gate.
538
- const exit = classA > 0 ? 1 : stale.length > 0 ? 3 : 0;
869
+ // Class-A findings are judged against the ratchet, not against zero.
870
+ //
871
+ // This gate shipped deliberately OUTSIDE `task ci` because it exits 1 on
872
+ // pre-existing class-A debt, and "a gate that turns CI red on debt it did
873
+ // not create" is the exact failure this track exists to avoid — its own
874
+ // Taskfile entry says so, and defers the wiring to the
875
+ // `dead-gate-finding-triage` disposition. That disposition is now decided
876
+ // (repair + ratchet, council 2026-08-02), so the deferral is spent: a count
877
+ // at or under the recorded baseline passes and the sweep can run in CI,
878
+ // while any RISE still exits 1. A tree with no baseline file behaves exactly
879
+ // as before — any class-A finding fails.
880
+ const ratchet = checkRatchet({ gate: 'sweep_dead_scan_roots', actual: classA, repoRoot });
881
+ const exit = !ratchet.ok ? 1 : stale.length > 0 ? 3 : 0;
539
882
  if (!args.quiet || exit !== 0) {
540
883
  process.stdout.write(
541
884
  `sweep_dead_scan_roots: ${confirmedCount} confirmed, ${unprovenCount} unproven, ` +
542
885
  `${classA} class-A, ${stale.length} stale → exit ${exit}\n`,
543
886
  );
544
887
  }
888
+ if (classA > 0 || ratchet.status !== 'unbaselined') {
889
+ (ratchet.ok ? process.stdout : process.stderr).write(`${ratchet.message}\n`);
890
+ }
545
891
  return exit;
546
892
  }
547
893