@event4u/agent-config 9.20.0 → 9.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +86 -0
  3. package/dist/agent-src/commands/worktree/cleanup.md +17 -0
  4. package/dist/agent-src/commands/worktree/status.md +6 -0
  5. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +22 -0
  6. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  7. package/dist/cli-delegate/{chunk-XGA2NTU4.js → chunk-3EWYC3YK.js} +40 -0
  8. package/dist/cli-delegate/cmd_handoff.js +1 -1
  9. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  10. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  11. package/dist/discovery/deprecation-report.md +1 -1
  12. package/dist/discovery/discovery-manifest.json +5 -5
  13. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  14. package/dist/discovery/discovery-manifest.summary.md +1 -1
  15. package/dist/discovery/orphan-report.md +1 -1
  16. package/dist/discovery/packs.json +2 -2
  17. package/dist/discovery/trust-report.md +1 -1
  18. package/dist/discovery/workspaces.json +2 -2
  19. package/dist/hooks/dispatch.js +58 -2
  20. package/dist/mcp/registry-manifest.json +2 -2
  21. package/dist/mcp/server.json +2 -2
  22. package/dist/mcp/server.mjs +47 -29
  23. package/package.json +1 -1
  24. package/src/config/gate-coverage.yml +22 -4
  25. package/src/config/gate-violation-baselines.json +3 -8
  26. package/src/scripts/_lib/bench_ab_activation.ts +368 -0
  27. package/src/scripts/_lib/gate_population.ts +70 -0
  28. package/src/scripts/_lib/roadmap_granularity.ts +157 -0
  29. package/src/scripts/_lib/scan_scope.ts +44 -0
  30. package/src/scripts/_lib/stdin.ts +82 -0
  31. package/src/scripts/audit_adr_coverage.ts +38 -0
  32. package/src/scripts/audit_auto_rules.ts +17 -3
  33. package/src/scripts/audit_command_surface.ts +51 -0
  34. package/src/scripts/audit_initial_context.ts +39 -3
  35. package/src/scripts/audit_mcp_tools.ts +25 -0
  36. package/src/scripts/audit_overlap.ts +24 -1
  37. package/src/scripts/audit_skill_descriptions.ts +19 -0
  38. package/src/scripts/{verify_before_complete_hook.ts → before_complete_hook.ts} +16 -3
  39. package/src/scripts/bench_ab_task_runner.ts +18 -0
  40. package/src/scripts/bench_ab_v2_run.ts +147 -6
  41. package/src/scripts/bench_ab_v2_stats.ts +74 -2
  42. package/src/scripts/check_adr_frontmatter.ts +23 -3
  43. package/src/scripts/check_always_budget.ts +24 -0
  44. package/src/scripts/check_artefact_checksums.ts +24 -1
  45. package/src/scripts/check_artefact_count_messaging.ts +20 -0
  46. package/src/scripts/check_augmentignore.ts +22 -15
  47. package/src/scripts/check_backstop_debt.ts +22 -0
  48. package/src/scripts/check_beta_review_markers.ts +22 -1
  49. package/src/scripts/check_bridge_derivation.ts +22 -1
  50. package/src/scripts/check_bundle_path_leakage.ts +26 -0
  51. package/src/scripts/check_ci_strict_superset.ts +21 -3
  52. package/src/scripts/check_claims.ts +22 -0
  53. package/src/scripts/check_cli_registry_budget_sync.ts +33 -4
  54. package/src/scripts/check_cluster_patterns.ts +26 -4
  55. package/src/scripts/check_command_count_messaging.ts +20 -4
  56. package/src/scripts/check_comparison.ts +18 -0
  57. package/src/scripts/check_condensed_paths.ts +23 -2
  58. package/src/scripts/check_consumer_scope_flip.ts +21 -2
  59. package/src/scripts/check_council_config_location.ts +20 -0
  60. package/src/scripts/check_council_layout.ts +22 -2
  61. package/src/scripts/check_council_references.ts +23 -1
  62. package/src/scripts/check_dependency_floors.ts +19 -3
  63. package/src/scripts/check_discovery_determinism.ts +26 -0
  64. package/src/scripts/check_enforcement_coverage.ts +20 -0
  65. package/src/scripts/check_evaluator_budgets.ts +23 -0
  66. package/src/scripts/check_finding_dispositions.ts +29 -0
  67. package/src/scripts/check_gate_coverage.ts +63 -22
  68. package/src/scripts/check_gate_paths.ts +30 -5
  69. package/src/scripts/check_generated_artefact_headers.ts +21 -0
  70. package/src/scripts/check_generator_output_coverage.ts +23 -0
  71. package/src/scripts/check_gitignore_freshness.ts +23 -0
  72. package/src/scripts/check_host_loadability.ts +34 -8
  73. package/src/scripts/check_installer_import_purity.ts +22 -8
  74. package/src/scripts/check_kernel_prefix_stability.ts +19 -1
  75. package/src/scripts/check_kernel_rule_bundle.ts +23 -0
  76. package/src/scripts/check_knowledge_cards.ts +13 -0
  77. package/src/scripts/check_knowledge_pages.ts +23 -0
  78. package/src/scripts/check_knowledge_sharing.ts +27 -0
  79. package/src/scripts/check_md_language.ts +24 -0
  80. package/src/scripts/check_media_deps.ts +23 -0
  81. package/src/scripts/check_memory.ts +26 -1
  82. package/src/scripts/check_memory_contradiction.ts +16 -1
  83. package/src/scripts/check_memory_proposal.ts +15 -0
  84. package/src/scripts/check_memory_similarity.ts +16 -0
  85. package/src/scripts/check_module_management_neutral.ts +19 -11
  86. package/src/scripts/check_no_conflict_markers.ts +22 -1
  87. package/src/scripts/check_no_external_sources.ts +26 -1
  88. package/src/scripts/check_no_local_settings_committed.ts +37 -10
  89. package/src/scripts/check_no_new_legacy_path.ts +30 -1
  90. package/src/scripts/check_one_off_location.ts +43 -0
  91. package/src/scripts/check_originality_freshness.ts +25 -1
  92. package/src/scripts/check_overlay_cascade_subdirs.ts +21 -0
  93. package/src/scripts/check_pack_size.ts +22 -0
  94. package/src/scripts/check_preamble_payload_budget.ts +35 -5
  95. package/src/scripts/check_proposal.ts +24 -1
  96. package/src/scripts/check_public_catalog_links.ts +27 -0
  97. package/src/scripts/check_public_links.ts +19 -0
  98. package/src/scripts/check_quality_regression.ts +19 -0
  99. package/src/scripts/check_reach_channels.ts +44 -0
  100. package/src/scripts/check_reach_staleness.ts +27 -0
  101. package/src/scripts/check_release_adjacent_health.ts +28 -0
  102. package/src/scripts/check_release_highlights.ts +34 -0
  103. package/src/scripts/check_release_includes_discovery.ts +25 -6
  104. package/src/scripts/check_release_pr_shape.ts +18 -1
  105. package/src/scripts/check_release_published.ts +21 -0
  106. package/src/scripts/check_release_surface_equality.ts +22 -0
  107. package/src/scripts/check_release_trunk_sync.ts +19 -0
  108. package/src/scripts/check_reply_consistency.ts +22 -0
  109. package/src/scripts/check_roadmap_trackable.ts +20 -0
  110. package/src/scripts/check_role_doc_links.ts +23 -0
  111. package/src/scripts/check_rule_invariants.ts +14 -0
  112. package/src/scripts/check_secret_leak.ts +47 -0
  113. package/src/scripts/check_skill_gaps.ts +32 -0
  114. package/src/scripts/check_skill_requires.ts +19 -0
  115. package/src/scripts/check_source_pointer_freshness.ts +20 -0
  116. package/src/scripts/check_structural_breaking.ts +29 -2
  117. package/src/scripts/check_surface_tiers.ts +34 -0
  118. package/src/scripts/check_template_pin_drift.ts +23 -1
  119. package/src/scripts/check_test_coverage_diff.ts +17 -1
  120. package/src/scripts/check_token_optimizer_freshness.ts +18 -3
  121. package/src/scripts/check_token_quality_golden.ts +21 -0
  122. package/src/scripts/check_token_regression.ts +25 -0
  123. package/src/scripts/check_tracked_but_ignored.ts +26 -0
  124. package/src/scripts/check_trigger_eval_presence.ts +23 -1
  125. package/src/scripts/check_trigger_evals.ts +36 -0
  126. package/src/scripts/check_trunk_drift.ts +23 -0
  127. package/src/scripts/check_update_banner.ts +24 -0
  128. package/src/scripts/gate_scope_classify.ts +513 -0
  129. package/src/scripts/hook_manifest.yaml +1 -1
  130. package/src/scripts/hooks/block_kernel_rule_writes.ts +1 -1
  131. package/src/scripts/hooks/concern_registry.ts +2 -2
  132. package/src/scripts/lint_agent_security.ts +19 -0
  133. package/src/scripts/lint_agent_skill_names.ts +30 -0
  134. package/src/scripts/lint_agents_layout.ts +20 -0
  135. package/src/scripts/lint_agents_md.ts +20 -0
  136. package/src/scripts/lint_archived_skills.ts +24 -3
  137. package/src/scripts/lint_behavioural_eval_freshness.ts +29 -3
  138. package/src/scripts/lint_bench_ab.ts +25 -0
  139. package/src/scripts/lint_bench_corpus.ts +20 -4
  140. package/src/scripts/lint_breaking_changes_index.ts +24 -0
  141. package/src/scripts/lint_budget_ownership.ts +20 -3
  142. package/src/scripts/lint_changelog_rollback.ts +24 -0
  143. package/src/scripts/lint_command_flow_coverage.ts +22 -0
  144. package/src/scripts/lint_command_tiers.ts +29 -5
  145. package/src/scripts/lint_commit_subjects.ts +15 -0
  146. package/src/scripts/lint_confusables.ts +24 -0
  147. package/src/scripts/lint_consumer_internal_refs.ts +24 -0
  148. package/src/scripts/lint_design_quality.ts +22 -0
  149. package/src/scripts/lint_design_slop.ts +23 -0
  150. package/src/scripts/lint_discovery_manifest.ts +23 -0
  151. package/src/scripts/lint_discovery_vocabulary.ts +19 -0
  152. package/src/scripts/lint_documented_commands.ts +23 -0
  153. package/src/scripts/lint_empty_roadmaps.ts +23 -0
  154. package/src/scripts/lint_eval_fixture_citations.ts +19 -0
  155. package/src/scripts/lint_eval_freshness.ts +35 -0
  156. package/src/scripts/lint_examples.ts +18 -3
  157. package/src/scripts/lint_explain_trace.ts +23 -0
  158. package/src/scripts/lint_featured_skills.ts +26 -1
  159. package/src/scripts/lint_flows.ts +19 -0
  160. package/src/scripts/lint_frontmatter_boilerplate.ts +20 -0
  161. package/src/scripts/lint_ghostwriter_source.ts +26 -6
  162. package/src/scripts/lint_glama_drift.ts +24 -0
  163. package/src/scripts/lint_global_paths.ts +24 -0
  164. package/src/scripts/lint_governed_writes.ts +20 -0
  165. package/src/scripts/lint_hidden_unicode.ts +25 -0
  166. package/src/scripts/lint_hook_concern_budget.ts +39 -6
  167. package/src/scripts/lint_hook_manifest.ts +22 -0
  168. package/src/scripts/lint_instruction_smuggling.ts +23 -0
  169. package/src/scripts/lint_knowledge_scale.ts +32 -2
  170. package/src/scripts/lint_legal_pack.ts +31 -0
  171. package/src/scripts/lint_marketplace.ts +26 -0
  172. package/src/scripts/lint_marketplace_install_completeness.ts +23 -0
  173. package/src/scripts/lint_mcp_config_security.ts +24 -0
  174. package/src/scripts/lint_mcp_registry_manifest.ts +22 -1
  175. package/src/scripts/lint_memory_tripwire.ts +35 -0
  176. package/src/scripts/lint_missions.ts +23 -7
  177. package/src/scripts/lint_model_tier_coverage.ts +18 -0
  178. package/src/scripts/lint_namespace_collisions.ts +19 -0
  179. package/src/scripts/lint_no_new_atomic_commands.ts +33 -0
  180. package/src/scripts/lint_no_python_twin_rationale.ts +18 -0
  181. package/src/scripts/lint_one_off_age.ts +38 -2
  182. package/src/scripts/lint_orchestration_dsl.ts +23 -3
  183. package/src/scripts/lint_orchestrator_auto_detect.ts +19 -12
  184. package/src/scripts/lint_originality.ts +22 -4
  185. package/src/scripts/lint_originality_shingles.ts +28 -3
  186. package/src/scripts/lint_output_slop.ts +19 -0
  187. package/src/scripts/lint_override_kernel_guard.ts +30 -0
  188. package/src/scripts/lint_pack_first_win.ts +23 -0
  189. package/src/scripts/lint_pack_risk_class.ts +19 -3
  190. package/src/scripts/lint_persistence.ts +19 -0
  191. package/src/scripts/lint_persona_governance.ts +18 -3
  192. package/src/scripts/lint_positioning.ts +23 -0
  193. package/src/scripts/lint_pre_migration_refs.ts +24 -0
  194. package/src/scripts/lint_profile_overlay_set_only.ts +24 -0
  195. package/src/scripts/lint_provenance.ts +21 -0
  196. package/src/scripts/lint_provenance_vocabulary.ts +24 -1
  197. package/src/scripts/lint_readme_jargon.ts +21 -3
  198. package/src/scripts/lint_readme_serial_comma.ts +21 -3
  199. package/src/scripts/lint_readme_size.ts +21 -3
  200. package/src/scripts/lint_regression.ts +28 -0
  201. package/src/scripts/lint_roadmap_blockers.ts +43 -0
  202. package/src/scripts/lint_roadmap_ci_steps.ts +45 -0
  203. package/src/scripts/lint_roadmap_complexity.ts +47 -0
  204. package/src/scripts/lint_roadmap_later_disposition.ts +23 -0
  205. package/src/scripts/lint_rule_interactions.ts +16 -2
  206. package/src/scripts/lint_rule_tiers.ts +19 -6
  207. package/src/scripts/lint_showcase_sessions.ts +19 -3
  208. package/src/scripts/lint_skill_descriptions.ts +19 -0
  209. package/src/scripts/lint_skill_frontmatter_safety.ts +24 -0
  210. package/src/scripts/lint_skill_originality.ts +18 -3
  211. package/src/scripts/lint_skill_scripts_readonly.ts +20 -0
  212. package/src/scripts/lint_skill_tools.ts +84 -145
  213. package/src/scripts/lint_spawn_payload.ts +27 -1
  214. package/src/scripts/lint_store_boundary.ts +19 -0
  215. package/src/scripts/lint_subagent_determinism.ts +23 -1
  216. package/src/scripts/lint_supported_tools_matrix.ts +18 -0
  217. package/src/scripts/lint_surface_matrix.ts +19 -3
  218. package/src/scripts/lint_ticket_buildable.ts +23 -0
  219. package/src/scripts/lint_topics_yaml.ts +19 -12
  220. package/src/scripts/lint_trigger_precision.ts +27 -0
  221. package/src/scripts/lint_trust_coherence.ts +21 -0
  222. package/src/scripts/lint_ui_stack_bundles.ts +17 -3
  223. package/src/scripts/lint_value_dashboard.ts +19 -3
  224. package/src/scripts/lint_versioned_cache.ts +19 -1
  225. package/src/scripts/lint_workflow_security.ts +22 -14
  226. package/src/scripts/pr_url_reminder_hook.ts +1 -1
  227. package/src/scripts/skill_collision_clusters.ts +37 -3
  228. package/src/scripts/skill_discovery.ts +20 -0
  229. package/src/scripts/skill_eval_coverage.ts +22 -0
  230. package/src/scripts/skill_overlap.ts +32 -6
  231. package/src/scripts/skill_preview.ts +31 -0
  232. package/src/scripts/skill_trigger_eval.ts +18 -0
  233. package/src/scripts/skill_usage_collect.ts +21 -0
  234. package/src/scripts/skill_usage_report.ts +21 -0
  235. package/src/scripts/sweep_dead_scan_roots.ts +11 -3
  236. package/src/scripts/verify_physical_move.ts +24 -0
  237. package/src/scripts/worktree_cleanup_check.ts +319 -4
  238. package/src/scripts/check_bite_sized_granularity.ts +0 -116
  239. package/src/scripts/lint_workspace_boundary.ts +0 -267
