@event4u/agent-config 9.19.0 → 9.21.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 (234) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +89 -0
  3. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  4. package/dist/cli-delegate/{chunk-XGA2NTU4.js → chunk-3EWYC3YK.js} +40 -0
  5. package/dist/cli-delegate/cmd_handoff.js +1 -1
  6. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  7. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  8. package/dist/discovery/deprecation-report.md +1 -1
  9. package/dist/discovery/discovery-manifest.json +1 -1
  10. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  11. package/dist/discovery/discovery-manifest.summary.md +1 -1
  12. package/dist/discovery/orphan-report.md +1 -1
  13. package/dist/discovery/packs.json +1 -1
  14. package/dist/discovery/trust-report.md +1 -1
  15. package/dist/discovery/workspaces.json +1 -1
  16. package/dist/hooks/dispatch.js +58 -2
  17. package/dist/mcp/registry-manifest.json +2 -2
  18. package/dist/mcp/server.json +2 -2
  19. package/dist/mcp/server.mjs +47 -29
  20. package/package.json +1 -1
  21. package/src/config/gate-coverage.yml +38 -4
  22. package/src/config/gate-violation-baselines.json +3 -8
  23. package/src/scripts/_lib/gate_population.ts +70 -0
  24. package/src/scripts/_lib/roadmap_granularity.ts +157 -0
  25. package/src/scripts/_lib/scan_scope.ts +44 -0
  26. package/src/scripts/_lib/stdin.ts +82 -0
  27. package/src/scripts/audit_adr_coverage.ts +38 -0
  28. package/src/scripts/audit_auto_rules.ts +17 -3
  29. package/src/scripts/audit_command_surface.ts +51 -0
  30. package/src/scripts/audit_initial_context.ts +39 -3
  31. package/src/scripts/audit_mcp_tools.ts +25 -0
  32. package/src/scripts/audit_overlap.ts +24 -1
  33. package/src/scripts/audit_skill_descriptions.ts +19 -0
  34. package/src/scripts/{verify_before_complete_hook.ts → before_complete_hook.ts} +16 -3
  35. package/src/scripts/check_adr_frontmatter.ts +23 -3
  36. package/src/scripts/check_always_budget.ts +24 -0
  37. package/src/scripts/check_artefact_checksums.ts +24 -1
  38. package/src/scripts/check_artefact_count_messaging.ts +20 -0
  39. package/src/scripts/check_augmentignore.ts +22 -15
  40. package/src/scripts/check_backstop_debt.ts +22 -0
  41. package/src/scripts/check_beta_review_markers.ts +22 -1
  42. package/src/scripts/check_bridge_derivation.ts +22 -1
  43. package/src/scripts/check_bundle_path_leakage.ts +26 -0
  44. package/src/scripts/check_ci_strict_superset.ts +21 -3
  45. package/src/scripts/check_claims.ts +22 -0
  46. package/src/scripts/check_cli_registry_budget_sync.ts +33 -4
  47. package/src/scripts/check_cluster_patterns.ts +26 -4
  48. package/src/scripts/check_command_count_messaging.ts +20 -4
  49. package/src/scripts/check_comparison.ts +18 -0
  50. package/src/scripts/check_completion_review.ts +19 -4
  51. package/src/scripts/check_condensed_paths.ts +23 -2
  52. package/src/scripts/check_consumer_scope_flip.ts +21 -2
  53. package/src/scripts/check_council_config_location.ts +20 -0
  54. package/src/scripts/check_council_layout.ts +22 -2
  55. package/src/scripts/check_council_references.ts +23 -1
  56. package/src/scripts/check_dependency_floors.ts +19 -3
  57. package/src/scripts/check_discovery_determinism.ts +26 -0
  58. package/src/scripts/check_enforcement_coverage.ts +20 -0
  59. package/src/scripts/check_evaluator_budgets.ts +23 -0
  60. package/src/scripts/check_finding_dispositions.ts +29 -0
  61. package/src/scripts/check_gate_coverage.ts +63 -22
  62. package/src/scripts/check_gate_paths.ts +30 -5
  63. package/src/scripts/check_generated_artefact_headers.ts +21 -0
  64. package/src/scripts/check_generator_output_coverage.ts +23 -0
  65. package/src/scripts/check_gitignore_freshness.ts +23 -0
  66. package/src/scripts/check_host_loadability.ts +34 -8
  67. package/src/scripts/check_installer_import_purity.ts +22 -8
  68. package/src/scripts/check_kernel_prefix_stability.ts +19 -1
  69. package/src/scripts/check_kernel_rule_bundle.ts +23 -0
  70. package/src/scripts/check_knowledge_cards.ts +13 -0
  71. package/src/scripts/check_knowledge_pages.ts +23 -0
  72. package/src/scripts/check_knowledge_sharing.ts +27 -0
  73. package/src/scripts/check_md_language.ts +24 -0
  74. package/src/scripts/check_media_deps.ts +23 -0
  75. package/src/scripts/check_memory.ts +26 -1
  76. package/src/scripts/check_memory_contradiction.ts +16 -1
  77. package/src/scripts/check_memory_proposal.ts +15 -0
  78. package/src/scripts/check_memory_similarity.ts +16 -0
  79. package/src/scripts/check_module_management_neutral.ts +19 -11
  80. package/src/scripts/check_no_conflict_markers.ts +22 -1
  81. package/src/scripts/check_no_external_sources.ts +26 -1
  82. package/src/scripts/check_no_local_settings_committed.ts +37 -10
  83. package/src/scripts/check_no_new_legacy_path.ts +30 -1
  84. package/src/scripts/check_one_off_location.ts +43 -0
  85. package/src/scripts/check_originality_freshness.ts +25 -1
  86. package/src/scripts/check_overlay_cascade_subdirs.ts +21 -0
  87. package/src/scripts/check_pack_size.ts +22 -0
  88. package/src/scripts/check_preamble_payload_budget.ts +35 -5
  89. package/src/scripts/check_proposal.ts +24 -1
  90. package/src/scripts/check_public_catalog_links.ts +27 -0
  91. package/src/scripts/check_public_links.ts +19 -0
  92. package/src/scripts/check_quality_regression.ts +19 -0
  93. package/src/scripts/check_reach_channels.ts +44 -0
  94. package/src/scripts/check_reach_staleness.ts +27 -0
  95. package/src/scripts/check_release_adjacent_health.ts +28 -0
  96. package/src/scripts/check_release_highlights.ts +34 -0
  97. package/src/scripts/check_release_includes_discovery.ts +25 -6
  98. package/src/scripts/check_release_pr_shape.ts +18 -1
  99. package/src/scripts/check_release_published.ts +21 -0
  100. package/src/scripts/check_release_surface_equality.ts +22 -0
  101. package/src/scripts/check_release_trunk_sync.ts +19 -0
  102. package/src/scripts/check_reply_consistency.ts +22 -0
  103. package/src/scripts/check_review_dispositions.ts +188 -0
  104. package/src/scripts/check_roadmap_trackable.ts +20 -0
  105. package/src/scripts/check_role_doc_links.ts +23 -0
  106. package/src/scripts/check_rule_invariants.ts +14 -0
  107. package/src/scripts/check_secret_leak.ts +47 -0
  108. package/src/scripts/check_skill_gaps.ts +32 -0
  109. package/src/scripts/check_skill_requires.ts +19 -0
  110. package/src/scripts/check_source_pointer_freshness.ts +20 -0
  111. package/src/scripts/check_structural_breaking.ts +29 -2
  112. package/src/scripts/check_surface_tiers.ts +34 -0
  113. package/src/scripts/check_template_pin_drift.ts +23 -1
  114. package/src/scripts/check_test_coverage_diff.ts +17 -1
  115. package/src/scripts/check_token_optimizer_freshness.ts +18 -3
  116. package/src/scripts/check_token_quality_golden.ts +21 -0
  117. package/src/scripts/check_token_regression.ts +25 -0
  118. package/src/scripts/check_tracked_but_ignored.ts +26 -0
  119. package/src/scripts/check_trigger_eval_presence.ts +23 -1
  120. package/src/scripts/check_trigger_evals.ts +36 -0
  121. package/src/scripts/check_trunk_drift.ts +23 -0
  122. package/src/scripts/check_update_banner.ts +24 -0
  123. package/src/scripts/dispatch_r2_reviewer.ts +3 -0
  124. package/src/scripts/gate_scope_classify.ts +513 -0
  125. package/src/scripts/hook_manifest.yaml +1 -1
  126. package/src/scripts/hooks/block_kernel_rule_writes.ts +1 -1
  127. package/src/scripts/hooks/concern_registry.ts +2 -2
  128. package/src/scripts/lint_agent_security.ts +19 -0
  129. package/src/scripts/lint_agent_skill_names.ts +30 -0
  130. package/src/scripts/lint_agents_layout.ts +20 -0
  131. package/src/scripts/lint_agents_md.ts +20 -0
  132. package/src/scripts/lint_archived_skills.ts +24 -3
  133. package/src/scripts/lint_behavioural_eval_freshness.ts +29 -3
  134. package/src/scripts/lint_bench_ab.ts +25 -0
  135. package/src/scripts/lint_bench_corpus.ts +20 -4
  136. package/src/scripts/lint_breaking_changes_index.ts +24 -0
  137. package/src/scripts/lint_budget_ownership.ts +20 -3
  138. package/src/scripts/lint_changelog_rollback.ts +24 -0
  139. package/src/scripts/lint_command_flow_coverage.ts +22 -0
  140. package/src/scripts/lint_command_tiers.ts +29 -5
  141. package/src/scripts/lint_commit_subjects.ts +15 -0
  142. package/src/scripts/lint_confusables.ts +24 -0
  143. package/src/scripts/lint_consumer_internal_refs.ts +24 -0
  144. package/src/scripts/lint_design_quality.ts +22 -0
  145. package/src/scripts/lint_design_slop.ts +23 -0
  146. package/src/scripts/lint_discovery_manifest.ts +23 -0
  147. package/src/scripts/lint_discovery_vocabulary.ts +19 -0
  148. package/src/scripts/lint_documented_commands.ts +23 -0
  149. package/src/scripts/lint_empty_roadmaps.ts +23 -0
  150. package/src/scripts/lint_eval_fixture_citations.ts +19 -0
  151. package/src/scripts/lint_eval_freshness.ts +35 -0
  152. package/src/scripts/lint_examples.ts +18 -3
  153. package/src/scripts/lint_explain_trace.ts +23 -0
  154. package/src/scripts/lint_featured_skills.ts +26 -1
  155. package/src/scripts/lint_flows.ts +19 -0
  156. package/src/scripts/lint_frontmatter_boilerplate.ts +20 -0
  157. package/src/scripts/lint_ghostwriter_source.ts +26 -6
  158. package/src/scripts/lint_glama_drift.ts +24 -0
  159. package/src/scripts/lint_global_paths.ts +24 -0
  160. package/src/scripts/lint_governed_writes.ts +20 -0
  161. package/src/scripts/lint_hidden_unicode.ts +25 -0
  162. package/src/scripts/lint_hook_concern_budget.ts +39 -6
  163. package/src/scripts/lint_hook_manifest.ts +22 -0
  164. package/src/scripts/lint_instruction_smuggling.ts +23 -0
  165. package/src/scripts/lint_knowledge_scale.ts +32 -2
  166. package/src/scripts/lint_legal_pack.ts +31 -0
  167. package/src/scripts/lint_marketplace.ts +26 -0
  168. package/src/scripts/lint_marketplace_install_completeness.ts +23 -0
  169. package/src/scripts/lint_mcp_config_security.ts +24 -0
  170. package/src/scripts/lint_mcp_registry_manifest.ts +22 -1
  171. package/src/scripts/lint_memory_tripwire.ts +35 -0
  172. package/src/scripts/lint_missions.ts +23 -7
  173. package/src/scripts/lint_model_tier_coverage.ts +18 -0
  174. package/src/scripts/lint_namespace_collisions.ts +19 -0
  175. package/src/scripts/lint_no_new_atomic_commands.ts +33 -0
  176. package/src/scripts/lint_no_python_twin_rationale.ts +18 -0
  177. package/src/scripts/lint_one_off_age.ts +38 -2
  178. package/src/scripts/lint_orchestration_dsl.ts +23 -3
  179. package/src/scripts/lint_orchestrator_auto_detect.ts +19 -12
  180. package/src/scripts/lint_originality.ts +22 -4
  181. package/src/scripts/lint_originality_shingles.ts +28 -3
  182. package/src/scripts/lint_output_slop.ts +19 -0
  183. package/src/scripts/lint_override_kernel_guard.ts +30 -0
  184. package/src/scripts/lint_pack_first_win.ts +23 -0
  185. package/src/scripts/lint_pack_risk_class.ts +19 -3
  186. package/src/scripts/lint_persistence.ts +19 -0
  187. package/src/scripts/lint_persona_governance.ts +18 -3
  188. package/src/scripts/lint_positioning.ts +23 -0
  189. package/src/scripts/lint_pre_migration_refs.ts +24 -0
  190. package/src/scripts/lint_profile_overlay_set_only.ts +24 -0
  191. package/src/scripts/lint_provenance.ts +21 -0
  192. package/src/scripts/lint_provenance_vocabulary.ts +24 -1
  193. package/src/scripts/lint_readme_jargon.ts +21 -3
  194. package/src/scripts/lint_readme_serial_comma.ts +21 -3
  195. package/src/scripts/lint_readme_size.ts +21 -3
  196. package/src/scripts/lint_regression.ts +28 -0
  197. package/src/scripts/lint_roadmap_blockers.ts +43 -0
  198. package/src/scripts/lint_roadmap_ci_steps.ts +45 -0
  199. package/src/scripts/lint_roadmap_complexity.ts +47 -0
  200. package/src/scripts/lint_roadmap_later_disposition.ts +23 -0
  201. package/src/scripts/lint_rule_interactions.ts +16 -2
  202. package/src/scripts/lint_rule_tiers.ts +19 -6
  203. package/src/scripts/lint_showcase_sessions.ts +19 -3
  204. package/src/scripts/lint_skill_descriptions.ts +19 -0
  205. package/src/scripts/lint_skill_frontmatter_safety.ts +24 -0
  206. package/src/scripts/lint_skill_originality.ts +18 -3
  207. package/src/scripts/lint_skill_scripts_readonly.ts +20 -0
  208. package/src/scripts/lint_skill_tools.ts +84 -145
  209. package/src/scripts/lint_spawn_payload.ts +27 -1
  210. package/src/scripts/lint_store_boundary.ts +19 -0
  211. package/src/scripts/lint_subagent_determinism.ts +23 -1
  212. package/src/scripts/lint_supported_tools_matrix.ts +18 -0
  213. package/src/scripts/lint_surface_matrix.ts +19 -3
  214. package/src/scripts/lint_ticket_buildable.ts +23 -0
  215. package/src/scripts/lint_topics_yaml.ts +19 -12
  216. package/src/scripts/lint_trigger_precision.ts +27 -0
  217. package/src/scripts/lint_trust_coherence.ts +21 -0
  218. package/src/scripts/lint_ui_stack_bundles.ts +17 -3
  219. package/src/scripts/lint_value_dashboard.ts +19 -3
  220. package/src/scripts/lint_versioned_cache.ts +19 -1
  221. package/src/scripts/lint_workflow_security.ts +22 -14
  222. package/src/scripts/pr_url_reminder_hook.ts +1 -1
  223. package/src/scripts/skill_collision_clusters.ts +37 -3
  224. package/src/scripts/skill_discovery.ts +20 -0
  225. package/src/scripts/skill_eval_coverage.ts +22 -0
  226. package/src/scripts/skill_overlap.ts +32 -6
  227. package/src/scripts/skill_preview.ts +31 -0
  228. package/src/scripts/skill_trigger_eval.ts +18 -0
  229. package/src/scripts/skill_usage_collect.ts +21 -0
  230. package/src/scripts/skill_usage_report.ts +21 -0
  231. package/src/scripts/sweep_dead_scan_roots.ts +11 -3
  232. package/src/scripts/verify_physical_move.ts +24 -0
  233. package/src/scripts/check_bite_sized_granularity.ts +0 -116
  234. package/src/scripts/lint_workspace_boundary.ts +0 -267
