@event4u/agent-config 9.20.0 → 9.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +51 -0
  3. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  4. package/dist/cli-delegate/{chunk-XGA2NTU4.js → chunk-3EWYC3YK.js} +40 -0
  5. package/dist/cli-delegate/cmd_handoff.js +1 -1
  6. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  7. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  8. package/dist/discovery/deprecation-report.md +1 -1
  9. package/dist/discovery/discovery-manifest.json +1 -1
  10. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  11. package/dist/discovery/discovery-manifest.summary.md +1 -1
  12. package/dist/discovery/orphan-report.md +1 -1
  13. package/dist/discovery/packs.json +1 -1
  14. package/dist/discovery/trust-report.md +1 -1
  15. package/dist/discovery/workspaces.json +1 -1
  16. package/dist/hooks/dispatch.js +58 -2
  17. package/dist/mcp/registry-manifest.json +2 -2
  18. package/dist/mcp/server.json +2 -2
  19. package/dist/mcp/server.mjs +47 -29
  20. package/package.json +1 -1
  21. package/src/config/gate-coverage.yml +22 -4
  22. package/src/config/gate-violation-baselines.json +3 -8
  23. package/src/scripts/_lib/gate_population.ts +70 -0
  24. package/src/scripts/_lib/roadmap_granularity.ts +157 -0
  25. package/src/scripts/_lib/scan_scope.ts +44 -0
  26. package/src/scripts/_lib/stdin.ts +82 -0
  27. package/src/scripts/audit_adr_coverage.ts +38 -0
  28. package/src/scripts/audit_auto_rules.ts +17 -3
  29. package/src/scripts/audit_command_surface.ts +51 -0
  30. package/src/scripts/audit_initial_context.ts +39 -3
  31. package/src/scripts/audit_mcp_tools.ts +25 -0
  32. package/src/scripts/audit_overlap.ts +24 -1
  33. package/src/scripts/audit_skill_descriptions.ts +19 -0
  34. package/src/scripts/{verify_before_complete_hook.ts → before_complete_hook.ts} +16 -3
  35. package/src/scripts/check_adr_frontmatter.ts +23 -3
  36. package/src/scripts/check_always_budget.ts +24 -0
  37. package/src/scripts/check_artefact_checksums.ts +24 -1
  38. package/src/scripts/check_artefact_count_messaging.ts +20 -0
  39. package/src/scripts/check_augmentignore.ts +22 -15
  40. package/src/scripts/check_backstop_debt.ts +22 -0
  41. package/src/scripts/check_beta_review_markers.ts +22 -1
  42. package/src/scripts/check_bridge_derivation.ts +22 -1
  43. package/src/scripts/check_bundle_path_leakage.ts +26 -0
  44. package/src/scripts/check_ci_strict_superset.ts +21 -3
  45. package/src/scripts/check_claims.ts +22 -0
  46. package/src/scripts/check_cli_registry_budget_sync.ts +33 -4
  47. package/src/scripts/check_cluster_patterns.ts +26 -4
  48. package/src/scripts/check_command_count_messaging.ts +20 -4
  49. package/src/scripts/check_comparison.ts +18 -0
  50. package/src/scripts/check_condensed_paths.ts +23 -2
  51. package/src/scripts/check_consumer_scope_flip.ts +21 -2
  52. package/src/scripts/check_council_config_location.ts +20 -0
  53. package/src/scripts/check_council_layout.ts +22 -2
  54. package/src/scripts/check_council_references.ts +23 -1
  55. package/src/scripts/check_dependency_floors.ts +19 -3
  56. package/src/scripts/check_discovery_determinism.ts +26 -0
  57. package/src/scripts/check_enforcement_coverage.ts +20 -0
  58. package/src/scripts/check_evaluator_budgets.ts +23 -0
  59. package/src/scripts/check_finding_dispositions.ts +29 -0
  60. package/src/scripts/check_gate_coverage.ts +63 -22
  61. package/src/scripts/check_gate_paths.ts +30 -5
  62. package/src/scripts/check_generated_artefact_headers.ts +21 -0
  63. package/src/scripts/check_generator_output_coverage.ts +23 -0
  64. package/src/scripts/check_gitignore_freshness.ts +23 -0
  65. package/src/scripts/check_host_loadability.ts +34 -8
  66. package/src/scripts/check_installer_import_purity.ts +22 -8
  67. package/src/scripts/check_kernel_prefix_stability.ts +19 -1
  68. package/src/scripts/check_kernel_rule_bundle.ts +23 -0
  69. package/src/scripts/check_knowledge_cards.ts +13 -0
  70. package/src/scripts/check_knowledge_pages.ts +23 -0
  71. package/src/scripts/check_knowledge_sharing.ts +27 -0
  72. package/src/scripts/check_md_language.ts +24 -0
  73. package/src/scripts/check_media_deps.ts +23 -0
  74. package/src/scripts/check_memory.ts +26 -1
  75. package/src/scripts/check_memory_contradiction.ts +16 -1
  76. package/src/scripts/check_memory_proposal.ts +15 -0
  77. package/src/scripts/check_memory_similarity.ts +16 -0
  78. package/src/scripts/check_module_management_neutral.ts +19 -11
  79. package/src/scripts/check_no_conflict_markers.ts +22 -1
  80. package/src/scripts/check_no_external_sources.ts +26 -1
  81. package/src/scripts/check_no_local_settings_committed.ts +37 -10
  82. package/src/scripts/check_no_new_legacy_path.ts +30 -1
  83. package/src/scripts/check_one_off_location.ts +43 -0
  84. package/src/scripts/check_originality_freshness.ts +25 -1
  85. package/src/scripts/check_overlay_cascade_subdirs.ts +21 -0
  86. package/src/scripts/check_pack_size.ts +22 -0
  87. package/src/scripts/check_preamble_payload_budget.ts +35 -5
  88. package/src/scripts/check_proposal.ts +24 -1
  89. package/src/scripts/check_public_catalog_links.ts +27 -0
  90. package/src/scripts/check_public_links.ts +19 -0
  91. package/src/scripts/check_quality_regression.ts +19 -0
  92. package/src/scripts/check_reach_channels.ts +44 -0
  93. package/src/scripts/check_reach_staleness.ts +27 -0
  94. package/src/scripts/check_release_adjacent_health.ts +28 -0
  95. package/src/scripts/check_release_highlights.ts +34 -0
  96. package/src/scripts/check_release_includes_discovery.ts +25 -6
  97. package/src/scripts/check_release_pr_shape.ts +18 -1
  98. package/src/scripts/check_release_published.ts +21 -0
  99. package/src/scripts/check_release_surface_equality.ts +22 -0
  100. package/src/scripts/check_release_trunk_sync.ts +19 -0
  101. package/src/scripts/check_reply_consistency.ts +22 -0
  102. package/src/scripts/check_roadmap_trackable.ts +20 -0
  103. package/src/scripts/check_role_doc_links.ts +23 -0
  104. package/src/scripts/check_rule_invariants.ts +14 -0
  105. package/src/scripts/check_secret_leak.ts +47 -0
  106. package/src/scripts/check_skill_gaps.ts +32 -0
  107. package/src/scripts/check_skill_requires.ts +19 -0
  108. package/src/scripts/check_source_pointer_freshness.ts +20 -0
  109. package/src/scripts/check_structural_breaking.ts +29 -2
  110. package/src/scripts/check_surface_tiers.ts +34 -0
  111. package/src/scripts/check_template_pin_drift.ts +23 -1
  112. package/src/scripts/check_test_coverage_diff.ts +17 -1
  113. package/src/scripts/check_token_optimizer_freshness.ts +18 -3
  114. package/src/scripts/check_token_quality_golden.ts +21 -0
  115. package/src/scripts/check_token_regression.ts +25 -0
  116. package/src/scripts/check_tracked_but_ignored.ts +26 -0
  117. package/src/scripts/check_trigger_eval_presence.ts +23 -1
  118. package/src/scripts/check_trigger_evals.ts +36 -0
  119. package/src/scripts/check_trunk_drift.ts +23 -0
  120. package/src/scripts/check_update_banner.ts +24 -0
  121. package/src/scripts/gate_scope_classify.ts +513 -0
  122. package/src/scripts/hook_manifest.yaml +1 -1
  123. package/src/scripts/hooks/block_kernel_rule_writes.ts +1 -1
  124. package/src/scripts/hooks/concern_registry.ts +2 -2
  125. package/src/scripts/lint_agent_security.ts +19 -0
  126. package/src/scripts/lint_agent_skill_names.ts +30 -0
  127. package/src/scripts/lint_agents_layout.ts +20 -0
  128. package/src/scripts/lint_agents_md.ts +20 -0
  129. package/src/scripts/lint_archived_skills.ts +24 -3
  130. package/src/scripts/lint_behavioural_eval_freshness.ts +29 -3
  131. package/src/scripts/lint_bench_ab.ts +25 -0
  132. package/src/scripts/lint_bench_corpus.ts +20 -4
  133. package/src/scripts/lint_breaking_changes_index.ts +24 -0
  134. package/src/scripts/lint_budget_ownership.ts +20 -3
  135. package/src/scripts/lint_changelog_rollback.ts +24 -0
  136. package/src/scripts/lint_command_flow_coverage.ts +22 -0
  137. package/src/scripts/lint_command_tiers.ts +29 -5
  138. package/src/scripts/lint_commit_subjects.ts +15 -0
  139. package/src/scripts/lint_confusables.ts +24 -0
  140. package/src/scripts/lint_consumer_internal_refs.ts +24 -0
  141. package/src/scripts/lint_design_quality.ts +22 -0
  142. package/src/scripts/lint_design_slop.ts +23 -0
  143. package/src/scripts/lint_discovery_manifest.ts +23 -0
  144. package/src/scripts/lint_discovery_vocabulary.ts +19 -0
  145. package/src/scripts/lint_documented_commands.ts +23 -0
  146. package/src/scripts/lint_empty_roadmaps.ts +23 -0
  147. package/src/scripts/lint_eval_fixture_citations.ts +19 -0
  148. package/src/scripts/lint_eval_freshness.ts +35 -0
  149. package/src/scripts/lint_examples.ts +18 -3
  150. package/src/scripts/lint_explain_trace.ts +23 -0
  151. package/src/scripts/lint_featured_skills.ts +26 -1
  152. package/src/scripts/lint_flows.ts +19 -0
  153. package/src/scripts/lint_frontmatter_boilerplate.ts +20 -0
  154. package/src/scripts/lint_ghostwriter_source.ts +26 -6
  155. package/src/scripts/lint_glama_drift.ts +24 -0
  156. package/src/scripts/lint_global_paths.ts +24 -0
  157. package/src/scripts/lint_governed_writes.ts +20 -0
  158. package/src/scripts/lint_hidden_unicode.ts +25 -0
  159. package/src/scripts/lint_hook_concern_budget.ts +39 -6
  160. package/src/scripts/lint_hook_manifest.ts +22 -0
  161. package/src/scripts/lint_instruction_smuggling.ts +23 -0
  162. package/src/scripts/lint_knowledge_scale.ts +32 -2
  163. package/src/scripts/lint_legal_pack.ts +31 -0
  164. package/src/scripts/lint_marketplace.ts +26 -0
  165. package/src/scripts/lint_marketplace_install_completeness.ts +23 -0
  166. package/src/scripts/lint_mcp_config_security.ts +24 -0
  167. package/src/scripts/lint_mcp_registry_manifest.ts +22 -1
  168. package/src/scripts/lint_memory_tripwire.ts +35 -0
  169. package/src/scripts/lint_missions.ts +23 -7
  170. package/src/scripts/lint_model_tier_coverage.ts +18 -0
  171. package/src/scripts/lint_namespace_collisions.ts +19 -0
  172. package/src/scripts/lint_no_new_atomic_commands.ts +33 -0
  173. package/src/scripts/lint_no_python_twin_rationale.ts +18 -0
  174. package/src/scripts/lint_one_off_age.ts +38 -2
  175. package/src/scripts/lint_orchestration_dsl.ts +23 -3
  176. package/src/scripts/lint_orchestrator_auto_detect.ts +19 -12
  177. package/src/scripts/lint_originality.ts +22 -4
  178. package/src/scripts/lint_originality_shingles.ts +28 -3
  179. package/src/scripts/lint_output_slop.ts +19 -0
  180. package/src/scripts/lint_override_kernel_guard.ts +30 -0
  181. package/src/scripts/lint_pack_first_win.ts +23 -0
  182. package/src/scripts/lint_pack_risk_class.ts +19 -3
  183. package/src/scripts/lint_persistence.ts +19 -0
  184. package/src/scripts/lint_persona_governance.ts +18 -3
  185. package/src/scripts/lint_positioning.ts +23 -0
  186. package/src/scripts/lint_pre_migration_refs.ts +24 -0
  187. package/src/scripts/lint_profile_overlay_set_only.ts +24 -0
  188. package/src/scripts/lint_provenance.ts +21 -0
  189. package/src/scripts/lint_provenance_vocabulary.ts +24 -1
  190. package/src/scripts/lint_readme_jargon.ts +21 -3
  191. package/src/scripts/lint_readme_serial_comma.ts +21 -3
  192. package/src/scripts/lint_readme_size.ts +21 -3
  193. package/src/scripts/lint_regression.ts +28 -0
  194. package/src/scripts/lint_roadmap_blockers.ts +43 -0
  195. package/src/scripts/lint_roadmap_ci_steps.ts +45 -0
  196. package/src/scripts/lint_roadmap_complexity.ts +47 -0
  197. package/src/scripts/lint_roadmap_later_disposition.ts +23 -0
  198. package/src/scripts/lint_rule_interactions.ts +16 -2
  199. package/src/scripts/lint_rule_tiers.ts +19 -6
  200. package/src/scripts/lint_showcase_sessions.ts +19 -3
  201. package/src/scripts/lint_skill_descriptions.ts +19 -0
  202. package/src/scripts/lint_skill_frontmatter_safety.ts +24 -0
  203. package/src/scripts/lint_skill_originality.ts +18 -3
  204. package/src/scripts/lint_skill_scripts_readonly.ts +20 -0
  205. package/src/scripts/lint_skill_tools.ts +84 -145
  206. package/src/scripts/lint_spawn_payload.ts +27 -1
  207. package/src/scripts/lint_store_boundary.ts +19 -0
  208. package/src/scripts/lint_subagent_determinism.ts +23 -1
  209. package/src/scripts/lint_supported_tools_matrix.ts +18 -0
  210. package/src/scripts/lint_surface_matrix.ts +19 -3
  211. package/src/scripts/lint_ticket_buildable.ts +23 -0
  212. package/src/scripts/lint_topics_yaml.ts +19 -12
  213. package/src/scripts/lint_trigger_precision.ts +27 -0
  214. package/src/scripts/lint_trust_coherence.ts +21 -0
  215. package/src/scripts/lint_ui_stack_bundles.ts +17 -3
  216. package/src/scripts/lint_value_dashboard.ts +19 -3
  217. package/src/scripts/lint_versioned_cache.ts +19 -1
  218. package/src/scripts/lint_workflow_security.ts +22 -14
  219. package/src/scripts/pr_url_reminder_hook.ts +1 -1
  220. package/src/scripts/skill_collision_clusters.ts +37 -3
  221. package/src/scripts/skill_discovery.ts +20 -0
  222. package/src/scripts/skill_eval_coverage.ts +22 -0
  223. package/src/scripts/skill_overlap.ts +32 -6
  224. package/src/scripts/skill_preview.ts +31 -0
  225. package/src/scripts/skill_trigger_eval.ts +18 -0
  226. package/src/scripts/skill_usage_collect.ts +21 -0
  227. package/src/scripts/skill_usage_report.ts +21 -0
  228. package/src/scripts/sweep_dead_scan_roots.ts +11 -3
  229. package/src/scripts/verify_physical_move.ts +24 -0
  230. package/src/scripts/check_bite_sized_granularity.ts +0 -116
  231. package/src/scripts/lint_workspace_boundary.ts +0 -267
