@event4u/agent-config 9.20.0 → 9.22.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 (239) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +86 -0
  3. package/dist/agent-src/commands/worktree/cleanup.md +17 -0
  4. package/dist/agent-src/commands/worktree/status.md +6 -0
  5. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +22 -0
  6. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  7. package/dist/cli-delegate/{chunk-XGA2NTU4.js → chunk-3EWYC3YK.js} +40 -0
  8. package/dist/cli-delegate/cmd_handoff.js +1 -1
  9. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  10. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  11. package/dist/discovery/deprecation-report.md +1 -1
  12. package/dist/discovery/discovery-manifest.json +5 -5
  13. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  14. package/dist/discovery/discovery-manifest.summary.md +1 -1
  15. package/dist/discovery/orphan-report.md +1 -1
  16. package/dist/discovery/packs.json +2 -2
  17. package/dist/discovery/trust-report.md +1 -1
  18. package/dist/discovery/workspaces.json +2 -2
  19. package/dist/hooks/dispatch.js +58 -2
  20. package/dist/mcp/registry-manifest.json +2 -2
  21. package/dist/mcp/server.json +2 -2
  22. package/dist/mcp/server.mjs +47 -29
  23. package/package.json +1 -1
  24. package/src/config/gate-coverage.yml +22 -4
  25. package/src/config/gate-violation-baselines.json +3 -8
  26. package/src/scripts/_lib/bench_ab_activation.ts +368 -0
  27. package/src/scripts/_lib/gate_population.ts +70 -0
  28. package/src/scripts/_lib/roadmap_granularity.ts +157 -0
  29. package/src/scripts/_lib/scan_scope.ts +44 -0
  30. package/src/scripts/_lib/stdin.ts +82 -0
  31. package/src/scripts/audit_adr_coverage.ts +38 -0
  32. package/src/scripts/audit_auto_rules.ts +17 -3
  33. package/src/scripts/audit_command_surface.ts +51 -0
  34. package/src/scripts/audit_initial_context.ts +39 -3
  35. package/src/scripts/audit_mcp_tools.ts +25 -0
  36. package/src/scripts/audit_overlap.ts +24 -1
  37. package/src/scripts/audit_skill_descriptions.ts +19 -0
  38. package/src/scripts/{verify_before_complete_hook.ts → before_complete_hook.ts} +16 -3
  39. package/src/scripts/bench_ab_task_runner.ts +18 -0
  40. package/src/scripts/bench_ab_v2_run.ts +147 -6
  41. package/src/scripts/bench_ab_v2_stats.ts +74 -2
  42. package/src/scripts/check_adr_frontmatter.ts +23 -3
  43. package/src/scripts/check_always_budget.ts +24 -0
  44. package/src/scripts/check_artefact_checksums.ts +24 -1
  45. package/src/scripts/check_artefact_count_messaging.ts +20 -0
  46. package/src/scripts/check_augmentignore.ts +22 -15
  47. package/src/scripts/check_backstop_debt.ts +22 -0
  48. package/src/scripts/check_beta_review_markers.ts +22 -1
  49. package/src/scripts/check_bridge_derivation.ts +22 -1
  50. package/src/scripts/check_bundle_path_leakage.ts +26 -0
  51. package/src/scripts/check_ci_strict_superset.ts +21 -3
  52. package/src/scripts/check_claims.ts +22 -0
  53. package/src/scripts/check_cli_registry_budget_sync.ts +33 -4
  54. package/src/scripts/check_cluster_patterns.ts +26 -4
  55. package/src/scripts/check_command_count_messaging.ts +20 -4
  56. package/src/scripts/check_comparison.ts +18 -0
  57. package/src/scripts/check_condensed_paths.ts +23 -2
  58. package/src/scripts/check_consumer_scope_flip.ts +21 -2
  59. package/src/scripts/check_council_config_location.ts +20 -0
  60. package/src/scripts/check_council_layout.ts +22 -2
  61. package/src/scripts/check_council_references.ts +23 -1
  62. package/src/scripts/check_dependency_floors.ts +19 -3
  63. package/src/scripts/check_discovery_determinism.ts +26 -0
  64. package/src/scripts/check_enforcement_coverage.ts +20 -0
  65. package/src/scripts/check_evaluator_budgets.ts +23 -0
  66. package/src/scripts/check_finding_dispositions.ts +29 -0
  67. package/src/scripts/check_gate_coverage.ts +63 -22
  68. package/src/scripts/check_gate_paths.ts +30 -5
  69. package/src/scripts/check_generated_artefact_headers.ts +21 -0
  70. package/src/scripts/check_generator_output_coverage.ts +23 -0
  71. package/src/scripts/check_gitignore_freshness.ts +23 -0
  72. package/src/scripts/check_host_loadability.ts +34 -8
  73. package/src/scripts/check_installer_import_purity.ts +22 -8
  74. package/src/scripts/check_kernel_prefix_stability.ts +19 -1
  75. package/src/scripts/check_kernel_rule_bundle.ts +23 -0
  76. package/src/scripts/check_knowledge_cards.ts +13 -0
  77. package/src/scripts/check_knowledge_pages.ts +23 -0
  78. package/src/scripts/check_knowledge_sharing.ts +27 -0
  79. package/src/scripts/check_md_language.ts +24 -0
  80. package/src/scripts/check_media_deps.ts +23 -0
  81. package/src/scripts/check_memory.ts +26 -1
  82. package/src/scripts/check_memory_contradiction.ts +16 -1
  83. package/src/scripts/check_memory_proposal.ts +15 -0
  84. package/src/scripts/check_memory_similarity.ts +16 -0
  85. package/src/scripts/check_module_management_neutral.ts +19 -11
  86. package/src/scripts/check_no_conflict_markers.ts +22 -1
  87. package/src/scripts/check_no_external_sources.ts +26 -1
  88. package/src/scripts/check_no_local_settings_committed.ts +37 -10
  89. package/src/scripts/check_no_new_legacy_path.ts +30 -1
  90. package/src/scripts/check_one_off_location.ts +43 -0
  91. package/src/scripts/check_originality_freshness.ts +25 -1
  92. package/src/scripts/check_overlay_cascade_subdirs.ts +21 -0
  93. package/src/scripts/check_pack_size.ts +22 -0
  94. package/src/scripts/check_preamble_payload_budget.ts +35 -5
  95. package/src/scripts/check_proposal.ts +24 -1
  96. package/src/scripts/check_public_catalog_links.ts +27 -0
  97. package/src/scripts/check_public_links.ts +19 -0
  98. package/src/scripts/check_quality_regression.ts +19 -0
  99. package/src/scripts/check_reach_channels.ts +44 -0
  100. package/src/scripts/check_reach_staleness.ts +27 -0
  101. package/src/scripts/check_release_adjacent_health.ts +28 -0
  102. package/src/scripts/check_release_highlights.ts +34 -0
  103. package/src/scripts/check_release_includes_discovery.ts +25 -6
  104. package/src/scripts/check_release_pr_shape.ts +18 -1
  105. package/src/scripts/check_release_published.ts +21 -0
  106. package/src/scripts/check_release_surface_equality.ts +22 -0
  107. package/src/scripts/check_release_trunk_sync.ts +19 -0
  108. package/src/scripts/check_reply_consistency.ts +22 -0
  109. package/src/scripts/check_roadmap_trackable.ts +20 -0
  110. package/src/scripts/check_role_doc_links.ts +23 -0
  111. package/src/scripts/check_rule_invariants.ts +14 -0
  112. package/src/scripts/check_secret_leak.ts +47 -0
  113. package/src/scripts/check_skill_gaps.ts +32 -0
  114. package/src/scripts/check_skill_requires.ts +19 -0
  115. package/src/scripts/check_source_pointer_freshness.ts +20 -0
  116. package/src/scripts/check_structural_breaking.ts +29 -2
  117. package/src/scripts/check_surface_tiers.ts +34 -0
  118. package/src/scripts/check_template_pin_drift.ts +23 -1
  119. package/src/scripts/check_test_coverage_diff.ts +17 -1
  120. package/src/scripts/check_token_optimizer_freshness.ts +18 -3
  121. package/src/scripts/check_token_quality_golden.ts +21 -0
  122. package/src/scripts/check_token_regression.ts +25 -0
  123. package/src/scripts/check_tracked_but_ignored.ts +26 -0
  124. package/src/scripts/check_trigger_eval_presence.ts +23 -1
  125. package/src/scripts/check_trigger_evals.ts +36 -0
  126. package/src/scripts/check_trunk_drift.ts +23 -0
  127. package/src/scripts/check_update_banner.ts +24 -0
  128. package/src/scripts/gate_scope_classify.ts +513 -0
  129. package/src/scripts/hook_manifest.yaml +1 -1
  130. package/src/scripts/hooks/block_kernel_rule_writes.ts +1 -1
  131. package/src/scripts/hooks/concern_registry.ts +2 -2
  132. package/src/scripts/lint_agent_security.ts +19 -0
  133. package/src/scripts/lint_agent_skill_names.ts +30 -0
  134. package/src/scripts/lint_agents_layout.ts +20 -0
  135. package/src/scripts/lint_agents_md.ts +20 -0
  136. package/src/scripts/lint_archived_skills.ts +24 -3
  137. package/src/scripts/lint_behavioural_eval_freshness.ts +29 -3
  138. package/src/scripts/lint_bench_ab.ts +25 -0
  139. package/src/scripts/lint_bench_corpus.ts +20 -4
  140. package/src/scripts/lint_breaking_changes_index.ts +24 -0
  141. package/src/scripts/lint_budget_ownership.ts +20 -3
  142. package/src/scripts/lint_changelog_rollback.ts +24 -0
  143. package/src/scripts/lint_command_flow_coverage.ts +22 -0
  144. package/src/scripts/lint_command_tiers.ts +29 -5
  145. package/src/scripts/lint_commit_subjects.ts +15 -0
  146. package/src/scripts/lint_confusables.ts +24 -0
  147. package/src/scripts/lint_consumer_internal_refs.ts +24 -0
  148. package/src/scripts/lint_design_quality.ts +22 -0
  149. package/src/scripts/lint_design_slop.ts +23 -0
  150. package/src/scripts/lint_discovery_manifest.ts +23 -0
  151. package/src/scripts/lint_discovery_vocabulary.ts +19 -0
  152. package/src/scripts/lint_documented_commands.ts +23 -0
  153. package/src/scripts/lint_empty_roadmaps.ts +23 -0
  154. package/src/scripts/lint_eval_fixture_citations.ts +19 -0
  155. package/src/scripts/lint_eval_freshness.ts +35 -0
  156. package/src/scripts/lint_examples.ts +18 -3
  157. package/src/scripts/lint_explain_trace.ts +23 -0
  158. package/src/scripts/lint_featured_skills.ts +26 -1
  159. package/src/scripts/lint_flows.ts +19 -0
  160. package/src/scripts/lint_frontmatter_boilerplate.ts +20 -0
  161. package/src/scripts/lint_ghostwriter_source.ts +26 -6
  162. package/src/scripts/lint_glama_drift.ts +24 -0
  163. package/src/scripts/lint_global_paths.ts +24 -0
  164. package/src/scripts/lint_governed_writes.ts +20 -0
  165. package/src/scripts/lint_hidden_unicode.ts +25 -0
  166. package/src/scripts/lint_hook_concern_budget.ts +39 -6
  167. package/src/scripts/lint_hook_manifest.ts +22 -0
  168. package/src/scripts/lint_instruction_smuggling.ts +23 -0
  169. package/src/scripts/lint_knowledge_scale.ts +32 -2
  170. package/src/scripts/lint_legal_pack.ts +31 -0
  171. package/src/scripts/lint_marketplace.ts +26 -0
  172. package/src/scripts/lint_marketplace_install_completeness.ts +23 -0
  173. package/src/scripts/lint_mcp_config_security.ts +24 -0
  174. package/src/scripts/lint_mcp_registry_manifest.ts +22 -1
  175. package/src/scripts/lint_memory_tripwire.ts +35 -0
  176. package/src/scripts/lint_missions.ts +23 -7
  177. package/src/scripts/lint_model_tier_coverage.ts +18 -0
  178. package/src/scripts/lint_namespace_collisions.ts +19 -0
  179. package/src/scripts/lint_no_new_atomic_commands.ts +33 -0
  180. package/src/scripts/lint_no_python_twin_rationale.ts +18 -0
  181. package/src/scripts/lint_one_off_age.ts +38 -2
  182. package/src/scripts/lint_orchestration_dsl.ts +23 -3
  183. package/src/scripts/lint_orchestrator_auto_detect.ts +19 -12
  184. package/src/scripts/lint_originality.ts +22 -4
  185. package/src/scripts/lint_originality_shingles.ts +28 -3
  186. package/src/scripts/lint_output_slop.ts +19 -0
  187. package/src/scripts/lint_override_kernel_guard.ts +30 -0
  188. package/src/scripts/lint_pack_first_win.ts +23 -0
  189. package/src/scripts/lint_pack_risk_class.ts +19 -3
  190. package/src/scripts/lint_persistence.ts +19 -0
  191. package/src/scripts/lint_persona_governance.ts +18 -3
  192. package/src/scripts/lint_positioning.ts +23 -0
  193. package/src/scripts/lint_pre_migration_refs.ts +24 -0
  194. package/src/scripts/lint_profile_overlay_set_only.ts +24 -0
  195. package/src/scripts/lint_provenance.ts +21 -0
  196. package/src/scripts/lint_provenance_vocabulary.ts +24 -1
  197. package/src/scripts/lint_readme_jargon.ts +21 -3
  198. package/src/scripts/lint_readme_serial_comma.ts +21 -3
  199. package/src/scripts/lint_readme_size.ts +21 -3
  200. package/src/scripts/lint_regression.ts +28 -0
  201. package/src/scripts/lint_roadmap_blockers.ts +43 -0
  202. package/src/scripts/lint_roadmap_ci_steps.ts +45 -0
  203. package/src/scripts/lint_roadmap_complexity.ts +47 -0
  204. package/src/scripts/lint_roadmap_later_disposition.ts +23 -0
  205. package/src/scripts/lint_rule_interactions.ts +16 -2
  206. package/src/scripts/lint_rule_tiers.ts +19 -6
  207. package/src/scripts/lint_showcase_sessions.ts +19 -3
  208. package/src/scripts/lint_skill_descriptions.ts +19 -0
  209. package/src/scripts/lint_skill_frontmatter_safety.ts +24 -0
  210. package/src/scripts/lint_skill_originality.ts +18 -3
  211. package/src/scripts/lint_skill_scripts_readonly.ts +20 -0
  212. package/src/scripts/lint_skill_tools.ts +84 -145
  213. package/src/scripts/lint_spawn_payload.ts +27 -1
  214. package/src/scripts/lint_store_boundary.ts +19 -0
  215. package/src/scripts/lint_subagent_determinism.ts +23 -1
  216. package/src/scripts/lint_supported_tools_matrix.ts +18 -0
  217. package/src/scripts/lint_surface_matrix.ts +19 -3
  218. package/src/scripts/lint_ticket_buildable.ts +23 -0
  219. package/src/scripts/lint_topics_yaml.ts +19 -12
  220. package/src/scripts/lint_trigger_precision.ts +27 -0
  221. package/src/scripts/lint_trust_coherence.ts +21 -0
  222. package/src/scripts/lint_ui_stack_bundles.ts +17 -3
  223. package/src/scripts/lint_value_dashboard.ts +19 -3
  224. package/src/scripts/lint_versioned_cache.ts +19 -1
  225. package/src/scripts/lint_workflow_security.ts +22 -14
  226. package/src/scripts/pr_url_reminder_hook.ts +1 -1
  227. package/src/scripts/skill_collision_clusters.ts +37 -3
  228. package/src/scripts/skill_discovery.ts +20 -0
  229. package/src/scripts/skill_eval_coverage.ts +22 -0
  230. package/src/scripts/skill_overlap.ts +32 -6
  231. package/src/scripts/skill_preview.ts +31 -0
  232. package/src/scripts/skill_trigger_eval.ts +18 -0
  233. package/src/scripts/skill_usage_collect.ts +21 -0
  234. package/src/scripts/skill_usage_report.ts +21 -0
  235. package/src/scripts/sweep_dead_scan_roots.ts +11 -3
  236. package/src/scripts/verify_physical_move.ts +24 -0
  237. package/src/scripts/worktree_cleanup_check.ts +319 -4
  238. package/src/scripts/check_bite_sized_granularity.ts +0 -116
  239. package/src/scripts/lint_workspace_boundary.ts +0 -267
