@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
@@ -26,6 +26,8 @@ import * as fs from 'node:fs';
26
26
  import * as path from 'node:path';
27
27
  import { fileURLToPath, pathToFileURL } from 'node:url';
28
28
 
29
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
30
+
29
31
  const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
30
32
  const _HERE = fileURLToPath(import.meta.url);
31
33
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -131,6 +133,25 @@ const _hooks: Hooks = {
131
133
  function main(argv: readonly string[] = [], hooks: Hooks = _hooks): number {
132
134
  const args = parse_args(argv);
133
135
 
136
+ // This gate walks no tree: one named file supplies BOTH sides of every
137
+ // invariant — the version the git tag must match and the package name npm
138
+ // is queried for. A repointed PACKAGE_JSON constant is the whole failure
139
+ // surface, so the watch list is the scope assertion. Exit 3 (internal
140
+ // error — the gate could not run), never 1, which asserts real drift.
141
+ try {
142
+ assertWatchlistResolves({
143
+ gate: 'check_release_published',
144
+ candidates: [path.relative(REPO_ROOT, PACKAGE_JSON)],
145
+ repoRoot: REPO_ROOT,
146
+ });
147
+ } catch (exc) {
148
+ if (exc instanceof DeadScopeError) {
149
+ process.stderr.write(`❌ ${exc.message}\n`);
150
+ return 3;
151
+ }
152
+ throw exc;
153
+ }
154
+
134
155
  let version: string;
135
156
  try {
136
157
  version = hooks._package_version();
@@ -28,6 +28,7 @@ import {
28
28
  strip_pr_wrapper,
29
29
  surface_divergence,
30
30
  } from './_lib/release_material.js';
31
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
31
32
 
32
33
  const _HERE = fileURLToPath(import.meta.url);
33
34
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -102,6 +103,27 @@ function main(argv: readonly string[]): number {
102
103
  ) as Record<string, unknown>;
103
104
  version = String(pkg['version']);
104
105
  }
106
+ // The changelog is this gate's one fixed named input — the other surface is
107
+ // either fetched over the network (`--pr`) or a caller-supplied fixture. It
108
+ // is read unguarded below, so a moved or renamed CHANGELOG would surface as
109
+ // an ENOENT stack trace rather than a verdict. Listed alone on purpose: a
110
+ // watch list passes as soon as ANY candidate resolves, so pairing it with
111
+ // `--body-file` would let an existing fixture mask an absent changelog.
112
+ // Exit 2 — this gate's usage/extraction class — never 1, which asserts
113
+ // the two surfaces genuinely diverged.
114
+ try {
115
+ assertWatchlistResolves({
116
+ gate: 'check_release_surface_equality',
117
+ candidates: [path.relative(REPO_ROOT, path.resolve(changelogPath))],
118
+ repoRoot: REPO_ROOT,
119
+ });
120
+ } catch (err) {
121
+ if (err instanceof DeadScopeError) {
122
+ process.stderr.write(`❌ ${err.message}\n`);
123
+ return 2;
124
+ }
125
+ throw err;
126
+ }
105
127
  const prBody = pr !== null ? _pr_body(pr) : fs.readFileSync(bodyFile!, 'utf-8');
106
128
  const changelogText = fs.readFileSync(changelogPath, 'utf-8');
107
129
  const result = check_surface_equality({ prBody, changelogText, version });
@@ -21,6 +21,8 @@ import * as fs from 'node:fs';
21
21
  import * as path from 'node:path';
22
22
  import { fileURLToPath, pathToFileURL } from 'node:url';
23
23
 
24
+ import { assertScanned } from './_lib/scan_scope.js';
25
+
24
26
  const RELEASE_BRANCH_RE = /^release\/(\d+)\.(\d+)\.(\d+)$/;
25
27
  const SEMVER_TAG_RE = /^(\d+)\.(\d+)\.(\d+)$/;
26
28
  const BOOTSTRAP_FILE = 'docs/contracts/release-trunk-sync.bootstrap';
@@ -137,6 +139,23 @@ function main(): number {
137
139
  }
138
140
  const m = RELEASE_BRANCH_RE.exec(branch);
139
141
  if (!m) {
142
+ // Declared no-op, not a silent one. The gate's other empty states —
143
+ // no semver tags, no tag reachable from main — are NOT swallowed
144
+ // either: each prints its own `::warning::… skipped` line below, and
145
+ // that wording is the pinned py2ts contract, so neither is converted
146
+ // into a DeadScopeError here.
147
+ assertScanned({
148
+ gate: 'check_release_trunk_sync',
149
+ scanned: 0,
150
+ units: 'release-prep branch(es)',
151
+ roots: ['git rev-parse --abbrev-ref HEAD'],
152
+ allowEmpty:
153
+ 'OPTIONAL_INPUT: the checked-out branch is not release/X.Y.Z, so there is no ' +
154
+ 'target version to compare main against — an absent question, not an empty ' +
155
+ 'corpus. Deletion test: wiping every git ref would not produce this branch; ' +
156
+ 'the input read here is the branch NAME, it was read successfully, and a ' +
157
+ 'branch that could not be read exits at the detached-HEAD warning above.',
158
+ });
140
159
  return 0; // non-release branch class — gate is a no-op
141
160
  }
142
161
  const target: SemVer = [Number(m[1]), Number(m[2]), Number(m[3])];
@@ -45,6 +45,7 @@ import * as path from 'node:path';
45
45
  import { fileURLToPath, pathToFileURL } from 'node:url';
46
46
 
47
47
  import { artefact_roots } from './_lib/agent_src.js';
48
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
48
49
 
49
50
  const _HERE = fileURLToPath(import.meta.url);
50
51
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -269,8 +270,10 @@ export function cmd_scan_dir(root: string): number {
269
270
  roots = [root];
270
271
  }
271
272
  const violations: Array<[string, number, string]> = [];
273
+ let scanned = 0;
272
274
  for (const r of roots) {
273
275
  for (const md of _rglobMdSorted(r)) {
276
+ scanned += 1;
274
277
  const text = fs.readFileSync(md, 'utf-8');
275
278
  const lines = _splitlines(text);
276
279
  for (let idx = 0; idx < lines.length; idx++) {
@@ -282,6 +285,25 @@ export function cmd_scan_dir(root: string): number {
282
285
  }
283
286
  }
284
287
  }
288
+ // The CI invocation points at the retired `.agent-src.uncondensed` root and
289
+ // relies on the fallback above; an EMPTY leftover of that directory still
290
+ // passes `_isDir`, so the fallback never fires and the sweep reads nothing.
291
+ try {
292
+ assertScanned({
293
+ gate: 'check_reply_consistency',
294
+ scanned,
295
+ units: 'markdown file(s)',
296
+ roots: roots.map((r) => _relToPosixOrAbs(r, ROOT)),
297
+ });
298
+ } catch (exc) {
299
+ if (exc instanceof DeadScopeError) {
300
+ // 1 = usage/IO error, as for the unresolvable-root case above
301
+ // (2 is reserved for findings).
302
+ process.stderr.write(`❌ ${exc.message}\n`);
303
+ return 1;
304
+ }
305
+ throw exc;
306
+ }
285
307
  if (violations.length > 0) {
286
308
  for (const [p, line, snippet] of violations) {
287
309
  process.stderr.write(` 🔴 ${p}:${String(line)} — inline-tag — ${snippet}\n`);
@@ -29,6 +29,8 @@ import * as fs from 'node:fs';
29
29
  import * as path from 'node:path';
30
30
  import { fileURLToPath, pathToFileURL } from 'node:url';
31
31
 
32
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
33
+
32
34
  const QUIET = process.argv.slice(2).includes('--quiet');
33
35
 
34
36
  const ROADMAP_ROOT = 'agents/roadmaps';
@@ -224,6 +226,24 @@ function main(): number {
224
226
  process.stderr.write(`❌ ${ROADMAP_ROOT} not found — run from project root.\n`);
225
227
  return 1;
226
228
  }
229
+ // The dir check above cannot see an existing-but-empty tree, and the unit
230
+ // has to be the unfiltered walk: "0 active roadmaps" is a real success
231
+ // state (everything archived), while 0 `.md` under the root never is.
232
+ try {
233
+ assertScanned({
234
+ gate: 'check_roadmap_trackable',
235
+ scanned: _rglobMdSorted(ROADMAP_ROOT).length,
236
+ units: 'markdown file(s)',
237
+ roots: [ROADMAP_ROOT],
238
+ });
239
+ } catch (exc) {
240
+ if (exc instanceof DeadScopeError) {
241
+ // 1 is this gate's only failure code (0 clean / 1 violation).
242
+ process.stderr.write(`❌ ${exc.message}\n`);
243
+ return 1;
244
+ }
245
+ throw exc;
246
+ }
227
247
  const findings: string[] = [];
228
248
  for (const p of find_active_roadmaps(ROADMAP_ROOT)) {
229
249
  findings.push(...violations_for(p));
@@ -17,6 +17,8 @@ import * as fs from 'node:fs';
17
17
  import * as path from 'node:path';
18
18
  import { fileURLToPath, pathToFileURL } from 'node:url';
19
19
 
20
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
21
+
20
22
  const _HERE = fileURLToPath(import.meta.url);
21
23
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
22
24
  const DOCS_DIR = path.join(ROOT, 'docs');
@@ -159,6 +161,27 @@ function main(argv?: readonly string[]): number {
159
161
  }
160
162
  }
161
163
 
164
+ // `scan` exits 2 when a role doc is missing outright, but a doc that still
165
+ // exists and has lost every internal link — rewritten to external URLs, or
166
+ // the how-to section moved elsewhere — leaves `checked` at 0 and prints
167
+ // "0 links OK across 2 files", which is indistinguishable from a pass.
168
+ try {
169
+ assertScanned({
170
+ gate: 'check_role_doc_links',
171
+ scanned: checked,
172
+ units: 'internal link(s)',
173
+ roots: ROLE_DOCS.map((d) => _relTo(d)),
174
+ });
175
+ } catch (exc) {
176
+ if (exc instanceof DeadScopeError) {
177
+ // 2 is this gate's existing scope-failure code (missing role doc);
178
+ // 1 means "broken links", which is not what happened here.
179
+ process.stderr.write(`${exc.message}\n`);
180
+ return 2;
181
+ }
182
+ throw exc;
183
+ }
184
+
162
185
  if (failures.length > 0) {
163
186
  process.stderr.write('Broken links in role docs:\n');
164
187
  for (const [doc, line_no, url] of failures) {
@@ -33,6 +33,8 @@ import * as fs from 'node:fs';
33
33
  import * as path from 'node:path';
34
34
  import { fileURLToPath, pathToFileURL } from 'node:url';
35
35
 
36
+ import { assertWatchlistResolves } from './_lib/scan_scope.js';
37
+
36
38
  // src/scripts/check_rule_invariants.ts → two dirs up is the repo root.
37
39
  const _HERE = fileURLToPath(import.meta.url);
38
40
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -247,6 +249,18 @@ function main(): number {
247
249
  return mutationSelftest(args.root);
248
250
  }
249
251
  const entries = loadEntries(args.root);
252
+ // This guard has no corpus to walk — it byte-pins fragments of named
253
+ // kernel rules, so its scope IS that watch list. An emptied
254
+ // invariants.json yields no candidates at all and this prints
255
+ // "All 0 kernel rule invariants present in src + dist"; a wholesale
256
+ // move of `src/rules/` + `dist/agent-src/rules/` leaves it watching
257
+ // phantoms. DeadScopeError lands in the catch below → stderr + exit 3,
258
+ // the could-not-run code; 1 means a clause was genuinely lost.
259
+ assertWatchlistResolves({
260
+ gate: 'check_rule_invariants',
261
+ candidates: entries.flatMap((e) => [e.file, e.dist]),
262
+ repoRoot: args.root,
263
+ });
250
264
  const invariantCount = entries.reduce((n, e) => n + e.strings.length, 0);
251
265
  const misses = scan(args.root);
252
266
  process.stdout.write(formatText(misses, invariantCount) + '\n');
@@ -37,6 +37,7 @@ import * as fs from 'node:fs';
37
37
  import * as path from 'node:path';
38
38
  import { fileURLToPath, pathToFileURL } from 'node:url';
39
39
 
40
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
40
41
  import { scanText, type SecretFinding } from './_lib/secret_detector.js';
41
42
 
42
43
  const _HERE = path.dirname(fileURLToPath(import.meta.url));
@@ -253,6 +254,52 @@ function _stderr(s: string): void {
253
254
  export function main(argv?: readonly string[]): number {
254
255
  const args = parseArgs(argv ?? process.argv.slice(2));
255
256
  const mode: Mode = args.paths.length > 0 ? 'explicit' : args.all ? 'all' : 'diff';
257
+
258
+ // Scope assertion, deliberately here and not inside `scanRepo`: that export
259
+ // is called directly by tests and other callers against arbitrary roots, and
260
+ // a secret gate is the last place to widen a throw. The cost is one extra
261
+ // `resolveFiles` (a `git ls-files` / `git diff`); the file reads are not
262
+ // repeated. Assert the RESOLVED set, before the exclude/binary filters —
263
+ // those legitimately drop everything (a diff touching only `tests/`), so
264
+ // filtering to zero is not the same signal as resolving to zero.
265
+ // Exit 2 (usage/env) over 1 (leak found) — a dead scope means the gate could
266
+ // not run. `resolveFiles` already hard-throws on an unresolvable base, so
267
+ // that case never arrives here as a quiet zero; a non-DeadScopeError is
268
+ // re-thrown untouched to preserve it.
269
+ try {
270
+ const targets = resolveFiles(REPO_ROOT, mode, { explicit: args.paths, base: args.base });
271
+ assertScanned({
272
+ gate: 'check_secret_leak',
273
+ scanned: targets.length,
274
+ units: 'candidate file(s)',
275
+ roots:
276
+ mode === 'all'
277
+ ? ['git ls-files']
278
+ : mode === 'explicit'
279
+ ? ['<explicit path arguments>']
280
+ : [`git diff --name-only --diff-filter=ACMR ${args.base ?? 'origin/main'}`, 'git ls-files --others --exclude-standard'],
281
+ // `all` and `explicit` carry no reason: zero tracked files, or zero
282
+ // named paths, is blindness in a repo that has content.
283
+ ...(mode === 'diff'
284
+ ? {
285
+ allowEmpty:
286
+ 'EMPTY_VALID: shift-left mode scans what the change introduces, and a ' +
287
+ 'branch identical to its base with no untracked files introduces nothing ' +
288
+ '— an absent question, not an empty corpus. Deletion test: wiping the ' +
289
+ 'tracked tree would show up here as changed paths, and an unresolvable ' +
290
+ 'base (the dangerous zero, which shallow CI produces) already throws in ' +
291
+ 'resolveFiles above rather than reaching this line.',
292
+ }
293
+ : {}),
294
+ });
295
+ } catch (exc) {
296
+ if (exc instanceof DeadScopeError) {
297
+ _stderr(`❌ ${exc.message}\n`);
298
+ return 2;
299
+ }
300
+ throw exc;
301
+ }
302
+
256
303
  const hits = scanRepo(REPO_ROOT, mode, { explicit: args.paths, base: args.base });
257
304
 
258
305
  if (args.json) {
@@ -16,6 +16,8 @@ import * as path from 'node:path';
16
16
  import { fileURLToPath, pathToFileURL } from 'node:url';
17
17
  import { parse as parseYaml } from 'yaml';
18
18
 
19
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
20
+
19
21
  const _HERE = fileURLToPath(import.meta.url);
20
22
  export const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
21
23
  const SKILLS_DIR = path.join(ROOT, 'src', 'skills');
@@ -76,8 +78,38 @@ export function findBrokenPointers(entries: SkillGaps[], root: string = ROOT): s
76
78
  return errs;
77
79
  }
78
80
 
81
+ /** SKILL.md files under `skillsDir` — the corpus `collectSkillGaps` reads. */
82
+ function countSkillFiles(skillsDir: string = SKILLS_DIR): number {
83
+ try {
84
+ return fs
85
+ .readdirSync(skillsDir)
86
+ .filter((name) => fs.existsSync(path.join(skillsDir, name, 'SKILL.md'))).length;
87
+ } catch {
88
+ // A missing root counts zero, which the caller's assertion turns into a
89
+ // loud failure — never a silent "no gaps to check".
90
+ return 0;
91
+ }
92
+ }
93
+
79
94
  export function main(argv: string[] = process.argv.slice(2)): number {
80
95
  const quiet = argv.includes('--quiet');
96
+ // The scanned unit is every SKILL.md, not the subset declaring `gaps:` —
97
+ // asserting on the declaration count would read a vanished skills tree as
98
+ // "no gaps declared" and stay green, the exact blindness this guards.
99
+ try {
100
+ assertScanned({
101
+ gate: 'check_skill_gaps',
102
+ scanned: countSkillFiles(),
103
+ units: 'SKILL.md file(s)',
104
+ roots: ['src/skills'],
105
+ });
106
+ } catch (exc) {
107
+ if (exc instanceof DeadScopeError) {
108
+ process.stderr.write(`❌ ${exc.message}\n`);
109
+ return 1;
110
+ }
111
+ throw exc;
112
+ }
81
113
  const entries = collectSkillGaps();
82
114
  const errs = findBrokenPointers(entries);
83
115
  if (errs.length > 0) {
@@ -19,6 +19,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
19
19
  import YAML from 'yaml';
20
20
 
21
21
  import { ROOT, iter_artefacts } from './_lib/agent_src.js';
22
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
22
23
  import { parse_frontmatter } from './validate_frontmatter.js';
23
24
 
24
25
  const _HERE = fileURLToPath(import.meta.url);
@@ -107,6 +108,24 @@ function _set_hooks_for_test(overrides: Partial<typeof _hooks>): void {
107
108
  function main(): number {
108
109
  const closure = _hooks.load_pack_closure();
109
110
  const skills = _hooks.collect_skills();
111
+ // The unit is every parsed SKILL.md, not the subset declaring
112
+ // `requires_skills:` — over a vanished skills tree "0 composition edges"
113
+ // is indistinguishable from a suite that legitimately declares none.
114
+ try {
115
+ assertScanned({
116
+ gate: 'check_skill_requires',
117
+ scanned: Object.keys(skills).length,
118
+ units: 'SKILL.md file(s)',
119
+ roots: ['src/agent-src', 'src/skills', 'src/rules'],
120
+ });
121
+ } catch (exc) {
122
+ if (exc instanceof DeadScopeError) {
123
+ // 1 is this gate's only failure code (0 clean / 1 violations).
124
+ process.stderr.write(`❌ ${exc.message}\n`);
125
+ return 1;
126
+ }
127
+ throw exc;
128
+ }
110
129
  const errors: string[] = [];
111
130
 
112
131
  for (const skill_id of Object.keys(skills).sort()) {
@@ -36,6 +36,8 @@ import * as path from 'node:path';
36
36
  import process from 'node:process';
37
37
  import { fileURLToPath, pathToFileURL } from 'node:url';
38
38
 
39
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
40
+
39
41
  const _HERE = fileURLToPath(import.meta.url);
40
42
  // parents[2] of src/scripts/<file> is the repo root.
41
43
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -142,6 +144,24 @@ function main(argv: readonly string[]): number {
142
144
  return _selftest();
143
145
  }
144
146
  const asJson = argv.includes('--json');
147
+ // Scope is the allowlist itself — there is no tree to walk. An emptied
148
+ // AUTHORING_FILES scans nothing and reports "No retired source-of-truth
149
+ // pointers" forever, which is the one result this gate must never produce
150
+ // by accident. Exit 2 (the usage / self-test class, i.e. the gate could
151
+ // not run), never 1 — 1 asserts a retired pointer was actually found.
152
+ try {
153
+ assertWatchlistResolves({
154
+ gate: 'check_source_pointer_freshness',
155
+ candidates: AUTHORING_FILES,
156
+ repoRoot: ROOT,
157
+ });
158
+ } catch (err) {
159
+ if (err instanceof DeadScopeError) {
160
+ process.stderr.write(`❌ ${err.message}\n`);
161
+ return 2;
162
+ }
163
+ throw err;
164
+ }
145
165
  const hits: Hit[] = [];
146
166
  for (const rel of AUTHORING_FILES) {
147
167
  hits.push(..._scanFile(rel));
@@ -30,6 +30,8 @@ import * as path from 'node:path';
30
30
  import * as fs from 'node:fs';
31
31
  import { fileURLToPath, pathToFileURL } from 'node:url';
32
32
 
33
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
34
+
33
35
  const QUIET = process.argv.includes('--quiet');
34
36
  // src/scripts/check_structural_breaking.ts → three levels up is the repo root
35
37
  // (Python: Path(__file__).resolve().parent.parent.parent).
@@ -91,12 +93,37 @@ function main(): number {
91
93
  return 0; // no trunk to diff against (e.g. shallow CI) → no-op
92
94
  }
93
95
  const names = _git('diff', '--name-status', `${base}...HEAD`);
94
- if (names.trim() === '') {
96
+ const changed = _splitlines(names);
97
+ // Replaces the bare `names.trim() === ''` early return: a branch identical
98
+ // to trunk genuinely changes nothing, so zero is allowed — but it now says
99
+ // so through the shared primitive instead of a silent `return 0`.
100
+ // Exit 1 is this pinned CLI's only failure code.
101
+ try {
102
+ assertScanned({
103
+ gate: 'check_structural_breaking',
104
+ scanned: changed.length,
105
+ units: 'changed path(s)',
106
+ roots: [`git diff --name-status ${BASE_REF}...HEAD`],
107
+ allowEmpty:
108
+ 'EMPTY_VALID: a branch with no diff against trunk has no structural change to ' +
109
+ 'judge — an absent question, not an empty corpus. Deletion test: deleting the ' +
110
+ 'watched artifact trees would show up here as D lines (the very break this gate ' +
111
+ 'reports), so a wiped tree cannot produce this zero. An unresolvable base does ' +
112
+ 'not reach here either — it exits at the no-trunk no-op above.',
113
+ });
114
+ } catch (exc) {
115
+ if (exc instanceof DeadScopeError) {
116
+ process.stderr.write(`❌ ${exc.message}\n`);
117
+ return 1;
118
+ }
119
+ throw exc;
120
+ }
121
+ if (changed.length === 0) {
95
122
  return 0; // nothing changed vs trunk
96
123
  }
97
124
 
98
125
  const breaks: string[] = [];
99
- for (const line of _splitlines(names)) {
126
+ for (const line of changed) {
100
127
  const parts = line.split('\t');
101
128
  const status = parts[0]!;
102
129
  if (status.startsWith('D') && ARTIFACT_RE.test(parts[1]!)) {
@@ -37,6 +37,8 @@ import * as path from 'node:path';
37
37
  import { fileURLToPath, pathToFileURL } from 'node:url';
38
38
  import { parse as parseYaml } from 'yaml';
39
39
 
40
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
41
+
40
42
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
41
43
  const SCRIPTS = path.join(ROOT, 'src', 'scripts');
42
44
  const REGISTRY = path.join(SCRIPTS, 'surface-tiers.yml');
@@ -105,6 +107,20 @@ function check_exhaustive(clusters: Map<string, string>): string[] {
105
107
  return errors;
106
108
  }
107
109
 
110
+ /** Cluster dirs on disk under SCRIPTS — the corpus `check_exhaustive` classifies. */
111
+ function _count_cluster_dirs(): number {
112
+ let children: fs.Dirent[];
113
+ try {
114
+ children = fs.readdirSync(SCRIPTS, { withFileTypes: true });
115
+ } catch {
116
+ // A missing root counts zero; the caller's assertion makes that loud.
117
+ return 0;
118
+ }
119
+ return children.filter(
120
+ (c) => !_IGNORE_DIRS.has(c.name) && _isDir(path.join(SCRIPTS, c.name)),
121
+ ).length;
122
+ }
123
+
108
124
  function _module_tier(
109
125
  dotted: string,
110
126
  clusters: Map<string, string>,
@@ -527,6 +543,24 @@ function main(argv?: readonly string[]): number {
527
543
  args.skip_imports || envSkip === '1' || envSkip === 'true' || envSkip === 'yes';
528
544
 
529
545
  const { clusters, lab_modules } = _load_registry();
546
+ // The unit is the cluster dirs found on disk, not `clusters.size` (the
547
+ // registry's own entries): over a vanished src/scripts/ every registry
548
+ // entry goes unchallenged and both assertions are trivially clean.
549
+ try {
550
+ assertScanned({
551
+ gate: 'check_surface_tiers',
552
+ scanned: _count_cluster_dirs(),
553
+ units: 'cluster dir(s)',
554
+ roots: ['src/scripts'],
555
+ });
556
+ } catch (exc) {
557
+ if (exc instanceof DeadScopeError) {
558
+ // 1 = violation(s); 2 is reserved for argparse usage errors.
559
+ process.stderr.write(`❌ ${exc.message}\n`);
560
+ return 1;
561
+ }
562
+ throw exc;
563
+ }
530
564
  const errors = check_exhaustive(clusters);
531
565
  if (skip_imports) {
532
566
  process.stdout.write('surface-tiers: import boundary check SKIPPED (kill-switch).\n');
@@ -27,6 +27,7 @@ import * as path from 'node:path';
27
27
  import { fileURLToPath, pathToFileURL } from 'node:url';
28
28
 
29
29
  import { resolve_logical } from './_lib/agent_src.js';
30
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
30
31
 
31
32
  const _HERE = fileURLToPath(import.meta.url);
32
33
  // Python `Path(__file__).resolve().parents[2]` — two dirs up from src/scripts.
@@ -159,8 +160,29 @@ function main(argv: readonly string[] = []): number {
159
160
  return 1;
160
161
  }
161
162
 
163
+ // Scope is two named templates, one of them resolved through
164
+ // `resolve_logical` — a resolver returning a path under an artefact root
165
+ // that no longer exists is exactly how this comparison ends up with
166
+ // nothing to compare. Exit 1: it is this gate's only failure code, and it
167
+ // already carries the could-not-run meaning (a missing template file is
168
+ // reported as a failure below); 2 is reserved for CLI misuse.
169
+ const templates = _template_files();
170
+ try {
171
+ assertWatchlistResolves({
172
+ gate: 'check_template_pin_drift',
173
+ candidates: templates.map((p) => path.relative(REPO_ROOT, p)),
174
+ repoRoot: REPO_ROOT,
175
+ });
176
+ } catch (exc) {
177
+ if (exc instanceof DeadScopeError) {
178
+ process.stderr.write(`❌ check_template_pin_drift: ${exc.message}\n`);
179
+ return 1;
180
+ }
181
+ throw exc;
182
+ }
183
+
162
184
  const failures: string[] = [];
163
- for (const template of _template_files()) {
185
+ for (const template of templates) {
164
186
  const rel = _relToPosixOrAbs(template, REPO_ROOT);
165
187
  if (!_isFile(template)) {
166
188
  failures.push(`missing template file: ${rel}`);
@@ -17,6 +17,8 @@ import * as fs from 'node:fs';
17
17
  import * as path from 'node:path';
18
18
  import { fileURLToPath, pathToFileURL } from 'node:url';
19
19
 
20
+ import { assertScanned } from './_lib/scan_scope.js';
21
+
20
22
  const _HERE = fileURLToPath(import.meta.url);
21
23
 
22
24
  // Injectable for tests (monkeypatch parity): REPO_ROOT, _git_name_status,
@@ -177,7 +179,21 @@ function parse_args(argv: readonly string[]): ParsedArgs {
177
179
 
178
180
  function main(argv?: readonly string[]): number {
179
181
  const opts = parse_args(argv ?? process.argv.slice(2));
180
- const changed = _hooks.git_name_status(_resolve_base_ref(opts.base_ref));
182
+ const base_ref = _resolve_base_ref(opts.base_ref);
183
+ const changed = _hooks.git_name_status(base_ref);
184
+ assertScanned({
185
+ gate: 'check_test_coverage_diff',
186
+ scanned: changed.length,
187
+ units: 'changed path(s)',
188
+ roots: [`git diff --name-status ${base_ref}...HEAD`],
189
+ allowEmpty:
190
+ 'EMPTY_VALID: a diff with no changed paths adds no gate, so there is nothing for a ' +
191
+ 'coverage forcing-function to warn about. Deletion test: a deleted worktree cannot ' +
192
+ 'produce this state — an UNREADABLE range is a different case and is not silent, ' +
193
+ '`_git_name_status` prints `⚠️ coverage-diff: git diff failed (…); skipping.` to ' +
194
+ 'stderr before returning empty. The exit code is 0 either way by pinned contract ' +
195
+ '(warn-only this phase), so this assertion declares the scope rather than gating it.',
196
+ });
181
197
  const [warnings, suppressed] = evaluate(changed, _hooks.pragma_reason_from_tree);
182
198
  if (warnings.length > 0) {
183
199
  process.stdout.write(
@@ -20,6 +20,7 @@ import * as path from 'node:path';
20
20
  import { fileURLToPath, pathToFileURL } from 'node:url';
21
21
 
22
22
  import { resolve_logical, strip_source_prefix } from './_lib/agent_src.js';
23
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
23
24
 
24
25
  const _HERE = fileURLToPath(import.meta.url);
25
26
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -150,9 +151,23 @@ function main(): number {
150
151
  }
151
152
  const text = fs.readFileSync(SKILL, 'utf-8');
152
153
  const rows = parse_catalog(text);
153
- if (rows.length === 0) {
154
- process.stderr.write('ERROR: token-optimizer SKILL.md has no parseable catalog rows\n');
155
- return 1;
154
+ // Supersedes the hand-rolled "no parseable catalog rows" guard: same exit
155
+ // code and stream, but the shared assertion names the root, so a renamed
156
+ // `## Catalog` heading or a moved SKILL.md reports as a dead scope instead
157
+ // of a bare ERROR line.
158
+ try {
159
+ assertScanned({
160
+ gate: 'check_token_optimizer_freshness',
161
+ scanned: rows.length,
162
+ units: 'catalog row(s)',
163
+ roots: [path.relative(REPO_ROOT, SKILL)],
164
+ });
165
+ } catch (exc) {
166
+ if (exc instanceof DeadScopeError) {
167
+ process.stderr.write(`ERROR: ${exc.message}\n`);
168
+ return 1;
169
+ }
170
+ throw exc;
156
171
  }
157
172
  const allErrs: string[] = [];
158
173
  let checked = 0;