@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
@@ -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 { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
26
+
25
27
  const _HERE = fileURLToPath(import.meta.url);
26
28
  // src/scripts/lint_showcase_sessions.ts → parent.parent.parent is the repo root.
27
29
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -174,9 +176,23 @@ function _validate_session(slug: string): string[] {
174
176
  }
175
177
 
176
178
  function main(): number {
177
- if (!_isFile(SHOWCASE_MD)) {
178
- process.stderr.write(`❌ ${_relTo(SHOWCASE_MD, ROOT)} not found\n`);
179
- return 1;
179
+ // There is no corpus to count — "0 referenced, 0 on disk" is the shipped
180
+ // state and a legitimate pass. The one thing that must resolve is the
181
+ // manifest both rules are derived from: with showcase.md gone there are no
182
+ // references, so no missing file and no orphan can ever be reported.
183
+ // Supersedes the previous bare `_isFile` check, same exit code.
184
+ try {
185
+ assertWatchlistResolves({
186
+ gate: 'lint_showcase_sessions',
187
+ candidates: [_relTo(SHOWCASE_MD, ROOT)],
188
+ repoRoot: ROOT,
189
+ });
190
+ } catch (e) {
191
+ if (e instanceof DeadScopeError) {
192
+ process.stderr.write(`❌ ${e.message}\n`);
193
+ return 1;
194
+ }
195
+ throw e;
180
196
  }
181
197
 
182
198
  const text = fs.readFileSync(SHOWCASE_MD, 'utf-8');
@@ -61,6 +61,7 @@ import process from 'node:process';
61
61
  import { fileURLToPath } from 'node:url';
62
62
 
63
63
  import { OVERLAP_THRESHOLD, _cosine, collect } from './audit_skill_overlap.js';
64
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
64
65
  import { parse_frontmatter } from './skill_overlap.js';
65
66
 
66
67
  const _HERE = fileURLToPath(import.meta.url);
@@ -289,6 +290,24 @@ function main(argv: string[]): number {
289
290
  }
290
291
  }
291
292
 
293
+ // `skillMds` yields nothing for a root that does not exist, so a moved
294
+ // `src/skills` would report "no description defects" over zero skills —
295
+ // routing quality certified against an empty corpus.
296
+ try {
297
+ assertScanned({
298
+ gate: 'lint_skill_descriptions',
299
+ scanned,
300
+ units: 'skill(s)',
301
+ roots: [path.relative(REPO, args.root) || args.root],
302
+ });
303
+ } catch (exc) {
304
+ if (exc instanceof DeadScopeError) {
305
+ process.stderr.write(`❌ ${exc.message}\n`);
306
+ return 1;
307
+ }
308
+ throw exc;
309
+ }
310
+
292
311
  if (args.json) {
293
312
  fs.mkdirSync(path.dirname(args.json), { recursive: true });
294
313
  fs.writeFileSync(args.json, JSON.stringify({ scanned, violations }, null, 2) + '\n');
@@ -27,6 +27,7 @@ import * as fs from 'node:fs';
27
27
  import { fileURLToPath, pathToFileURL } from 'node:url';
28
28
 
29
29
  import * as sl from './_lib/security_lint.js';
30
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
30
31
 
31
32
  export const CHECK = 'dangerous-frontmatter';
32
33
 
@@ -258,12 +259,35 @@ export function main(argv: readonly string[] | null = null): number {
258
259
 
259
260
  const findings: sl.Finding[] = [];
260
261
  const roots = ['src/skills', 'src/agent-src', 'src/domains'];
262
+ let scanned = 0;
261
263
  for (const sf of sl.iter_corpus(roots, ['.md'])) {
264
+ scanned += 1;
262
265
  for (const h of _scan(sf)) {
263
266
  findings.push(h);
264
267
  }
265
268
  }
266
269
 
270
+ // `iter_corpus` skips a root that does not exist, so all three moving at
271
+ // once yields zero files and a clean report — the quietest possible failure
272
+ // for a security lint. `scanned` counts files READ, never findings: zero
273
+ // findings over 900 files is the success state, zero files is blindness.
274
+ // Exit 1 = "this gate is not clean" (reds the build); 2 stays the argv
275
+ // usage code that `parse_args` owns.
276
+ try {
277
+ assertScanned({
278
+ gate: 'lint_skill_frontmatter_safety',
279
+ scanned,
280
+ units: 'markdown file(s)',
281
+ roots,
282
+ });
283
+ } catch (exc) {
284
+ if (exc instanceof DeadScopeError) {
285
+ process.stderr.write(`❌ ${exc.message}\n`);
286
+ return 1;
287
+ }
288
+ throw exc;
289
+ }
290
+
267
291
  if (args.json) {
268
292
  const payload = sl.py_json_dumps_indent2(findings.map((f) => f.toDict()));
269
293
  process.stdout.write(payload + '\n');
@@ -55,6 +55,7 @@ import * as path from 'node:path';
55
55
  import process from 'node:process';
56
56
  import { fileURLToPath, pathToFileURL } from 'node:url';
57
57
 
58
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
58
59
  import { jaccard, parse_frontmatter, tokenize } from './skill_overlap.js';
59
60
 
60
61
  const _HERE = fileURLToPath(import.meta.url);
@@ -257,9 +258,23 @@ function main(argv?: readonly string[]): number {
257
258
  const args = parse_args(argv ?? process.argv.slice(2));
258
259
 
259
260
  const skills = load_skills(SKILLS);
260
- if (skills.length === 0) {
261
- process.stderr.write(`no skills under ${SKILLS}\n`);
262
- return 1;
261
+ try {
262
+ assertScanned({
263
+ gate: 'lint_skill_originality',
264
+ scanned: skills.length,
265
+ units: 'skill(s)',
266
+ roots: ['src/skills'],
267
+ });
268
+ } catch (exc) {
269
+ if (exc instanceof DeadScopeError) {
270
+ // The `no skills under <root>` line is part of the pinned CLI
271
+ // contract, so it stays first; the assertion's named-root detail
272
+ // follows it rather than replacing it.
273
+ process.stderr.write(`no skills under ${SKILLS}\n`);
274
+ process.stderr.write(`❌ ${exc.message}\n`);
275
+ return 1;
276
+ }
277
+ throw exc;
263
278
  }
264
279
  const allow = load_allowlist();
265
280
  const findings = analyse(skills, allow);
@@ -28,6 +28,8 @@ import * as path from 'node:path';
28
28
  import process from 'node:process';
29
29
  import { fileURLToPath } 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 REPO = path.resolve(path.dirname(_HERE), '..', '..');
33
35
  const DEFAULT_SKILLS = path.join(REPO, 'src', 'skills');
@@ -127,6 +129,24 @@ function main(argv: string[]): number {
127
129
  violations.push({ rel, detail: 'contains a content-mutation primitive but no write-gating flag (--writable/--apply/--write/--out/--fix) and is not allowlisted' });
128
130
  }
129
131
 
132
+ // `scanned` is the script files read, which is also the number the success
133
+ // line publishes; a `--root` that resolves to nothing would otherwise
134
+ // report every script gated while having opened none.
135
+ try {
136
+ assertScanned({
137
+ gate: 'lint_skill_scripts_readonly',
138
+ scanned,
139
+ units: 'skill script(s)',
140
+ roots: [args.root === DEFAULT_SKILLS ? 'src/skills' : args.root],
141
+ });
142
+ } catch (exc) {
143
+ if (exc instanceof DeadScopeError) {
144
+ process.stderr.write(`❌ ${exc.message}\n`);
145
+ return 1;
146
+ }
147
+ throw exc;
148
+ }
149
+
130
150
  if (violations.length > 0) {
131
151
  process.stderr.write(`❌ lint_skill_scripts_readonly: ${violations.length} ungated write(s):\n`);
132
152
  for (const v of violations) process.stderr.write(` ${v.rel} — ${v.detail}\n`);
@@ -1,161 +1,71 @@
1
1
  #!/usr/bin/env tsx
2
2
  /**
3
- * Block D · D1 meta-linter for `scripts/skill_tools/*.py`.
3
+ * Block D · D1 meta-linter for `src/scripts/skill_tools/*.ts` (ADR-006).
4
4
  *
5
- * Ported from the retired Python `src/scripts/lint_skill_tools.py` (ADR-200,
6
- * Phase 4 / Wave 4b). The CLI contract is pinned — `--json`
7
- * / `--quiet` / `--tools-dir` flags, exit codes (0 clean, 1 violations,
8
- * 2 usage / missing-dir), stdout split (JSON or human), byte-identical
9
- * violation strings, same glob + sort order, same `_SAMPLE`/`__main__`
10
- * detection.
5
+ * ## Why this gate changed shape (2026-08-05)
11
6
  *
12
- * the retired Python implementation parses each tool with `ast` to find imports and
13
- * syntax errors. We replicate the import discovery with a focused
14
- * Python-import line scanner (handles `import a, b.c`, `from x.y import z`,
15
- * relative `from . import x`). It covers the real tools and the test
16
- * fixtures; an `import`/`from` keyword appearing as the first token of a
17
- * line inside a multi-line string literal would be a false positive the
18
- * `ast` walk avoids — flagged as a divergence candidate, not observed in
19
- * the corpus.
7
+ * ADR-006 piloted these tools in Python and this gate enforced five invariants
8
+ * over `skill_tools/*.py`. ADR-200 migrated the corpus to `.ts` without
9
+ * mentioning ADR-006, so the glob matched nothing: for ~7 weeks the gate printed
10
+ * `✅ scripts/skill_tools/ — all tools clean.` over **zero** tools on every CI
11
+ * run, and nobody noticed that two ADR-006 invariants had quietly gone unmet on
12
+ * the TS surface (`--help` in 0 of 6 files; `_SAMPLE` in 3 of 5).
20
13
  *
21
- * No behaviour changes.
14
+ * Repointing the glob and keeping all five checks was measured and rejected: the
15
+ * TS ports are 2.4–4.0× their Python originals (92–142 LOC → 253–470), so the
16
+ * 200-LOC cap reds 5 of 5 tools, and the only way to green it is raising the cap
17
+ * to ≥ 470 — threshold-lowering, which this gate's own ratchet exists to
18
+ * prevent. AI council 2026-08-05 resolved it as extract-the-checkable:
19
+ *
20
+ * - **KEPT** (pure regex over filename + text, no analyser): `snake_case_verb_noun`
21
+ * naming, a registered `--json` flag, and an embedded `_SAMPLE` or CLI-entry guard.
22
+ * - **DROPPED, recorded in ADR-006's Status**: the 200-LOC cap (cannot pass
23
+ * without weakening its own threshold); the `argparse` import and
24
+ * `add_help=False` checks (no TypeScript analogue); and the stdlib-only import
25
+ * scan (a Python `ast`-shaped scanner — re-implementing it for TS specifiers
26
+ * would be a new analyser, not a port, and the council drew that line
27
+ * explicitly for the sibling `lint_workspace_boundary` decision).
28
+ *
29
+ * The gate now asserts its scan scope, so an emptied or moved corpus fails
30
+ * loudly instead of certifying nothing. No `allowEmpty`: "no tools" must never
31
+ * read as "all tools clean" again.
32
+ *
33
+ * CLI contract unchanged — `--json` / `--quiet` / `--tools-dir`, exit codes
34
+ * (0 clean, 1 violations, 2 usage / missing-dir / dead scope), stdout split.
22
35
  */
23
36
 
24
37
  import * as fs from 'node:fs';
25
38
  import * as path from 'node:path';
26
39
  import { fileURLToPath, pathToFileURL } from 'node:url';
27
40
 
41
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
42
+
28
43
  const _HERE = fileURLToPath(import.meta.url);
29
44
  // src/scripts/lint_skill_tools.ts → parents[2] is the repo root.
30
45
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
31
46
  const TOOLS_DIR = path.join(ROOT, 'src', 'scripts', 'skill_tools');
32
- const NAME_RE = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+\.py$/;
33
- const SIZE_CAP = 200;
34
-
35
- const STDLIB: ReadonlySet<string> = new Set([
36
- '__future__', 'abc', 'argparse', 'ast', 'base64', 'collections', 'configparser',
37
- 'contextlib', 'copy', 'csv', 'dataclasses', 'datetime', 'decimal', 'difflib',
38
- 'enum', 'errno', 'fnmatch', 'functools', 'glob', 'gzip', 'hashlib', 'heapq',
39
- 'html', 'http', 'importlib', 'inspect', 'io', 'ipaddress', 'itertools', 'json',
40
- 'logging', 'math', 'mimetypes', 'os', 'pathlib', 'pickle', 'platform', 'posixpath',
41
- 'pprint', 'queue', 'random', 're', 'shlex', 'shutil', 'signal', 'socket',
42
- 'sqlite3', 'ssl', 'stat', 'string', 'struct', 'subprocess', 'sys', 'tempfile',
43
- 'textwrap', 'threading', 'time', 'tomllib', 'traceback', 'types', 'typing',
44
- 'unicodedata', 'unittest', 'urllib', 'uuid', 'venv', 'warnings', 'weakref',
45
- 'xml', 'zipfile', 'zlib',
46
- ]);
47
- const PROJECT_PACKAGES: ReadonlySet<string> = new Set(['scripts', 'skill_tools']);
48
-
49
- interface ParsedImport {
50
- kind: 'import';
51
- modules: string[]; // dotted module names from `import a, b.c as d`
52
- }
53
- interface ParsedFromImport {
54
- kind: 'from';
55
- module: string | null; // None for `from . import x` (level > 0)
56
- level: number;
57
- }
58
- type ImportNode = ParsedImport | ParsedFromImport;
59
-
60
- /**
61
- * Scan a Python source for top-of-line `import` / `from … import` statements.
62
- * Mirrors what `ast.walk` surfaces for `Import` / `ImportFrom` nodes (the
63
- * linter only reads `.names`, `.module`, `.level`). Continuation lines inside
64
- * a parenthesised `from x import (...)` do not affect the module name (which
65
- * sits before `import` on the opening line), so they need no special handling.
66
- */
67
- function _parse_imports(text: string): ImportNode[] {
68
- const out: ImportNode[] = [];
69
- for (const raw of text.split('\n')) {
70
- const stripped = raw.replace(/^[ \t]+/, '');
71
- const importMatch = /^import\s+(.+)$/.exec(stripped);
72
- if (importMatch) {
73
- const rest = importMatch[1]!.replace(/#.*$/, '');
74
- const modules: string[] = [];
75
- for (const part of rest.split(',')) {
76
- // `a.b.c as d` → dotted name is the token before `as`.
77
- const name = part.trim().split(/\s+as\s+/)[0]!.trim();
78
- if (name) {
79
- modules.push(name);
80
- }
81
- }
82
- out.push({ kind: 'import', modules });
83
- continue;
84
- }
85
- const fromMatch = /^from\s+(\.*)([A-Za-z0-9_.]*)\s+import\b/.exec(stripped);
86
- if (fromMatch) {
87
- const dots = fromMatch[1]!;
88
- const mod = fromMatch[2]!;
89
- const level = dots.length;
90
- // ast: `from . import x` → module is None when no dotted name follows.
91
- const module = mod === '' ? null : mod;
92
- out.push({ kind: 'from', module, level });
93
- continue;
94
- }
95
- }
96
- return out;
97
- }
47
+ const NAME_RE = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+\.ts$/;
98
48
 
99
49
  function _violations_for(p: string): string[] {
100
50
  const out: string[] = [];
101
51
  const name = path.basename(p);
102
52
  if (!NAME_RE.test(name)) {
103
- out.push(`naming: \`${name}\` is not snake_case_verb_noun.py`);
53
+ out.push(`naming: \`${name}\` is not snake_case_verb_noun.ts`);
104
54
  }
105
55
 
106
56
  const text = fs.readFileSync(p, 'utf-8');
107
- let loc = 0;
108
- for (const ln of text.split('\n')) {
109
- if (ln.trim() && !ln.replace(/^[ \t]+/, '').startsWith('#')) {
110
- loc += 1;
111
- }
112
- }
113
- if (loc > SIZE_CAP) {
114
- out.push(`size: ${loc} LOC > ${SIZE_CAP} cap`);
115
- }
116
-
117
- // Imports — flag any non-stdlib, non-project top-level module + record argparse.
118
- const imported = new Set<string>();
119
- for (const node of _parse_imports(text)) {
120
- if (node.kind === 'import') {
121
- for (const dotted of node.modules) {
122
- const root = dotted.split('.')[0]!;
123
- imported.add(root);
124
- if (!STDLIB.has(root) && !PROJECT_PACKAGES.has(root)) {
125
- out.push(`stdlib-only: imports \`${dotted}\` (third-party)`);
126
- }
127
- }
128
- } else {
129
- if (node.module === null || node.level > 0) {
130
- continue; // relative imports — package-internal
131
- }
132
- const root = node.module.split('.')[0]!;
133
- imported.add(root);
134
- if (!STDLIB.has(root) && !PROJECT_PACKAGES.has(root)) {
135
- out.push(`stdlib-only: imports from \`${node.module}\` (third-party)`);
136
- }
137
- }
138
- }
139
57
 
140
- // CLI flags — confirm argparse is imported and `--json` is registered.
141
- const hasArgparse = imported.has('argparse');
142
- const hasJsonFlag = /['"]--json['"]/.test(text);
143
- if (!hasArgparse) {
144
- out.push('cli: no `argparse` import detected');
145
- }
146
- if (!hasJsonFlag) {
58
+ // Machine-readable output — the tool must register a `--json` flag.
59
+ if (!/['"]--json['"]/.test(text)) {
147
60
  out.push('cli: missing `--json` flag');
148
61
  }
149
- if (/add_help\s*=\s*False/.test(text)) {
150
- out.push('cli: `add_help=False` disables --help');
151
- }
152
62
 
153
- // Embedded sample data — `_SAMPLE` constant or a `__main__` block.
154
- const hasSample = /^_SAMPLE\s*[:=]/m.test(text);
155
- const hasMain =
156
- text.includes('__name__ == "__main__"') || text.includes("__name__ == '__main__'");
157
- if (!(hasSample || hasMain)) {
158
- out.push('sample: no `_SAMPLE` constant or `__main__` block');
63
+ // Embedded sample data — an exported-or-bare `_SAMPLE` constant, or the
64
+ // CLI-entry guard that replaced Python's `__main__` block.
65
+ const hasSample = /^\s*(?:export\s+)?(?:const|let)?\s*_SAMPLE\s*[:=]/m.test(text);
66
+ const hasEntry = text.includes('import.meta.url');
67
+ if (!(hasSample || hasEntry)) {
68
+ out.push('sample: no `_SAMPLE` constant or CLI-entry guard');
159
69
  }
160
70
 
161
71
  return out;
@@ -191,8 +101,8 @@ function lint(toolsDir: string): [number, Record<string, string[]>] {
191
101
  } catch {
192
102
  entries = [];
193
103
  }
194
- const pyFiles = entries
195
- .filter((n) => n.endsWith('.py'))
104
+ const toolFiles = entries
105
+ .filter((n) => n.endsWith('.ts') && !n.endsWith('.d.ts') && !n.endsWith('.test.ts'))
196
106
  .map((n) => path.join(resolved, n))
197
107
  .filter((p) => {
198
108
  try {
@@ -202,10 +112,14 @@ function lint(toolsDir: string): [number, Record<string, string[]>] {
202
112
  }
203
113
  })
204
114
  .sort();
205
- for (const p of pyFiles) {
206
- if (path.basename(p) === '__init__.py') {
115
+ let linted = 0;
116
+ for (const p of toolFiles) {
117
+ // `index.ts` is the package marker — the `__init__.py` analogue. It is a
118
+ // re-export barrel, not a tool, so the per-tool invariants do not apply.
119
+ if (path.basename(p) === 'index.ts') {
207
120
  continue;
208
121
  }
122
+ linted += 1;
209
123
  const viols = _violations_for(p);
210
124
  if (viols.length > 0) {
211
125
  // Mirror Python's `str(path.relative_to(ROOT))` with ValueError fallback.
@@ -213,9 +127,24 @@ function lint(toolsDir: string): [number, Record<string, string[]>] {
213
127
  findings[key] = viols;
214
128
  }
215
129
  }
130
+ _lintedCount = linted;
216
131
  return [Object.keys(findings).length > 0 ? 1 : 0, findings];
217
132
  }
218
133
 
134
+ /**
135
+ * Tools actually inspected by the last `lint()` call.
136
+ *
137
+ * Kept out of `lint()`'s return value on purpose: 13 unit tests drive `lint()`
138
+ * directly and assert its `[code, findings]` shape, and the scan-scope assertion
139
+ * must fire once per process in `main()` — not inside a function the tests call
140
+ * with temp dirs, where an empty fixture is legitimate.
141
+ */
142
+ let _lintedCount = 0;
143
+
144
+ export function lintedCount(): number {
145
+ return _lintedCount;
146
+ }
147
+
219
148
  function _print_human(findings: Record<string, string[]>): void {
220
149
  if (Object.keys(findings).length === 0) {
221
150
  process.stdout.write('✅ scripts/skill_tools/ — all tools clean.\n');
@@ -292,6 +221,25 @@ function main(argv?: readonly string[]): number {
292
221
  const args = parse_args(argv ?? process.argv.slice(2));
293
222
 
294
223
  const [code, findings] = lint(args.toolsDir);
224
+ // Scan-scope assertion. Deliberately after `lint()` (which computes the
225
+ // count) and before the report, so a moved or emptied corpus fails loudly
226
+ // instead of printing "all tools clean" over zero tools — which is exactly
227
+ // what this gate did for ~7 weeks after ADR-200 deleted its `.py` corpus.
228
+ // No `allowEmpty`: an empty tools dir here is blindness, not success.
229
+ try {
230
+ assertScanned({
231
+ gate: 'lint_skill_tools',
232
+ scanned: lintedCount(),
233
+ units: 'tool(s)',
234
+ roots: ['src/scripts/skill_tools'],
235
+ });
236
+ } catch (e) {
237
+ if (e instanceof DeadScopeError) {
238
+ process.stderr.write(`❌ ${e.message}\n`);
239
+ return 2; // the gate's existing "internal / usage" code
240
+ }
241
+ throw e;
242
+ }
295
243
  if (args.json) {
296
244
  process.stdout.write(_json_dumps_ascii({ exit_code: code, findings }));
297
245
  process.stdout.write('\n');
@@ -327,13 +275,4 @@ if (_isCliEntry() || process.argv[1] === _HERE) {
327
275
  process.exit(main());
328
276
  }
329
277
 
330
- export {
331
- ROOT,
332
- TOOLS_DIR,
333
- NAME_RE,
334
- SIZE_CAP,
335
- STDLIB,
336
- PROJECT_PACKAGES,
337
- lint,
338
- main,
339
- };
278
+ export { ROOT, TOOLS_DIR, NAME_RE, lint, main };
@@ -41,6 +41,8 @@ import * as fs from 'node:fs';
41
41
  import * as path from 'node:path';
42
42
  import { fileURLToPath, pathToFileURL } from 'node:url';
43
43
 
44
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
45
+
44
46
  const _HERE = path.dirname(fileURLToPath(import.meta.url));
45
47
  const REPO_ROOT = path.resolve(_HERE, '..', '..');
46
48
 
@@ -204,9 +206,14 @@ function scanMarkdownTranscript(filePath: string, repoRoot: string): Finding[] {
204
206
 
205
207
  function scanRepo(repoRoot: string): Finding[] {
206
208
  const findings: Finding[] = [];
209
+ // Counted before the `*spawn*.json` / spawn-context filters: those decide
210
+ // what is JUDGED, and both are legitimately zero on a clean tree. Only the
211
+ // walk itself distinguishes "no spawn payloads" from "no files read".
212
+ let scanned = 0;
207
213
 
208
214
  const fixturesDir = path.join(repoRoot, 'tests', 'fixtures');
209
215
  for (const f of walkFiles(fixturesDir)) {
216
+ scanned += 1;
210
217
  if (f.endsWith('.json') && /spawn/i.test(path.basename(f))) {
211
218
  findings.push(...scanJsonFixture(f, repoRoot));
212
219
  }
@@ -216,11 +223,18 @@ function scanRepo(repoRoot: string): Finding[] {
216
223
  if (fs.existsSync(transcriptsDir)) {
217
224
  for (const entry of fs.readdirSync(transcriptsDir, { withFileTypes: true })) {
218
225
  if (entry.isFile() && entry.name.endsWith('.md')) {
226
+ scanned += 1;
219
227
  findings.push(...scanMarkdownTranscript(path.join(transcriptsDir, entry.name), repoRoot));
220
228
  }
221
229
  }
222
230
  }
223
231
 
232
+ assertScanned({
233
+ gate: 'lint_spawn_payload',
234
+ scanned,
235
+ units: 'fixture / transcript file(s)',
236
+ roots: ['tests/fixtures', 'tests/reasoning-layer-eval/golden-transcripts'],
237
+ });
224
238
  return findings;
225
239
  }
226
240
 
@@ -234,7 +248,19 @@ function parseArgs(argv: readonly string[]): { strict: boolean; quiet: boolean }
234
248
  function main(argv?: readonly string[], repoRootOverride?: string): number {
235
249
  const args = parseArgs(argv ?? process.argv.slice(2));
236
250
  const root = repoRootOverride ?? REPO_ROOT;
237
- const findings = scanRepo(root);
251
+ let findings: Finding[];
252
+ try {
253
+ findings = scanRepo(root);
254
+ } catch (exc) {
255
+ if (exc instanceof DeadScopeError) {
256
+ // 2 is this gate's only non-zero code. Returning it without
257
+ // `--strict` is deliberate: warn-only governs FINDINGS, and a dead
258
+ // scope means there were none to warn about.
259
+ process.stderr.write(`❌ ${exc.message}\n`);
260
+ return 2;
261
+ }
262
+ throw exc;
263
+ }
238
264
 
239
265
  if (!args.quiet) {
240
266
  for (const f of findings) {
@@ -29,6 +29,8 @@ import fs from 'node:fs';
29
29
  import path from 'node:path';
30
30
  import { fileURLToPath, pathToFileURL } from 'node:url';
31
31
 
32
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
33
+
32
34
  const _HERE = fileURLToPath(import.meta.url);
33
35
  export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
34
36
 
@@ -113,6 +115,23 @@ export function main(argv: string[] = process.argv.slice(2)): number {
113
115
  return 2;
114
116
  }
115
117
  }
118
+ // SCAN_FILES is a hand-maintained watch list and `scanFile` swallows a read
119
+ // error, so a renamed or moved index module drops out of scope silently and
120
+ // the gate reports "clean" over the files it can no longer read. Exit 3 is
121
+ // the "this gate could not run" code; 1 stays "violations found".
122
+ try {
123
+ assertWatchlistResolves({
124
+ gate: 'lint_store_boundary',
125
+ candidates: SCAN_FILES,
126
+ repoRoot: root,
127
+ });
128
+ } catch (exc) {
129
+ if (exc instanceof DeadScopeError) {
130
+ process.stderr.write(`❌ ${exc.message}\n`);
131
+ return 3;
132
+ }
133
+ throw exc;
134
+ }
116
135
  let violations: Violation[];
117
136
  try {
118
137
  violations = run(root);
@@ -24,6 +24,7 @@ import * as fs from 'node:fs';
24
24
  import * as path from 'node:path';
25
25
  import { fileURLToPath, pathToFileURL } from 'node:url';
26
26
 
27
+ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
27
28
  import { parse_frontmatter } from './validate_frontmatter.js';
28
29
 
29
30
  const QUIET = process.argv.includes('--quiet');
@@ -47,7 +48,28 @@ function main(dir: string = SUBAGENT_DIR): number {
47
48
  const findings: string[] = [];
48
49
  const namesSeen = new Map<string, string>();
49
50
 
50
- for (const file of _listSubagents(dir)) {
51
+ const files = _listSubagents(dir);
52
+ // The unit is every `.md` walked, not `namesSeen` — that map only grows on
53
+ // units that already passed the stem-match, so it reads 0 both for a moved
54
+ // root and for a corpus where every unit is broken.
55
+ try {
56
+ assertScanned({
57
+ gate: 'lint_subagent_determinism',
58
+ scanned: files.length,
59
+ units: 'subagent file(s)',
60
+ roots: [dir],
61
+ });
62
+ } catch (exc) {
63
+ if (exc instanceof DeadScopeError) {
64
+ // 1 is this gate's only non-zero code; here it means "could not
65
+ // run", not "found a determinism violation".
66
+ process.stderr.write(`❌ ${exc.message}\n`);
67
+ return 1;
68
+ }
69
+ throw exc;
70
+ }
71
+
72
+ for (const file of files) {
51
73
  const rel = path.relative(REPO, file);
52
74
  const stem = path.basename(file, '.md');
53
75
  const text = fs.readFileSync(file, 'utf-8');
@@ -12,6 +12,7 @@ import * as path from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
 
14
14
  import { lint_matrix } from './_lib/tool_adapter_registry.js';
15
+ import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
15
16
 
16
17
  const _HERE = fileURLToPath(import.meta.url);
17
18
  const REPO_ROOT = path.join(path.dirname(_HERE), '..', '..');
@@ -25,6 +26,23 @@ const isMain = process.argv[1] !== undefined && path.resolve(process.argv[1]) ==
25
26
  if (isMain) {
26
27
  const idx = process.argv.indexOf('--readme');
27
28
  const readmePath = idx !== -1 ? (process.argv[idx + 1] ?? '') : path.join(REPO_ROOT, 'README.md');
29
+ // The whole README leg of this gate is one named file. A moved README turns
30
+ // `run()` into an ENOENT stack trace, which reads as a crash rather than as
31
+ // a gate whose scope died — name the scope first. Exit 1 is the gate's only
32
+ // failure code; here it means "could not run", not "found drift".
33
+ try {
34
+ assertWatchlistResolves({
35
+ gate: 'lint_supported_tools_matrix',
36
+ candidates: [path.relative(REPO_ROOT, path.resolve(readmePath))],
37
+ repoRoot: REPO_ROOT,
38
+ });
39
+ } catch (e) {
40
+ if (e instanceof DeadScopeError) {
41
+ process.stderr.write(`❌ ${e.message}\n`);
42
+ process.exit(1);
43
+ }
44
+ throw e;
45
+ }
28
46
  const errors = run(readmePath);
29
47
  if (errors.length > 0) {
30
48
  for (const e of errors) process.stderr.write(`❌ ${e}\n`);