@@ -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();
@@ -28,6 +28,8 @@ import * as fs from 'node:fs';
28
28
  import * as path from 'node:path';
29
29
  import { fileURLToPath, pathToFileURL } from 'node:url';
30
30
 
31
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
32
+
31
33
  const _HERE = fileURLToPath(import.meta.url);
32
34
  // src/scripts/audit_overlap.ts → parents[2] is the repo root (mirrors
33
35
  // `Path(__file__).resolve().parent.parent.parent` in the .py).
@@ -381,7 +383,28 @@ export function main(): number {
381
383
  return 1;
382
384
  }
383
385
  const data = JSON.parse(fs.readFileSync(AUDIT_JSON, 'utf-8')) as { rules: RuleEntry[] };
384
- const pairs = analyse(data.rules);
386
+ // The existence check above does not cover a present-but-ruleless input:
387
+ // an upstream `audit_auto_rules` that itself scanned nothing yields a
388
+ // "0 pairs flagged" report, which is the answer a real clean run gives.
389
+ // The unit is the rules read, never the pairs flagged.
390
+ const rules = Array.isArray(data.rules) ? data.rules : [];
391
+ try {
392
+ assertScanned({
393
+ gate: 'audit_overlap',
394
+ scanned: rules.length,
395
+ units: 'rule(s)',
396
+ roots: ['agents/reports/auto-rules-audit.json'],
397
+ });
398
+ } catch (exc) {
399
+ if (exc instanceof DeadScopeError) {
400
+ // 1 is this gate's only non-zero code — same one the missing-input
401
+ // path uses, and for the same reason: it could not run.
402
+ process.stderr.write(`❌ ${exc.message}\n`);
403
+ return 1;
404
+ }
405
+ throw exc;
406
+ }
407
+ const pairs = analyse(rules);
385
408
  // Build the JSON payload with the two `round(x, 3)` fields wrapped as