@@ -20,7 +20,9 @@
20
20
  * synchronous. No behaviour changes.
21
21
  *
22
22
  * Exit codes: 0 = all enforced targets resolve under the source tree ·
23
- * 1 = at least one missing / out-of-tree target · 2 = a gate failed to import.
23
+ * 1 = at least one missing / out-of-tree target · 2 = a gate failed to import
24
+ * (which now includes a dead scan scope — zero enforced targets across the
25
+ * whole GATES list — reported through `_lib/scan_scope`).
24
26
  */
25
27
 
26
28
  import { spawnSync } from 'node:child_process';
@@ -28,6 +30,8 @@ import * as fs from 'node:fs';
28
30
  import * as path from 'node:path';
29
31
  import { fileURLToPath, pathToFileURL } from 'node:url';
30
32
 
33
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
34
+
31
35
  const _HERE = fileURLToPath(import.meta.url);
32
36
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
33
37
 
@@ -190,6 +194,31 @@ function main(): number {
190
194
  process.stderr.write(`❌ check-gate-paths: ${msg}\n`);
191
195
  return 2;
192
196
  }
197
+ let total = 0;
198
+ for (const v of named.values()) {
199
+ total += v.length;
200
+ }
201
+ // The enforced targets ARE the corpus. A per-gate empty GATE_CORE_PATHS is
202
+ // already exit 2 inside the subprocess, but an emptied GATES list is not:
203
+ // it yields zero targets, zero failures, and a green "0 enforced target(s)
204
+ // … resolve" — the path-integrity gate certifying that nothing is enforced.
205
+ try {
206
+ assertScanned({
207
+ gate: 'check_gate_paths',
208
+ scanned: total,
209
+ units: 'enforced target(s)',
210
+ roots: ['src/scripts (GATE_CORE_PATHS of the GATES list)'],
211
+ });
212
+ } catch (exc) {
213
+ if (exc instanceof DeadScopeError) {
214
+ // Exit 2, not 1: 1 means "a declared target failed to resolve" (a
215
+ // finding), while 2 is this file's existing "a gate cannot be
216
+ // checked" code — which is exactly what an empty target set is.
217
+ process.stderr.write(`❌ ${exc.message}\n`);
218
+ return 2;
219
+ }
220
+ throw exc;
221
+ }
193
222
  const failures = check_paths(named);
