@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
@@ -23,6 +23,7 @@ import YAML from 'yaml';
23
23
 
24
24
  import { USER_SCOPE_PATHS } from './install.js';
25
25
  import { CLAUDE_MARKETPLACE_NAME, CLAUDE_PLUGIN_ID } from './_lib/claude_plugin.js';
26
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
26
27
 
27
28
  const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
28
29
  export const MATRIX_PATH = path.join(REPO_ROOT, 'src', 'config', 'surface-matrix.yml');
@@ -59,9 +60,24 @@ export function load_surface_matrix(matrixPath: string = MATRIX_PATH): Record<st
59
60
  }
60
61
 
61
62
  export function main(): number {
62
- if (!fs.existsSync(MATRIX_PATH)) {
63
- process.stdout.write(`❌ ${MATRIX_PATH} not found\n`);
64
- return 1;
63
+ // Replaces the ad-hoc `existsSync(MATRIX_PATH)` precondition. The matrix
64
+ // yaml is the gate's only file input — an emptied `tools:` mapping is
65
+ // already caught by the set-equality against USER_SCOPE_PATHS (a code
66
+ // constant a path migration cannot empty), so a moved file is the one
67
+ // remaining way this gate can go quiet. Exit 1 = "could not run" here; 3
68
+ // stays reserved for an unexpected throw at the CLI boundary.
69
+ try {
70
+ assertWatchlistResolves({
71
+ gate: 'lint_surface_matrix',
72
+ candidates: [path.relative(REPO_ROOT, MATRIX_PATH)],
73
+ repoRoot: REPO_ROOT,
74
+ });
75
+ } catch (e) {
76
+ if (e instanceof DeadScopeError) {
77
+ process.stderr.write(`❌ ${e.message}\n`);
78
+ return 1;
79
+ }
80
+ throw e;
65
81
  }
66
82
 
67
83
  const errors: string[] = [];
@@ -42,6 +42,8 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
42
42
 
43
43
  import YAML from 'yaml';
44
44
 
45
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
46
+
45
47
  const _HERE = fileURLToPath(import.meta.url);
46
48
 
47
49
  // REPO = Path(__file__).resolve().parents[2]
@@ -676,6 +678,27 @@ function lint(): number {
676
678
  return 3;
677
679
  }
678
680
 
681
+ // The asserted unit is the roadmap corpus, not the ticket bundles: a repo
682
+ // with no bundles yet is a real state (the early return below), whereas a
683
+ // vanished agents/roadmaps/ answers "no orphan markers" for the spine
684
+ // check and "not materialised" for nothing, both silently.
685
+ try {
686
+ assertScanned({
687
+ gate: 'lint_ticket_buildable',
688
+ scanned: _glob(ROADMAPS, '*.md').length,
689
+ units: 'roadmap file(s)',
690
+ roots: [ROADMAPS],
691
+ });
692
+ } catch (exc) {
693
+ if (exc instanceof DeadScopeError) {
694
+ // 3 = IO/setup error, this gate's "could not run" code; 1 means
695
+ // lint failures were found.
696
+ process.stderr.write(`${exc.message}\n`);
697
+ return 3;
698
+ }
699
+ throw exc;
700
+ }
701
+
679
702
  if (!_exists(TICKETS_ROOT)) {
680
703
  process.stdout.write('agents/tickets/: no bundles yet — nothing to lint\n');
681
704
  return 0;
@@ -26,6 +26,8 @@ import * as path from 'node:path';
26
26
  import { fileURLToPath, pathToFileURL } from 'node:url';
27
27
  import { parse as parseYaml, YAMLParseError } from 'yaml';
28
28
 
29
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
30
+
29
31
  // src/scripts/lint_topics_yaml.ts → two dirs up is the repo root
30
32
  // (mirrors Path(__file__).resolve().parents[2]).
31
33
  const _HERE = fileURLToPath(import.meta.url);
@@ -39,15 +41,6 @@ function _relTo(target: string, root: string): string {
39
41
  return path.relative(root, target).split(path.sep).join('/');
40
42
  }
41
43
 
42
- function _exists(p: string): boolean {
43
- try {
44
- fs.statSync(p);
45
- return true;
46
- } catch {
47
- return false;
48
- }
49
- }
50
-
51
44
  /**
52
45
  * Render a value the way Python's `repr()` / `{x!r}` would for the values
53
46
  * that flow into the finding messages: strings (single-quoted, with escape
@@ -103,9 +96,23 @@ function _fail(msg: string): void {
103
96
  }
104
97
 
105
98
  function main(): number {
106
- if (!_exists(TOPICS_FILE)) {
107
- _fail(`missing file: ${_relTo(TOPICS_FILE, ROOT)}`);
108
- return 1;
99
+ // Replaces the ad-hoc `_exists(TOPICS_FILE)` precondition: the gate's whole
100
+ // corpus is this one named file, so its absence is a dead scope rather than
101
+ // a content finding. Same stderr channel, same exit 1 (the only failure code).
102
+ try {
103
+ assertWatchlistResolves({
104
+ gate: 'lint_topics_yaml',
105
+ candidates: [_relTo(TOPICS_FILE, ROOT)],
106
+ repoRoot: ROOT,
107
+ });
108
+ } catch (e) {
109
+ if (e instanceof DeadScopeError) {
110
+ // Not via `_fail`: its `topics.yml:` prefix would stutter against
111
+ // the gate name the DeadScopeError message already carries.
112
+ process.stderr.write(`❌ ${e.message}\n`);
113
+ return 1;
114
+ }
115
+ throw e;
109
116
  }
110
117
  let doc: unknown;
111
118
  try {
@@ -43,6 +43,8 @@ import * as fs from 'node:fs';
43
43
  import * as path from 'node:path';
44
44
  import { fileURLToPath, pathToFileURL } from 'node:url';
45
45
 
46
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
47
+
46
48
  const _HERE = fileURLToPath(import.meta.url);
47
49
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
48
50
  const ROUTER = path.join(REPO_ROOT, 'dist', 'router.json');
@@ -108,6 +110,31 @@ export function main(): number {
108
110
  return 1;
109
111
  }
110
112
 
113
+ // The routed-rule population, not `found` below: `found` counts VIOLATIONS,
114
+ // so zero is the direction this ratchet is pushing toward. A router.json
115
+ // regenerated over a moved rule tree parses fine and yields zero short
116
+ // keywords — indistinguishable from a clean run. Exit 1 is the existing
117
+ // "gate could not run" code (missing / unparseable router); 2 stays
118
+ // "over budget".
119
+ const routed = ['tier_1', 'tier_2'].reduce(
120
+ (n, tier) => n + (Array.isArray(router[tier]) ? (router[tier] as unknown[]).length : 0),
121
+ 0,
122
+ );
123
+ try {
124
+ assertScanned({
125
+ gate: 'lint_trigger_precision',
126
+ scanned: routed,
127
+ units: 'routed rule(s)',
128
+ roots: ['dist/router.json'],
129
+ });
130
+ } catch (e) {
131
+ if (e instanceof DeadScopeError) {
132
+ process.stderr.write(`error: ${e.message}\n`);
133
+ return 1;
134
+ }
135
+ throw e;
136
+ }
137
+
111
138
  const found = short_keywords(router);
112
139
  const over = found.length > SHORT_KEYWORD_BUDGET;
113
140
 
@@ -26,6 +26,7 @@ import * as fs from 'node:fs';
26
26
  import * as path from 'node:path';
27
27
  import { fileURLToPath, pathToFileURL } from 'node:url';
28
28
  import { strip_source_prefix } from './_lib/agent_src.js';
29
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
29
30
 
30
31
  const _HERE = fileURLToPath(import.meta.url);
31
32
  const _DEFAULT_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -317,6 +318,26 @@ function main(argv: readonly string[] = []): number {
317
318
  const manifest = _load_manifest(args.manifest);
318
319
  const kernel = _load_kernel(args.router);
319
320
 
321
+ // All three invariants iterate `artefacts`; the existing guard only covers a
322
+ // manifest that is ABSENT. One that parses but carries no artefacts — a
323
+ // discovery build over a moved source tree — visits nothing and prints the
324
+ // same success line, just with zeroes in it. Exit 1 is the gate's only
325
+ // failure code (the missing-manifest SystemExit lands on 1 too).
326
+ try {
327
+ assertScanned({
328
+ gate: 'lint_trust_coherence',
329
+ scanned: _asArray(manifest['artefacts']).length,
330
+ units: 'manifest artefact(s)',
331
+ roots: [args.manifest],
332
+ });
333
+ } catch (e) {
334
+ if (e instanceof DeadScopeError) {
335
+ process.stderr.write(`ERROR: ${e.message}\n`);
336
+ return 1;
337
+ }
338
+ throw e;
339
+ }
340
+
320
341
  const errs: string[] = [];
321
342
  errs.push(..._check_pack_safety_floors(manifest));
322
343
  errs.push(..._check_human_review_banners(manifest, args.compiledSrc));
@@ -33,6 +33,7 @@ import {
33
33
  type StackBundle,
34
34
  compose_bundle,
35
35
  } from '../agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.js';
36
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
36
37
 
37
38
  const QUIET = process.argv.includes('--quiet');
38
39
 
@@ -153,9 +154,22 @@ function check_lane(lane: string, bundle: StackBundle): void {
153
154
 
154
155
  function main(): number {
155
156
  const lanes = Object.keys(STACK_BUNDLES).sort();
156
- if (lanes.length === 0) {
157
- process.stderr.write('lint-ui-stack-bundles: STACK_BUNDLES is empty — nothing to check.\n');
158
- return 1;
157
+ // Replaces the ad-hoc `lanes.length === 0` check with the shared assertion,
158
+ // so an emptied bundle map names the root it came from instead of a bare
159
+ // "nothing to check". Exit 1 is the gate's only failure code.
160
+ try {
161
+ assertScanned({
162
+ gate: 'lint_ui_stack_bundles',
163
+ scanned: lanes.length,
164
+ units: 'stack lane(s)',
165
+ roots: ['src/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts'],
166
+ });
167
+ } catch (e) {
168
+ if (e instanceof DeadScopeError) {
169
+ process.stderr.write(`❌ ${e.message}\n`);
170
+ return 1;
171
+ }
172
+ throw e;
159
173
  }
160
174
  for (const lane of lanes) {
161
175
  check_lane(lane, STACK_BUNDLES[lane] as StackBundle);
@@ -20,6 +20,8 @@ import * as fs from 'node:fs';
20
20
  import * as path from 'node:path';
21
21
  import { fileURLToPath, pathToFileURL } from 'node:url';
22
22
 
23
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
24
+
23
25
  const _HERE = fileURLToPath(import.meta.url);
24
26
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
25
27
  const DASHBOARD = path.join(REPO_ROOT, 'docs', 'value.md');
@@ -176,9 +178,23 @@ function _seqEqual(a: readonly unknown[], b: readonly unknown[]): boolean {
176
178
  function lint(quiet = false): number {
177
179
  const violations: string[] = [];
178
180
 
179
- if (!_exists(DASHBOARD)) {
180
- _log(`FAIL: dashboard not found: ${_relPosix(DASHBOARD, REPO_ROOT)}`, quiet, true);
181
- return 1;
181
+ // Replaces the ad-hoc `_exists(DASHBOARD)` precondition. Only the dashboard
182
+ // is guarded: `LATEST` is genuinely optional (its absence is the documented
183
+ // "placeholder" success path below), so listing it here would let a moved
184
+ // dashboard pass whenever the report happened to exist. Exit 1 = the gate's
185
+ // only failure code.
186
+ try {
187
+ assertWatchlistResolves({
188
+ gate: 'lint_value_dashboard',
189
+ candidates: [_relPosix(DASHBOARD, REPO_ROOT)],
190
+ repoRoot: REPO_ROOT,
191
+ });
192
+ } catch (e) {
193
+ if (e instanceof DeadScopeError) {
194
+ _log(`FAIL: ${e.message}`, quiet, true);
195
+ return 1;
196
+ }
197
+ throw e;
182
198
  }
183
199
  const text = fs.readFileSync(DASHBOARD, 'utf-8');
184
200
  violations.push(...check_required_sections(text));
@@ -31,6 +31,8 @@ import * as path from 'node:path';
31
31
  import process from 'node:process';
32
32
  import { fileURLToPath } from 'node:url';
33
33
 
34
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
35
+
34
36
  const PROG = 'lint_versioned_cache.ts';
35
37
 
36
38
  /** Window (lines) around a cache-path literal scanned for a justification. */
@@ -135,7 +137,17 @@ export function scanFile(file: string, source: string): Violation[] {
135
137
 
136
138
  export function runChecks(root: string): Violation[] {
137
139
  const violations: Violation[] = [];
138
- for (const file of listTsFiles(root)) {
140
+ const files = listTsFiles(root);
141
+ // The unit is every `.ts` walked — `.ts` defines this gate's corpus, while
142
+ // the cache-suffix match is the finding. Asserting on matches would read
143
+ // green for a moved `--dir` exactly as it does for a clean tree.
144
+ assertScanned({
145
+ gate: 'lint_versioned_cache',
146
+ scanned: files.length,
147
+ units: 'TypeScript file(s)',
148
+ roots: [root],
149
+ });
150
+ for (const file of files) {
139
151
  // Never lint this file itself — its regexes contain the very suffixes
140
152
  // it hunts for.
141
153
  if (path.basename(file) === 'lint_versioned_cache.ts') continue;
@@ -177,6 +189,12 @@ export function main(argv: string[]): number {
177
189
  try {
178
190
  violations = runChecks(path.resolve(dir));
179
191
  } catch (exc) {
192
+ // 3 (internal) over 2 (violations found): a dead `--dir` means the
193
+ // gate could not run, not that it found an unversioned cache.
194
+ if (exc instanceof DeadScopeError) {
195
+ process.stderr.write(`${PROG}: ${exc.message}\n`);
196
+ return 3;
197
+ }
180
198
  process.stderr.write(`${PROG}: internal error: ${String(exc)}\n`);
181
199
  return 3;
182
200
  }
@@ -31,6 +31,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
31
31
 
32
32
  import YAML from 'yaml';
33
33
 
34
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
34
35
  import { py_json_dumps_indent2 } from './_lib/security_lint.js';
35
36
 
36
37
  const _HERE = fileURLToPath(import.meta.url);
@@ -72,14 +73,6 @@ function _isFile(p: string): boolean {
72
73
  }
73
74
  }
74
75
 
75
- function _isDir(p: string): boolean {
76
- try {
77
- return fs.statSync(p).isDirectory();
78
- } catch {
79
- return false;
80
- }
81
- }
82
-
83
76
  function _asObject(v: JsonValue | undefined): JsonObject | null {
84
77
  if (v !== null && v !== undefined && typeof v === 'object' && !Array.isArray(v)) {
85
78
  return v as JsonObject;
@@ -557,18 +550,33 @@ export function main(argv?: string[]): number {
557
550
  throw e;
558
551
  }
559
552
 
560
- if (!_isDir(WORKFLOWS_DIR)) {
561
- if (!args.quiet) {
562
- process.stderr.write(`No workflows directory found at ${WORKFLOWS_DIR}\n`);
553
+ // Replaces the former `!_isDir(WORKFLOWS_DIR)` → exit 0: a missing dir and
554
+ // an existing-but-empty one are the same blindness, and reporting
555
+ // "0 HIGH, 0 MEDIUM" over zero workflows is the vacuous pass.
556
+ const yml = _sortedGlob('.yml');
557
+ const yaml = _sortedGlob('.yaml');
558
+ try {
559
+ assertScanned({
560
+ gate: 'lint_workflow_security',
561
+ scanned: yml.length + yaml.length,
562
+ units: 'workflow file(s)',
563
+ roots: [WORKFLOWS_DIR],
564
+ });
565
+ } catch (e) {
566
+ if (e instanceof DeadScopeError) {
567
+ // 2 (the usage / could-not-run code) over 1, which means
568
+ // "a HIGH finding blocked under --strict".
569
+ process.stderr.write(`❌ ${e.message}\n`);
570
+ return 2;
563
571
  }
564
- return 0;
572
+ throw e;
565
573
  }
566
574
 
567
575
  const all_findings: Finding[] = [];
568
- for (const wf_path of _sortedGlob('.yml')) {
576
+ for (const wf_path of yml) {
569
577
  all_findings.push(...scan_workflow(wf_path, allowlist));
570
578
  }
571
- for (const wf_path of _sortedGlob('.yaml')) {
579
+ for (const wf_path of yaml) {
572
580
  all_findings.push(...scan_workflow(wf_path, allowlist));
573
581
  }
574
582
 
@@ -37,7 +37,7 @@ type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]:
37
37
  type JsonObject = { [k: string]: JsonValue };
38
38
 
39
39
  // Tool names across platforms whose command field carries a shell command
40
- // (kept in lock-step with verify_before_complete_hook.COMMAND_TOOLS).
40
+ // (kept in lock-step with before_complete_hook.COMMAND_TOOLS).
41
41
  const COMMAND_TOOLS: ReadonlySet<string> = new Set([
42
42
  "launch-process",
43
43
  "launch_process",
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Ported from the retired Python `skill_collision_clusters.py` (Phase 8 / Wave 8e).
6
6
  *
7
- * Walks `.agent-src.uncondensed/skills/<id>/SKILL.md`, extracts the
7
+ * Walks `src/skills/<id>/SKILL.md`, extracts the
8
8
  * `description` frontmatter, computes pairwise keyword overlap, and groups
9
9
  * high-overlap skill pairs into clusters. The output drives the
10
10
  * selection-accuracy fixture set defined by council file 05 (Round-3
@@ -18,11 +18,26 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
18
18
 
19
19
  import { parse as parseYaml } from 'yaml';
20
20
 
21
+ import { SRC_SKILLS } from './_lib/agent_src.js';
22
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
23
+
21
24
  const _HERE = fileURLToPath(import.meta.url);
22
25
 
23
26
  const REPO_ROOT = path.resolve(_HERE, '..', '..', '..');
24
- const SKILLS_DIR = path.join(REPO_ROOT, '.agent-src.uncondensed', 'skills');
25
- const OUT_JSON = path.join(REPO_ROOT, 'agents', 'reports', 'skill-collision-clusters.json');
27
+ /**
28
+ * The live skills tree, via the shared ADR-051 resolver.
29
+ *
30
+ * This was a hardcoded path into the source container ADR-051 retired, so the
31
+ * gate read a directory that does not exist and clustered 0 of 288 skills. It
32
+ * was invisible for two reasons at once: the dead-root sweep and the hardening
33
+ * ratchet both defined "a gate" with a prefix set that excluded `skill_*`, so
34
+ * nothing in the repo was looking at it.
35
+ */
36
+ const SKILLS_DIR = SRC_SKILLS();
37
+ // `agents/runtime/reports/`, as this file's own header has always declared. The
38
+ // code said `agents/reports/` (tracked) — harmless while the gate read a dead
39
+ // root and never got far enough to write, load-bearing the moment it did.
40
+ const OUT_JSON = path.join(REPO_ROOT, 'agents', 'runtime', 'reports', 'skill-collision-clusters.json');
26
41
 
27
42
  const KEYWORD_OVERLAP_THRESHOLD = 0.4;
28
43
  const MIN_SHARED_KEYWORDS = 3;
@@ -274,6 +289,25 @@ export function main(): number {
274
289
  process.stderr.write(`❌ Skills dir not found: ${SKILLS_DIR}\n`);
275
290
  return 2;
276
291
  }
292
+ // The dir-exists check above does not cover an existing-but-empty root — a
293
+ // stray retired-container directory left behind by a local test run passes
294
+ // it — which would write a `0 clusters from 0 skills` report and exit green.
295
+ // The unit is the SKILL.md files found, not the subset that parsed a
296
+ // description.
297
+ try {
298
+ assertScanned({
299
+ gate: 'skill_collision_clusters',
300
+ scanned: _sortedSkillMds().length,
301
+ units: 'SKILL.md file(s)',
302
+ roots: ['src/skills'],
303
+ });
304
+ } catch (exc) {
305
+ if (exc instanceof DeadScopeError) {
306
+ process.stderr.write(`❌ ${exc.message}\n`);
307
+ return 2;
308
+ }
309
+ throw exc;
310
+ }
277
311
  const skills = load_skills();
278
312
  const clusters = build_clusters(skills);
279
313
  fs.mkdirSync(path.dirname(OUT_JSON), { recursive: true });
@@ -31,6 +31,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
31
31
  import { parse as parseYaml } from 'yaml';
32
32
 
33
33
  import { load_agent_settings } from './_lib/agent_settings.js';
34
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
34
35
  import { event4u_root as _event4u_root_impl } from './_lib/user_global_paths.js';
35
36
 
36
37
  const _HERE = fileURLToPath(import.meta.url);
@@ -339,6 +340,25 @@ export function main(argv: string[] | null = null): number {
339
340
  }
340
341
 
341
342
  const catalog = load_catalog();
343
+ // A recommender still owes an honest scope: analytics events are optional
344
+ // and degrade gracefully by design, but a vanished skills projection makes
345
+ // every class recommend nothing while the run still reports success.
346
+ try {
347
+ assertScanned({
348
+ gate: 'skill_discovery',
349
+ scanned: catalog.size,
350
+ units: 'catalog skill(s)',
351
+ roots: ['dist/agent-src/skills'],
352
+ });
353
+ } catch (exc) {
354
+ if (exc instanceof DeadScopeError) {
355
+ // 2 is this gate's only non-zero code (bad / missing role); here it
356
+ // reads as "could not run".
357
+ process.stderr.write(`${exc.message}\n`);
358
+ return 2;
359
+ }
360
+ throw exc;
361
+ }
342
362
  const shortlist = load_role_shortlist(role);
343
363
  const use_analytics = analytics_enabled(settings);
344
364
  const events = use_analytics ? load_analytics_events() : [];
@@ -43,6 +43,8 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
43
43
 
44
44
  import { parse as parseYaml } from 'yaml';
45
45
 
46
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
47
+
46
48
  const _HERE = fileURLToPath(import.meta.url);
47
49
  export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
48
50
 
@@ -327,6 +329,26 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
327
329
  }
328
330
  }
329
331
 
332
+ // Every mode below is decided by the skills walk, and every one of them
333
+ // reads innocently at zero: `--check` passes a floor of 0/0, `--write-floor`
334
+ // pins that emptiness as the new floor, and the report prints `n/a`.
335
+ try {
336
+ assertScanned({
337
+ gate: 'skill_eval_coverage',
338
+ scanned: _allSkills(_skillsDir({})).length,
339
+ units: 'skill(s)',
340
+ roots: ['src/skills'],
341
+ });
342
+ } catch (exc) {
343
+ if (exc instanceof DeadScopeError) {
344
+ // 2 (usage) over 1, which this gate documents as a ratchet
345
+ // regression — a finding, not an inability to run.
346
+ process.stderr.write(`❌ ${exc.message}\n`);
347
+ return 2;
348
+ }
349
+ throw exc;
350
+ }
351
+
330
352
  if (argv.includes('--write-floor')) {
331
353
  const report = writeFloor();
332
354
  process.stdout.write(
@@ -16,14 +16,25 @@ import * as fs from 'node:fs';
16
16
  import * as path from 'node:path';
17
17
  import { fileURLToPath, pathToFileURL } from 'node:url';
18
18
 
19
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
20
+ import { SRC_SKILLS } from './_lib/agent_src.js';
19
21
  import { pyRound } from './_lib/value_ladder.js';
20
22
 
21
23
  const _HERE = path.dirname(fileURLToPath(import.meta.url));
22
24
  // parent.parent.parent of the .py file (src/scripts/skill_overlap.py) is the
23
25
  // repo root — two dirs up from src/scripts.
24
26
  const REPO = path.resolve(_HERE, '..', '..');
25
- const SKILLS = path.join(REPO, '.agent-src.uncondensed', 'skills');
26
- const OUT = path.join(REPO, 'agents', 'metrics', 'skill-overlap.md');
27
+ // The live skills tree via the shared ADR-051 resolver. This was a hardcoded
28
+ // path into the source container ADR-051 retired, so the gate
29
+ // scored overlap across 0 of 288 skills. Invisible until the hardening
30
+ // population stopped excluding the `skill_*` prefix.
31
+ const SKILLS = SRC_SKILLS();
32
+ // `agents/metrics/` was retired by the agents/ taxonomy consolidation, and
33
+ // `agents/reports/skill-overlap.md` is written by the successor gate
34
+ // `audit_skill_overlap` — writing there would silently clobber it. Runtime is
35
+ // the typed, gitignored home for a report nothing tracks. Reviving a gate whose
36
+ // scan root was dead also revives its writes; this one had nowhere legal to land.
37
+ const OUT = path.join(REPO, 'agents', 'runtime', 'reports', 'skill-overlap.md');
27
38
 
28
39
  const STRONG_TOKEN = 0.6;
29
40
  const STRONG_SYMBOL = 0.6;
@@ -290,7 +301,7 @@ export function render(pairs: Pair[], total: number): string {
290
301
  '# Skill Structural Overlap (baseline)',
291
302
  '',
292
303
  '> Generated by `scripts/skill_overlap.py`. Scans',
293
- '> `.agent-src.uncondensed/skills/*/SKILL.md` frontmatter (description +',
304
+ '> `src/skills/*/SKILL.md` frontmatter (description +',
294
305
  '> trigger metadata) and body symbol references. Reports pairs in two',
295
306
  `> tiers: **strong** ≥ ${pyFloatStr(STRONG_TOKEN)} description-token Jaccard or ≥ ${pyFloatStr(STRONG_SYMBOL)}`,
296
307
  `> symbol-set Jaccard (roadmap floor); **candidate** ≥ ${pyFloatStr(CANDIDATE_TOKEN)} / ≥ ${pyFloatStr(CANDIDATE_SYMBOL)}`,
@@ -375,9 +386,24 @@ export function parse_args(argv: string[]): ParsedArgs {
375
386
  export function main(argv?: string[]): number {
376
387
  const args = parse_args(argv ?? process.argv.slice(2));
377
388
  const skills = load_skills(SKILLS);
378
- if (!skills.length) {
379
- process.stderr.write(`no skills under ${SKILLS}\n`);
380
- return 1;
389
+ try {
390
+ // A report is a certification too: writing `Skills scanned: 0` into the
391
+ // baseline is the same lie a green gate tells, in a quieter voice.
392
+ assertScanned({
393
+ gate: 'skill_overlap',
394
+ scanned: skills.length,
395
+ units: 'skill(s)',
396
+ roots: ['src/skills'],
397
+ });
398
+ } catch (exc) {
399
+ if (exc instanceof DeadScopeError) {
400
+ // `no skills under <root>` is the pinned line; the assertion's
401
+ // named-root detail follows rather than replaces it.
402
+ process.stderr.write(`no skills under ${SKILLS}\n`);
403
+ process.stderr.write(`❌ ${exc.message}\n`);
404
+ return 1;
405
+ }
406
+ throw exc;
381
407
  }
382
408
  const pairs = analyse(skills);
383
409
  fs.mkdirSync(path.dirname(args.out), { recursive: true });
@@ -24,6 +24,8 @@ import { fileURLToPath } from 'node:url';
24
24
  import { pathToFileURL } from 'node:url';
25
25
  import { parse as parseYaml } from 'yaml';
26
26
 
27
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
28
+
27
29
  const _HERE = fileURLToPath(import.meta.url);
28
30
  // src/scripts/skill_preview.ts → parents[2] is the package root (mirrors the
29
31
  // Python module's parent.parent.parent resolution).
@@ -513,6 +515,35 @@ export function main(argv: string[] | null = null): number {
513
515
  const args = parse_args(argv ?? process.argv.slice(2));
514
516
  const name = args.name as string;
515
517
 
518
+ // A reporting tool, not a CI gate — but a moved corpus makes it answer
519
+ // "no skill named X" for EVERY name, which reads as the caller's typo
520
+ // rather than as a dead root. The corpus is in-repo (unlike a $HOME
521
+ // session directory, which may legitimately be empty in CI), so zero
522
+ // skill directories is never a valid state. Exit 2 is the existing
523
+ // "cannot preview" code; there is no other failure code.
524
+ let corpus = 0;
525
+ try {
526
+ corpus = fs
527
+ .readdirSync(_SKILLS_DIR, { withFileTypes: true })
528
+ .filter((e) => e.isDirectory()).length;
529
+ } catch {
530
+ corpus = 0;
531
+ }
532
+ try {
533
+ assertScanned({
534
+ gate: 'skill_preview',
535
+ scanned: corpus,
536
+ units: 'skill director(ies)',
537
+ roots: [path.relative(REPO_ROOT, _SKILLS_DIR) || _SKILLS_DIR],
538
+ });
539
+ } catch (exc) {
540
+ if (exc instanceof DeadScopeError) {
541
+ process.stderr.write(`❌ ${exc.message}\n`);
542
+ return 2;
543
+ }
544
+ throw exc;
545
+ }
546
+
516
547
  let preview: Preview;
517
548
  try {
518
549
  preview = load_preview(name);
@@ -22,6 +22,7 @@ import * as path from 'node:path';
22
22
  import { fileURLToPath, pathToFileURL } from 'node:url';
23
23
 
24
24
  import { artefact_roots, resolve_logical } from './_lib/agent_src.js';
25
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
25
26
  import * as user_global_paths from './_lib/user_global_paths.js';
26
27
 
27
28
  const _HERE = path.dirname(fileURLToPath(import.meta.url));
@@ -966,6 +967,23 @@ export function main(argv?: string[]): number {
966
967
  }
967
968
 
968
969
  const skills = load_skill_metas();
970
+ // The full catalogue IS the routing condition — an empty one still yields
971
+ // a complete-looking result file with precision/recall computed over a
972
+ // router that was never given anything to choose from.
973
+ try {
974
+ assertScanned({
975
+ gate: 'skill_trigger_eval',
976
+ scanned: skills.length,
977
+ units: 'catalogue skill(s)',
978
+ roots: ['src/skills', 'src/agent-src/skills'],
979
+ });
980
+ } catch (exc) {
981
+ if (exc instanceof DeadScopeError) {
982
+ process.stderr.write(`❌ ${exc.message}\n`);
983
+ return 2;
984
+ }
985
+ throw exc;
986
+ }
969
987
  let router: TriggerRouter;
970
988
  let defaultOutput: string;
971
989
  if (args.dry_run) {