@@ -19,6 +19,8 @@ import * as path from 'node:path';
19
19
  import { fileURLToPath, pathToFileURL } from 'node:url';
20
20
  import { parse as parseYaml } from 'yaml';
21
21
 
22
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
23
+
22
24
  const _HERE = path.resolve(fileURLToPath(import.meta.url));
23
25
  // REPO_ROOT = Path(__file__).resolve().parent.parent.parent
24
26
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -179,6 +181,17 @@ function lint(quiet: boolean): number {
179
181
  const workspaces = _load(WORKSPACES_YML);
180
182
  const packs = _load(PACKS_YML);
181
183
 
184
+ // `_load` accepts an empty YAML list, so two present-but-emptied files read
185
+ // as a vocabulary of nothing. Assert the entries actually loaded — the ADR
186
+ // parity check below would report that as missing ids, which describes the
187
+ // symptom and not the cause.
188
+ assertScanned({
189
+ gate: 'lint_discovery_vocabulary',
190
+ scanned: workspaces.length + packs.length,
191
+ units: 'vocabulary entr(y/ies)',
192
+ roots: [_relToRoot(WORKSPACES_YML), _relToRoot(PACKS_YML)],
193
+ });
194
+
182
195
  const ws_ids = new Set<unknown>(workspaces.map((entry) => entry['id']));
183
196
  const pack_ids = new Set<unknown>(packs.map((entry) => entry['id']));
184
197
 
@@ -373,6 +386,12 @@ function main(): number {
373
386
  process.stderr.write(`${err.detail}\n`);
374
387
  return 1;
375
388
  }
389
+ if (err instanceof DeadScopeError) {
390
+ // Same code as a missing vocabulary file (1) — the gate has one
391
+ // failure code, and 2 belongs to the pinned YAML-loader contract.
392
+ process.stderr.write(`❌ ${err.message}\n`);
393
+ return 1;
394
+ }
376
395
  throw err;
377
396
  }