@@ -0,0 +1,70 @@
1
+ /**
2
+ * "What is a gate?" — asserted once, read by everything that needs the answer.
3
+ *
4
+ * Three call sites each carried their own regex and each got a different number
5
+ * (measured 2026-08-04):
6
+ *
7
+ * | Site | Pattern | Population |
8
+ * |---|---|---:|
9
+ * | `check_gate_coverage` (the ratchet) | `(lint\|check\|audit)_` minus `.d.ts` | 223 |
10
+ * | `sweep_dead_scan_roots` | `(lint\|audit\|check\|verify)_` | 225 |
11
+ * | the manifest-registration test | `(lint\|check\|audit\|skill)_` | 232 |
12
+ *
13
+ * Harmless while the numbers were only reported. Load-bearing the moment the
14
+ * ratchet read one of them, which it now does — and it read the narrowest:
15
+ *
16
+ * - it counted `check_secret_leak.test.ts`, a **test file**, which can never be
17
+ * hardened, so its target of 0 was unreachable by construction;
18
+ * - it missed 10 real gates the other two definitions caught — 8 × `skill_*`
19
+ * (including `skill_linter`, which already carries an enforced floor of 380 in
20
+ * the coverage manifest, so the manifest and the ratchet openly disagreed
21
+ * about whether it is a gate) and 2 × `verify_*`.
22
+ *
23
+ * ## Why a prefix filter and not manifest membership
24
+ *
25
+ * AI council 2026-08-04 (anthropic/claude-sonnet-4-5 + openai/gpt-4o, 3 rounds):
26
+ * making manifest registration the classifier inverts a dependency that runs the
27
+ * other way in practice. A new gate script lands BEFORE anyone registers it —
28
+ * that is precisely how the registration test found the drift — so a
29
+ * manifest-driven population would ignore every gate during the window when it
30
+ * is least proven. Classification is structural (this filter); registration is
31
+ * operational and layered on top (`enforced_manifest_ids`).
32
+ *
33
+ * The prefix set is a proxy for "this script certifies something and can lie
34
+ * about it", and an imperfect one: `skill_usage_report` reports rather than
35
+ * validates. It is kept because it is mechanical and over-inclusive in the safe
36
+ * direction — a reporting script that asserts its scan scope loses nothing,
37
+ * while a validator missing from the population is invisible exposure.
38
+ */
39
+
40
+ /** Prefixes that mark a script as gate-shaped. */
41
+ const GATE_PREFIX = /^(lint|check|audit|skill|verify)_/;
42
+
43
+ /**
44
+ * Is this filename a gate script?
45
+ *
46
+ * Structural only — takes a bare filename, never touches disk, so it answers
47
+ * for a file that does not exist yet (a diff, a plan, a test fixture).
48
+ */
49
+ export function matchesGatePattern(filename: string): boolean {
50
+ if (!filename.endsWith('.ts')) return false;
51
+ // A type declaration has no runtime behaviour to harden; a test file cannot
52
+ // be hardened at all, and counting one made the ratchet's own target
53
+ // unreachable.
54
+ if (filename.endsWith('.d.ts') || filename.endsWith('.test.ts')) return false;
55
+ return GATE_PREFIX.test(filename);
56
+ }
57
+
58
+ /** The gate scripts in `dir`, as bare ids (no `.ts`), sorted. Missing dir → `[]`. */
59
+ export function listGateScripts(dir: string, readdir: (d: string) => string[]): string[] {
60
+ let entries: string[];
61
+ try {
62
+ entries = readdir(dir);
63
+ } catch {
64
+ return [];
65
+ }
66
+ return entries
67
+ .filter(matchesGatePattern)
68
+ .map((f) => f.replace(/\.ts$/, ''))
69
+ .sort();
70
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Bite-sized task-granularity check for structural roadmaps.
3
+ *
4
+ * A `complexity: structural` roadmap's task bullets must name real work. A
5
+ * placeholder in a task bullet — `<something>`, TODO, FIXME, XXX, TBD, `???` —
6
+ * means the step was filed before it was decided, which is the failure the
7
+ * bite-sized standard exists to catch.
8
+ *
9
+ * ## Why this lives in `_lib/` and not in a gate of its own
10
+ *
11
+ * It used to be `src/scripts/check_bite_sized_granularity.ts`: a pure library
12
+ * with no CLI, no `main`, no exit code, and — measured — no production caller
13
+ * outside its own test. It was carried over from a retired Python module for
14
+ * API parity and never wired up, so a real check that no other gate covers ran
15
+ * nowhere, while the `check_` prefix kept it inside the gate population where
16
+ * the scan-scope ratchet counted it as a gate that could not assert a scan
17
+ * scope.
18
+ *
19
+ * AI council 2026-08-05 resolved it by folding rather than deleting: the checks
20
+ * are the only placeholder coverage in the tree (`lint_roadmap_complexity`
21
+ * covers the complexity tag, the lightweight caps, and the plate/horizon
22
+ * prohibition, and greps clean for every placeholder pattern), so deleting them
23
+ * would have discarded live value to reach a count. Folding into the gate that
24
+ * already owns the roadmap corpus adds no gate script, no CI job, and no
25
+ * manifest entry, and the population member disappears with the old file.
26
+ *
27
+ * ## Inline-code masking (the reason this is not a verbatim move)
28
+ *
29
+ * The original patterns matched inside backticked spans, which made CLI
30
+ * metasyntax a violation. Measured across 22 structural roadmaps, the only hit
31
+ * in the whole tree was `` `settings set <key> <value>` `` — argument
32
+ * metasyntax in a code span, on a step that is otherwise fully specified. The
33
+ * council flagged folding a check with an unaudited hit in an out-of-scope
34
+ * directory as a time-bomb: it would fire the day the gate's scope widened. So
35
+ * the fix is precision, not scope-limiting — inline code spans are masked
36
+ * before matching, the same technique `lint_readme_serial_comma` already uses
37
+ * on README prose. With masking the count is 0 across every structural roadmap,
38
+ * `later/` and `stubs/` included, so widening the gate's scope later cannot
39
+ * surprise anyone.
40
+ */
41
+
42
+ /** Placeholder patterns in (kind, regex) order. */
43
+ const PLACEHOLDER_PATTERNS: ReadonlyArray<[string, RegExp]> = [
44
+ ['angle-placeholder', /<[a-z][a-z0-9 _\-/]*>/i],
45
+ ['todo', /\bTODO\b/],
46
+ ['fixme', /\bFIXME\b/],
47
+ ['xxx', /\bXXX\b/],
48
+ ['tbd', /\btbd\b/i],
49
+ ['triple-question', /\?\?\?/],
50
+ ];
51
+
52
+ const COMPLEXITY_PAT = /^complexity:\s*(lightweight|structural)\s*$/m;
53
+
54
+ const TASK_PREFIXES = ['- [ ]', '- [x]', '- [/]', '- [-]'] as const;
55
+
56
+ export interface Placeholder {
57
+ kind: string;
58
+ line: number;
59
+ text: string;
60
+ }
61
+
62
+ export interface GranularityResult {
63
+ complexity: string | null;
64
+ gated: boolean;
65
+ violations: Placeholder[];
66
+ }
67
+
68
+ function frontmatter(text: string): string {
69
+ if (!text.startsWith('---\n')) {
70
+ return '';
71
+ }
72
+ const end = text.indexOf('\n---\n', 4);
73
+ return end !== -1 ? text.slice(4, end) : '';
74
+ }
75
+
76
+ export function read_complexity(text: string): string | null {
77
+ const fm = frontmatter(text);
78
+ if (fm === '') {
79
+ return null;
80
+ }
81
+ const m = COMPLEXITY_PAT.exec(fm);
82
+ return m ? m[1]! : null;
83
+ }
84
+
85
+ const ANGLE_PLACEHOLDER = /<[a-z][a-z0-9 _\-/]*>/gi;
86
+
87
+ /**
88
+ * Blank inline code spans that are COMMAND TEMPLATES, so CLI argument
89
+ * metasyntax is not read as an undecided step.
90
+ *
91
+ * Blanking every backtick span (the first attempt) was too blunt, and the ported
92
+ * test corpus proved it: `` `<file>` `` in *"Edit `<file>` and add the new
93
+ * method"* is a real placeholder — the file is genuinely undecided — while
94
+ * `` `settings set <key> <value>` `` on a step that goes on to specify
95
+ * zod-validation, atomicity and key-class refusal is a command signature.
96
+ *
97
+ * The discriminator is what else the span holds: strip the placeholders and if
98
+ * nothing but whitespace is left, the span IS the placeholder (violation); if
99
+ * real content remains (`settings set`), it is a command template naming its
100
+ * arguments (masked). Only the angle pattern gets this treatment — a literal
101
+ * TODO / FIXME / XXX / TBD / `???` in a task bullet is unfinished work wherever
102
+ * it sits, backticks included.
103
+ */
104
+ function mask_command_templates(line: string): string {
105
+ return line.replace(/`[^`]*`/g, (span) => {
106
+ const withoutPlaceholders = span.slice(1, -1).replace(ANGLE_PLACEHOLDER, '');
107
+ return withoutPlaceholders.trim() === '' ? span : ' ';
108
+ });
109
+ }
110
+
111
+ export function scan_placeholders(text: string): Placeholder[] {
112
+ const hits: Placeholder[] = [];
113
+ const lines = text.split('\n');
114
+ for (let idx = 0; idx < lines.length; idx++) {
115
+ const line = lines[idx]!;
116
+ const stripped = line.replace(/^\s+/, '');
117
+ if (!TASK_PREFIXES.some((p) => stripped.startsWith(p))) {
118
+ continue;
119
+ }
120
+ // Command-template masking applies to the angle pattern ONLY. A literal
121
+ // TODO / FIXME / XXX / TBD / `???` is unfinished work wherever it sits,
122
+ // so those patterns read the raw line.
123
+ const masked = mask_command_templates(line);
124
+ for (const [kind, pat] of PLACEHOLDER_PATTERNS) {
125
+ const probe = kind === 'angle-placeholder' ? masked : line;
126
+ if (pat.test(probe)) {
127
+ hits.push({ kind, line: idx + 1, text: line.replace(/\s+$/, '') });
128
+ break;
129
+ }
130
+ }
131
+ }
132
+ return hits;
133
+ }
134
+
135
+ /**
136
+ * Gated only for `complexity: structural`. Violations are empty when the gate
137
+ * is not active, regardless of placeholder presence.
138
+ */
139
+ export function check_granularity(text: string): GranularityResult {
140
+ const complexity = read_complexity(text);
141
+ const gated = complexity === 'structural';
142
+ if (!gated) {
143
+ return { complexity, gated: false, violations: [] };
144
+ }
145
+ return { complexity, gated: true, violations: scan_placeholders(text) };
146
+ }
147
+
148
+ /** One human-readable problem line per violation, for a gate's problem list. */
149
+ export function granularity_problems(text: string): string[] {
150
+ return check_granularity(text).violations.map(
151
+ (v) =>
152
+ `bite-sized granularity: line ${v.line} has a ${v.kind} placeholder in a ` +
153
+ `task bullet — a structural roadmap's steps name decided work: ${v.text.trim()}`,
154
+ );
155
+ }
156
+
157
+ export { PLACEHOLDER_PATTERNS, COMPLEXITY_PAT };
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import * as fs from 'node:fs';
25
25
  import * as path from 'node:path';
