@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
@@ -30,6 +30,8 @@ import * as fs from 'node:fs';
30
30
  import * as path from 'node:path';
31
31
  import { fileURLToPath } from 'node:url';
32
32
 
33
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
34
+
33
35
  const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
34
36
 
35
37
  const PATTERNS: readonly { re: RegExp; label: string }[] = [
@@ -72,6 +74,7 @@ export function scanText(relPath: string, text: string): string[] {
72
74
 
73
75
  export function main(): number {
74
76
  const findings: string[] = [];
77
+ let scanned = 0;
75
78
  for (const rel of shippedTrackedFiles()) {
76
79
  if (CARVE_OUTS.has(rel)) continue;
77
80
  if (SKIP_EXT.has(path.extname(rel))) continue;
@@ -83,8 +86,29 @@ export function main(): number {
83
86
  } catch {
84
87
  continue;
85
88
  }
89
+ scanned += 1;
86
90
  findings.push(...scanText(rel, text));
87
91
  }
92
+ // The scope is an intersection — `git ls-files` ∩ the package.json `files`
93
+ // whitelist — and either side collapsing to nothing (no git, a rewritten
94
+ // whitelist) leaves the gate printing "no pre-migration install hints"
95
+ // over an empty set. Counts files whose text was READ, which is zero
96
+ // whenever the enumeration itself is empty. Exit 2 is the internal-error
97
+ // code ("could not run"); 1 means a live hint was found.
98
+ try {
99
+ assertScanned({
100
+ gate: 'lint_pre_migration_refs',
101
+ scanned,
102
+ units: 'shipped tracked file(s)',
103
+ roots: ['git ls-files ∩ package.json files[]'],
104
+ });
105
+ } catch (exc) {
106
+ if (exc instanceof DeadScopeError) {
107
+ process.stderr.write(`❌ ${exc.message}\n`);
108
+ return 2;
109
+ }
110
+ throw exc;
111
+ }
88
112
  if (findings.length > 0) {
89
113
  for (const f of findings) {
90
114
  process.stderr.write(`❌ pre-migration reference: ${f}\n`);
@@ -21,6 +21,8 @@ import * as path from 'node:path';
21
21
  import { fileURLToPath, pathToFileURL } from 'node:url';
22
22
  import { parse as parseYaml } from 'yaml';
23
23
 
24
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
25
+
24
26
  const _HERE = fileURLToPath(import.meta.url);
25
27
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
26
28
  const ALIASES_YML = path.join(REPO_ROOT, 'src/config/discovery/session-profiles.yml');
@@ -237,11 +239,13 @@ function lint(quiet = false): number {
237
239
  ['profile', PROFILES_DIR, 'profile', 'packs'],
238
240
  ['pack', PACKS_DIR, 'pack', null],
239
241
  ];
242
+ let definitions = 0;
240
243
  for (const [, directory, rootKey, seedKey] of dirs) {
241
244
  if (!_exists(directory)) {
242
245
  continue;
243
246
  }
244
247
  for (const p of _globYmlSorted(directory)) {
248
+ definitions += 1;
245
249
  const doc = _load_yaml(p);
246
250
  if (!_isPlainObject(doc)) {
247
251
  continue;
@@ -291,6 +295,26 @@ function lint(quiet = false): number {
291
295
  }
292
296
  }
293
297
 
298
+ // Clauses 1b/2a/2b run per static definition file, and both their roots are
299
+ // skipped silently when absent — so the whole per-file half of this lint can
300
+ // go quiet while clause 1a keeps printing an alias count. Counted before the
301
+ // parse filter above: an unparseable file is still a file that was walked.
302
+ try {
303
+ assertScanned({
304
+ gate: 'lint_profile_overlay_set_only',
305
+ scanned: definitions,
306
+ units: 'static definition file(s)',
307
+ roots: ['src/agent-src/profiles', 'src/agent-src/packs'],
308
+ });
309
+ } catch (e) {
310
+ if (e instanceof DeadScopeError) {
311
+ // 1 is this CLI's only failure code — pinned by the ported contract.
312
+ process.stderr.write(`❌ ${e.message}\n`);
313
+ return 1;
314
+ }
315
+ throw e;
316
+ }
317
+
294
318
  if (errors.length > 0) {
295
319
  for (const e of errors) {
296
320
  process.stderr.write(`❌ ${e}\n`);
@@ -54,6 +54,8 @@ import * as path from 'node:path';
54
54
  import { fileURLToPath, pathToFileURL } from 'node:url';
55
55
  import { parse as parseYaml } from 'yaml';
56
56
 
57
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
58
+
57
59
  const _FILE = fileURLToPath(import.meta.url);
58
60
  const _HERE = path.dirname(_FILE);
59
61
  // src/scripts/lint_provenance.ts → two levels up is the repo root.
@@ -480,6 +482,25 @@ export function main(argv: string[] = process.argv.slice(2)): number {
480
482
  const args = parseArgs(argv);
481
483
  const ledgerPath = path.join(REPO, LEDGER_REL);
482
484
  const noticesPath = path.join(REPO, NOTICES_REL);
485
+ // The ledger is the record this gate audits. Record COUNT is not the scope
486
+ // signal — an empty ledger is a legitimate "no borrows yet" state (it is
487
+ // empty today) — but an absent ledger file is not: the `existsSync ? … : ''`
488
+ // fallback below would read a moved ledger as zero borrows and re-render the
489
+ // notices to match, laundering the loss into a clean sync. Exit 2 is this
490
+ // linter's single failure code ("this run produced nothing trustworthy").
491
+ try {
492
+ assertWatchlistResolves({
493
+ gate: 'lint_provenance',
494
+ candidates: [LEDGER_REL],
495
+ repoRoot: REPO,
496
+ });
497
+ } catch (exc) {
498
+ if (exc instanceof DeadScopeError) {
499
+ process.stderr.write(`❌ ${exc.message}\n`);
500
+ return 2;
501
+ }
502
+ throw exc;
503
+ }
483
504
  const text = fs.existsSync(ledgerPath) ? fs.readFileSync(ledgerPath, 'utf-8') : '';
484
505
  const deny = resolveDenyPolicy(REPO);
485
506
  const { records, findings } = lintLedgerText(text, REPO, deny);
@@ -38,6 +38,8 @@ import * as fs from 'node:fs';
38
38
  import * as path from 'node:path';
39
39
  import { fileURLToPath, pathToFileURL } from 'node:url';
40
40
 
41
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
42
+
41
43
  const _HERE = fileURLToPath(import.meta.url);
42
44
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
43
45
  const CLAIMS_LEDGER_REL = 'docs/CLAIMS.md';
@@ -384,6 +386,16 @@ export function lintProvenanceVocabulary(repoRoot: string = REPO_ROOT): Violatio
384
386
  for (const root of SCAN_ROOTS) {
385
387
  for (const f of _collectMd(path.join(repoRoot, root))) files.add(f);
386
388
  }
389
+ // `_collectMd` returns [] for a root that is not there, so a moved docs
390
+ // tree turns the honesty floor into a no-op that still prints its ✅ line.
391
+ // Counted over every markdown file walked — the banned-phrase scan reads
392
+ // all of them, long before any approved-vocabulary filtering.
393
+ assertScanned({
394
+ gate: 'lint_provenance_vocabulary',
395
+ scanned: files.size,
396
+ units: 'prose file(s)',
397
+ roots: SCAN_ROOTS,
398
+ });
387
399
  for (const abs of [...files].sort()) {
388
400
  const rel = path.relative(repoRoot, abs);
389
401
  const text = fs.readFileSync(abs, 'utf-8');
@@ -398,7 +410,18 @@ function _print(quiet: boolean, msg: string): void {
398
410
 
399
411
  export function main(): number {
400
412
  const quiet = process.argv.slice(2).includes('--quiet');
401
- const violations = lintProvenanceVocabulary();
413
+ let violations: Violation[];
414
+ try {
415
+ violations = lintProvenanceVocabulary();
416
+ } catch (e) {
417
+ // 1 is the only failure code this gate defines, so a dead scan root and
418
+ // a real violation share it — the message distinguishes them.
419
+ if (e instanceof DeadScopeError) {
420
+ process.stderr.write(`❌ ${e.message}\n`);
421
+ return 1;
422
+ }
423
+ throw e;
424
+ }
402
425
  if (violations.length > 0) {
403
426
  for (const v of violations) {
404
427
  process.stderr.write(`❌ [${v.rule}] ${v.file}: ${v.msg}\n`);
@@ -18,6 +18,8 @@ import * as fs from 'node:fs';
18
18
  import * as path from 'node:path';
19
19
  import { fileURLToPath, pathToFileURL } from 'node:url';
20
20
 
21
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
22
+
21
23
  const _HERE = fileURLToPath(import.meta.url);
22
24
 
23
25
  const README = 'README.md';
@@ -124,9 +126,25 @@ function _pyRepr(s: string): string {
124
126
 
125
127
  function main(): number {
126
128
  const quiet = process.argv.slice(2).includes('--quiet');
127
- if (!fs.existsSync(README)) {
128
- process.stderr.write(`error: ${README} not found\n`);
129
- return 1;
129
+ // README is this gate's whole scope and resolves against cwd, so a wrong
130
+ // cwd is indistinguishable from a deleted file. Replaces the bare
131
+ // existsSync check: same exit code, but the failure now names the root it
132
+ // looked under. Exit 1 is the only failure code this CLI has (over budget /
133
+ // missing) — here it means "could not run", not "over budget".
134
+ try {
135
+ assertWatchlistResolves({
136
+ gate: 'lint_readme_jargon',
137
+ candidates: [README],
138
+ repoRoot: process.cwd(),
139
+ });
140
+ } catch (exc) {
141
+ if (exc instanceof DeadScopeError) {
142
+ // First line is the pinned py2ts wording; the second names the root.
143
+ process.stderr.write(`error: ${README} not found\n`);
144
+ process.stderr.write(`❌ ${exc.message}\n`);
145
+ return 1;
146
+ }
147
+ throw exc;
130
148
  }
131
149
 
132
150
  const allLines = _splitlines(fs.readFileSync(README, 'utf-8'));
@@ -20,6 +20,8 @@ import * as fs from 'node:fs';
20
20
  import * as path from 'node:path';
21
21
  import { fileURLToPath, pathToFileURL } from 'node:url';
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 README = 'README.md';
@@ -116,9 +118,25 @@ function collect_hits(text: string): Hit[] {
116
118
 
117
119
  function main(): number {
118
120
  const quiet = process.argv.slice(2).includes('--quiet');
119
- if (!fs.existsSync(README)) {
120
- process.stderr.write(`error: ${README} not found\n`);
121
- return 1;
121
+ // README is this gate's whole scope and resolves against cwd, so a wrong
122
+ // cwd is indistinguishable from a deleted file. Replaces the bare
123
+ // existsSync check: same exit code, but the failure now names the root it
124
+ // looked under. Exit 1 is the only failure code this CLI has (violation /
125
+ // missing) — here it means "could not run", not "violation found".
126
+ try {
127
+ assertWatchlistResolves({
128
+ gate: 'lint_readme_serial_comma',
129
+ candidates: [README],
130
+ repoRoot: process.cwd(),
131
+ });
132
+ } catch (exc) {
133
+ if (exc instanceof DeadScopeError) {
134
+ // First line is the pinned wording; the second names the root.
135
+ process.stderr.write(`error: ${README} not found\n`);
136
+ process.stderr.write(`❌ ${exc.message}\n`);
137
+ return 1;
138
+ }
139
+ throw exc;
122
140
  }
123
141
 
124
142
  const hits = collect_hits(fs.readFileSync(README, 'utf-8'));
@@ -16,6 +16,8 @@ import * as fs from 'node:fs';
16
16
  import * as path from 'node:path';
17
17
  import { fileURLToPath, pathToFileURL } from 'node:url';
18
18
 
19
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
20
+
19
21
  const _HERE = fileURLToPath(import.meta.url);
20
22
 
21
23
  const README = 'README.md';
@@ -45,9 +47,25 @@ function _splitlinesCount(text: string): number {
45
47
 
46
48
  function main(): number {
47
49
  const quiet = process.argv.slice(2).includes('--quiet');
48
- if (!fs.existsSync(README)) {
49
- process.stderr.write(`error: ${README} not found\n`);
50
- return 1;
50
+ // README is this gate's whole scope and resolves against cwd, so a wrong
51
+ // cwd is indistinguishable from a deleted file. Replaces the bare
52
+ // existsSync check: same exit code, but the failure now names the root it
53
+ // looked under. Exit 1 is the only failure code this CLI has (over budget /
54
+ // missing) — here it means "could not run", not "over budget".
55
+ try {
56
+ assertWatchlistResolves({
57
+ gate: 'lint_readme_size',
58
+ candidates: [README],
59
+ repoRoot: process.cwd(),
60
+ });
61
+ } catch (exc) {
62
+ if (exc instanceof DeadScopeError) {
63
+ // First line is the pinned py2ts wording; the second names the root.
64
+ process.stderr.write(`error: ${README} not found\n`);
65
+ process.stderr.write(`❌ ${exc.message}\n`);
66
+ return 1;
67
+ }
68
+ throw exc;
51
69
  }
52
70
  const text = fs.readFileSync(README, 'utf-8');
53
71
  const n = _splitlinesCount(text);
@@ -24,6 +24,8 @@ import * as os from 'node:os';
24
24
  import * as path from 'node:path';
25
25
  import { fileURLToPath, pathToFileURL } from 'node:url';
26
26
 
27
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
28
+
27
29
  interface FileEntry {
28
30
  status: string;
29
31
  codes: Set<string>;
@@ -519,6 +521,32 @@ function main(): number {
519
521
  // Sanity guard: two non-empty result sets sharing no file → not comparable.
520
522
  const baseKeys = Object.keys(baseline_map);
521
523
  const currKeys = Object.keys(current_map);
524
+
525
+ // The asserted unit is the WORKING TREE's linted-file population, not the
526
+ // baseline's. Baseline emptiness has two documented, legitimate causes (no
527
+ // linter file in the ref; a pre-migration `.py` baseline with no python3)
528
+ // and its own guard below, so asserting there would re-litigate the false
529
+ // reds this script already carries scars from. An empty CURRENT tree has no
530
+ // legitimate cause: it means the linter's corpus moved. Both guards below
531
+ // require `currKeys.length`, so today that case slips through all of them
532
+ // and every prior finding is reported as an improvement — exit 0.
533
+ // Exit 2 is the "runs are not comparable / could not run" code both guards
534
+ // already use; 1 stays "regressions found".
535
+ try {
536
+ assertScanned({
537
+ gate: 'lint_regression',
538
+ scanned: currKeys.length,
539
+ units: 'linted file(s) in the working tree',
540
+ roots: [`${root} (skill_linter --all)`],
541
+ });
542
+ } catch (e) {
543
+ if (e instanceof DeadScopeError) {
544
+ process.stderr.write(`Error: ${e.message}\n`);
545
+ return 2;
546
+ }
547
+ throw e;
548
+ }
549
+
522
550
  const shared = baseKeys.some((k) => k in current_map);
523
551
  if (baseKeys.length && currKeys.length && !shared) {
524
552
  process.stderr.write(
@@ -20,6 +20,8 @@ import * as fs from 'node:fs';
20
20
  import * as path from 'node:path';
21
21
  import { fileURLToPath, pathToFileURL } from 'node:url';
22
22
 
23
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
24
+
23
25
  const _HERE = fileURLToPath(import.meta.url);
24
26
  const QUIET = process.argv.slice(2).includes('--quiet');
25
27
 
@@ -154,7 +156,48 @@ function _relPosix(target: string, root: string): string {
154
156
  return path.relative(root, target).split(path.sep).join('/');
155
157
  }
156
158
 
159
+ /**
160
+ * Every `*.md` anywhere under the roadmap tree — active plus `archive/`,
161
+ * `later/`, `skipped/`.
162
+ *
163
+ * The active glob is the subset this gate judges, and it reaches zero
164
+ * legitimately once everything is archived, so it cannot double as the scope
165
+ * assertion. The whole tree reaching zero has only one cause: the root moved.
166
+ */
167
+ function _countRoadmapTree(dir: string): number {
168
+ let entries: fs.Dirent[];
169
+ try {
170
+ entries = fs.readdirSync(dir, { withFileTypes: true });
171
+ } catch {
172
+ return 0;
173
+ }
174
+ let n = 0;
175
+ for (const entry of entries) {
176
+ if (entry.isDirectory()) {
177
+ n += _countRoadmapTree(path.join(dir, entry.name));
178
+ } else if (entry.name.endsWith('.md')) {
179
+ n += 1;
180
+ }
181
+ }
182
+ return n;
183
+ }
184
+
157
185
  function main(): number {
186
+ try {
187
+ assertScanned({
188
+ gate: 'lint_roadmap_blockers',
189
+ scanned: _countRoadmapTree(path.join(REPO_ROOT, 'agents', 'roadmaps')),
190
+ units: 'roadmap file(s)',
191
+ roots: ['agents/roadmaps'],
192
+ });
193
+ } catch (e) {
194
+ // 1 is the only failure code this gate defines.
195
+ if (e instanceof DeadScopeError) {
196
+ process.stderr.write(`❌ ${e.message}\n`);
197
+ return 1;
198
+ }
199
+ throw e;
200
+ }
158
201
  const roadmaps = _globRoadmaps();
159
202
  if (roadmaps.length === 0) {
160
203
  if (!QUIET) {
@@ -21,6 +21,7 @@ import * as path from 'node:path';
21
21
  import { fileURLToPath, pathToFileURL } from 'node:url';
22
22
 
23
23
  import { project_settings_path } from './_lib/agent_settings.js';
24
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
24
25
 
25
26
  const _HERE = fileURLToPath(import.meta.url);
26
27
  const QUIET = process.argv.slice(2).includes('--quiet');
@@ -173,6 +174,32 @@ function _relPosix(target: string, root: string): string {
173
174
  return path.relative(root, target).split(path.sep).join('/');
174
175
  }
175
176
 
177
+ /**
178
+ * Every `*.md` anywhere under the roadmap tree — active plus `archive/`,
179
+ * `later/`, `skipped/`.
180
+ *
181
+ * The active glob is the subset this gate judges, and it reaches zero
182
+ * legitimately once everything is archived, so it cannot double as the scope
183
+ * assertion. The whole tree reaching zero has only one cause: the root moved.
184
+ */
185
+ function _countRoadmapTree(dir: string): number {
186
+ let entries: fs.Dirent[];
187
+ try {
188
+ entries = fs.readdirSync(dir, { withFileTypes: true });
189
+ } catch {
190
+ return 0;
191
+ }
192
+ let n = 0;
193
+ for (const entry of entries) {
194
+ if (entry.isDirectory()) {
195
+ n += _countRoadmapTree(path.join(dir, entry.name));
196
+ } else if (entry.name.endsWith('.md')) {
197
+ n += 1;
198
+ }
199
+ }
200
+ return n;
201
+ }
202
+
176
203
  function main(): number {
177
204
  if (_read_local_auto_run()) {
178
205
  if (!QUIET) {
@@ -182,6 +209,24 @@ function main(): number {
182
209
  }
183
210
  return 0;
184
211
  }
212
+ // Asserted only past the disabled-return: a declared no-op reads nothing by
213
+ // design, and failing there would red every repo that leaves the setting
214
+ // unset. Past it the gate claims to be armed, so its root must resolve.
215
+ try {
216
+ assertScanned({
217
+ gate: 'lint_roadmap_ci_steps',
218
+ scanned: _countRoadmapTree(path.join(REPO_ROOT, 'agents', 'roadmaps')),
219
+ units: 'roadmap file(s)',
220
+ roots: ['agents/roadmaps'],
221
+ });
222
+ } catch (e) {
223
+ // 1 is the only failure code this gate defines.
224
+ if (e instanceof DeadScopeError) {
225
+ process.stderr.write(`❌ ${e.message}\n`);
226
+ return 1;
227
+ }
228
+ throw e;
229
+ }
185
230
  const roadmaps = _globRoadmaps();
186
231
  if (roadmaps.length === 0) {
187
232
  if (!QUIET) {
@@ -20,6 +20,8 @@ import * as path from 'node:path';
20
20
  import { fileURLToPath, pathToFileURL } from 'node:url';
21
21
 
22
22
  import { project_settings_path } from './_lib/agent_settings.js';
23
+ import { granularity_problems } from './_lib/roadmap_granularity.js';
24
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
23
25
 
24
26
  /** Mirror `QUIET = "--quiet" in sys.argv` (computed at import). */
25
27
  const QUIET = process.argv.slice(2).includes('--quiet');
@@ -184,6 +186,10 @@ function lint_roadmap(
184
186
  if (complexity === 'lightweight') {
185
187
  _check_lightweight(text, line_count, problems);
186
188
  }
189
+ // Bite-sized granularity — self-gated to `complexity: structural`. Folded in
190
+ // here rather than shipped as its own gate; `_lib/roadmap_granularity.ts`
191
+ // carries the reason.
192
+ problems.push(...granularity_problems(text));
187
193
  if (horizon_weeks <= 0) {
188
194
  _check_no_plate(text, problems);
189
195
  }
@@ -383,6 +389,21 @@ function _check_execution_mode(fm: string, problems: string[]): void {
383
389
  }
384
390
 
385
391
  function main(): number {
392
+ try {
393
+ assertScanned({
394
+ gate: 'lint_roadmap_complexity',
395
+ scanned: _countRoadmapTree(path.join(REPO_ROOT, 'agents', 'roadmaps')),
396
+ units: 'roadmap file(s)',
397
+ roots: ['agents/roadmaps'],
398
+ });
399
+ } catch (e) {
400
+ // 1 is the only failure code this gate defines.
401
+ if (e instanceof DeadScopeError) {
402
+ process.stderr.write(`❌ ${e.message}\n`);
403
+ return 1;
404
+ }
405
+ throw e;
406
+ }
386
407
  const roadmaps = _globRoadmaps();
387
408
  const horizon_weeks = _read_horizon_weeks();
388
409
  if (roadmaps.length === 0) {
@@ -477,6 +498,32 @@ function _relPosix(target: string, root: string): string {
477
498
  return path.relative(root, target).split(path.sep).join('/');
478
499
  }
479
500
 
501
+ /**
502
+ * Every `*.md` anywhere under the roadmap tree — active plus `archive/`,
503
+ * `later/`, `skipped/`.
504
+ *
505
+ * The active glob is the subset this gate judges, and it reaches zero
506
+ * legitimately once everything is archived, so it cannot double as the scope
507
+ * assertion. The whole tree reaching zero has only one cause: the root moved.
508
+ */
509
+ function _countRoadmapTree(dir: string): number {
510
+ let entries: fs.Dirent[];
511
+ try {
512
+ entries = fs.readdirSync(dir, { withFileTypes: true });
513
+ } catch {
514
+ return 0;
515
+ }
516
+ let n = 0;
517
+ for (const entry of entries) {
518
+ if (entry.isDirectory()) {
519
+ n += _countRoadmapTree(path.join(dir, entry.name));
520
+ } else if (entry.name.endsWith('.md')) {
521
+ n += 1;
522
+ }
523
+ }
524
+ return n;
525
+ }
526
+
480
527
  function _isCliEntry(): boolean {
481
528
  if (process.argv[1] === undefined) {
482
529
  return false;
@@ -44,6 +44,8 @@ import * as fs from 'node:fs';
44
44
  import * as path from 'node:path';
45
45
  import { fileURLToPath, pathToFileURL } from 'node:url';
46
46
 
47
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
48
+
47
49
  const _HERE = fileURLToPath(import.meta.url);
48
50
 
49
51
  // REPO_ROOT = Path(__file__).resolve().parent.parent.parent
@@ -313,6 +315,27 @@ function main(argv?: readonly string[]): number {
313
315
  }
314
316
  const args = parsed.args as Args;
315
317
 
318
+ // `check` walks the whole tree already, but returns only violations, and it
319
+ // opens with `if (!_isDir(root)) return []` — a moved root reads as "every
320
+ // blocked-for-later roadmap is parked correctly". Assert the unfiltered
321
+ // walk here rather than inside `check`, which is exercised against empty
322
+ // sandbox roots. Exit 2 is this CLI's internal-error slot; 1 stays
323
+ // "disposition violations found".
324
+ try {
325
+ assertScanned({
326
+ gate: 'lint_roadmap_later_disposition',
327
+ scanned: _rglobMdSorted(ROADMAP_ROOT).length,
328
+ units: 'roadmap file(s)',
329
+ roots: ['agents/roadmaps'],
330
+ });
331
+ } catch (e) {
332
+ if (e instanceof DeadScopeError) {
333
+ process.stderr.write(`❌ ${e.message}\n`);
334
+ return 2;
335
+ }
336
+ throw e;
337
+ }
338
+
316
339
  const violations = check(ROADMAP_ROOT);
317
340
 
318
341
  if (args.json) {
@@ -21,6 +21,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
21
21
  import { parse as parseYaml } from 'yaml';
22
22
 
23
23
  import { resolve_logical, strip_source_prefix } from './_lib/agent_src.js';
24
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
24
25
 
25
26
  const _HERE = fileURLToPath(import.meta.url);
26
27
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -125,8 +126,21 @@ function fail(errors: string[]): never {
125
126
  }
126
127
 
127
128
  function _run(): number {
128
- if (!_exists(MATRIX)) {
129
- fail([`${_relTo(MATRIX, ROOT)} is missing`]);
129
+ // Replaces the ad-hoc `_exists(MATRIX)` precondition: this gate's entire
130
+ // corpus is one named contract file, so a moved matrix is a dead scope, not
131
+ // a content finding. Exit 1 either way — the gate has a single failure code.
132
+ try {
133
+ assertWatchlistResolves({
134
+ gate: 'lint_rule_interactions',
135
+ candidates: [_relTo(MATRIX, ROOT)],
136
+ repoRoot: ROOT,
137
+ });
138
+ } catch (e) {
139
+ if (e instanceof DeadScopeError) {
140
+ process.stderr.write(`❌ ${e.message}\n`);
141
+ throw new FailExit([e.message]);
142
+ }
143
+ throw e;
130
144
  }
131
145
 
132
146
  const data = parseYaml(fs.readFileSync(MATRIX, 'utf-8'), { version: '1.1' });
@@ -19,9 +19,12 @@ import fs from "node:fs";
19
19
  import path from "node:path";
20
20
  import { fileURLToPath, pathToFileURL } from "node:url";
21
21
  import { artefact_roots } from "./_lib/agent_src.js";
22
+ import { assertScanned, DeadScopeError } from "./_lib/scan_scope.js";
22
23
 
23
24
  const QUIET = process.argv.includes("--quiet");
24
25
 
26
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
27
+
25
28
  // Rules live under every artefact root post-monorepo Phase 4.
26
29
  function RULES_DIRS(): string[] {
27
30
  return artefact_roots()
@@ -106,12 +109,22 @@ export function main(): number {
106
109
  rules.push(...globMd(rulesDir));
107
110
  }
108
111
  rules.sort();
109
- if (rules.length === 0) {
110
- const rootsLabel = RULES_DIRS().join(", ") || "<no rules root>";
111
- process.stderr.write(
112
- `lint_rule_tiers: no rules found under ${rootsLabel}\n`,
113
- );
114
- return 1;
112
+ // Supersedes the previous bare `rules.length === 0` message: same exit code
113
+ // (1 — the only failure this CLI contract defines), but the shared assertion
114
+ // names the roots and says the scope is dead rather than merely "no rules".
115
+ try {
116
+ assertScanned({
117
+ gate: "lint_rule_tiers",
118
+ scanned: rules.length,
119
+ units: "rule file(s)",
120
+ roots: artefact_roots().map((r) => `${path.relative(REPO_ROOT, r)}/rules`),
121
+ });
122
+ } catch (e) {
123
+ if (e instanceof DeadScopeError) {
124
+ process.stderr.write(`${e.message}\n`);
125
+ return 1;
126
+ }
127
+ throw e;
115
128
  }
116
129
 
117
130
  const missing: string[] = [];