378
397
  }
@@ -32,6 +32,8 @@ import * as path from 'node:path';
32
32
  import { fileURLToPath, pathToFileURL } from 'node:url';
33
33
  import { load as yamlLoad } from 'js-yaml';
34
34
 
35
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
36
+
35
37
  // NOTE: deliberately no runtime module dependency on the CLI registry —
36
38
  // src/cli/ is outside the package.json `files` whitelist, so importing it
37
39
  // would crash a global install (prepack-check). The registry file is parsed
@@ -625,6 +627,27 @@ function main(): number {
625
627
  const scanTrees: ScanTree[] = args.scanRoots.length
626
628
  ? args.scanRoots.map((dir) => ({ dir, match: (n: string) => n.endsWith('.md') }))
627
629
  : [...DEFAULT_SCAN_TREES];
630
+ // `_collectTargets` skips a scan tree whose dir is absent, so a renamed
631
+ // src/skills (or a --scan-root typo) yields zero targets and the run prints
632
+ // "Every documented package command resolves" having opened no doc. Exit 3
633
+ // matches the scan-failure code below: no targets means the linter did not
634
+ // run, which is not "0 violations" (0).
635
+ const targets = _collectTargets(args.root, scanTrees).length;
636
+ try {
637
+ assertScanned({
638
+ gate: 'lint_documented_commands',
639
+ scanned: targets,
640
+ units: 'doc file(s)',
641
+ roots: scanTrees.map((t) => t.dir),
642
+ });
643
+ } catch (exc) {
644
+ if (exc instanceof DeadScopeError) {
645
+ process.stderr.write(`${exc.message}\n`);
646
+ return 3;
647
+ }
648
+ throw exc;
649
+ }
650
+
628
651
  let violations: Violation[];
629
652
  try {
630
653
  violations = scan(args.root, scanTrees);
@@ -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 { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
19
+
18
20
  /** Mirror `QUIET = "--quiet" in sys.argv` (computed at import). */
19
21
  const QUIET = process.argv.slice(2).includes('--quiet');
20
22
 
@@ -115,6 +117,27 @@ function find_empty_roadmaps(root: string): string[] {
115
117
 
116
118
  function main(): number {
117
119
  const root = _repo_root();
120
+ // The scanned unit is every roadmap `*.md`, never the empty ones: this gate
121
+ // reports an ABSENCE, so "no empty roadmaps" and "no roadmaps at all" print
122
+ // the same green line. `_repo_root()` falls back to CWD when it finds no
123
+ // `agents/roadmaps` anywhere up the chain, which is exactly how a moved tree
124
+ // reaches zero silently.
125
+ const base = path.join(root, ROADMAP_DIR);
126
+ try {
127
+ assertScanned({
128
+ gate: 'lint_empty_roadmaps',
129
+ scanned: _isDir(base) ? _rglobMdSorted(base).length : 0,
130
+ units: 'roadmap file(s)',
131
+ roots: [ROADMAP_DIR],
132
+ });
133
+ } catch (exc) {
134
+ if (exc instanceof DeadScopeError) {
135
+ process.stderr.write(`❌ ${exc.message}\n`);
136
+ return 1;
137
+ }
138
+ throw exc;
139
+ }
140
+
118
141
  const empties = find_empty_roadmaps(root);
119
142
 
120
143
  if (empties.length === 0) {
@@ -19,6 +19,8 @@ import * as fs from 'node:fs';
19
19
  import * as path from 'node:path';
20
20
  import { fileURLToPath } from 'node:url';
21
21
 
22
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
23
+
22
24
  const QUIET = process.argv.includes('--quiet');
23
25
 
24
26
  const _HERE = fileURLToPath(import.meta.url);
@@ -79,6 +81,23 @@ function main(): number {
79
81
  for (const root of SCAN_ROOTS) {
80
82
  walk(root, files);
81
83
  }
84
+ // The citation haystack is the scanned unit. A vanished SCAN_ROOTS tree
85
+ // still fails — as "N of N uncited" — but names the fixtures as the
86
+ // culprit; asserting the walk here reports the real cause instead.
87
+ try {
88
+ assertScanned({
89
+ gate: 'lint_eval_fixture_citations',
90
+ scanned: files.length,
91
+ units: 'citation-surface file(s)',
92
+ roots: SCAN_ROOTS.map((r) => path.relative(REPO, r)),
93
+ });
94
+ } catch (exc) {
95
+ if (exc instanceof DeadScopeError) {
96
+ process.stderr.write(`${exc.message}\n`);
97
+ return 1;
98
+ }
99
+ throw exc;
100
+ }
82
101
  const haystack = files
83
102
  .map((f) => {
84
103
  try {
@@ -40,6 +40,8 @@ import * as fs from 'node:fs';
40
40
  import * as path from 'node:path';
41
41
  import { fileURLToPath, pathToFileURL } from 'node:url';
42
42
 
43
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
44
+
43
45
  const _HERE = fileURLToPath(import.meta.url);
44
46
 
45
47
  // REPO = Path(__file__).resolve().parents[2]
@@ -119,6 +121,19 @@ function _manifestPaths(): string[] {
119
121
  return out;
120
122
  }
121
123
 
124
+ /** Immediate `src/skills/*` directories — the population `_manifestPaths` filters. */
125
+ function _skillDirs(): string[] {
126
+ let entries: fs.Dirent[];
127
+ try {
128
+ entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
129
+ } catch {
130
+ return [];
131
+ }
132
+ return entries
133
+ .map((ent) => path.join(SKILLS_DIR, ent.name))
134
+ .filter((p) => _isDirPath(p));
135
+ }
136
+
122
137
  function _isDirPath(p: string): boolean {
123
138
  try {
124
139
  return fs.statSync(p).isDirectory();
@@ -309,6 +324,26 @@ function main(argv?: readonly string[]): number {
309
324
  }
310
325
  const args = parsed.args as Args;
311
326
 
327
+ // The scanned unit is the skill directory, not the corpus manifests it
328
+ // filters down to: a skill without `data/manifest.json` is legitimately out
329
+ // of scope, so a zero-manifest count cannot be told from a healthy tree.
330
+ // Zero skill directories can — and `check()` returns clean over a missing
331
+ // SKILLS_DIR, which is the ✅ line printed over nothing.
332
+ try {
333
+ assertScanned({
334
+ gate: 'lint_eval_freshness',
335
+ scanned: _skillDirs().length,
336
+ units: 'skill director(ies)',
337
+ roots: [path.relative(REPO, SKILLS_DIR)],
338
+ });
339
+ } catch (exc) {
340
+ if (exc instanceof DeadScopeError) {
341
+ process.stderr.write(`${exc.message}\n`);
342
+ return 1;
343
+ }
344
+ throw exc;
345
+ }
346
+
312
347
  const errors = check();
313
348
  if (errors.length) {
314
349
  process.stderr.write('eval-freshness: corpus-backed skills missing a current last_eval:\n');
@@ -23,6 +23,8 @@ import * as fs from 'node:fs';
23
23
  import * as path from 'node:path';
24
24
  import { fileURLToPath, pathToFileURL } from 'node:url';
25
25
 
26
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
27
+
26
28
  const _HERE = fileURLToPath(import.meta.url);
27
29
  // Path(__file__).resolve().parent.parent.parent — three dirs up from the file.
28
30
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -145,9 +147,22 @@ function _pyRepr(s: string): string {
145
147
 
146
148
  export function main(): number {
147
149
  const demos = _sorted_demos(REPO_ROOT);
148
- if (demos.length === 0) {
149
- process.stderr.write(`❌ no demo files matched ${DEMO_GLOB}\n`);
150
- return 1;
150
+ // Supersedes the bare `demos.length === 0 → exit 1` check: same exit code,
151
+ // but the message names the root, so a moved demo directory reads as a dead
152
+ // scope instead of "the glob matched nothing".
153
+ try {
154
+ assertScanned({
155
+ gate: 'lint_examples',
156
+ scanned: demos.length,
157
+ units: 'demo file(s)',
158
+ roots: [DEMO_GLOB],
159
+ });
160
+ } catch (exc) {
161
+ if (exc instanceof DeadScopeError) {
162
+ process.stderr.write(`❌ ${exc.message}\n`);
163
+ return 1;
164
+ }
165
+ throw exc;
151
166
  }
152
167
  let failed = 0;
153
168
  for (const demo of demos) {
@@ -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 = fileURLToPath(import.meta.url);
36
38
 
37
39
  function _repo_root(): string {
@@ -344,6 +346,27 @@ function main(argv?: readonly string[]): number {
344
346
  const opts = parse_args(argv ?? process.argv.slice(2));
345
347
 
346
348
  const schema = _load_schema();
349
+ // The payload is caller-supplied, so the gate's own scope is the schema —
350
+ // and `_load_schema` only proves the FILE is there. A schema that parses to
351
+ // an empty (or property-less) document validates every payload silently and
352
+ // prints `✅ explain-trace OK`. Count the property rules that will actually
353
+ // be applied; the keyword filtering below is judgement, this is reach.
354
+ try {
355
+ assertScanned({
356
+ gate: 'lint_explain_trace',
357
+ scanned: Object.keys(_isObject(schema['properties']) ? schema['properties'] : {}).length,
358
+ units: 'schema property rule(s)',
359
+ roots: ['docs/contracts/explain-trace.schema.json'],
360
+ });
361
+ } catch (exc) {
362
+ if (exc instanceof DeadScopeError) {
363
+ // 2 = invocation error — the same code a missing schema returns.
364
+ // 1 would claim the payload is invalid, which is not what happened.
365
+ process.stderr.write(`❌ ${exc.message}\n`);
366
+ throw new SystemExit(2);
367
+ }
368
+ throw exc;
369
+ }
347
370
  // Python: jsonschema.Draft202012Validator.check_schema(schema) — the
348
371
  // explain-trace schema is a valid Draft-2020-12 document, so this never
349
372
  // raises in practice; the subset validator has no schema-validation step.
@@ -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
 
26
28
  // CWD-relative, exactly as the Python module's Path(...) literals.
@@ -132,9 +134,32 @@ function main(argv: readonly string[]): number {
132
134
  const [skills, commands, packs] = manifest_names(manifest);
133
135
  const body = fs.readFileSync(DOC, 'utf-8');
134
136
 
137
+ // The doc existing is not the doc being readable by this gate: LINK_RE is
138
+ // pinned to the `../dist/agent-src/{skills,commands}/…` link shape, so a
139
+ // restructured or emptied Featured Skills page yields zero entries and the
140
+ // OK line reports "0 artefact entries validated". Assert the raw match list,
141
+ // before the dedupe the checks below run on.
142
+ const links = _findLinks(body);
143
+ try {
144
+ assertScanned({
145
+ gate: 'lint_featured_skills',
146
+ scanned: links.length,
147
+ units: 'featured artefact link(s)',
148
+ roots: [DOC],
149
+ });
150
+ } catch (exc) {
151
+ if (exc instanceof DeadScopeError) {
152
+ // 1 is the gate's only failure code — the same one a missing DOC or
153
+ // manifest already returns.
154
+ process.stderr.write(`❌ ${exc.message}\n`);
155
+ return 1;
156
+ }
157
+ throw exc;
158
+ }
159
+
135
160
  const missing: string[] = [];
136
161
  const seen = new Set<string>();
137
- for (const [cat, raw] of _findLinks(body)) {
162
+ for (const [cat, raw] of links) {
138
163
  const slug = slug_from_path(cat, raw);
139
164
  const key = `${cat}\0${slug}`;
140
165
  if (seen.has(key)) {
@@ -41,6 +41,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
41
41
  import YAML from 'yaml';
42
42
 
43
43
  import { resolve_logical } from './_lib/agent_src.js';
44
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
44
45
 
45
46
  const _HERE = fileURLToPath(import.meta.url);
46
47
  // Path(__file__).resolve().parents[2] — repo root, two dirs up from src/scripts.
@@ -647,6 +648,24 @@ export function main(argv?: string[]): number {
647
648
  return 3;
648
649
  }
649
650
 
651
+ // Exit 3, not 1: an empty flows dir is the same class of failure as the
652
+ // `flows dir not found` / `schema not found` short-circuits above (the gate
653
+ // could not do its job), not a flow that violates the schema.
654
+ try {
655
+ assertScanned({
656
+ gate: 'lint_flows',
657
+ scanned: files.length,
658
+ units: 'flow file(s)',
659
+ roots: ['src/flows'],
660
+ });
661
+ } catch (exc) {
662
+ if (exc instanceof DeadScopeError) {
663
+ process.stderr.write(`${exc.message}\n`);
664
+ return 3;
665
+ }
666
+ throw exc;
667
+ }
668
+
650
669
  if (vios.length > 0) {
651
670
  process.stderr.write(`lint_flows: ${vios.length} violation(s):\n`);
652
671
  for (const v of vios) {
@@ -27,6 +27,7 @@ import * as path from 'node:path';
27
27
  import { fileURLToPath, pathToFileURL } from 'node:url';
28
28
 
29
29
  import { artefact_roots } from './_lib/agent_src.js';
30
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
30
31
  import {
31
32
  load_schema,
32
33
  parse_frontmatter,
@@ -303,6 +304,25 @@ function main(argv: readonly string[]): number {
303
304
  }
304
305
  }
305
306
 
307
+ // `_iter` skips a category whose base directory is absent, so an
308
+ // `artefact_roots()` that resolves to nothing (or four renamed category
309
+ // subdirs) yields no files at all and the gate reports "0 artefact(s)
310
+ // clean" — clean being exactly what it cannot know.
311
+ try {
312
+ assertScanned({
313
+ gate: 'lint_frontmatter_boilerplate',
314
+ scanned: total,
315
+ units: 'artefact(s)',
316
+ roots: artefact_roots().map((r) => path.relative(ROOT, r).split(path.sep).join('/') || '.'),
317
+ });
318
+ } catch (exc) {
319
+ if (exc instanceof DeadScopeError) {
320
+ process.stderr.write(`❌ ${exc.message}\n`);
321
+ return 1;
322
+ }
323
+ throw exc;
324
+ }
325
+
306
326
  if (offenders > 0) {
307
327
  process.stderr.write(
308
328
  `\n== frontmatter-boilerplate: ${offenders}/${total} artefact(s) carry a ` +
@@ -28,6 +28,8 @@ import * as path from 'node:path';
28
28
  import { fileURLToPath, pathToFileURL } from 'node:url';
29
29
  import YAML from 'yaml';
30
30
 
31
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
32
+
31
33
  const _HERE = fileURLToPath(import.meta.url);
32
34
  const QUIET = process.argv.slice(2).includes('--quiet');
33
35
 
@@ -294,6 +296,30 @@ function main(): number {
294
296
  const cons_errors = lint_consumer_side();
295
297
  const errors = [...pkg_errors, ...cons_errors];
296
298
 
299
+ // Both tiers together are the corpus. The consumer tier is legitimately
300
+ // empty in the package repo, so counting it alone would read a vanished
301
+ // package-fixture dir as "no profiles to lint" and stay green.
302
+ const pkg_count = _exists(PACKAGE_DIR)
303
+ ? _globMdSorted(PACKAGE_DIR).filter((p) => !EXEMPT_STEMS.has(_stem(p))).length
304
+ : 0;
305
+ const cons_count = _exists(CONSUMER_DIR)
306
+ ? _globMdSorted(CONSUMER_DIR).filter((p) => !EXEMPT_STEMS.has(_stem(p))).length
307
+ : 0;
308
+ try {
309
+ assertScanned({
310
+ gate: 'lint_ghostwriter_source',
311
+ scanned: pkg_count + cons_count,
312
+ units: 'profile(s)',
313
+ roots: ['src/agent-src/ghostwriter', 'agents/ghostwriter'],
314
+ });
315
+ } catch (exc) {
316
+ if (exc instanceof DeadScopeError) {
317
+ process.stderr.write(`❌ ${exc.message}\n`);
318
+ return 1;
319
+ }
320
+ throw exc;
321
+ }
322
+
297
323
  if (errors.length) {
298
324
  process.stderr.write(`❌ lint_ghostwriter_source: ${errors.length} violation(s)\n`);
299
325
  for (const line of errors) {
@@ -304,12 +330,6 @@ function main(): number {
304
330
  }
305
331
 
306
332
  if (!QUIET) {
307
- const pkg_count = _exists(PACKAGE_DIR)
308
- ? _globMdSorted(PACKAGE_DIR).filter((p) => !EXEMPT_STEMS.has(_stem(p))).length
309
- : 0;
310
- const cons_count = _exists(CONSUMER_DIR)
311
- ? _globMdSorted(CONSUMER_DIR).filter((p) => !EXEMPT_STEMS.has(_stem(p))).length
312
- : 0;
313
333
  process.stdout.write(
314
334
  `✅ lint_ghostwriter_source: ${pkg_count} package fixture(s), ` +
315
335
  `${cons_count} consumer profile(s), all compliant\n`,
@@ -18,6 +18,8 @@ import * as fs from 'node:fs';
18
18
  import * as path from 'node:path';
19
19
  import { fileURLToPath } 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
  const REPO = path.resolve(path.dirname(_HERE), '..', '..');
23
25
  const GLAMA_DIR = path.join(REPO, 'internal', 'glama');
@@ -44,6 +46,28 @@ function _canonicalCommand(scriptPath: string, stripPrefix?: string): string {
44
46
 
45
47
  function main(): number {
46
48
  const readmePath = path.join(GLAMA_DIR, 'README.md');
49
+
50
+ // No corpus — the gate guards three named files. If internal/glama/ moves,
51
+ // every read below throws rather than reporting; name the scope instead so
52
+ // the failure says which paths went missing.
53
+ try {
54
+ assertWatchlistResolves({
55
+ gate: 'lint_glama_drift',
56
+ candidates: [
57
+ path.posix.join('internal', 'glama', 'README.md'),
58
+ ...SCRIPTS.map((s) => path.posix.join('internal', 'glama', s.file)),
59
+ ],
60
+ repoRoot: REPO,
61
+ });
62
+ } catch (exc) {
63
+ if (exc instanceof DeadScopeError) {
64
+ // 1 is the gate's only failure code — drift and "cannot run" share it.
65
+ console.error(`❌ ${exc.message}`);
66
+ return 1;
67
+ }
68
+ throw exc;
69
+ }
70
+
47
71
  const readme = fs.readFileSync(readmePath, 'utf8');
48
72
 
49
73
  let failed = false;
@@ -21,6 +21,8 @@ import os from "node:os";
21
21
  import 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 DEFAULT_POLICY = path.join(
25
27
  path.dirname(fileURLToPath(import.meta.url)),
26
28
  "expected_perms.json",
@@ -171,7 +173,29 @@ export function lint(policyPath: string, quiet = false): number {
171
173
  const policyObj: PolicyObject = isObject(policy) ? policy : {};
172
174
  const findings: string[] = [];
173
175
 
176
+ // The unit is the POLICY rule, not the audited path: the global install tree
177
+ // is machine-state and is legitimately absent (CI, a fresh clone), so a count
178
+ // of audited paths would have to be `allowEmpty` and assert nothing. An empty
179
+ // policy is the real dead scope — it audits nothing while exiting 0. Exit 2
180
+ // matches the other policy-load failures above.
174
181
  const rootSpecRaw = policyObj["global_root"];
182
+ try {
183
+ assertScanned({
184
+ gate: "lint_global_paths",
185
+ scanned:
186
+ (isObject(rootSpecRaw) ? 1 : 0) +
187
+ asArray(policyObj["files"]).length +
188
+ asArray(policyObj["directories"]).length,
189
+ units: "policy rule(s)",
190
+ roots: [policyPath],
191
+ });
192
+ } catch (exc) {
193
+ if (exc instanceof DeadScopeError) {
194
+ process.stderr.write(`error: ${exc.message}\n`);
195
+ return 2;
196
+ }
197
+ throw exc;
198
+ }
175
199
  const rootSpec: PolicyObject = isObject(rootSpecRaw) ? rootSpecRaw : {};
176
200
  const rootPath = _expand(asString(rootSpec["path"], "~/.event4u/agent-config"));
177
201
  if (_exists(rootPath)) {
@@ -46,6 +46,8 @@ import fs from "node:fs";
46
46
  import path from "node:path";
47
47
  import { fileURLToPath, pathToFileURL } from "node:url";
48
48
 
49
+ import { assertScanned, DeadScopeError } from "./_lib/scan_scope.js";
50
+
49
51
  const _HERE = path.dirname(fileURLToPath(import.meta.url));
50
52
  export const REPO_ROOT = path.resolve(_HERE, "..", "..");
51
53
  export const SCRIPTS_DIR = path.join(REPO_ROOT, "src", "scripts");
@@ -288,6 +290,24 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
288
290
  const jsonMode = argv.includes("--json");
289
291
  const quiet = argv.includes("--quiet");
290
292
 
293
+ // The asserted unit is every eligible `.ts` file the walk yields, never the
294
+ // finding count: an empty findings list IS the pass state here, so a moved
295
+ // SCRIPTS_DIR would read as "clean" instead of "read nothing".
296
+ try {
297
+ assertScanned({
298
+ gate: "lint_governed_writes",
299
+ scanned: [...walkTsFiles(SCRIPTS_DIR, SCRIPTS_DIR)].length,
300
+ units: "script file(s)",
301
+ roots: ["src/scripts"],
302
+ });
303
+ } catch (exc) {
304
+ if (exc instanceof DeadScopeError) {
305
+ process.stderr.write(`❌ ${exc.message}\n`);
306
+ return 2;
307
+ }
308
+ throw exc;
309
+ }
310
+
291
311
  const findings = scanScriptsDir(SCRIPTS_DIR);
292
312
 
293
313
  if (jsonMode) {
@@ -34,6 +34,7 @@ import { spawnSync } from 'node:child_process';
34
34
  import { fileURLToPath, pathToFileURL } from 'node:url';
35
35
 
36
36
  import * as sl from './_lib/security_lint.js';
37
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
37
38
 
38
39
  export const CHECK = 'hidden-unicode';
39
40
 
@@ -499,7 +500,9 @@ export function main(argv: readonly string[] | null = null): number {
499
500
 
500
501
  const findings: sl.Finding[] = [];
501
502
  const flagged = new Set<string>();
503
+ let corpusFiles = 0;
502
504
  for (const sf of sl.iter_corpus()) {
505
+ corpusFiles += 1;
503
506
  const hits = _scan(sf);
504
507
  for (const h of hits) {
505
508
  findings.push(h);
@@ -509,6 +512,28 @@ export function main(argv: readonly string[] | null = null): number {
509
512
  }
510
513
  }
511
514
 
515
+ // `iter_corpus` skips a root that does not exist, so a moved corpus yields
516
+ // zero files and this security lint reports "clean" — and the umbrella
517
+ // aggregate reads that as 0 blocking findings. Count files READ, not
518
+ // findings. The second (source-bytes) pass states its own file count below
519
+ // and is separately visible, so this assertion guards the `.md` corpus.
520
+ // Exit 1 is this CLI's only failure code (2 is the pinned argparse usage
521
+ // error), so it carries "could not run" here.
522
+ try {
523
+ assertScanned({
524
+ gate: 'lint_hidden_unicode',
525
+ scanned: corpusFiles,
526
+ units: 'corpus file(s)',
527
+ roots: sl.DEFAULT_SCAN_ROOTS,
528
+ });
529
+ } catch (exc) {
530
+ if (exc instanceof DeadScopeError) {
531
+ process.stderr.write(`❌ ${exc.message}\n`);
532
+ return 1;
533
+ }
534
+ throw exc;
535
+ }
536
+
512
537
  // Second pass — raw control bytes in text-intended SOURCE files, which the
513
538
  // `.md` corpus above does not cover. Deliberately not folded into
514
539
  // `iter_corpus`: different scope (all tracked text files, not the `.md`