26
+ import process from 'node:process';
26
27
 
27
28
  /** Raised when a gate's scope is empty — it cannot have checked anything. */
28
29
  export class DeadScopeError extends Error {
@@ -49,6 +50,24 @@ export interface ScannedOptions {
49
50
  * corpus is a real, expected state (an optional consumer surface that may
50
51
  * genuinely be absent) — never to silence a moved root. The string is the
51
52
  * reason and is printed, so it is reviewable in a diff.
53
+ *
54
+ * **Carry one of the three prefixes below.** A reason a reviewer cannot
55
+ * classify from the comment alone is how a justified exception becomes an
56
+ * allowlist of boilerplate (`road-to-gate-hardening-adoption`, AI council
57
+ * 2026-08-04). The prefix states which kind of emptiness is claimed:
58
+ *
59
+ * - `OPTIONAL_INPUT:` — the root belongs to a surface that may legitimately
60
+ * be absent in this project (a consumer tree, an opt-in pack).
61
+ * - `EMPTY_VALID:` — zero units IS the success state (`check_no_todos`: an
62
+ * empty tree genuinely has zero TODOs).
63
+ * - `WATCHLIST_DRIVEN:` — the gate has no corpus at all; it guards named
64
+ * files and its scope is asserted by {@link assertWatchlistResolves}.
65
+ *
66
+ * The operational test every reason must pass, applied from the comment
67
+ * alone: **if this gate's scan root were deleted, would the reason still
68
+ * make sense?** `check_no_todos` → yes. `check_frontmatter_valid` → no; no
69
+ * docs to validate is blindness, not cleanliness, and that gate must
70
+ * reclassify rather than claim `allowEmpty`.
52
71
  */
53
72
  allowEmpty?: string;
54
73
  }
@@ -75,6 +94,31 @@ export function assertScanned(opts: ScannedOptions): void {
75
94
  );
76
95
  }
