@event4u/agent-config 9.19.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 (234) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +89 -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 +38 -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_completion_review.ts +19 -4
  51. package/src/scripts/check_condensed_paths.ts +23 -2
  52. package/src/scripts/check_consumer_scope_flip.ts +21 -2
  53. package/src/scripts/check_council_config_location.ts +20 -0
  54. package/src/scripts/check_council_layout.ts +22 -2
  55. package/src/scripts/check_council_references.ts +23 -1
  56. package/src/scripts/check_dependency_floors.ts +19 -3
  57. package/src/scripts/check_discovery_determinism.ts +26 -0
  58. package/src/scripts/check_enforcement_coverage.ts +20 -0
  59. package/src/scripts/check_evaluator_budgets.ts +23 -0
  60. package/src/scripts/check_finding_dispositions.ts +29 -0
  61. package/src/scripts/check_gate_coverage.ts +63 -22
  62. package/src/scripts/check_gate_paths.ts +30 -5
  63. package/src/scripts/check_generated_artefact_headers.ts +21 -0
  64. package/src/scripts/check_generator_output_coverage.ts +23 -0
  65. package/src/scripts/check_gitignore_freshness.ts +23 -0
  66. package/src/scripts/check_host_loadability.ts +34 -8
  67. package/src/scripts/check_installer_import_purity.ts +22 -8
  68. package/src/scripts/check_kernel_prefix_stability.ts +19 -1
  69. package/src/scripts/check_kernel_rule_bundle.ts +23 -0
  70. package/src/scripts/check_knowledge_cards.ts +13 -0
  71. package/src/scripts/check_knowledge_pages.ts +23 -0
  72. package/src/scripts/check_knowledge_sharing.ts +27 -0
  73. package/src/scripts/check_md_language.ts +24 -0
  74. package/src/scripts/check_media_deps.ts +23 -0
  75. package/src/scripts/check_memory.ts +26 -1
  76. package/src/scripts/check_memory_contradiction.ts +16 -1
  77. package/src/scripts/check_memory_proposal.ts +15 -0
  78. package/src/scripts/check_memory_similarity.ts +16 -0
  79. package/src/scripts/check_module_management_neutral.ts +19 -11
  80. package/src/scripts/check_no_conflict_markers.ts +22 -1
  81. package/src/scripts/check_no_external_sources.ts +26 -1
  82. package/src/scripts/check_no_local_settings_committed.ts +37 -10
  83. package/src/scripts/check_no_new_legacy_path.ts +30 -1
  84. package/src/scripts/check_one_off_location.ts +43 -0
  85. package/src/scripts/check_originality_freshness.ts +25 -1
  86. package/src/scripts/check_overlay_cascade_subdirs.ts +21 -0
  87. package/src/scripts/check_pack_size.ts +22 -0
  88. package/src/scripts/check_preamble_payload_budget.ts +35 -5
  89. package/src/scripts/check_proposal.ts +24 -1
  90. package/src/scripts/check_public_catalog_links.ts +27 -0
  91. package/src/scripts/check_public_links.ts +19 -0
  92. package/src/scripts/check_quality_regression.ts +19 -0
  93. package/src/scripts/check_reach_channels.ts +44 -0
  94. package/src/scripts/check_reach_staleness.ts +27 -0
  95. package/src/scripts/check_release_adjacent_health.ts +28 -0
  96. package/src/scripts/check_release_highlights.ts +34 -0
  97. package/src/scripts/check_release_includes_discovery.ts +25 -6
  98. package/src/scripts/check_release_pr_shape.ts +18 -1
  99. package/src/scripts/check_release_published.ts +21 -0
  100. package/src/scripts/check_release_surface_equality.ts +22 -0
  101. package/src/scripts/check_release_trunk_sync.ts +19 -0
  102. package/src/scripts/check_reply_consistency.ts +22 -0
  103. package/src/scripts/check_review_dispositions.ts +188 -0
  104. package/src/scripts/check_roadmap_trackable.ts +20 -0
  105. package/src/scripts/check_role_doc_links.ts +23 -0
  106. package/src/scripts/check_rule_invariants.ts +14 -0
  107. package/src/scripts/check_secret_leak.ts +47 -0
  108. package/src/scripts/check_skill_gaps.ts +32 -0
  109. package/src/scripts/check_skill_requires.ts +19 -0
  110. package/src/scripts/check_source_pointer_freshness.ts +20 -0
  111. package/src/scripts/check_structural_breaking.ts +29 -2
  112. package/src/scripts/check_surface_tiers.ts +34 -0
  113. package/src/scripts/check_template_pin_drift.ts +23 -1
  114. package/src/scripts/check_test_coverage_diff.ts +17 -1
  115. package/src/scripts/check_token_optimizer_freshness.ts +18 -3
  116. package/src/scripts/check_token_quality_golden.ts +21 -0
  117. package/src/scripts/check_token_regression.ts +25 -0
  118. package/src/scripts/check_tracked_but_ignored.ts +26 -0
  119. package/src/scripts/check_trigger_eval_presence.ts +23 -1
  120. package/src/scripts/check_trigger_evals.ts +36 -0
  121. package/src/scripts/check_trunk_drift.ts +23 -0
  122. package/src/scripts/check_update_banner.ts +24 -0
  123. package/src/scripts/dispatch_r2_reviewer.ts +3 -0
  124. package/src/scripts/gate_scope_classify.ts +513 -0
  125. package/src/scripts/hook_manifest.yaml +1 -1
  126. package/src/scripts/hooks/block_kernel_rule_writes.ts +1 -1
  127. package/src/scripts/hooks/concern_registry.ts +2 -2
  128. package/src/scripts/lint_agent_security.ts +19 -0
  129. package/src/scripts/lint_agent_skill_names.ts +30 -0
  130. package/src/scripts/lint_agents_layout.ts +20 -0
  131. package/src/scripts/lint_agents_md.ts +20 -0
  132. package/src/scripts/lint_archived_skills.ts +24 -3
  133. package/src/scripts/lint_behavioural_eval_freshness.ts +29 -3
  134. package/src/scripts/lint_bench_ab.ts +25 -0
  135. package/src/scripts/lint_bench_corpus.ts +20 -4
  136. package/src/scripts/lint_breaking_changes_index.ts +24 -0
  137. package/src/scripts/lint_budget_ownership.ts +20 -3
  138. package/src/scripts/lint_changelog_rollback.ts +24 -0
  139. package/src/scripts/lint_command_flow_coverage.ts +22 -0
  140. package/src/scripts/lint_command_tiers.ts +29 -5
  141. package/src/scripts/lint_commit_subjects.ts +15 -0
  142. package/src/scripts/lint_confusables.ts +24 -0
  143. package/src/scripts/lint_consumer_internal_refs.ts +24 -0
  144. package/src/scripts/lint_design_quality.ts +22 -0
  145. package/src/scripts/lint_design_slop.ts +23 -0
  146. package/src/scripts/lint_discovery_manifest.ts +23 -0
  147. package/src/scripts/lint_discovery_vocabulary.ts +19 -0
  148. package/src/scripts/lint_documented_commands.ts +23 -0
  149. package/src/scripts/lint_empty_roadmaps.ts +23 -0
  150. package/src/scripts/lint_eval_fixture_citations.ts +19 -0
  151. package/src/scripts/lint_eval_freshness.ts +35 -0
  152. package/src/scripts/lint_examples.ts +18 -3
  153. package/src/scripts/lint_explain_trace.ts +23 -0
  154. package/src/scripts/lint_featured_skills.ts +26 -1
  155. package/src/scripts/lint_flows.ts +19 -0
  156. package/src/scripts/lint_frontmatter_boilerplate.ts +20 -0
  157. package/src/scripts/lint_ghostwriter_source.ts +26 -6
  158. package/src/scripts/lint_glama_drift.ts +24 -0
  159. package/src/scripts/lint_global_paths.ts +24 -0
  160. package/src/scripts/lint_governed_writes.ts +20 -0
  161. package/src/scripts/lint_hidden_unicode.ts +25 -0
  162. package/src/scripts/lint_hook_concern_budget.ts +39 -6
  163. package/src/scripts/lint_hook_manifest.ts +22 -0
  164. package/src/scripts/lint_instruction_smuggling.ts +23 -0
  165. package/src/scripts/lint_knowledge_scale.ts +32 -2
  166. package/src/scripts/lint_legal_pack.ts +31 -0
  167. package/src/scripts/lint_marketplace.ts +26 -0
  168. package/src/scripts/lint_marketplace_install_completeness.ts +23 -0
  169. package/src/scripts/lint_mcp_config_security.ts +24 -0
  170. package/src/scripts/lint_mcp_registry_manifest.ts +22 -1
  171. package/src/scripts/lint_memory_tripwire.ts +35 -0
  172. package/src/scripts/lint_missions.ts +23 -7
  173. package/src/scripts/lint_model_tier_coverage.ts +18 -0
  174. package/src/scripts/lint_namespace_collisions.ts +19 -0
  175. package/src/scripts/lint_no_new_atomic_commands.ts +33 -0
  176. package/src/scripts/lint_no_python_twin_rationale.ts +18 -0
  177. package/src/scripts/lint_one_off_age.ts +38 -2
  178. package/src/scripts/lint_orchestration_dsl.ts +23 -3
  179. package/src/scripts/lint_orchestrator_auto_detect.ts +19 -12
  180. package/src/scripts/lint_originality.ts +22 -4
  181. package/src/scripts/lint_originality_shingles.ts +28 -3
  182. package/src/scripts/lint_output_slop.ts +19 -0
  183. package/src/scripts/lint_override_kernel_guard.ts +30 -0
  184. package/src/scripts/lint_pack_first_win.ts +23 -0
  185. package/src/scripts/lint_pack_risk_class.ts +19 -3
  186. package/src/scripts/lint_persistence.ts +19 -0
  187. package/src/scripts/lint_persona_governance.ts +18 -3
  188. package/src/scripts/lint_positioning.ts +23 -0
  189. package/src/scripts/lint_pre_migration_refs.ts +24 -0
  190. package/src/scripts/lint_profile_overlay_set_only.ts +24 -0
  191. package/src/scripts/lint_provenance.ts +21 -0
  192. package/src/scripts/lint_provenance_vocabulary.ts +24 -1
  193. package/src/scripts/lint_readme_jargon.ts +21 -3
  194. package/src/scripts/lint_readme_serial_comma.ts +21 -3
  195. package/src/scripts/lint_readme_size.ts +21 -3
  196. package/src/scripts/lint_regression.ts +28 -0
  197. package/src/scripts/lint_roadmap_blockers.ts +43 -0
  198. package/src/scripts/lint_roadmap_ci_steps.ts +45 -0
  199. package/src/scripts/lint_roadmap_complexity.ts +47 -0
  200. package/src/scripts/lint_roadmap_later_disposition.ts +23 -0
  201. package/src/scripts/lint_rule_interactions.ts +16 -2
  202. package/src/scripts/lint_rule_tiers.ts +19 -6
  203. package/src/scripts/lint_showcase_sessions.ts +19 -3
  204. package/src/scripts/lint_skill_descriptions.ts +19 -0
  205. package/src/scripts/lint_skill_frontmatter_safety.ts +24 -0
  206. package/src/scripts/lint_skill_originality.ts +18 -3
  207. package/src/scripts/lint_skill_scripts_readonly.ts +20 -0
  208. package/src/scripts/lint_skill_tools.ts +84 -145
  209. package/src/scripts/lint_spawn_payload.ts +27 -1
  210. package/src/scripts/lint_store_boundary.ts +19 -0
  211. package/src/scripts/lint_subagent_determinism.ts +23 -1
  212. package/src/scripts/lint_supported_tools_matrix.ts +18 -0
  213. package/src/scripts/lint_surface_matrix.ts +19 -3
  214. package/src/scripts/lint_ticket_buildable.ts +23 -0
  215. package/src/scripts/lint_topics_yaml.ts +19 -12
  216. package/src/scripts/lint_trigger_precision.ts +27 -0
  217. package/src/scripts/lint_trust_coherence.ts +21 -0
  218. package/src/scripts/lint_ui_stack_bundles.ts +17 -3
  219. package/src/scripts/lint_value_dashboard.ts +19 -3
  220. package/src/scripts/lint_versioned_cache.ts +19 -1
  221. package/src/scripts/lint_workflow_security.ts +22 -14
  222. package/src/scripts/pr_url_reminder_hook.ts +1 -1
  223. package/src/scripts/skill_collision_clusters.ts +37 -3
  224. package/src/scripts/skill_discovery.ts +20 -0
  225. package/src/scripts/skill_eval_coverage.ts +22 -0
  226. package/src/scripts/skill_overlap.ts +32 -6
  227. package/src/scripts/skill_preview.ts +31 -0
  228. package/src/scripts/skill_trigger_eval.ts +18 -0
  229. package/src/scripts/skill_usage_collect.ts +21 -0
  230. package/src/scripts/skill_usage_report.ts +21 -0
  231. package/src/scripts/sweep_dead_scan_roots.ts +11 -3
  232. package/src/scripts/verify_physical_move.ts +24 -0
  233. package/src/scripts/check_bite_sized_granularity.ts +0 -116
  234. package/src/scripts/lint_workspace_boundary.ts +0 -267