194
223
  if (failures.length > 0) {
195
224
  process.stdout.write(
@@ -202,10 +231,6 @@ function main(): number {
202
231
  process.stdout.write(' GATE_CORE_PATHS or the move.\n');
203
232
  return 1;
204
233
  }
205
- let total = 0;
206
- for (const v of named.values()) {
207
- total += v.length;
208
- }
209
234
  process.stdout.write(
210
235
  `✅ check-gate-paths: ${total} enforced target(s) across ` +
211
236
  `${named.size} gate(s) resolve under the source tree.\n`,
@@ -15,6 +15,8 @@ import * as fs from 'node:fs';
15
15
  import * as path from 'node:path';
16
16
  import { fileURLToPath, pathToFileURL } from 'node:url';
17
17
 
18
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
19
+
18
20
  const _HERE = fileURLToPath(import.meta.url);
19
21
  const REPO_ROOT = path.join(path.dirname(_HERE), '..', '..');
20
22
 
@@ -55,6 +57,25 @@ function main(argv?: readonly string[]): number {
55
57
  const args = argv ?? process.argv.slice(2);
56
58
  const quiet = args.includes('--quiet');
57
59
 
60
+ // Every artefact is skipped when absent, so all three moving at once prints
61
+ // "✅ All 3 generated artefacts carry the freshness marker" over zero reads.
62
+ // The per-file skip below stays — a single gitignored artefact is a real
63
+ // state; none of them existing is a moved root. 1 is the only failure code
64
+ // this gate has, so "could not run" and "found something" share it.
65
+ try {
66
+ assertWatchlistResolves({
67
+ gate: 'check_generated_artefact_headers',
68
+ candidates: GENERATED_ARTEFACTS.map((e) => e.path),
69
+ repoRoot: REPO_ROOT,
70
+ });
71
+ } catch (err) {
72
+ if (err instanceof DeadScopeError) {
73
+ process.stderr.write(`❌ ${err.message}\n`);
74
+ return 1;
75
+ }
76
+ throw err;
77
+ }
78
+
58
79
  const errors: string[] = [];
59
80
  const warnings: string[] = [];
60
81
 
@@ -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 { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
22
+
21
23
  const _HERE = fileURLToPath(import.meta.url);
22
24
  const REPO_ROOT = path.join(path.dirname(_HERE), '..', '..');
23
25
 
@@ -103,6 +105,27 @@ function main(argv?: readonly string[]): number {
103
105
  .filter((l) => Boolean(l) && !l.startsWith('#')),
104
106
  );
105
107
 
108
+ // The registry below is the corpus — the manifest and .gitignore are only
109
+ // what each root is checked AGAINST, and both already fail closed above. An
110
+ // emptied registry (a refactor that moves the list, a bad merge) leaves the
111
+ // loop with nothing to classify and prints "✅ All 0 generator output roots
112
+ // are classified". Exit 2, the code this gate already uses for "a required
113
+ // input is not there", never 1, which means a root is unclassified.
114
+ try {
115
+ assertScanned({
116
+ gate: 'check_generator_output_coverage',
117
+ scanned: GENERATOR_OUTPUT_ROOTS.length,
118
+ units: 'generator output root(s)',
119
+ roots: ['src/scripts/check_generator_output_coverage.ts (GENERATOR_OUTPUT_ROOTS)'],
120
+ });
121
+ } catch (err) {
122
+ if (err instanceof DeadScopeError) {
123
+ process.stderr.write(`❌ ${err.message}\n`);
124
+ return 2;
125
+ }
126
+ throw err;
127
+ }
128
+
106
129
  const unclassified: typeof GENERATOR_OUTPUT_ROOTS[number][] = [];
107
130
  for (const entry of GENERATOR_OUTPUT_ROOTS) {
108
131
  if (!isClassified(entry.root, manifestPaths, gitignoreLines)) {
@@ -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 REPO_ROOT = path.join(path.dirname(_HERE), '..', '..');
22
24
 
@@ -83,6 +85,27 @@ function main(argv?: readonly string[]): number {
83
85
  const blockLines = new Set(blockText.split('\n').map((l) => l.trim()).filter(Boolean));
84
86
 
85
87
  const entries = parseManifest(manifestText);
88
+ // Counted on the whole parse, not on the consumer-scoped subset the loop
89
+ // judges: `_isFile` above only proves the manifest exists, and a shape change
90
+ // that makes `- path:` stop matching yields zero entries, zero missing paths,
91
+ // and "✅ covers all 0 consumer-scoped ignored paths". Exit 2 (documented
92
+ // "invalid args or missing file" — the gate could not run), not 1, which
93
+ // means the block is genuinely missing a path.
94
+ try {
95
+ assertScanned({
96
+ gate: 'check_gitignore_freshness',
97
+ scanned: entries.length,
98
+ units: 'manifest entr(y|ies)',
99
+ roots: ['src/config/agents-paths.yml'],
100
+ });
101
+ } catch (err) {
102
+ if (err instanceof DeadScopeError) {
103
+ process.stderr.write(`❌ ${err.message}\n`);
104
+ return 2;
105
+ }
106
+ throw err;
107
+ }
108
+
86
109
  const missing: string[] = [];
87
110
 
88
111
  for (const e of entries) {
@@ -16,10 +16,14 @@ import * as path from 'node:path';
16
16
 
17
17
  import { parse as parseYaml } from 'yaml';
18
18
 
19
- export function check_claude_skills(root: string, errors: string[]): void {
19
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
20
+
21
+ /** @returns how many skill directories were examined. */
22
+ export function check_claude_skills(root: string, errors: string[]): number {
20
23
  const dir = path.join(root, '.claude', 'skills');
21
- if (!fs.existsSync(dir)) return;
22
- for (const name of fs.readdirSync(dir)) {
24
+ if (!fs.existsSync(dir)) return 0;
25
+ const names = fs.readdirSync(dir);
26
+ for (const name of names) {
23
27
  const skillMd = path.join(dir, name, 'SKILL.md');
24
28
  if (!fs.existsSync(skillMd)) {
25
29
  errors.push(`${skillMd}: missing SKILL.md`);
@@ -35,13 +39,17 @@ export function check_claude_skills(root: string, errors: string[]): void {
35
39
  else if (meta['name'] !== name) errors.push(`${skillMd}: name '${meta['name']}' != dir '${name}'`);
36
40
  if (typeof meta['description'] !== 'string' || !meta['description']) errors.push(`${skillMd}: missing description`);
37
41
  }
42
+ return names.length;
38
43
  }
39
44
 
40
- export function check_cursor_rules(root: string, errors: string[]): void {
45
+ /** @returns how many `.mdc` rule files were examined. */
46
+ export function check_cursor_rules(root: string, errors: string[]): number {
41
47
  const dir = path.join(root, '.cursor', 'rules');
42
- if (!fs.existsSync(dir)) return;
48
+ if (!fs.existsSync(dir)) return 0;
49
+ let scanned = 0;
43
50
  for (const name of fs.readdirSync(dir)) {
44
51
  if (!name.endsWith('.mdc')) continue;
52
+ scanned += 1;
45
53
  const p = path.join(dir, name);
46
54
  const src = fs.readFileSync(p, 'utf-8');
47
55
  const m = src.match(/^---\n([\s\S]*?)\n---/);
@@ -57,12 +65,22 @@ export function check_cursor_rules(root: string, errors: string[]): void {
57
65
  try { parseYaml(lenient); }
58
66
  catch (e) { errors.push(`${p}: frontmatter YAML invalid (${(e as Error).message})`); }
59
67
  }
68
+ return scanned;
60
69
  }
61
70
 
62
71
  export function run(root: string): string[] {
63
72
  const errors: string[] = [];
64
- check_claude_skills(root, errors);
65
- check_cursor_rules(root, errors);
73
+ // Both host trees are generated and gitignored, and each checker returns
74
+ // early when its tree is absent — so "no malformed artefacts" and "no
75
+ // artefacts" are the same green. Thrown, not returned: `errors` names
76
+ // malformed files, and an unprojected tree is not one.
77
+ const scanned = check_claude_skills(root, errors) + check_cursor_rules(root, errors);
78
+ assertScanned({
79
+ gate: 'check_host_loadability',
80
+ scanned,
81
+ units: 'host artefact(s)',
82
+ roots: ['.claude/skills', '.cursor/rules'],
83
+ });
66
84
  return errors;
67
85
  }
68
86
 
@@ -71,7 +89,15 @@ const isMain = process.argv[1] !== undefined
71
89
  if (isMain) {
72
90
  const rootIdx = process.argv.indexOf('--root');
73
91
  const root = rootIdx !== -1 ? process.argv[rootIdx + 1] ?? '.' : '.';
74
- const errors = run(root);
92
+ let errors: string[];
93
+ try {
94
+ errors = run(root);
95
+ } catch (exc) {
96
+ if (!(exc instanceof DeadScopeError)) throw exc;
97
+ // Exit 1 is this gate's only failure code.
98
+ process.stderr.write(`❌ ${exc.message}\n`);
99
+ process.exit(1);
100
+ }
75
101
  if (errors.length > 0) {
76
102
  for (const e of errors) process.stderr.write(`❌ ${e}\n`);
77
103
  process.exit(1);
@@ -59,6 +59,8 @@ import { fileURLToPath } from 'node:url';
59
59
 
60
60
  import ts from 'typescript';
61
61
 
62
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
63
+
62
64
  const _HERE = path.dirname(fileURLToPath(import.meta.url));
63
65
  const REPO_ROOT = path.resolve(_HERE, '..', '..');
64
66
 
@@ -208,14 +210,6 @@ function parse(file: string): ts.SourceFile | null {
208
210
  */
209
211
  function auditEntry(entryRel: string, root: string = REPO_ROOT): Finding[] {
210
212
  const entryAbs = path.join(root, entryRel);
211
- if (!fs.existsSync(entryAbs)) {
212
- process.stderr.write(
213
- `check-installer-import-purity: entry ${entryRel} does not exist — ` +
214
- 'update BUNDLE_ENTRIES\n',
215
- );
216
- process.exitCode = 1;
217
- return [];
218
- }
219
213
  const findings: Finding[] = [];
220
214
  const seen = new Set<string>([entryAbs]);
221
215
  // BFS so the reported chain is a shortest path — the least confusing one.
@@ -245,6 +239,26 @@ function auditEntry(entryRel: string, root: string = REPO_ROOT): Finding[] {
245
239
  }
246
240
 
247
241
  function main(): number {
242
+ // BUNDLE_ENTRIES is the whole scan scope — every module examined is reached
243
+ // from one of them. This REPLACES the per-entry existence guard in
244
+ // `auditEntry`, which could not fail: it set `process.exitCode = 1` and
245
+ // returned no findings, and `process.exit(main())` then overwrote that with
246
+ // main's 0, so a moved entry printed "✅ … reach no module-level
247
+ // process.exit()". 1 is the only failure code this gate has.
248
+ try {
249
+ assertWatchlistResolves({
250
+ gate: 'check_installer_import_purity',
251
+ candidates: BUNDLE_ENTRIES,
252
+ repoRoot: REPO_ROOT,
253
+ });
254
+ } catch (err) {
255
+ if (err instanceof DeadScopeError) {
256
+ process.stderr.write(`❌ ${err.message}\n`);
257
+ return 1;
258
+ }
259
+ throw err;
260
+ }
261
+
248
262
  const all: Finding[] = [];
249
263
  for (const entry of BUNDLE_ENTRIES) {
250
264
  all.push(...auditEntry(entry));
@@ -37,6 +37,8 @@ import * as path from 'node:path';
37
37
  import process from 'node:process';
38
38
  import { fileURLToPath, pathToFileURL } from 'node:url';
39
39
 
40
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
41
+
40
42
  const _HERE = fileURLToPath(import.meta.url);
41
43
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
42
44
  const ROUTER = path.join(REPO_ROOT, 'dist/router.json');
@@ -96,7 +98,17 @@ function _live_prefix(): KernelPrefix | null {
96
98
  const router = _readJson(ROUTER);
97
99
  if (router === null) return null;
98
100
  const ids = kernel_ids(router);
99
- if (ids.length === 0) return null;
101
+ // Scope only — the baseline and the comparison are untouched. An empty kernel
102
+ // list used to collapse into the same `null` as an unreadable router, so a
103
+ // router that compiled to zero kernel rules was reported as a missing file;
104
+ // it is a dead scan scope, and a digest over no bodies is a constant that
105
+ // would then look byte-stable forever.
106
+ assertScanned({
107
+ gate: 'check_kernel_prefix_stability',
108
+ scanned: ids.length,
109
+ units: 'kernel rule(s)',
110
+ roots: ['dist/router.json (kernel)', 'dist/agent-src/rules'],
111
+ });
100
112
  return compute_prefix(ids, (id) => fs.readFileSync(path.join(RULES_DIR, `${id}.md`), 'utf-8'));
101
113
  }
102
114
 
@@ -118,6 +130,12 @@ function main(argv: string[]): number {
118
130
  try {
119
131
  current = _live_prefix();
120
132
  } catch (e) {
133
+ // 1 is the documented "file error" code — the gate could not run. 2 is
134
+ // reserved for drift, i.e. a prefix that was read and did change.
135
+ if (e instanceof DeadScopeError) {
136
+ process.stderr.write(`error: ${e.message}\n`);
137
+ return 1;
138
+ }
121
139
  process.stderr.write(`error: cannot read kernel bodies: ${(e as Error).message}\n`);
122
140
  return 1;
123
141
  }
@@ -37,6 +37,7 @@ import * as path from 'node:path';
37
37
  import { fileURLToPath, pathToFileURL } from 'node:url';
38
38
  import { KERNEL_RULE_FILENAMES } from './_lib/kernel_rules.js';
39
39
  import { runCountedProbe } from './_lib/counted_probe.js';
40
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
40
41
 
41
42
  const KERNEL_RULES: ReadonlySet<string> = KERNEL_RULE_FILENAMES;
42
43
 
@@ -55,6 +56,8 @@ const KERNEL_RULES: ReadonlySet<string> = KERNEL_RULE_FILENAMES;
55
56
  */
56
57
  const KERNEL_DIR = 'src/rules';
57
58
  const DEFAULT_LABEL = 'bundled-always-rules-acknowledged';
59
+ // src/scripts/check_kernel_rule_bundle.ts → two levels up is the repo root.
60
+ const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
58
61
 
59
62
  function _git_changed_files(base_ref: string): string[] {
60
63
  // Bounded read: a truncated diff would shrink the change set and let the
@@ -188,6 +191,26 @@ function parse_args(argv: readonly string[]): Args {
188
191
  export function main(argv?: readonly string[]): number {
189
192
  const args = parse_args(argv ?? process.argv.slice(2));
190
193
 
194
+ // This gate has no corpus — it matches changed paths against a fixed set of
195
+ // 9 kernel rules, so its scope IS that watch list. It has already been
196
+ // inert once for exactly this reason (see KERNEL_DIR above): a prefix that
197
+ // no longer exists never matches, and every kernel-rule edit scored
198
+ // "no kernel rule touched". Exit 3 (internal error) over 1 (bundle
199
+ // violated) — a phantom watch list means the gate could not run.
200
+ try {
201
+ assertWatchlistResolves({
202
+ gate: 'check_kernel_rule_bundle',
203
+ candidates: [...KERNEL_RULES].sort().map((name) => `${KERNEL_DIR}/${name}`),
204
+ repoRoot: REPO,
205
+ });
206
+ } catch (exc) {
207
+ if (exc instanceof DeadScopeError) {
208
+ process.stderr.write(`❌ ${exc.message}\n`);
209
+ return 3;
210
+ }
211
+ throw exc;
212
+ }
213
+
191
214
  // Python: args.files or _git_changed_files(...). An empty list from
192
215
  // `--files` (no values) is falsy in Python → falls through to git diff.
193
216
  const files =
@@ -39,6 +39,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
39
39
  // byte-faithful for behaviour (the only observable effect is the --global path).
40
40
  import * as _kg from './_lib/knowledge_global.js';
41
41
  import * as _kgr from './_lib/knowledge_global_redaction.js';
42
+ import { assertScanned } from './_lib/scan_scope.js';
42
43
 
43
44
  // ROOT = Path(__file__).resolve().parent.parent.parent — src/scripts/ → repo root.
44
45
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
@@ -373,6 +374,18 @@ function main(): number {
373
374
  }
374
375
 
375
376
  const cards = _sortedGlobMd(card_dir).filter((p) => path.basename(p).toLowerCase() !== 'readme.md');
377
+ assertScanned({
378
+ gate: 'check_knowledge_cards',
379
+ scanned: cards.length,
380
+ units: 'knowledge card(s)',
381
+ roots: [path.relative(ROOT, card_dir) || card_dir],
382
+ allowEmpty:
383
+ 'OPTIONAL_INPUT: cards are project-authored runtime content under agents/knowledge/ '
384
+ + '(or a --dir / --global store), never shipped package source. Delete the root and '
385
+ + 'zero still reads as "this project has captured no cards yet" — which is why both '
386
+ + 'the missing-dir and empty-dir paths are pinned as exit 0 by the CLI contract '
387
+ + 'below. A stale root cannot hide here: --dir is caller-supplied per invocation.',
388
+ });
376
389
  if (cards.length === 0) {
377
390
  process.stdout.write('No knowledge cards found — nothing to check.\n');
378
391
  return 0;
@@ -29,6 +29,8 @@ import process from 'node:process';
29
29
  import { fileURLToPath } from 'node:url';
30
30
  import YAML from 'yaml';
31
31
 
32
+ import { assertScanned } from './_lib/scan_scope.js';
33
+
32
34
  const PROG = 'check_knowledge_pages.ts';
33
35
  const PAGE_LINE_BUDGET = 200;
34
36
  const CONTESTED_WARN_COUNT = 2;
@@ -135,6 +137,14 @@ function listMarkdownFiles(dirPath: string): string[] {
135
137
  .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
136
138
  }
137
139
 
140
+ /** Pages `lintAll` reads — the corpus, independent of how many warn. */
141
+ function countPages(knowledgeRoot: string): number {
142
+ return TYPED_DIRS.reduce(
143
+ (n, dir) => n + listMarkdownFiles(path.join(knowledgeRoot, dir)).length,
144
+ 0,
145
+ );
146
+ }
147
+
138
148
  export function lintAll(knowledgeRoot: string, today: Date = new Date()): Warning[] {
139
149
  const warnings: Warning[] = [];
140
150
  for (const dir of TYPED_DIRS) {
@@ -187,6 +197,19 @@ export function main(argv: string[]): number {
187
197
 
188
198
  const knowledgeRoot = path.join(dir, 'agents', 'knowledge');
189
199
  const warnings = lintAll(knowledgeRoot);
200
+ // Pages read, not warnings emitted — this lint never fails the build, so a
201
+ // moved root would print "no warnings" indefinitely with nothing behind it.
202
+ assertScanned({
203
+ gate: 'check_knowledge_pages',
204
+ scanned: countPages(knowledgeRoot),
205
+ units: 'knowledge page(s)',
206
+ roots: TYPED_DIRS.map((d) => path.join('agents', 'knowledge', d)),
207
+ allowEmpty:
208
+ 'OPTIONAL_INPUT: the lifecycle-typed pages are project-authored runtime content, '
209
+ + 'not shipped package source, and the four typed subdirs are created on first '
210
+ + 'capture. Delete them and zero still reads as "this project has written no '
211
+ + 'knowledge pages yet" — the state every consumer starts in.',
212
+ });
190
213
 
191
214
  if (format === 'json') {
192
215
  process.stdout.write(JSON.stringify(warnings, null, 2) + '\n');
@@ -29,6 +29,8 @@ import process from 'node:process';
29
29
  import { fileURLToPath } from 'node:url';
30
30
  import YAML from 'yaml';
31
31
 
32
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
33
+
32
34
  const PROG = 'check_knowledge_sharing.ts';
33
35
  const CREATION_BUDGET_WARN = 5;
34
36
  const FRONTMATTER_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/;
@@ -150,6 +152,31 @@ export function main(argv: string[]): number {
150
152
  return 3;
151
153
  }
152
154
 
155
+ // Diff-driven: the corpus is whatever is staged, and an empty index is the
156
+ // normal state of a clean tree — so zero is allowed here. The dangerous
157
+ // empty (git itself failing) is NOT swallowed: it exits 3 above rather than
158
+ // reaching this point with an empty list. Exit 3 (internal error) is the
159
+ // code a dead scope would share, for the same "could not run" reason.
160
+ try {
161
+ assertScanned({
162
+ gate: 'check_knowledge_sharing',
163
+ scanned: staged.length,
164
+ units: 'staged path(s)',
165
+ roots: ['git diff --cached --name-status'],
166
+ allowEmpty:
167
+ 'EMPTY_VALID: nothing staged means there is no commit to judge — an absent ' +
168
+ 'question, not an empty corpus. Deletion test: deleting agents/knowledge/ ' +
169
+ 'would not produce this zero (the staged deletions would themselves show up ' +
170
+ 'in the diff); only an empty index does, and that is a clean tree.',
171
+ });
172
+ } catch (err) {
173
+ if (err instanceof DeadScopeError) {
174
+ process.stderr.write(`❌ ${err.message}\n`);
175
+ return 3;
176
+ }
177
+ throw err;
178
+ }
179
+
153
180
  const { blocked, warnings } = checkSharing(staged, gitStagedContent);
154
181
 
155
182
  for (const w of warnings) {
@@ -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
  // Umlauts and German-only characters.
24
26
  const UMLAUT_RE = /[äöüÄÖÜß]/g;
25
27
 
@@ -247,6 +249,7 @@ export function main(argv?: readonly string[]): number {
247
249
  const args = parse_args(argv ?? process.argv.slice(2));
248
250
 
249
251
  const all_violations: Violation[] = [];
252
+ let scanned = 0;
250
253
  for (const raw_path of args.paths) {
251
254
  if (!_exists(raw_path)) {
252
255
  process.stderr.write(`⚠️ Not found: ${raw_path}\n`);
@@ -256,9 +259,30 @@ export function main(argv?: readonly string[]): number {
256
259
  process.stderr.write(`⚠️ Skipping non-.md: ${raw_path}\n`);
257
260
  continue;
258
261
  }
262
+ scanned += 1;
259
263
  all_violations.push(...scan_file(raw_path));
260
264
  }
261
265
 
266
+ // The caller expands the path list (`find docs -name '*.md' …`), so a moved
267
+ // docs tree hands this gate paths that every skip above swallows into a
268
+ // stderr warning while stdout still reports "No German content detected".
269
+ // Exit 3 is the documented "internal error" code — the gate could not run;
270
+ // 1 means it read files and found German in them.
271
+ try {
272
+ assertScanned({
273
+ gate: 'check_md_language',
274
+ scanned,
275
+ units: '.md file(s)',
276
+ roots: [`${String(args.paths.length)} caller-supplied path(s)`],
277
+ });
278
+ } catch (exc) {
279
+ if (exc instanceof DeadScopeError) {
280
+ process.stderr.write(`❌ ${exc.message}\n`);
281
+ return 3;
282
+ }
283
+ throw exc;
284
+ }
285
+
262
286
  if (args.format === 'json') {
263
287
  process.stdout.write(JSON.stringify(all_violations, null, 2) + '\n');
264
288
  } else if (all_violations.length === 0) {
@@ -21,6 +21,8 @@ import * as path from 'node:path';
21
21
  import * as fs from 'node:fs';
22
22
  import { fileURLToPath, pathToFileURL } from 'node:url';
23
23
 
24
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
25
+
24
26
  const _FILE = fileURLToPath(import.meta.url);
25
27
 
26
28
  export interface MediaTool {
@@ -71,6 +73,27 @@ export function missingTools(tools: MediaTool[] = MEDIA_TOOLS): MediaTool[] {
71
73
 
72
74
  export function main(argv: string[] = process.argv.slice(2)): number {
73
75
  const quiet = argv.includes('--quiet');
76
+
77
+ // The scan scope is the declared tool list, not a directory: an empty
78
+ // MEDIA_TOOLS probes nothing and still prints the ✅ "all present" line.
79
+ // Deletion test: emptying the declaration would legitimately yield 0 only
80
+ // if "no tooling is required", which is never true for a prereq check.
81
+ // Exit 1 is the only failure code this CLI has, so a dead scope shares it.
82
+ try {
83
+ assertScanned({
84
+ gate: 'check_media_deps',
85
+ scanned: MEDIA_TOOLS.length,
86
+ units: 'declared media tool(s)',
87
+ roots: ['MEDIA_TOOLS (declared in src/scripts/check_media_deps.ts)'],
88
+ });
89
+ } catch (exc) {
90
+ if (exc instanceof DeadScopeError) {
91
+ process.stderr.write(`❌ ${exc.message}\n`);
92
+ return 1;
93
+ }
94
+ throw exc;
95
+ }
96
+
74
97
  const missing = missingTools();
75
98
  if (missing.length === 0) {
76
99
  if (!quiet) {
@@ -53,6 +53,8 @@ import process from 'node:process';
53
53
  import { fileURLToPath, pathToFileURL } from 'node:url';
54
54
  import YAML, { parseDocument } from 'yaml';
55
55
 
56
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
57
+
56
58
  type Severity = 'error' | 'warning' | 'info';
57
59
 
58
60
  const REQUIRED_KEYS: ReadonlySet<string> = new Set([
@@ -722,11 +724,34 @@ function main(): number {
722
724
  }
723
725
  return 0;
724
726
  }
727
+ // An ABSENT root stays a pinned exit-0 no-op (the branch above; the memory
728
+ // tree is an optional surface and `task check-memory` short-circuits it).
729
+ // A root that EXISTS but walks to zero `*.yml` is the case nothing covered:
730
+ // entries moved to a subtree this walk does not reach, and the validator
731
+ // reported "0 error(s)" over an empty read. No `allowEmpty` for that half —
732
+ // a present-but-unreadable corpus is blindness, not a clean bill of health.
733
+ // Exit 1 is this gate's only failure code (see `_emit`).
734
+ const ymls = _rglobYmlSorted(root);
735
+ try {
736
+ assertScanned({
737
+ gate: 'check_memory',
738
+ scanned: ymls.length,
739
+ units: 'memory entry file(s)',
740
+ roots: [`${root}/**/*.yml`],
741
+ });
742
+ } catch (exc) {
743
+ if (exc instanceof DeadScopeError) {
744
+ process.stderr.write(`❌ ${exc.message}\n`);
745
+ return 1;
746
+ }
747
+ throw exc;
748
+ }
749
+
725
750
  const criticalCounts: Record<string, number> = {};
726
751
  for (const key of Object.keys(_TYPE_COUNTS)) {
727
752
  delete _TYPE_COUNTS[key];
728
753
  }
729
- for (const yml of _rglobYmlSorted(root)) {
754
+ for (const yml of ymls) {
730
755
  _validateFile(yml, findings, criticalCounts);
731
756
  }
732
757
  // Tier-0 inflation warning — soft cap on `priority: critical` per type.
@@ -34,6 +34,7 @@ import process from 'node:process';
34
34
  import { fileURLToPath } from 'node:url';
35
35
  import YAML from 'yaml';
36
36
 
37
+ import { assertScanned } from './_lib/scan_scope.js';
37
38
  import { jaccardSimilarity } from './_lib/text_similarity.js';
38
39
 
39
40
  const PROG = 'check_memory_contradiction.ts';
@@ -234,7 +235,21 @@ function main(argv: string[]): number {
234
235
 
235
236
  let hit: ContradictionHit | null;
236
237
  try {
237
- hit = findContradiction(key, body, loadCuratedEntries(path.resolve(memoryRoot), type));
238
+ const existing = loadCuratedEntries(path.resolve(memoryRoot), type);
239
+ // Curated entries READ — the corpus every verdict below is decided
240
+ // against. Zero of them makes "no contradiction" unfalsifiable.
241
+ assertScanned({
242
+ gate: 'check_memory_contradiction',
243
+ scanned: existing.length,
244
+ units: 'curated entry(ies)',
245
+ roots: [path.join(memoryRoot, type), path.join(memoryRoot, `${type}.yml`)],
246
+ allowEmpty:
247
+ 'OPTIONAL_INPUT: the per-type curated store is created by the first '
248
+ + '/memory promote of that type. Delete it and zero still reads as "nothing '
249
+ + 'of this type has been curated yet" — a state every consumer starts in, and '
250
+ + 'one in which no contradiction can exist by definition.',
251
+ });
252
+ hit = findContradiction(key, body, existing);
238
253
  } catch (exc) {
239
254
  process.stderr.write(`${PROG}: internal error: ${String(exc)}\n`);
240
255
  return 3;