386
409
  // PyFloat so 0.0 / 0.5 / 1.0 render with a trailing `.0` like json.dumps.
387
410
  const pairsJson: Json = pairs.map(
@@ -25,6 +25,7 @@ import * as path from 'node:path';
25
25
  import { fileURLToPath, pathToFileURL } from 'node:url';
26
26
 
27
27
  import { artefact_roots } from './_lib/agent_src.js';
28
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
28
29
 
29
30
  export const MIN_LENGTH = 150;
30
31
  // Mirrors scripts/skill_linter.py `description_too_long` threshold.
@@ -386,6 +387,24 @@ export function main(argv: string[] | null = null): number {
386
387
  for (const r of roots) {
387
388
  findings.push(...collect_findings(r));
388
389
  }
390
+ // The roots check above only proves a `skills/` DIRECTORY exists; roots that
391
+ // hold no SKILL.md still render "Audited 0 skills … ✅ All descriptions look
392
+ // reasonable." Exit 2 is this gate's only failure code, and it already
393
+ // covers the sibling "no skills/ directories found" case.
394
+ try {
395
+ assertScanned({
396
+ gate: 'audit_skill_descriptions',
397
+ scanned: findings.length,
398
+ units: 'SKILL.md file(s)',
399
+ roots,
400
+ });
401
+ } catch (exc) {
402
+ if (exc instanceof DeadScopeError) {
403
+ process.stderr.write(`error: ${exc.message}\n`);
404
+ return 2;
405
+ }
406
+ throw exc;
407
+ }
389
408
  if (args.json) {
390
409
  process.stdout.write(pyJsonDumpsIndent2(findings.map(_asdict)) + '\n');
391
410
  } else {
@@ -2,9 +2,22 @@
2
2
  /**
3
3
  * Platform-agnostic hook for the `verify-before-complete` rule.
4
4
  *
5
- * Ported from the retired Python `src/scripts/verify_before_complete_hook.py` (ADR-200 —
6
- * Python→TS migration, Phase 6 / hooks). Public API mirrors the Python
7
- * module exactly (snake_case kept deliberately — fidelity over TS idiom).
5
+ * Named `before_complete_hook`, not `verify_before_complete_hook`: `verify_` is
6
+ * one of the gate-shaped prefixes in `_lib/gate_population.ts`, so the old name
7
+ * put an observability hook into the gate population, where the scan-scope
8
+ * ratchet counted it as a gate that cannot assert a scan scope (it scans no
9
+ * corpus — its only inputs are its stdin envelope and the state file it writes).
10
+ * AI council 2026-08-05 rejected the alternative of excluding `_hook.ts` in the
11
+ * population filter as population-shrinking: `.d.ts` / `.test.ts` are excluded
12
+ * because the language and the test runner make them structurally
13
+ * non-executable, whereas "a hook never blocks" is an operational property you
14
+ * can only learn by reading `hook_manifest.yaml` — and manifest-driven
15
+ * classification was already rejected once. Renaming keeps classification
16
+ * structural and needs no exclusion rule.
17
+ *
18
+ * Ported from the retired Python `src/scripts/verify_before_complete_hook.py`
19
+ * (ADR-200 — Python→TS migration, Phase 6 / hooks). Public API mirrors the
20
+ * Python module exactly (snake_case kept deliberately — fidelity over TS idiom).
8
21
  *
9
22
  * Records observable evidence that a verification command (tests, quality
10
23
  * tools, build) ran. The rule body cites the resulting state file as the
@@ -194,6 +194,12 @@ interface RunResult {
194
194
  tokens?: number;
195
195
  /** Empty object `{}` on dry-run (key absent in Python); breakdown otherwise. */
196
196
  tokens_breakdown?: TokensBreakdown | Record<string, never>;
197
+ /**
198
+ * Model ids the envelope actually billed (`modelUsage` keys) — the only
199
+ * place a run states which model answered it. Empty when the envelope
200
+ * omits the block. Feeds the delta-#3 model-id check.
201
+ */
202
+ models_seen?: string[];
197
203
  errored?: boolean;
198
204
  num_turns?: number;
199
205
  subtype?: string;
@@ -295,6 +301,7 @@ export function run_live(
295
301
  const returncode = result.status ?? -1;
296
302
  let transcript: string = stdout;
297
303
  let tokens = 0;
304
+ let modelsSeen: string[] = [];
298
305
  let isError = false;
299
306
  let errReason = 'ok';
300
307
  let numTurns = 0;
@@ -337,6 +344,16 @@ export function run_live(
337
344
  breakdown.output_tokens +
338
345
  breakdown.cache_read_input_tokens +
339
346
  breakdown.cache_creation_input_tokens;
347
+ // `modelUsage` is keyed by the model ids the run actually billed. Record
348
+ // them unconditionally: the delta-#3 check needs the id even on a run
349
+ // whose top-level `usage` was fine, and a mid-sweep model swap is exactly
350
+ // the case where the totals look ordinary.
351
+ modelsSeen = Object.keys(
352
+ (_isPlainObj(obj['modelUsage']) ? (obj['modelUsage'] as Record<string, unknown>) : {}) as Record<
353
+ string,
354
+ unknown
355
+ >,
356
+ );
340
357
  // The top-level `usage` block is zeroed on a budget-capped / errored run
341
358
  // (and unreliable even on some completions). `modelUsage` carries the
342
359
  // authoritative per-model counts — sum it as the fallback so token deltas
@@ -387,6 +404,7 @@ export function run_live(
387
404
  wall_time_seconds: _pyRound(duration, 3),
388
405
  tokens,
389
406
  tokens_breakdown: breakdown,
407
+ models_seen: modelsSeen,
390
408
  errored: isError || returncode !== 0,
391
409
  num_turns: numTurns,
392
410
  subtype,