@@ -21,6 +21,8 @@ import * as fs from 'node:fs';
21
21
  import * as path from 'node:path';
22
22
  import { fileURLToPath, pathToFileURL } from 'node:url';
23
23
 
24
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
25
+
24
26
  const _HERE = fileURLToPath(import.meta.url);
25
27
  // src/scripts/lint_no_new_atomic_commands.ts → parent.parent.parent is repo root.
26
28
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -288,6 +290,37 @@ function main(argv?: readonly string[]): number {
288
290
 
289
291
  const clusters = load_locked_clusters();
290
292
  const targets = args.all ? all_command_files() : added_command_files(args.baseline);
293
+ // Two scopes behind one variable. `--all` walks src/domains, and an empty
294
+ // walk there is a moved root — the gate would print "No new commands added"
295
+ // and pass over a corpus it never found. The default mode reads a git diff,
296
+ // where zero added command files is the ordinary clean-branch state, so it
297
+ // carries a justified allowEmpty. Exit 3 is the internal-error code ("could
298
+ // not run"); 1 means a real policy violation.
299
+ try {
300
+ assertScanned({
301
+ gate: 'lint_no_new_atomic_commands',
302
+ scanned: targets.length,
303
+ units: args.all ? 'command file(s)' : 'added command file(s)',
304
+ roots: [COMMANDS_DIR],
305
+ ...(args.all
306
+ ? {}
307
+ : {
308
+ allowEmpty:
309
+ 'EMPTY_VALID: the default mode reads `git diff --diff-filter=A` plus ' +
310
+ 'untracked status, so zero added command files IS the pass state on a ' +
311
+ 'branch that adds none. Deletion test: deleting src/domains would not ' +
312
+ 'produce this zero — a deletion shows up as diff-filter=D, never as A — ' +
313
+ 'so an empty diff cannot launder a dead root. The tree-walking half of ' +
314
+ 'this gate (--all) is asserted without the exemption.',
315
+ }),
316
+ });
317
+ } catch (exc) {
318
+ if (exc instanceof DeadScopeError) {
319
+ process.stderr.write(`❌ ${exc.message}\n`);
320
+ return 3;
321
+ }
322
+ throw exc;
323
+ }
291
324
  if (targets.length === 0) {
292
325
  process.stdout.write(
293
326
  `✅ No new commands added under ${COMMANDS_DIR} ` +
@@ -22,6 +22,8 @@ import * as fs from 'node:fs';
22
22
  import * as path from 'node:path';
23
23
  import { fileURLToPath, pathToFileURL } from 'node:url';
24
24
 
25
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
26
+
25
27
  const _HERE = fileURLToPath(import.meta.url);
26
28
  export const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
27
29
  const SCOPE = path.join(ROOT, 'src', 'scripts');
@@ -79,11 +81,27 @@ export function scan_file(rel: string, text: string): string[] {
79
81
  export function main(argv: readonly string[] = process.argv.slice(2)): number {
80
82
  const QUIET = argv.includes('--quiet');
81
83
  const findings: string[] = [];
84
+ let scanned = 0;
82
85
  for (const f of walk(SCOPE)) {
83
86
  if (path.resolve(f) === path.resolve(_HERE)) continue; // this guard documents the shapes
87
+ scanned += 1;
84
88
  const rel = path.relative(ROOT, f).split(path.sep).join('/');
85
89
  findings.push(...scan_file(rel, fs.readFileSync(f, 'utf-8')));
86
90
  }
91
+ try {
92
+ assertScanned({
93
+ gate: 'lint_no_python_twin_rationale',
94
+ scanned,
95
+ units: 'source file(s)',
96
+ roots: ['src/scripts'],
97
+ });
98
+ } catch (exc) {
99
+ if (exc instanceof DeadScopeError) {
100
+ process.stdout.write(`❌ ${exc.message}\n`);
101
+ return 1;
102
+ }
103
+ throw exc;
104
+ }
87
105
  if (findings.length === 0) {
88
106
  if (!QUIET) process.stdout.write('✅ No Python-twin rationale in src/scripts comments.\n');
89
107
  return 0;
@@ -28,6 +28,8 @@ import * as fs from 'node:fs';
28
28
  import * as path from 'node:path';
29
29
  import { fileURLToPath, pathToFileURL } from 'node:url';
30
30
 
31
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
32
+
31
33
  const _HERE = fileURLToPath(import.meta.url);
32
34
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
33
35
  // Mirrors the (scan-unused) module constant; kept for export parity.
@@ -159,7 +161,12 @@ function _exists(p: string): boolean {
159
161
  }
160
162
  }
161
163
 
162
- export function scan(root: string, today?: SimpleDate): Finding[] {
164
+ /** Out-param so the caller can assert the scope `scan` just walked. */
165
+ export interface ScanStats {
166
+ entries: number;
167
+ }
168
+
169
+ export function scan(root: string, today?: SimpleDate, stats?: ScanStats): Finding[] {
163
170
  const day = today ?? _today_utc();
164
171
  const base = path.join(root, 'scripts', '_one_off');
165
172
  if (!_exists(base)) {
@@ -170,6 +177,11 @@ export function scan(root: string, today?: SimpleDate): Finding[] {
170
177
  if (!_isDir(month_dir)) {
171
178
  continue;
172
179
  }
180
+ // A month dir is itself a checked unit — its name shape is validated
181
+ // below, so an empty one is still something this walk read.
182
+ if (stats) {
183
+ stats.entries += 1;
184
+ }
173
185
  const monthName = path.basename(month_dir);
174
186
  const anchor = _month_anchor(monthName);
175
187
  if (anchor === null) {
@@ -186,6 +198,9 @@ export function scan(root: string, today?: SimpleDate): Finding[] {
186
198
  if (fname === 'README.md' || _isDir(f)) {
187
199
  continue;
188
200
  }
201
+ if (stats) {
202
+ stats.entries += 1;
203
+ }
189
204
  if (!NAME_RE.test(fname)) {
190
205
  out.push({
191
206
  path: _relTo(f, root),
@@ -315,13 +330,34 @@ function parse_args(argv: readonly string[]): ParsedArgs {
315
330
  export function main(): number {
316
331
  const args = parse_args(process.argv.slice(2));
317
332
  let findings: Finding[];
333
+ const stats: ScanStats = { entries: 0 };
318
334
  try {
319
- findings = scan(args.root);
335
+ findings = scan(args.root, undefined, stats);
320
336
  } catch (e) {
321
337
  const msg = e instanceof Error ? e.message : String(e);
322
338
  process.stderr.write(`Internal error: ${msg}\n`);
323
339
  return 3;
324
340
  }
341
+ try {
342
+ assertScanned({
343
+ gate: 'lint_one_off_age',
344
+ scanned: stats.entries,
345
+ units: 'one-off entr(y/ies)',
346
+ roots: ['scripts/_one_off'],
347
+ allowEmpty:
348
+ 'OPTIONAL_INPUT: `scripts/_one_off/` is a TTL-managed scratch tree created on ' +
349
+ 'demand and purged by this very policy — an absent or empty root is the ' +
350
+ 'intended steady state, not a lost corpus. Delete the root and zero still ' +
351
+ 'means "no one-off scripts exist", which is exactly what the TTL is for.',
352
+ });
353
+ } catch (e) {
354
+ // 1 is the hard-fail code (2 is argparse, 3 an internal error).
355
+ if (e instanceof DeadScopeError) {
356
+ process.stderr.write(`❌ ${e.message}\n`);
357
+ return 1;
358
+ }
359
+ throw e;
360
+ }
325
361
  if (args.format === 'json') {
326
362
  // json.dumps([asdict(f) for f in findings], indent=2) — keys keep the
327
363
  // dataclass declaration order (path, age_days, severity, reason).
@@ -24,6 +24,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
24
24
  import { parse as parseYaml } from 'yaml';
25
25
 
26
26
  import { resolve_logical } from './_lib/agent_src.js';
27
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
27
28
 
28
29
  const _HERE = fileURLToPath(import.meta.url);
29
30
 
@@ -301,11 +302,30 @@ function main(argv?: readonly string[]): number {
301
302
  if (args.file !== null) {
302
303
  return lint(args.file);
303
304
  }
304
- if (!_isDir(args.dir)) {
305
- return 0; // opt-in directory; absence is not a failure
305
+ const pipelines = _isDir(args.dir) ? _globYamlSorted(args.dir) : [];
306
+ try {
307
+ assertScanned({
308
+ gate: 'lint_orchestration_dsl',
309
+ scanned: pipelines.length,
310
+ units: 'pipeline file(s)',
311
+ roots: ['.agent-config/orchestrations'],
312
+ allowEmpty:
313
+ 'OPTIONAL_INPUT: `.agent-config/orchestrations/` is an opt-in consumer ' +
314
+ 'directory — most projects author no pipelines at all, and the DSL contract ' +
315
+ 'only binds files that exist. Delete the root and zero still means "this ' +
316
+ 'project declares no pipelines", never a corpus that went missing.',
317
+ });
318
+ } catch (exc) {
319
+ // 2 is the file/schema-load code; 1 is reserved for a pipeline that
320
+ // violates the contract.
321
+ if (exc instanceof DeadScopeError) {
322
+ process.stderr.write(`lint_orchestration_dsl: ${exc.message}\n`);
323
+ return 2;
324
+ }
325
+ throw exc;
306
326
  }
307
327
  let rc = 0;
308
- for (const p of _globYamlSorted(args.dir)) {
328
+ for (const p of pipelines) {
309
329
  rc = Math.max(rc, lint(p));
310
330
  }
311
331
  return rc;
@@ -13,6 +13,8 @@ import * as fs from 'node:fs';
13
13
  import * as path from 'node:path';
14
14
  import { fileURLToPath, pathToFileURL } from 'node:url';
15
15
 
16
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
17
+
16
18
  const SCRIPTS_DIR = path.dirname(fileURLToPath(import.meta.url));
17
19
  const ROOT = path.dirname(path.dirname(SCRIPTS_DIR));
18
20
  const DOMAINS = path.join(ROOT, 'src', 'domains');
@@ -28,14 +30,6 @@ export interface Violation {
28
30
  reason: string;
29
31
  }
30
32
 
31
- function isDir(p: string): boolean {
32
- try {
33
- return fs.statSync(p).isDirectory();
34
- } catch {
35
- return false;
36
- }
37
- }
38
-
39
33
  /** Recursively collect files named `command.md`, sorted by POSIX path. */
40
34
  function rglobCommandMd(dir: string): string[] {
41
35
  const out: string[] = [];
@@ -78,10 +72,17 @@ export function _split_frontmatter(text: string): [string, string] {
78
72
 
79
73
  export function check(): Violation[] {
80
74
  const violations: Violation[] = [];
81
- if (!isDir(DOMAINS)) {
82
- return violations;
83
- }
84
- for (const filePath of rglobCommandMd(DOMAINS)) {
75
+ // Assert the whole walk, not the `auto_detect:`-carrying subset: opting out
76
+ // is the common case, so "0 opted in" is normal while "0 command files" can
77
+ // only mean the domains root moved out from under the literal above.
78
+ const commands = rglobCommandMd(DOMAINS);
79
+ assertScanned({
80
+ gate: 'lint_orchestrator_auto_detect',
81
+ scanned: commands.length,
82
+ units: 'command file(s)',
83
+ roots: ['src/domains'],
84
+ });
85
+ for (const filePath of commands) {
85
86
  const text = fs.readFileSync(filePath, 'utf-8');
86
87
  const [fm, body] = _split_frontmatter(text);
87
88
  const m = AUTO_DETECT_RE.exec(fm);
@@ -149,6 +150,12 @@ export function main(argv?: readonly string[]): number {
149
150
  try {
150
151
  violations = check();
151
152
  } catch (exc) {
153
+ // 3 = "the gate could not run" (1 is reserved for contract violations
154
+ // it actually found), so a dead scan root reports as the former.
155
+ if (exc instanceof DeadScopeError) {
156
+ process.stderr.write(`❌ ${exc.message}\n`);
157
+ return 3;
158
+ }
152
159
  const msg = exc instanceof Error ? exc.message : String(exc);
153
160
  process.stderr.write(`❌ lint-orchestrator-auto-detect: internal error: ${msg}\n`);
154
161
  return 3;
@@ -57,6 +57,7 @@ import * as fs from 'node:fs';
57
57
  import * as path from 'node:path';
58
58
  import { fileURLToPath, pathToFileURL } from 'node:url';
59
59
 
60
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
60
61
  import { shingles } from './_lib/shingle_similarity.js';
61
62
 
62
63
  const _HERE = fileURLToPath(import.meta.url);
@@ -411,6 +412,27 @@ export function main(argv?: string[]): number {
411
412
  else { process.stderr.write(`unexpected argument: ${a} (did you mean --changed ${a}?)\n${_usage()}`); return 2; }
412
413
  }
413
414
 
415
+ // Asserted for BOTH modes, and before either: `--changed` compares each
416
+ // candidate against the same corpus, so a dead root there reports "no
417
+ // overlap" instead of "nothing to compare against" — the quieter failure of
418
+ // the two. Supersedes the post-corpus `scanned === 0` check the full audit
419
+ // used to carry alone; exit 3 is that check's code, kept so 1 still means
420
+ // "a real overlap was found".
421
+ try {
422
+ assertScanned({
423
+ gate: 'lint_originality',
424
+ scanned: CLASSES.reduce((n, spec) => n + spec.files().length, 0),
425
+ units: 'corpus file(s)',
426
+ roots: ['src/skills', 'src/agent-src/personas', 'src/domains'],
427
+ });
428
+ } catch (e) {
429
+ if (e instanceof DeadScopeError) {
430
+ process.stderr.write(`❌ ${e.message}\n`);
431
+ return 3;
432
+ }
433
+ throw e;
434
+ }
435
+
414
436
  if (changedFiles !== null) {
415
437
  const { pairs, fails } = _changed(changedFiles);
416
438
  if (pairs.length > 0) {
@@ -428,10 +450,6 @@ export function main(argv?: string[]): number {
428
450
  // Full audit
429
451
  const byClass = _corpus();
430
452
  const scanned = [...byClass.values()].reduce((n, a) => n + a.length, 0);
431
- if (scanned === 0) {
432
- process.stderr.write('❌ No artifacts found under the corpus roots.\n');
433
- return 3;
434
- }
435
453
  const dist = _distribution(byClass);
436
454
  const pairs = _fullPairs(byClass);
437
455
  _writeReport(byClass, pairs, dist);
@@ -32,6 +32,8 @@ import * as fs from 'node:fs';
32
32
  import * as path from 'node:path';
33
33
  import { fileURLToPath, pathToFileURL } from 'node:url';
34
34
 
35
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
36
+
35
37
  const _HERE = path.dirname(fileURLToPath(import.meta.url));
36
38
  const REPO = path.resolve(_HERE, '..', '..');
37
39
 
@@ -84,8 +86,12 @@ function _shingles(words: string[]): Set<string> {
84
86
  return out;
85
87
  }
86
88
 
87
- function _collect(): Doc[] {
89
+ /** `read` counts every corpus file opened, `docs` only those long enough to
90
+ * compare — a corpus of stubs is a different problem from a corpus that is not
91
+ * there, and only the second one means the roots below have rotted. */
92
+ function _collect(): { docs: Doc[]; read: number } {
88
93
  const docs: Doc[] = [];
94
+ let read = 0;
89
95
  const push = (id: string, file: string): void => {
90
96
  let body: string;
91
97
  try {
@@ -93,6 +99,7 @@ function _collect(): Doc[] {
93
99
  } catch {
94
100
  return;
95
101
  }
102
+ read += 1;
96
103
  const sh = _shingles(_normalize(body));
97
104
  if (sh.size >= 20) {
98
105
  docs.push({ id, shingles: sh });
@@ -132,7 +139,7 @@ function _collect(): Doc[] {
132
139
  }
133
140
  }
134
141
  }
135
- return docs;
142
+ return { docs, read };
136
143
  }
137
144
 
138
145
  function _jaccardPct(a: Set<string>, b: Set<string>): number {
@@ -162,7 +169,25 @@ export function main(argv: string[] = process.argv.slice(2)): number {
162
169
  else if (arg === '--quiet') quiet = true;
163
170
  }
164
171
 
165
- const docs = _collect();
172
+ const { docs, read } = _collect();
173
+ // Two of the three roots are behind `existsSync` guards, which is exactly
174
+ // how the personas root went missing for a whole release (see `_collect`).
175
+ // Exit 1 is the only failure this gate has; report-only mode included, since
176
+ // an empty corpus makes the report itself a false all-clear.
177
+ try {
178
+ assertScanned({
179
+ gate: 'lint_originality_shingles',
180
+ scanned: read,
181
+ units: 'corpus file(s)',
182
+ roots: ['src/skills', 'src/agent-src/personas', 'src/subagents'],
183
+ });
184
+ } catch (e) {
185
+ if (e instanceof DeadScopeError) {
186
+ process.stderr.write(`❌ ${e.message}\n`);
187
+ return 1;
188
+ }
189
+ throw e;
190
+ }
166
191
  const pairs: Array<{ a: string; b: string; pct: number }> = [];
167
192
  for (let i = 0; i < docs.length; i++) {
168
193
  for (let j = i + 1; j < docs.length; j++) {
@@ -26,6 +26,8 @@ import fs from "node:fs";
26
26
  import path from "node:path";
27
27
  import { fileURLToPath } from "node:url";
28
28
 
29
+ import { assertScanned, DeadScopeError } from "./_lib/scan_scope.js";
30
+
29
31
  const _HERE = path.dirname(fileURLToPath(import.meta.url));
30
32
  const REPO_ROOT = path.resolve(_HERE, "..", "..");
31
33
 
@@ -191,6 +193,23 @@ function main(): void {
191
193
  const quiet = process.argv.includes("--quiet");
192
194
 
193
195
  const files = collectFiles();
196
+ // `walkDir` returns nothing for a root that does not exist, so all three
197
+ // literals above can rot into a clean run. Exit 1 is the "internal error"
198
+ // slot; 2 means findings were actually located and must stay distinct.
199
+ try {
200
+ assertScanned({
201
+ gate: "lint_output_slop",
202
+ scanned: files.length,
203
+ units: "file(s)",
204
+ roots: ["src/skills", "src/rules", "docs/guidelines"],
205
+ });
206
+ } catch (e) {
207
+ if (e instanceof DeadScopeError) {
208
+ process.stderr.write(`❌ ${e.message}\n`);
209
+ process.exit(1);
210
+ }
211
+ throw e;
212
+ }
194
213
  const allFindings: Finding[] = [];
195
214
 
196
215
  for (const file of files) {
@@ -47,6 +47,7 @@ import * as path from 'node:path';
47
47
  import { fileURLToPath, pathToFileURL } from 'node:url';
48
48
 
49
49
  import { KERNEL_RULE_IDS, is_kernel_rule } from './_lib/kernel_rules.js';
50
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
50
51
 
51
52
  const _HERE = path.dirname(fileURLToPath(import.meta.url));
52
53
  const REPO_ROOT = path.resolve(_HERE, '..', '..');
@@ -220,6 +221,35 @@ export function audit(): OverrideRow[] {
220
221
  function main(argv: string[]): number {
221
222
  const as_json = argv.includes('--json');
222
223
  const strict = argv.includes('--strict');
224
+
225
+ // Not the override corpus: an empty `agents/overrides/rules/` is a real and
226
+ // common state (the report says so in words). The scope that cannot be
227
+ // allowed to die is `safety_floor_ids`' input — with src/rules gone the
228
+ // floor set is empty, every safety-floor override downgrades to "ordinary",
229
+ // and the replace/registration checks stop firing while the audit still
230
+ // reads clean. Exit 2 is this CLI's env-error slot; 1 stays "contract
231
+ // violated".
232
+ let rule_sources = 0;
233
+ try {
234
+ rule_sources = fs.readdirSync(RULES_DIR).filter((n) => n.endsWith('.md')).length;
235
+ } catch {
236
+ rule_sources = 0;
237
+ }
238
+ try {
239
+ assertScanned({
240
+ gate: 'lint_override_kernel_guard',
241
+ scanned: rule_sources,
242
+ units: 'rule source(s)',
243
+ roots: ['src/rules'],
244
+ });
245
+ } catch (e) {
246
+ if (e instanceof DeadScopeError) {
247
+ process.stderr.write(`❌ ${e.message}\n`);
248
+ return 2;
249
+ }
250
+ throw e;
251
+ }
252
+
223
253
  const rows = audit();
224
254
 
225
255
  if (as_json) {
@@ -21,6 +21,8 @@ import * as fs from 'node:fs';
21
21
  import * as path from 'node:path';
22
22
  import { fileURLToPath, pathToFileURL } from 'node:url';
23
23
 
24
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
25
+
24
26
  const _HERE = fileURLToPath(import.meta.url);
25
27
  // src/scripts/lint_pack_first_win.ts → parents[2] is the repo root.
26
28
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -142,6 +144,7 @@ function _has_onboarding_block(packYaml: string): [boolean, string[]] {
142
144
 
143
145
  function main(): number {
144
146
  const errors: string[] = [];
147
+ let homesResolved = 0;
145
148
  for (const pid of [...FEATURED_PACK_IDS].sort()) {
146
149
  const packDir = _pack_home(pid);
147
150
  if (packDir === null) {
@@ -151,6 +154,7 @@ function main(): number {
151
154
  );
152
155
  continue;
153
156
  }
157
+ homesResolved += 1;
154
158
  const firstWin = path.join(packDir, 'FIRST_WIN.md');
155
159
  let firstWinOk = false;
156
160
  try {
@@ -169,6 +173,25 @@ function main(): number {
169
173
  );
170
174
  }
171
175
  }
176
+ // The success line below reports FEATURED_PACK_IDS.size — a constant that
177
+ // stays 5 no matter what is on disk. This asserts the number that does move:
178
+ // pack homes actually resolved through the three-candidate precedence. Zero
179
+ // means every candidate root is gone, which is a layout migration, not five
180
+ // separate missing packs. Exit 1 is the gate's only failure code.
181
+ try {
182
+ assertScanned({
183
+ gate: 'lint_pack_first_win',
184
+ scanned: homesResolved,
185
+ units: 'featured pack home(s)',
186
+ roots: ['src/packs', 'src/domains', 'packages'],
187
+ });
188
+ } catch (exc) {
189
+ if (exc instanceof DeadScopeError) {
190
+ process.stderr.write(`❌ ${exc.message}\n`);
191
+ return 1;
192
+ }
193
+ throw exc;
194
+ }
172
195
  if (errors.length > 0) {
173
196
  process.stderr.write('❌ pack first-win lint failed:\n');
174
197
  for (const e of errors) {
@@ -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 QUIET = process.argv.includes('--quiet');
22
24
  const _HERE = fileURLToPath(import.meta.url);
23
25
  const REPO = path.resolve(path.dirname(_HERE), '..', '..');
@@ -60,9 +62,23 @@ function _print(s: string): void {
60
62
  }
61
63
 
62
64
  export function main(): number {
63
- if (!fs.existsSync(PACKS_YML)) {
64
- _print('⚠️ packs.yml not found — skipping risk_class lint');
65
- return 0;
65
+ // packs.yml IS the scope — there is nowhere else a `risk_class: high`
66
+ // declaration can hide. Replaces the "not found → skip, exit 0" branch,
67
+ // which turned a moved declaration file into a permanent green. Exit 1 is
68
+ // the only failure code; here it means the invariant could not be checked,
69
+ // not that a pack violates it.
70
+ try {
71
+ assertWatchlistResolves({
72
+ gate: 'lint_pack_risk_class',
73
+ candidates: [path.relative(REPO, PACKS_YML)],
74
+ repoRoot: REPO,
75
+ });
76
+ } catch (exc) {
77
+ if (exc instanceof DeadScopeError) {
78
+ process.stderr.write(`❌ ${exc.message}\n`);
79
+ return 1;
80
+ }
81
+ throw exc;
66
82
  }
67
83
  const violations = riskClassViolations(fs.readFileSync(PACKS_YML, 'utf-8'));
68
84
  if (violations.length === 0) {
@@ -28,6 +28,7 @@ import * as fs from 'node:fs';
28
28
  import * as path from 'node:path';
29
29
  import { fileURLToPath } from 'node:url';
30
30
 
31
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
31
32
  import type { Finding, WaiverRecord } from './_lib/persistence/types.js';
32
33
  import { scan_dir as scan_sql_dir } from './_lib/persistence/adapter_raw_sql.js';
33
34
  import { scan_dir as scan_n1_dir } from './_lib/persistence/detect_n1_eloquent.js';
@@ -150,6 +151,24 @@ function main(argv: string[]): number {
150
151
  }
151
152
 
152
153
  const resolved_stacks = stacks.length > 0 ? stacks : detect_stacks(dir);
154
+ // `--dir` existing is not the same as `--dir` holding anything scannable:
155
+ // every adapter is keyed off a resolved stack, so zero stacks provably
156
+ // reads zero files while still printing "no gate findings". Exit 2 is the
157
+ // usage/env slot this CLI already uses for an unusable `--dir`.
158
+ try {
159
+ assertScanned({
160
+ gate: 'lint_persistence',
161
+ scanned: resolved_stacks.length,
162
+ units: 'stack(s) to scan',
163
+ roots: [dir],
164
+ });
165
+ } catch (e) {
166
+ if (e instanceof DeadScopeError) {
167
+ process.stderr.write(`❌ ${e.message}\n`);
168
+ return 2;
169
+ }
170
+ throw e;
171
+ }
153
172
  const report = run_lint(dir, resolved_stacks, audit_scope);
154
173
 
155
174
  if (format === 'json') {
@@ -22,6 +22,8 @@ import * as fs from 'node:fs';
22
22
  import * as path from 'node:path';
23
23
  import { fileURLToPath, pathToFileURL } from 'node:url';
24
24
 
25
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
26
+
25
27
  const QUIET = process.argv.includes('--quiet');
26
28
 
27
29
  const _HERE = fileURLToPath(import.meta.url);
@@ -214,9 +216,22 @@ function citations_for(personaId: string): string[] {
214
216
 
215
217
  function main(): number {
216
218
  const personas = collect_personas();
217
- if (personas.length === 0) {
218
- emit('persona-governance: no persona files found — nothing to lint.');
219
- return 0;
219
+ // Zero personas used to exit 0 with "nothing to lint" — but this gate has
220
+ // already been inert once on a stale root (see PERSONA_DIR above), and that
221
+ // message is indistinguishable from the root having moved again.
222
+ try {
223
+ assertScanned({
224
+ gate: 'lint_persona_governance',
225
+ scanned: personas.length,
226
+ units: 'persona file(s)',
227
+ roots: ['src/agent-src/personas'],
228
+ });
229
+ } catch (exc) {
230
+ if (exc instanceof DeadScopeError) {
231
+ process.stderr.write(`❌ ${exc.message}\n`);
232
+ return 1;
233
+ }
234
+ throw exc;
220
235
  }
221
236
 
222
237
  const byDomain = new Map<string, string[]>();
@@ -22,6 +22,8 @@ import * as path from 'node:path';
22
22
  import { fileURLToPath, pathToFileURL } from 'node:url';
23
23
  import { parse as parseYaml } from 'yaml';
24
24
 
25
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
26
+
25
27
  const _HERE = fileURLToPath(import.meta.url);
26
28
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
27
29
  const README = path.join(REPO_ROOT, 'README.md');
@@ -155,6 +157,27 @@ function parse_args(argv: readonly string[]): ParsedArgs {
155
157
  export function main(): number {
156
158
  const args = parse_args(process.argv.slice(2));
157
159
 
160
+ // Four named surfaces ARE the scope; the gate only compares them against
161
+ // each other, so if the set moved there is nothing left to disagree. Asserted
162
+ // before the reads so a relocated repo layout names the roots instead of
163
+ // surfacing as a bare ENOENT stack. Exit 1 is the drift code and doubles as
164
+ // "could not run" (2 is reserved for the missing-YAML-runtime case).
165
+ try {
166
+ assertWatchlistResolves({
167
+ gate: 'lint_positioning',
168
+ candidates: [README, PACKAGE_JSON, ABOUT_YML, TOPICS_YML].map((p) =>
169
+ path.relative(REPO_ROOT, p),
170
+ ),
171
+ repoRoot: REPO_ROOT,
172
+ });
173
+ } catch (e) {
174
+ if (e instanceof DeadScopeError) {
175
+ process.stderr.write(`❌ ${e.message}\n`);
176
+ return 1;
177
+ }
178
+ throw e;
179
+ }
180
+
158
181
  let anchor: string;
159
182
  let pkg_desc: string;
160
183
  let about_desc: string;