@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
@@ -19,6 +19,7 @@ import * as path from 'node:path';
19
19
  import { fileURLToPath, pathToFileURL } from 'node:url';
20
20
 
21
21
  import { SRC_AGENT } from './_lib/agent_src.js';
22
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
22
23
 
23
24
  const _HERE = fileURLToPath(import.meta.url);
24
25
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -253,6 +254,25 @@ function _fileSize(p: string): number {
253
254
  }
254
255
 
255
256
  function main(): number {
257
+ // No corpus walk — the contract binds two named files, so the watch list IS
258
+ // the scope. `SRC_AGENT()` has already moved once (ADR-051); if it moves
259
+ // again the per-target `not found` errors say nothing about the root.
260
+ try {
261
+ assertWatchlistResolves({
262
+ gate: 'lint_agents_md',
263
+ candidates: TARGETS.map((t) => _relPosix(t.path, ROOT)),
264
+ repoRoot: ROOT,
265
+ });
266
+ } catch (exc) {
267
+ if (exc instanceof DeadScopeError) {
268
+ // 1 is the gate's only failure code — a missing target already
269
+ // returns it, so "could not run at all" cannot be louder than that.
270
+ process.stdout.write(` ❌ ${exc.message}\n`);
271
+ return 1;
272
+ }
273
+ throw exc;
274
+ }
275
+
256
276
  let rc = 0;
257
277
  for (const t of TARGETS) {
258
278
  const [ok, errors, warnings] = lint_file(t);
@@ -30,6 +30,7 @@ import * as path from 'node:path';
30
30
  import { fileURLToPath, pathToFileURL } from 'node:url';
31
31
 
32
32
  import { artefact_roots } from './_lib/agent_src.js';
33
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
33
34
 
34
35
  const _HERE = fileURLToPath(import.meta.url);
35
36
 
@@ -40,9 +41,8 @@ const REPO = path.resolve(path.dirname(_HERE), '..', '..');
40
41
  const ARCHIVE_DIR = path.join(REPO, 'agents', 'evidence', 'archived-skills');
41
42
 
42
43
  // Live skill directories live under every artefact root.
43
- const SKILLS_DIRS = artefact_roots()
44
- .map((root) => path.join(root, 'skills'))
45
- .filter((d) => _isDir(d));
44
+ const ALL_SKILLS_DIRS = artefact_roots().map((root) => path.join(root, 'skills'));
45
+ const SKILLS_DIRS = ALL_SKILLS_DIRS.filter((d) => _isDir(d));
46
46
 
47
47
  const REQUIRED_FIELDS = [
48
48
  'slug',
@@ -128,6 +128,27 @@ function main(): number {
128
128
 
129
129
  const notes = archived_slugs();
130
130
  const live = live_skill_slugs();
131
+
132
+ // The scanned unit is the LIVE skills tree, not the archive notes. Zero
133
+ // notes is a real success state (nothing has been archived yet), but three
134
+ // of the five contract checks — replacement-resolves, no-zombie, and the
135
+ // replaced_by cross-check — are decided entirely by `live`, so an empty or
136
+ // moved skills root silently answers "no zombies, all replacements fine".
137
+ try {
138
+ assertScanned({
139
+ gate: 'lint_archived_skills',
140
+ scanned: live.size,
141
+ units: 'live skill(s)',
142
+ roots: ALL_SKILLS_DIRS.map((d) => path.relative(REPO, d)),
143
+ });
144
+ } catch (exc) {
145
+ if (exc instanceof DeadScopeError) {
146
+ process.stderr.write(`❌ ${exc.message}\n`);
147
+ return 1;
148
+ }
149
+ throw exc;
150
+ }
151
+
131
152
  const errors: string[] = [];
132
153
 
133
154
  const archivedKeys = new Set<string>();
@@ -31,6 +31,8 @@ import * as fs from 'node:fs';
31
31
  import * as path from 'node:path';
32
32
  import { fileURLToPath, pathToFileURL } from 'node:url';
33
33
 
34
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
35
+
34
36
  const _HERE = fileURLToPath(import.meta.url);
35
37
  export const REPO = path.resolve(path.dirname(_HERE), '..', '..');
36
38
  let SKILLS_DIR = path.join(REPO, 'src', 'skills');
@@ -57,20 +59,28 @@ export interface FreshnessResult {
57
59
  ok: boolean;
58
60
  stale: Array<{ skill: string; pinned: string; current: string }>;
59
61
  inScope: number;
62
+ /**
63
+ * Skill directories walked. `inScope` counts only the PINNED subset, which
64
+ * is legitimately 0 today (nothing has opted in yet) — so it can never
65
+ * anchor the scope assertion; this can.
66
+ */
67
+ scanned: number;
60
68
  }
61
69
 
62
70
  export function checkFreshness(): FreshnessResult {
63
71
  const stale: FreshnessResult['stale'] = [];
64
72
  let inScope = 0;
73
+ let scanned = 0;
65
74
  let entries: fs.Dirent[];
66
75
  try {
67
76
  entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
68
77
  } catch {
69
- return { ok: true, stale, inScope };
78
+ return { ok: true, stale, inScope, scanned };
70
79
  }
71
80
  entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
72
81
  for (const e of entries) {
73
82
  if (!e.isDirectory()) continue;
83
+ scanned += 1;
74
84
  const evalsPath = path.join(SKILLS_DIR, e.name, 'evals', 'evals.json');
75
85
  const skillMd = path.join(SKILLS_DIR, e.name, 'SKILL.md');
76
86
  if (!fs.existsSync(evalsPath) || !fs.existsSync(skillMd)) continue;
@@ -91,7 +101,7 @@ export function checkFreshness(): FreshnessResult {
91
101
  stale.push({ skill: e.name, pinned, current });
92
102
  }
93
103
  }
94
- return { ok: stale.length === 0, stale, inScope };
104
+ return { ok: stale.length === 0, stale, inScope, scanned };
95
105
  }
96
106
 
97
107
  export function main(argv: readonly string[] = process.argv.slice(2)): number {
@@ -103,7 +113,23 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
103
113
  return 2;
104
114
  }
105
115
  }
106
- const { ok, stale, inScope } = checkFreshness();
116
+ const { ok, stale, inScope, scanned } = checkFreshness();
117
+ // `checkFreshness` swallows an unreadable skills root and returns ok:true,
118
+ // so a moved `src/skills` prints "0 pinned eval(s) current" and passes.
119
+ try {
120
+ assertScanned({
121
+ gate: 'lint_behavioural_eval_freshness',
122
+ scanned,
123
+ units: 'skill director(ies)',
124
+ roots: ['src/skills'],
125
+ });
126
+ } catch (e) {
127
+ if (e instanceof DeadScopeError) {
128
+ process.stderr.write(`${e.message}\n`);
129
+ return 1;
130
+ }
131
+ throw e;
132
+ }
107
133
  if (!ok) {
108
134
  for (const s of stale) {
109
135
  process.stderr.write(
@@ -20,6 +20,8 @@ import * as path from 'node:path';
20
20
  import { fileURLToPath, pathToFileURL } from 'node:url';
21
21
  import { parse as parseYaml } from 'yaml';
22
22
 
23
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
24
+
23
25
  const _HERE = fileURLToPath(import.meta.url);
24
26
 
25
27
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -269,6 +271,29 @@ function parse_args(argv: readonly string[]): Args {
269
271
 
270
272
  function main(argv?: readonly string[]): number {
271
273
  const args = parse_args(argv ?? process.argv.slice(2));
274
+
275
+ // The two corpora are the scope. `docs/benchmark.md` is deliberately NOT on
276
+ // the list: it is optional (lint_doc skips when unrendered), and a watch list
277
+ // passes when ANY entry resolves — including it would let both corpora
278
+ // disappear behind a rendered doc.
279
+ try {
280
+ assertWatchlistResolves({
281
+ gate: 'lint_bench_ab',
282
+ candidates: [TRACK_A_PATH, TRACK_B_PATH].map((p) =>
283
+ path.relative(REPO_ROOT, p).split(path.sep).join('/'),
284
+ ),
285
+ repoRoot: REPO_ROOT,
286
+ });
287
+ } catch (exc) {
288
+ if (exc instanceof DeadScopeError) {
289
+ // 1 = the gate's violation code; 2 is reserved for the pinned
290
+ // argparse usage contract, so it cannot carry "could not run".
291
+ process.stderr.write(`lint_bench_ab: ${exc.message}\n`);
292
+ return 1;
293
+ }
294
+ throw exc;
295
+ }
296
+
272
297
  try {
273
298
  lint_track_a();
274
299
  lint_track_b();
@@ -10,7 +10,8 @@
10
10
  * Exit codes:
11
11
  * 0 contract holds across every corpus
12
12
  * 1 one or more violations
13
- * 2 invocation error (no corpora found; corpus dir missing)
13
+ * 2 invocation error (no corpora found — now reported through _lib/scan_scope,
14
+ * which names the empty roots; corpus dir missing)
14
15
  *
15
16
  * Flags:
16
17
  * --quiet suppress per-file OK lines
@@ -21,6 +22,7 @@ import path from "node:path";
21
22
  import { fileURLToPath, pathToFileURL } from "node:url";
22
23
  import { parse as parseYaml, YAMLParseError } from "yaml";
23
24
  import { artefact_roots } from "./_lib/agent_src.js";
25
+ import { assertScanned, DeadScopeError } from "./_lib/scan_scope.js";
24
26
 
25
27
  const QUIET = process.argv.includes("--quiet");
26
28
  const REQUIRE_FULL = process.argv.includes("--require-full");
@@ -348,9 +350,23 @@ export function main(): number {
348
350
  if (_isDir(ROUTER_COVERAGE_DIR)) {
349
351
  corpora.push(...globSorted(ROUTER_COVERAGE_DIR, "", ".yaml"));
350
352
  }
351
- if (corpora.length === 0) {
352
- process.stderr.write("error: no corpora found\n");
353
- return 2;
353
+ try {
354
+ assertScanned({
355
+ gate: "lint_bench_corpus",
356
+ scanned: corpora.length,
357
+ units: "benchmark corpus file(s)",
358
+ roots: ["tests/eval", "internal/bench/corpora/router-coverage"],
359
+ });
360
+ } catch (exc) {
361
+ if (exc instanceof DeadScopeError) {
362
+ // Exit 2 stays — the documented code for "no corpora found". The Taskfile
363
+ // fails on any non-zero, so this was never a silent green; what the
364
+ // assertion adds is a message that names the roots that came back empty
365
+ // instead of a bare "error: no corpora found".
366
+ process.stderr.write(`❌ ${exc.message}\n`);
367
+ return 2;
368
+ }
369
+ throw exc;
354
370
  }
355
371
 
356
372
  const skills = live_skills();
@@ -30,6 +30,8 @@ import * as fs from 'node:fs';
30
30
  import * as path from 'node:path';
31
31
  import { fileURLToPath, pathToFileURL } from 'node:url';
32
32
 
33
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
34
+
33
35
  const _HERE = fileURLToPath(import.meta.url);
34
36
  // src/scripts/lint_breaking_changes_index.ts → two levels up is the repo root.
35
37
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -237,6 +239,28 @@ function main(argv?: readonly string[]): number {
237
239
  ? _splitlines(fs.readFileSync(changelogPath, 'utf-8'))
238
240
  : [];
239
241
 
242
+ // The diff is between two blobs, but the corpus is the working-tree
243
+ // CHANGELOG: an absent one yields no lines, hence no added majors, hence the
244
+ // green "no new released BREAKING entries vs trunk" — a clean bill of health
245
+ // for a file that was never read. The base blob is legitimately empty for a
246
+ // newly added CHANGELOG, so only this side is asserted.
247
+ try {
248
+ assertScanned({
249
+ gate: 'lint_breaking_changes_index',
250
+ scanned: newLines.length,
251
+ units: `${CHANGELOG_REL} line(s)`,
252
+ roots: [CHANGELOG_REL],
253
+ });
254
+ } catch (exc) {
255
+ if (exc instanceof DeadScopeError) {
256
+ // 1 = the gate's violation code; 2 is reserved for the pinned
257
+ // argparse usage contract, so it cannot carry "could not run".
258
+ process.stderr.write(`❌ ${exc.message}\n`);
259
+ return 1;
260
+ }
261
+ throw exc;
262
+ }
263
+
240
264
  // Was BREAKING_CHANGES.md touched (staged or unstaged) vs the base?
241
265
  const indexChanged = _git('diff', '--name-only', base, '--', INDEX_REL).trim() !== '';
242
266
 
@@ -21,6 +21,8 @@ import * as fs from 'node:fs';
21
21
  import * as path from 'node:path';
22
22
  import { fileURLToPath } from 'node:url';
23
23
 
24
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
25
+
24
26
  const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
25
27
  const CONFIG_DIR = path.join(REPO_ROOT, 'src', 'config');
26
28
 
@@ -52,9 +54,24 @@ export function budgetFiles(dir: string = CONFIG_DIR): string[] {
52
54
 
53
55
  export function main(): number {
54
56
  const files = budgetFiles();
55
- if (files.length === 0) {
56
- process.stdout.write('ℹ️ no budget configs under src/config/ — nothing to lint\n');
57
- return 0;
57
+ // `*budget*.json` IS this gate's corpus definition, not a content-derived
58
+ // subset: zero matches means src/config/ moved or the budgets were deleted,
59
+ // and the old "nothing to lint" exit 0 could not tell either from a healthy
60
+ // tree. Of the two documented failure codes, 1 is the one the gate actually
61
+ // returns when it will not vouch for the tree; 2 is reserved for a throw.
62
+ try {
63
+ assertScanned({
64
+ gate: 'lint_budget_ownership',
65
+ scanned: files.length,
66
+ units: 'budget config(s)',
67
+ roots: ['src/config'],
68
+ });
69
+ } catch (exc) {
70
+ if (exc instanceof DeadScopeError) {
71
+ process.stderr.write(`❌ ${exc.message}\n`);
72
+ return 1;
73
+ }
74
+ throw exc;
58
75
  }
59
76
  const errors: string[] = [];
60
77
  const today = new Date().toISOString().slice(0, 10);
@@ -33,6 +33,7 @@ import {
33
33
  REPO_ROOT,
34
34
  VERSION_HEADING_RE,
35
35
  } from './_lib/changelog_eras.js';
36
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
36
37
 
37
38
  const _HERE = fileURLToPath(import.meta.url);
38
39
 
@@ -243,6 +244,29 @@ function main(argv?: readonly string[]): number {
243
244
  const text = fs.readFileSync(changelog_path, 'utf-8');
244
245
  const lines = text.split(/\r\n|\r|\n/);
245
246
 
247
+ // Scope is the version sections the heading regex actually recognises, not
248
+ // the line count: a CHANGELOG whose heading shape drifts away from
249
+ // VERSION_HEADING_RE still reads as a big file while presenting zero
250
+ // sections to check, and the gate reports clean. Counted before the cutoff
251
+ // and patch filters — those are judgement, this is reach.
252
+ const sections = lines.filter((ln) => VERSION_HEADING_RE.test(ln)).length;
253
+ try {
254
+ assertScanned({
255
+ gate: 'lint_changelog_rollback',
256
+ scanned: sections,
257
+ units: 'version section(s)',
258
+ roots: [path.relative(REPO_ROOT, changelog_path)],
259
+ });
260
+ } catch (exc) {
261
+ if (exc instanceof DeadScopeError) {
262
+ // 1 = the gate's violation code; 2 is reserved for the pinned
263
+ // argparse usage contract, so it cannot carry "could not run".
264
+ process.stderr.write(`❌ ${exc.message}\n`);
265
+ return 1;
266
+ }
267
+ throw exc;
268
+ }
269
+
246
270
  const violations = check_rollback_lines(lines, cutoff);
247
271
  if (violations.length > 0) {
248
272
  process.stderr.write(
@@ -22,6 +22,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
22
22
  import { parse as parseYaml } from 'yaml';
23
23
 
24
24
  import { _iter_domains_commands } from './_lib/agent_src.js';
25
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
25
26
 
26
27
  const _HERE = fileURLToPath(import.meta.url);
27
28
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -119,6 +120,27 @@ export function main(): number {
119
120
  return 3;
120
121
  }
121
122
 
123
+ // `_iter_domains_commands` returns nothing when the command tree is not a
124
+ // directory, so a moved root turns every mapped ref into a "phantom" — the
125
+ // gate would blame surface-map.yaml for a walk that never happened. Counted
126
+ // on the walk itself, before any comparison with the map.
127
+ try {
128
+ assertScanned({
129
+ gate: 'lint_command_flow_coverage',
130
+ scanned: real.size,
131
+ units: 'command file(s)',
132
+ roots: ['src/domains/<pack>/**/command.md'],
133
+ });
134
+ } catch (exc) {
135
+ if (exc instanceof DeadScopeError) {
136
+ // 3 = internal error, i.e. "this gate could not run" — 1 would
137
+ // claim the surface map is wrong, which is exactly the wrong story.
138
+ process.stderr.write(`lint_command_flow_coverage: ${exc.message}\n`);
139
+ return 3;
140
+ }
141
+ throw exc;
142
+ }
143
+
122
144
  const user_flows = new Set<string>(
123
145
  (Array.isArray(data['user_work_flows']) ? (data['user_work_flows'] as unknown[]) : [])
124
146
  .map((x) => String(x)),
@@ -20,6 +20,7 @@ import fs from "node:fs";
20
20
  import path from "node:path";
21
21
  import { fileURLToPath, pathToFileURL } from "node:url";
22
22
  import { artefact_roots } from "./_lib/agent_src.js";
23
+ import { assertScanned, DeadScopeError } from "./_lib/scan_scope.js";
23
24
 
24
25
  const QUIET = process.argv.includes("--quiet");
25
26
 
@@ -315,11 +316,34 @@ function rglobNamed(root: string, name: string): string[] {
315
316
 
316
317
  export function main(): number {
317
318
  const commandsDirs = COMMANDS_DIRS();
318
- if (commandsDirs.length === 0 && !_isDir(DOMAINS_DIR)) {
319
- process.stderr.write(
320
- "lint_command_tiers: no commands dir found under any artefact root\n",
321
- );
322
- return 1;
319
+ // Each pass below guards its own emptiness, but nothing guarded the case
320
+ // where every pass is skipped: COMMANDS_DIRS() drops a root that holds no
321
+ // .md, and both DOMAINS_DIR and the condensed projection are conditional, so
322
+ // rc could stay 0 having read no command at all. The sum replaces the older
323
+ // "no commands dir found" check, which only saw the directory, not the files.
324
+ const commandsIn = (d: string): number =>
325
+ rglobMd(d).filter((p) => path.basename(p) !== "AGENTS.md").length;
326
+ const scanned =
327
+ (_isDir(DOMAINS_DIR) ? rglobNamed(DOMAINS_DIR, "command.md").length : 0) +
328
+ commandsDirs.reduce((n, d) => n + commandsIn(d), 0) +
329
+ (_isDir(COMMANDS_DIR_CONDENSED) ? commandsIn(COMMANDS_DIR_CONDENSED) : 0);
330
+ try {
331
+ assertScanned({
332
+ gate: "lint_command_tiers",
333
+ scanned,
334
+ units: "command file(s)",
335
+ roots: [
336
+ relPosix(DOMAINS_DIR, REPO),
337
+ ...commandsDirs.map((d) => relPosix(d, REPO)),
338
+ relPosix(COMMANDS_DIR_CONDENSED, REPO),
339
+ ],
340
+ });
341
+ } catch (exc) {
342
+ if (exc instanceof DeadScopeError) {
343
+ process.stderr.write(`❌ ${exc.message}\n`);
344
+ return 1;
345
+ }
346
+ throw exc;
323
347
  }
324
348
  let rc = 0;
325
349
  if (_isDir(DOMAINS_DIR)) {
@@ -28,6 +28,8 @@ import * as path from 'node:path';
28
28
  import * as fs from 'node:fs';
29
29
  import { fileURLToPath, pathToFileURL } from 'node:url';
30
30
 
31
+ import { assertScanned } from './_lib/scan_scope.js';
32
+
31
33
  const _HERE = fileURLToPath(import.meta.url);
32
34
 
33
35
  const BLOCKLIST: ReadonlySet<string> = new Set([
@@ -182,6 +184,19 @@ function main(argv?: readonly string[]): number {
182
184
  const args = parse_args(argv ?? process.argv.slice(2));
183
185
 
184
186
  const subjects = fetch_subjects(args.base, args.head);
187
+ assertScanned({
188
+ gate: 'lint_commit_subjects',
189
+ scanned: subjects.length,
190
+ units: 'commit subject(s)',
191
+ roots: [`git log ${args.base}..${args.head}`],
192
+ allowEmpty:
193
+ 'EMPTY_VALID: a branch that is level with its base contributes no subjects to the ' +
194
+ 'release changelog, so zero subjects is the success state. Deletion test: no ' +
195
+ 'deleted root can produce it — the range is computed from refs, not a tree. An ' +
196
+ 'UNREADABLE range is the distinct case and is announced, not swallowed: ' +
197
+ '`fetch_subjects` writes `⚠️ git log <base>..<head> failed: …` to stderr first, ' +
198
+ 'and staying advisory there is the pinned contract (never block on git plumbing).',
199
+ });
185
200
  if (subjects.length === 0) {
186
201
  if (!args.quiet) {
187
202
  process.stdout.write(
@@ -35,6 +35,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
35
35
 
36
36
  import * as sl from './_lib/security_lint.js';
37
37
  import { TOKEN_RE, classifyToken } from './_lib/confusables.js';
38
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
38
39
 
39
40
  export const CHECK = 'mixed-script-confusable';
40
41
 
@@ -113,12 +114,35 @@ export function main(argv: readonly string[] | null = null): number {
113
114
  const args = parse_args(argv ?? process.argv.slice(2));
114
115
 
115
116
  const findings: sl.Finding[] = [];
117
+ let scanned = 0;
116
118
  for (const sf of sl.iter_corpus()) {
119
+ // Counted on the corpus walk, not on `_scan`'s return: `iter_corpus`
120
+ // skips a root that does not exist, so every root moving at once yields
121
+ // zero files and the report line still says "clean (scanned <roots>)".
122
+ scanned += 1;
117
123
  for (const h of _scan(sf)) {
118
124
  findings.push(h);
119
125
  }
120
126
  }
121
127
 
128
+ try {
129
+ assertScanned({
130
+ gate: 'lint_confusables',
131
+ scanned,
132
+ units: 'file(s)',
133
+ roots: sl.DEFAULT_SCAN_ROOTS,
134
+ });
135
+ } catch (exc) {
136
+ if (exc instanceof DeadScopeError) {
137
+ // 2 = "could not run" (the usage-error code), never 0 and never the
138
+ // finding code 1: a homoglyph lint that read nothing is blind, and
139
+ // `--json` callers must not read an empty payload as clean either.
140
+ process.stderr.write(`❌ ${exc.message}\n`);
141
+ return 2;
142
+ }
143
+ throw exc;
144
+ }
145
+
122
146
  if (args.json) {
123
147
  const payload = sl.py_json_dumps_indent2(findings.map((f) => f.toDict()));
124
148
  process.stdout.write(payload + '\n');
@@ -52,6 +52,8 @@ import { fileURLToPath } from 'node:url';
52
52
 
53
53
  import { parse as parseYaml } from 'yaml';
54
54
 
55
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
56
+
55
57
  const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
56
58
  const SKILLS_DIR = path.join(REPO_ROOT, 'dist', 'agent-src', 'skills');
57
59
 
@@ -174,9 +176,11 @@ export function main(): number {
174
176
  }
175
177
 
176
178
  const findings: string[] = [];
179
+ let projected = 0;
177
180
  for (const name of skillDirs) {
178
181
  const abs = path.join(SKILLS_DIR, name, 'SKILL.md');
179
182
  if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) continue;
183
+ projected += 1;
180
184
  const ws = skillWorkspaces(abs);
181
185
  if (isExclusivelyMaintainer(ws)) continue;
182
186
  let text: string;
@@ -189,6 +193,26 @@ export function main(): number {
189
193
  findings.push(...scanText(name, relPath, text));
190
194
  }
191
195
 
196
+ // Counted BEFORE the maintainer-exclusive skip, so an all-maintainer
197
+ // projection is not mistaken for a dead root. The dir-exists guard above
198
+ // only catches a deleted tree; a regenerated-but-empty projection reaches
199
+ // here and would print the ✅ line over nothing. Exit 2 follows that same
200
+ // guard: the projection is unusable, which is not a "finding" (1).
201
+ try {
202
+ assertScanned({
203
+ gate: 'lint_consumer_internal_refs',
204
+ scanned: projected,
205
+ units: 'projected SKILL.md file(s)',
206
+ roots: ['dist/agent-src/skills'],
207
+ });
208
+ } catch (exc) {
209
+ if (exc instanceof DeadScopeError) {
210
+ process.stderr.write(`❌ ${exc.message}\n`);
211
+ return 2;
212
+ }
213
+ throw exc;
214
+ }
215
+
192
216
  if (findings.length > 0) {
193
217
  for (const f of findings) {
194
218
  process.stderr.write(`❌ consumer-internal reference: ${f}\n`);
@@ -44,6 +44,7 @@
44
44
  */
45
45
  import fs from "node:fs";
46
46
  import path from "node:path";
47
+ import { assertScanned, DeadScopeError } from "./_lib/scan_scope.js";
47
48
 
48
49
  interface Finding {
49
50
  rule: string;
@@ -277,6 +278,27 @@ function main(): void {
277
278
  }
278
279
  }
279
280
 
281
+ // The opt-in escape above is the "consumer has no UI tree" path. Reaching
282
+ // here means the root EXISTS and still yielded nothing scannable — a moved
283
+ // subtree, a drifted extension list, or an over-broad ignoreFiles glob. That
284
+ // prints "clean — 0 file(s) checked" today. Exit 1 is this linter's
285
+ // internal-error code; 2 is reserved for real findings.
286
+ try {
287
+ assertScanned({
288
+ gate: "lint_design_quality",
289
+ scanned: htmlCssFiles.length,
290
+ units: "HTML/CSS file(s)",
291
+ // Absolute: --dir may point outside the cwd (or outside any repo).
292
+ roots: [scanDir],
293
+ });
294
+ } catch (exc) {
295
+ if (exc instanceof DeadScopeError) {
296
+ process.stderr.write(`❌ ${exc.message}\n`);
297
+ process.exit(1);
298
+ }
299
+ throw exc;
300
+ }
301
+
280
302
  const allFindings: Finding[] = [];
281
303
  for (const filePath of htmlCssFiles) {
282
304
  const relPath = path.relative(process.cwd(), filePath);
@@ -37,6 +37,7 @@
37
37
  */
38
38
  import fs from "node:fs";
39
39
  import path from "node:path";
40
+ import { assertScanned, DeadScopeError } from "./_lib/scan_scope.js";
40
41
  import {
41
42
  SLOP_RULES,
42
43
  type DesignContext,
@@ -197,6 +198,28 @@ function main(): void {
197
198
  if (!ignoreGlobs.some((g) => f.includes(g.replace("**", "")))) files.push(f);
198
199
  }
199
200
 
201
+ // The opt-in escape above is the "consumer has no UI tree" path. Reaching
202
+ // here means the root EXISTS and still yielded nothing scannable — a moved
203
+ // subtree, a drifted extension list, or an over-broad ignoreFiles glob. This
204
+ // detector never hard-blocks on findings, so "no tells in 0 file(s)" is
205
+ // indistinguishable from a clean scan. Exit 1 is its internal-error code; 2
206
+ // is reserved for the --fail-on threshold.
207
+ try {
208
+ assertScanned({
209
+ gate: "lint_design_slop",
210
+ scanned: files.length,
211
+ units: "design source file(s)",
212
+ // Absolute: --dir may point outside the cwd (or outside any repo).
213
+ roots: [scanDir],
214
+ });
215
+ } catch (exc) {
216
+ if (exc instanceof DeadScopeError) {
217
+ process.stderr.write(`❌ ${exc.message}\n`);
218
+ process.exit(1);
219
+ }
220
+ throw exc;
221
+ }
222
+
200
223
  const findings: Finding[] = [];
201
224
  for (const fp of files) {
202
225
  const rel = path.relative(process.cwd(), fp);
@@ -31,6 +31,8 @@ import * as path from 'node:path';
31
31
  import { fileURLToPath, pathToFileURL } from 'node:url';
32
32
  import YAML from 'yaml';
33
33
 
34
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
35
+
34
36
  const _HERE = fileURLToPath(import.meta.url);
35
37
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
36
38
  const SCHEMA_PATH = path.join(ROOT, 'docs', 'contracts', 'discovery-manifest.schema.json');
@@ -247,6 +249,27 @@ function main(argv?: readonly string[]): number {
247
249
  return 1;
248
250
  }
249
251
 
252
+ // A manifest built over a dead source root is still well-formed and still
253
+ // checksums correctly — it just indexes nothing, and this gate signs it off
254
+ // with "OK …: 0 artefacts, 0 unassigned, checksum verified". Asserted after
255
+ // the structural checks so their own messages stay the first explanation.
256
+ try {
257
+ assertScanned({
258
+ gate: 'lint_discovery_manifest',
259
+ scanned: _asArray(manifest['artefacts']).length,
260
+ units: 'manifest artefact(s)',
261
+ roots: [_relPosix(args.manifest, ROOT)],
262
+ });
263
+ } catch (exc) {
264
+ if (exc instanceof DeadScopeError) {
265
+ // 1 = schema/integrity failure, the gate's only failure code; 2 is
266
+ // reserved for the pinned argparse usage contract.
267
+ process.stderr.write(`error: ${exc.message}\n`);
268
+ return 1;
269
+ }
270
+ throw exc;
271
+ }
272
+
250
273
  const vocab_errs = _check_vocab(manifest);
251
274
  const det_errs = _check_capability_pack_determinism(manifest);
252
275
  const all_errs = [...vocab_errs, ...det_errs];