77
96
 
97
+ /**
98
+ * Assert a corpus gate's scope AND publish the count it just validated.
99
+ *
100
+ * The two halves of scope hardening drifted apart in practice: 14 gates emit
101
+ * the machine-readable `scanned:` line the coverage guard reads, 12 assert
102
+ * their scope, and only 8 do both. A gate that publishes a number it never
103
+ * asserted can print `scanned: 0` and exit green; a gate that asserts without
104
+ * publishing is invisible to `check_gate_coverage`. Splitting the two also
105
+ * lets the published number drift from the number that was validated — the
106
+ * invented-count failure that is mechanically undetectable downstream.
107
+ *
108
+ * One call closes both: the emitted number is, by construction, the number the
109
+ * assertion just accepted. Use this for any gate that should also carry a floor
110
+ * in `src/config/gate-coverage.yml`; plain {@link assertScanned} stays correct
111
+ * for a gate that only needs its scope guarded.
112
+ *
113
+ * The line goes to stdout unconditionally — a count only visible without
114
+ * `--quiet` is not a count, since CI passes `--quiet` (the `lint_handoffs`
115
+ * lesson, preserved here so no caller has to rediscover it).
116
+ */
117
+ export function reportScanned(opts: ScannedOptions, write = process.stdout.write.bind(process.stdout)): void {
118
+ assertScanned(opts);
119
+ write(`scanned: ${String(opts.scanned)}\n`);
120
+ }
121
+
78
122
  export interface WatchlistOptions {
79
123
  /** Gate name, for the error message. */
80
124
  gate: string;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Read piped stdin completely — including payloads larger than the pipe buffer.
3
+ *
4
+ * WHY THIS EXISTS (measured 2026-08-04, PR "take the unhardened-gate count from
5
+ * 189 to 4"): the CI step
6
+ *
7
+ * gh pr diff "$PR" | ./scripts-run src/scripts/check_no_new_legacy_path --stdin
8
+ *
9
+ * crashed with `EAGAIN: resource temporarily unavailable, read` at
10
+ * `fs.readFileSync(0)`. Not flaky — **reproducible on that PR and green on every
11
+ * PR before it.** A pipe holds ~64 KB; while the diff fit, the single read
12
+ * returned everything. That PR's diff is ~6,400 lines, so the writer is still
13
+ * filling the pipe when the reader arrives, and Node leaves the inherited fd 0
14
+ * in non-blocking mode: the read fails instead of waiting.
15
+ *
16
+ * A size-dependent gate failure is the same family of defect this suite's
17
+ * scan-scope work exists to kill — the gate works on the changes small enough
18
+ * not to matter and breaks on the ones big enough to. And the sibling
19
+ * workaround is worse than the crash: `check_release_pr_shape` wraps the same
20
+ * call in `catch { data = '' }`, so an oversized diff there reads as an empty
21
+ * one and the gate passes over nothing.
22
+ *
23
+ * So: retry on EAGAIN rather than swallow it, and never substitute an empty
24
+ * string for a failed read. A caller that gets `''` here got a genuinely empty
25
+ * stdin.
26
+ */
27
+ import * as fs from 'node:fs';
28
+
29
+ const CHUNK = 64 * 1024;
30
+ /** Sleep between EAGAIN retries. Synchronous — these are sync CLI entry points. */
31
+ const RETRY_SLEEP_MS = 5;
32
+ /**
33
+ * Total patience before giving up, in retries. At 5 ms this is ~10 s, which is
34
+ * far beyond any `gh pr diff` and still terminates rather than spinning forever
35
+ * if fd 0 is a pipe nobody ever writes to.
36
+ */
37
+ const MAX_RETRIES = 2000;
38
+
39
+ /** Sleep without a busy loop, in synchronous code. */
40
+ function _sleepSync(ms: number): void {
41
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
42
+ }
43
+
44
+ /**
45
+ * Read all of fd 0 as UTF-8.
46
+ *
47
+ * @throws the underlying error for anything that is not EAGAIN, and after
48
+ * {@link MAX_RETRIES} consecutive EAGAINs — a caller must not be able to
49
+ * mistake a failed read for empty input.
50
+ */
51
+ export function readStdinText(fd = 0): string {
52
+ const chunks: Buffer[] = [];
53
+ const buf = Buffer.alloc(CHUNK);
54
+ let retries = 0;
55
+ for (;;) {
56
+ let read: number;
57
+ try {
58
+ read = fs.readSync(fd, buf, 0, CHUNK, null);
59
+ retries = 0;
60
+ } catch (exc) {
61
+ const code = (exc as NodeJS.ErrnoException).code;
62
+ if (code === 'EAGAIN') {
63
+ retries += 1;
64
+ if (retries > MAX_RETRIES) {
65
+ throw new Error(
66
+ `readStdinText: stdin stayed unreadable across ${String(MAX_RETRIES)} retries ` +
67
+ `(~${String((MAX_RETRIES * RETRY_SLEEP_MS) / 1000)}s). Refusing to report an ` +
68
+ 'empty read as empty input.',
69
+ );
70
+ }
71
+ _sleepSync(RETRY_SLEEP_MS);
72
+ continue;
73
+ }
74
+ // EOF is how some platforms end a pipe read rather than returning 0.
75
+ if (code === 'EOF') break;
76
+ throw exc;
77
+ }
78
+ if (read === 0) break;
79
+ chunks.push(Buffer.from(buf.subarray(0, read)));
80
+ }
81
+ return Buffer.concat(chunks).toString('utf-8');
82
+ }
@@ -22,6 +22,8 @@ import * as path from 'node:path';
22
22
  import process from 'node:process';
23
23
  import { fileURLToPath, pathToFileURL } from 'node:url';
24
24
 
25
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
26
+
25
27
  const _HERE = fileURLToPath(import.meta.url);
26
28
 
27
29
  // src/scripts/audit_adr_coverage.ts → parents[2] of the .py file is repo root.
@@ -251,14 +253,40 @@ export function render_area_readme(area: string, meta: AreaMeta, adrs: AdrEntry[
251
253
  return lines.join('\n') + '\n';
252
254
  }
253
255
 
256
+ /**
257
+ * A vanished `docs/adrs/` makes every area report zero ADRs — "missing
258
+ * bootstrap" across the board, which both modes treat as a warning and exit 0.
259
+ * Exit 1 is the hard-failure code (`cmd_check`); 2 is reserved for argparse
260
+ * usage errors.
261
+ */
262
+ function _dead_adr_scope(scanned: number): number | null {
263
+ try {
264
+ assertScanned({
265
+ gate: 'audit_adr_coverage',
266
+ scanned,
267
+ units: 'ADR file(s)',
268
+ roots: ['docs/adrs'],
269
+ });
270
+ return null;
271
+ } catch (exc) {
272
+ if (exc instanceof DeadScopeError) {
273
+ process.stderr.write(`❌ ${exc.message}\n`);
274
+ return 1;
275
+ }
276
+ throw exc;
277
+ }
278
+ }
279
+
254
280
  export function cmd_report(): number {
255
281
  process.stdout.write('## ADR coverage report\n');
256
282
  process.stdout.write('\n');
257
283
  process.stdout.write('| Area | Contract | ADRs | README | Status |\n');
258
284
  process.stdout.write('|---|---|---:|:---:|---|\n');
259
285
  let missingBootstrap = 0;
286
+ let totalAdrs = 0;
260
287
  for (const [area, meta] of Object.entries(AREAS)) {
261
288
  const [adrs] = scan_area(area);
289
+ totalAdrs += adrs.length;
262
290
  const readme = _exists(path.join(ADR_ROOT, area, 'README.md')) ? '✅' : '—';
263
291
  const contractPresent = _exists(_contract_path(meta));
264
292
  const status = adrs.length > 0 ? 'ok' : 'missing bootstrap';
@@ -268,6 +296,10 @@ export function cmd_report(): number {
268
296
  const contractCell = contractPresent ? meta.contract : `_${meta.contract}_ (no contract)`;
269
297
  process.stdout.write(`| \`${area}\` | ${contractCell} | ${adrs.length} | ${readme} | ${status} |\n`);
270
298
  }
299
+ const dead = _dead_adr_scope(totalAdrs);
300
+ if (dead !== null) {
301
+ return dead;
302
+ }
271
303
  process.stdout.write('\n');
272
304
  process.stdout.write(
273
305
  `BASELINE: ${Object.keys(AREAS).length} canonical areas · ${missingBootstrap} missing bootstrap ADR(s)\n`,
@@ -278,6 +310,7 @@ export function cmd_report(): number {
278
310
  export function cmd_check(): number {
279
311
  let hard = 0;
280
312
  let warn = 0;
313
+ let totalAdrs = 0;
281
314
  for (const dir of unlistedAreaDirs(ADR_ROOT)) {
282
315
  process.stderr.write(
283
316
  `❌ ${dir}/: area directory exists on disk but is absent from AREAS — it is not audited at all. ` +
@@ -287,6 +320,7 @@ export function cmd_check(): number {
287
320
  }
288
321
  for (const [area, meta] of Object.entries(AREAS)) {
289
322
  const [adrs, errs] = scan_area(area);
323
+ totalAdrs += adrs.length;
290
324
  for (const e of errs) {
291
325
  process.stderr.write(`❌ ${e}\n`);
292
326
  hard += 1;
@@ -300,6 +334,10 @@ export function cmd_check(): number {
300
334
  warn += 1;
301
335
  }
302
336
  }
337
+ const dead = _dead_adr_scope(totalAdrs);
338
+ if (dead !== null) {
339
+ return dead;
340
+ }
303
341
  process.stdout.write(`BASELINE: ${hard} hard fail(s) · ${warn} warn(s)\n`);
304
342
  return hard ? 1 : 0;
305
343
  }
@@ -28,6 +28,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
28
28
  import { parse as parseYaml } from 'yaml';
29
29
 
30
30
  import { artefact_roots } from './_lib/agent_src.js';
31
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
31
32
 
32
33
  const _HERE = fileURLToPath(import.meta.url);
33
34
  // src/scripts/audit_auto_rules.py → parent.parent.parent == repo root.
@@ -404,9 +405,22 @@ function _ruleToJson(r: RuleEntry): Record<string, Json> {
404
405
  }
405
406
 
406
407
  export function main(): number {
407
- if (_src_rule_paths().length === 0) {
408
- process.stderr.write("❌ No source rules found under any artefact root's rules/\n");
409
- return 1;
408
+ // The scanned unit is every source rule, not the `type: auto` subset
409
+ // `collect()` keeps: over a vanished rules/ tree "no auto-rules" still reads
410
+ // true, and the budget report would be regenerated with rule_count 0.
411
+ try {
412
+ assertScanned({
413
+ gate: 'audit_auto_rules',
414
+ scanned: _src_rule_paths().length,
415
+ units: 'source rule file(s)',
416
+ roots: artefact_roots().map((r) => `${_relPosix(r, REPO_ROOT) || '.'}/rules`),
417
+ });
418
+ } catch (exc) {
419
+ if (exc instanceof DeadScopeError) {
420
+ process.stderr.write(`❌ ${exc.message}\n`);
421
+ return 1;
422
+ }
423
+ throw exc;
410
424
  }
411
425
  const rules = collect();
412
426
  fs.mkdirSync(REPORT_DIR, { recursive: true });
@@ -27,6 +27,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
27
27
  import { parse as parseYaml } from 'yaml';
28
28
 
29
29
  import { SRC_DOMAINS } from './_lib/agent_src.js';
30
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
30
31
 
31
32
  const _HERE = fileURLToPath(import.meta.url);
32
33
 
@@ -974,8 +975,50 @@ export function grown_packs(baseline: string, commands: Command[]): Map<string,
974
975
  return grew;
975
976
  }
976
977
 
978
+ /**
979
+ * Scope guard shared by all three modes.
980
+ *
981
+ * `_rglobMdSorted` yields nothing for a root that does not exist, and every
982
+ * mode's verdict is a comparison over the collected commands — so a moved
983
+ * `src/domains` leaves the per-pack budget trivially satisfied, the overlap
984
+ * scan pair-less, and the surface report regenerated with `total: 0`. Only
985
+ * `main`'s `_exists(args.root)` guard covered that, and it does not see a
986
+ * present-but-command-less tree.
987
+ *
988
+ * @returns the exit code to return, or null when the scope is live.
989
+ */
990
+ function _dead_scope(commands: readonly Command[], roots: readonly string[]): number | null {
991
+ try {
992
+ assertScanned({
993
+ gate: 'audit_command_surface',
994
+ scanned: commands.length,
995
+ units: 'command file(s)',
996
+ roots,
997
+ });
998
+ return null;
999
+ } catch (exc) {
1000
+ if (exc instanceof DeadScopeError) {
1001
+ // Exit 2 — this file's existing "the root is not usable" code
1002
+ // (`error: <root> does not exist`), not 1, which means a pack
1003
+ // exceeded its budget.
1004
+ process.stderr.write(`❌ ${exc.message}\n`);
1005
+ return 2;
1006
+ }
1007
+ throw exc;
1008
+ }
1009
+ }
1010
+
1011
+ /** Repo-relative form of the roots `collect_all` walks, for the scope message. */
1012
+ function _command_roots_rel(): string[] {
1013
+ return _command_roots().map((r) => _relPosix(r, REPO_ROOT));
1014
+ }
1015
+
977
1016
  export function check_new_budget(baseline: string, quiet: boolean): number {
978
1017
  const commands = collect_all();
1018
+ const dead = _dead_scope(commands, _command_roots_rel());
1019
+ if (dead !== null) {
1020
+ return dead;
1021
+ }
979
1022
  const audit = build_budget_audit(commands, load_size_classes());
980
1023
  const byPack = new Map<string, PackEntry>();
981
1024
  for (const p of audit.packs) {
@@ -1155,6 +1198,10 @@ export function main(argv: string[] | null = null): number {
1155
1198
 
1156
1199
  if (args.budget) {
1157
1200
  const commands = collect_all();
1201
+ const dead = _dead_scope(commands, _command_roots_rel());
1202
+ if (dead !== null) {
1203
+ return dead;
1204
+ }
1158
1205
  const audit = build_budget_audit(commands, load_size_classes());
1159
1206
  fs.writeFileSync(OUT_BUDGET_JSON, _jsonDumpsIndent2(audit), 'utf-8');
1160
1207
  fs.writeFileSync(OUT_BUDGET_MD, render_budget_md(audit), 'utf-8');
@@ -1177,6 +1224,10 @@ export function main(argv: string[] | null = null): number {
1177
1224
  }
1178
1225
 
1179
1226
  const commands = collect(args.root);
1227
+ const dead = _dead_scope(commands, [_relPosix(args.root, REPO_ROOT)]);
1228
+ if (dead !== null) {
1229
+ return dead;
1230
+ }
1180
1231
  const pairs = find_overlap_pairs(commands);
1181
1232
 
1182
1233
  fs.writeFileSync(
@@ -32,6 +32,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
32
32
  import { parse as parseYaml } from 'yaml';
33
33
 
34
34
  import * as token_count from './_lib/token_count.js';
35
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
35
36
  import { SRC_AGENT, SRC_DOMAINS } from './_lib/agent_src.js';
36
37
  import * as projectThinRules from './project_thin_rules.js';
37
38
 
@@ -304,14 +305,30 @@ function _catalog(globPat: string): Measure {
304
305
  return m;
305
306
  }
306
307
 
308
+ const CATALOG_GLOBS = {
309
+ skills_projected: '.claude/skills/*/SKILL.md',
310
+ skills_core_source: 'src/skills/*/SKILL.md',
311
+ commands_core_source: 'src/domains/*/**/command.md',
312
+ } as const;
313
+
307
314
  export function description_catalog(): Record<string, Measure> {
308
315
  return {
309
- skills_projected: _catalog('.claude/skills/*/SKILL.md'),
310
- skills_core_source: _catalog('src/skills/*/SKILL.md'),
311
- commands_core_source: _catalog('src/domains/*/**/command.md'),
316
+ skills_projected: _catalog(CATALOG_GLOBS.skills_projected),
317
+ skills_core_source: _catalog(CATALOG_GLOBS.skills_core_source),
318
+ commands_core_source: _catalog(CATALOG_GLOBS.commands_core_source),
312
319
  };
313
320
  }
314
321
 
322
+ /**
323
+ * Files the catalogs glob — counted UNFILTERED, unlike the `entries` the
324
+ * measurement keeps (only those carrying a description). A vanished skills or
325
+ * commands tree must read as zero files, not as "nothing declares a
326
+ * description", which is indistinguishable from a clean tree at zero tokens.
327
+ */
328
+ function catalog_file_count(): number {
329
+ return Object.values(CATALOG_GLOBS).reduce((n, pat) => n + _glob(pat).length, 0);
330
+ }
331
+
315
332
  interface LongestRule {
316
333
  id: string;
317
334
  tokens_gpt: number;
@@ -700,6 +717,25 @@ function parse_args(argv: string[]): ParsedArgs {
700
717
  export function main(argv: string[] | null = null): number {
701
718
  const args = parse_args(argv ?? process.argv.slice(2));
702
719
 
720
+ // Every budget in BUDGETS is a token count over these catalogs: an empty
721
+ // catalog measures zero tokens and passes every cap, so the surface has to
722
+ // be proved non-empty before the caps mean anything. Exit 1 is the gate's
723
+ // failure code (`--fail-if-over-budget`); 2 is reserved for usage errors.
724
+ try {
725
+ assertScanned({
726
+ gate: 'audit_initial_context',
727
+ scanned: catalog_file_count(),
728
+ units: 'catalog file(s)',
729
+ roots: Object.values(CATALOG_GLOBS),
730
+ });
731
+ } catch (exc) {
732
+ if (exc instanceof DeadScopeError) {
733
+ process.stderr.write(`❌ ${exc.message}\n`);
734
+ return 1;
735
+ }
736
+ throw exc;
737
+ }
738
+
703
739
  const data = build();
704
740
 
705
741
  if (args.fail_if_over_budget) {
@@ -21,6 +21,8 @@ import * as path from 'node:path';
21
21
  import process from 'node:process';
22
22
  import { fileURLToPath, pathToFileURL } from 'node:url';
23
23
 
24
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
25
+
24
26
  const _HERE = fileURLToPath(import.meta.url);
25
27
 
26
28
  // src/scripts/audit_mcp_tools.ts → parent.parent.parent of the .py file = repo root.
@@ -228,6 +230,29 @@ function parse_args(argv: string[]): ParsedArgs {
228
230
  export function main(argv: string[] | null = null): number {
229
231
  const args = parse_args(argv ?? process.argv.slice(2));
230
232
 
233
+ // This generator walks no tree — it reads two named sources, and
234
+ // `_index_handlers` / `_index_catalog_lines` swallow a read failure as an
235
+ // empty index, so a moved `tools.ts` renders every tool `_stub-only_`
236
+ // rather than reporting that half the input vanished.
237
+ try {
238
+ assertWatchlistResolves({
239
+ gate: 'audit_mcp_tools',
240
+ candidates: [
241
+ path.posix.join('src', 'scripts', 'mcp_server', 'consumer_tool_catalog.json'),
242
+ path.posix.join('src', 'scripts', 'mcp_server', 'tools.ts'),
243
+ ],
244
+ repoRoot: ROOT,
245
+ });
246
+ } catch (e) {
247
+ if (e instanceof DeadScopeError) {
248
+ // 2 (the usage / could-not-run code) over 1, which this CLI
249
+ // documents as "the inventory drifted from the generator".
250
+ process.stderr.write(`❌ ${e.message}\n`);
251
+ return 2;
252
+ }
253
+ throw e;
254
+ }
255
+
231
256
  const catalog = JSON.parse(fs.readFileSync(CATALOG, 'utf-8')) as Catalog;
232
257
  const handlers = _index_handlers();
233
258
  const cat_lines = _index_catalog_lines();