@event4u/agent-config 9.13.0 → 9.15.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 (280) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +113 -67
  3. package/README.md +9 -1
  4. package/dist/agent-src/commands/agents/audit.md +7 -7
  5. package/dist/agent-src/commands/agents/optimize.md +1 -1
  6. package/dist/agent-src/commands/analyze/reference-repo.md +1 -1
  7. package/dist/agent-src/commands/chat-history.md +1 -0
  8. package/dist/agent-src/commands/context.md +1 -0
  9. package/dist/agent-src/commands/council.md +1 -1
  10. package/dist/agent-src/commands/ghostwriter.md +1 -1
  11. package/dist/agent-src/commands/memory.md +2 -1
  12. package/dist/agent-src/commands/module.md +1 -0
  13. package/dist/agent-src/commands/optimize/agents-dir.md +1 -1
  14. package/dist/agent-src/commands/optimize/deep.md +150 -0
  15. package/dist/agent-src/commands/optimize/skills.md +5 -5
  16. package/dist/agent-src/commands/optimize.md +12 -6
  17. package/dist/agent-src/commands/override.md +1 -0
  18. package/dist/agent-src/commands/prediction-pool.md +4 -4
  19. package/dist/agent-src/commands/research/deep.md +1 -1
  20. package/dist/agent-src/commands/roadmap.md +2 -1
  21. package/dist/agent-src/commands/rule-compliance-audit.md +1 -1
  22. package/dist/agent-src/commands/tdd.md +1 -0
  23. package/dist/agent-src/commands/tests.md +1 -0
  24. package/dist/agent-src/commands/upstream-contribute.md +5 -5
  25. package/dist/agent-src/commands/video/from-script.md +1 -1
  26. package/dist/agent-src/commands/video/from-song.md +1 -1
  27. package/dist/agent-src/commands/video/scene.md +1 -1
  28. package/dist/agent-src/commands/video/stitch.md +1 -1
  29. package/dist/agent-src/commands/video/storyboard.md +1 -1
  30. package/dist/agent-src/commands/worktree/create.md +2 -1
  31. package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
  32. package/dist/agent-src/contexts/contracts/artifact-engagement-flow.md +5 -5
  33. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +3 -3
  34. package/dist/agent-src/contexts/contracts/emergency-triage-block.md +2 -2
  35. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +4 -3
  36. package/dist/agent-src/contexts/execution/subagent-routing.md +19 -0
  37. package/dist/agent-src/contexts/override-system.md +1 -1
  38. package/dist/agent-src/ghostwriter/fictional-fixture-v1.md +1 -1
  39. package/dist/agent-src/rules/augment-edit-discipline.md +1 -1
  40. package/dist/agent-src/rules/autonomous-execution.md +2 -1
  41. package/dist/agent-src/rules/brand-consistency.md +1 -0
  42. package/dist/agent-src/rules/brand-source-of-truth.md +1 -0
  43. package/dist/agent-src/rules/code-comment-discipline.md +2 -0
  44. package/dist/agent-src/rules/communication-through-line.md +1 -0
  45. package/dist/agent-src/rules/content-quoting-floor.md +1 -0
  46. package/dist/agent-src/rules/cross-source-consistency.md +0 -1
  47. package/dist/agent-src/rules/downstream-changes.md +1 -0
  48. package/dist/agent-src/rules/external-code-graph-interop.md +1 -0
  49. package/dist/agent-src/rules/external-reference-deep-dive.md +1 -0
  50. package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
  51. package/dist/agent-src/rules/finance-safety-floor.md +0 -1
  52. package/dist/agent-src/rules/icon-consistency.md +1 -0
  53. package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
  54. package/dist/agent-src/rules/improve-before-implement.md +1 -1
  55. package/dist/agent-src/rules/invite-challenge.md +1 -0
  56. package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
  57. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  58. package/dist/agent-src/rules/markdown-safe-codeblocks.md +1 -0
  59. package/dist/agent-src/rules/media-governance-routing.md +4 -3
  60. package/dist/agent-src/rules/media-sync-ground-truth.md +1 -0
  61. package/dist/agent-src/rules/no-attribution-footers.md +1 -0
  62. package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +1 -0
  63. package/dist/agent-src/rules/no-pr-progress-comments.md +1 -0
  64. package/dist/agent-src/rules/onboarding-gate.md +1 -0
  65. package/dist/agent-src/rules/output-discipline.md +1 -0
  66. package/dist/agent-src/rules/prefer-enums-over-literals.md +1 -0
  67. package/dist/agent-src/rules/preservation-guard.md +1 -0
  68. package/dist/agent-src/rules/question-not-instruction.md +1 -0
  69. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +1 -0
  70. package/dist/agent-src/rules/role-mode-adherence.md +1 -0
  71. package/dist/agent-src/rules/runtime-safety.md +1 -0
  72. package/dist/agent-src/rules/security-sensitive-stop.md +1 -0
  73. package/dist/agent-src/rules/session-canary.md +9 -2
  74. package/dist/agent-src/rules/source-confidentiality.md +1 -0
  75. package/dist/agent-src/rules/source-of-truth.md +1 -0
  76. package/dist/agent-src/rules/spreadsheet-source-quality.md +1 -0
  77. package/dist/agent-src/rules/think-before-action.md +1 -0
  78. package/dist/agent-src/rules/token-budget-discipline.md +1 -0
  79. package/dist/agent-src/rules/token-efficiency.md +1 -0
  80. package/dist/agent-src/rules/token-optimizer-maintenance.md +5 -5
  81. package/dist/agent-src/rules/tool-safety.md +1 -0
  82. package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
  83. package/dist/agent-src/rules/user-interaction.md +1 -0
  84. package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
  85. package/dist/agent-src/skills/adr-create/SKILL.md +1 -1
  86. package/dist/agent-src/skills/check-refs/SKILL.md +1 -1
  87. package/dist/agent-src/skills/competitive-positioning/SKILL.md +1 -1
  88. package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
  89. package/dist/agent-src/skills/context-authoring/SKILL.md +1 -1
  90. package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +1 -1
  91. package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
  92. package/dist/agent-src/skills/github-ci/SKILL.md +1 -0
  93. package/dist/agent-src/skills/guideline-writing/SKILL.md +1 -1
  94. package/dist/agent-src/skills/lint-skills/SKILL.md +1 -1
  95. package/dist/agent-src/skills/readme-reviewer/SKILL.md +1 -1
  96. package/dist/agent-src/skills/roadmap-management/SKILL.md +8 -1
  97. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  98. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  99. package/dist/agent-src/skills/skill-writing/SKILL.md +4 -4
  100. package/dist/agent-src/skills/token-optimizer/SKILL.md +4 -4
  101. package/dist/agent-src/skills/upstream-contribute/SKILL.md +6 -6
  102. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -0
  103. package/dist/agent-src/templates/AGENTS.md +1 -1
  104. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  105. package/dist/agent-src/templates/command.md +1 -1
  106. package/dist/agent-src/templates/copilot-instructions.md +3 -3
  107. package/dist/agent-src/templates/rule.md +2 -2
  108. package/dist/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
  109. package/dist/agent-src/templates/skill-archive-note.md +5 -5
  110. package/dist/agent-src/templates/skill.md +1 -1
  111. package/dist/cli/agent-config.js +66 -19
  112. package/dist/cli/agent-config.js.map +1 -1
  113. package/dist/cli/main.js +9 -4
  114. package/dist/cli/main.js.map +1 -1
  115. package/dist/cli/python/workspace_drive.js +2 -0
  116. package/dist/cli/python/workspace_drive.js.map +1 -1
  117. package/dist/cli/python/workspace_drive_health.js +2 -0
  118. package/dist/cli/python/workspace_drive_health.js.map +1 -1
  119. package/dist/cli/python/workspace_hosts.js +2 -0
  120. package/dist/cli/python/workspace_hosts.js.map +1 -1
  121. package/dist/cli/python/workspace_sessions.js +2 -0
  122. package/dist/cli/python/workspace_sessions.js.map +1 -1
  123. package/dist/cli/registry.js +1 -0
  124. package/dist/cli/registry.js.map +1 -1
  125. package/dist/cli-delegate/{chunk-A3HJ6NTI.js → chunk-63WSP6YZ.js} +2 -1
  126. package/dist/cli-delegate/{chunk-EJYIGZ3T.js → chunk-BKO2AZ7Z.js} +1 -1
  127. package/dist/cli-delegate/{chunk-QFKQLQ7X.js → chunk-H574KOUH.js} +340 -462
  128. package/dist/cli-delegate/chunk-NXFWFAYV.js +339 -0
  129. package/dist/cli-delegate/{chunk-TEGJU2JX.js → chunk-ZXXJLL5K.js} +2 -2
  130. package/dist/cli-delegate/cmd_conformance.js +29 -23
  131. package/dist/cli-delegate/cmd_doctor.js +3 -3
  132. package/dist/cli-delegate/cmd_export.js +4 -4
  133. package/dist/cli-delegate/cmd_preflight.js +2 -2
  134. package/dist/cli-delegate/cmd_prune.js +4 -4
  135. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  136. package/dist/cli-delegate/cmd_sync.js +4 -4
  137. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  138. package/dist/cli-delegate/cmd_validate.js +4 -4
  139. package/dist/discovery/deprecation-report.md +1 -1
  140. package/dist/discovery/discovery-manifest.json +163 -103
  141. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  142. package/dist/discovery/discovery-manifest.summary.md +3 -3
  143. package/dist/discovery/orphan-report.md +1 -1
  144. package/dist/discovery/packs.json +6 -5
  145. package/dist/discovery/trust-report.md +3 -3
  146. package/dist/discovery/workspaces.json +5 -4
  147. package/dist/hooks/dispatch.js +729 -430
  148. package/dist/install/install.mjs +700 -625
  149. package/dist/install/rule_scope.js +18 -2
  150. package/dist/install/rule_scope.js.map +1 -1
  151. package/dist/mcp/registry-manifest.json +3 -3
  152. package/dist/mcp/server.json +2 -2
  153. package/dist/mcp/server.mjs +564 -424
  154. package/dist/router.json +1 -1
  155. package/dist/scripts/_lib/claude_settings_hooks.js +30 -5
  156. package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
  157. package/dist/scripts/_lib/scoped_projection.js +223 -0
  158. package/dist/scripts/_lib/scoped_projection.js.map +1 -0
  159. package/dist/scripts/_lib/surface_tiers.js +125 -0
  160. package/dist/scripts/_lib/surface_tiers.js.map +1 -0
  161. package/dist/scripts/_lib/token_count.js +166 -0
  162. package/dist/scripts/_lib/token_count.js.map +1 -0
  163. package/dist/scripts/_lib/trigger_eval_floors.js +8 -0
  164. package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -1
  165. package/dist/scripts/condense.js +139 -9
  166. package/dist/scripts/condense.js.map +1 -1
  167. package/dist/scripts/project_thin_rules.js +450 -0
  168. package/dist/scripts/project_thin_rules.js.map +1 -0
  169. package/dist/server/routes/workspace.js +2 -0
  170. package/dist/server/routes/workspace.js.map +1 -1
  171. package/dist/server/schemas/settings.js +9 -3
  172. package/dist/server/schemas/settings.js.map +1 -1
  173. package/dist/ui/assets/index-DJY_vQVn.js +40 -0
  174. package/dist/ui/assets/index-DJY_vQVn.js.map +1 -0
  175. package/dist/ui/index.html +1 -1
  176. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  177. package/package.json +11 -11
  178. package/src/agent-src/templates/AGENTS.md +1 -1
  179. package/src/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
  180. package/src/config/agent-settings.template.yml +59 -11
  181. package/src/config/ci-local-parity.yml +8 -0
  182. package/src/config/discovery/unassigned-artefacts.yml +23 -25
  183. package/src/config/evaluator-budgets.json +35 -5
  184. package/src/config/gate-coverage.yml +147 -0
  185. package/src/config/gate-violation-baselines.json +25 -0
  186. package/src/install/rule_scope.ts +27 -2
  187. package/src/scripts/_cli/cmd_conformance.ts +13 -24
  188. package/src/scripts/_dispatch.bash +10 -0
  189. package/src/scripts/_lib/claude_settings_hooks.ts +30 -4
  190. package/src/scripts/_lib/exec_evidence.ts +5 -0
  191. package/src/scripts/_lib/gate_baseline.ts +213 -0
  192. package/src/scripts/_lib/scoped_projection.ts +262 -0
  193. package/src/scripts/_lib/tier_budget_routing.ts +234 -0
  194. package/src/scripts/_lib/tool_adapter_registry.ts +8 -1
  195. package/src/scripts/_lib/trigger_eval_floors.ts +9 -0
  196. package/src/scripts/_lib/trigger_routers.ts +17 -5
  197. package/src/scripts/adr/regenerate_index.ts +4 -3
  198. package/src/scripts/ai-video/lib/parse-blueprint.sh +1 -1
  199. package/src/scripts/ai_council/compile_corpus.ts +1 -1
  200. package/src/scripts/audit_cloud_compatibility.ts +37 -2
  201. package/src/scripts/audit_initial_context.ts +53 -19
  202. package/src/scripts/audit_likelihood.ts +31 -2
  203. package/src/scripts/audit_user_type_axis.ts +58 -11
  204. package/src/scripts/bench_hook_latency.ts +172 -41
  205. package/src/scripts/build_mcp_catalog.ts +12 -1
  206. package/src/scripts/check_always_budget.ts +122 -11
  207. package/src/scripts/check_artefact_count_messaging.ts +128 -15
  208. package/src/scripts/check_augment_description_cap.ts +24 -3
  209. package/src/scripts/check_ci_strict_superset.ts +150 -0
  210. package/src/scripts/check_cluster_patterns.ts +100 -0
  211. package/src/scripts/check_consumer_scope_flip.ts +26 -4
  212. package/src/scripts/check_evaluator_budgets.ts +198 -19
  213. package/src/scripts/check_gate_coverage.ts +337 -1
  214. package/src/scripts/check_generator_output_coverage.ts +2 -1
  215. package/src/scripts/check_iron_law_prominence.ts +1 -1
  216. package/src/scripts/check_kernel_rule_bundle.ts +1 -1
  217. package/src/scripts/check_no_new_legacy_path.ts +167 -3
  218. package/src/scripts/check_pack_size.ts +18 -4
  219. package/src/scripts/check_portability.ts +37 -7
  220. package/src/scripts/check_references.ts +19 -3
  221. package/src/scripts/check_release_pr_shape.ts +13 -0
  222. package/src/scripts/check_token_regression.ts +9 -2
  223. package/src/scripts/condense.ts +155 -12
  224. package/src/scripts/cost/budget.mjs +63 -2
  225. package/src/scripts/count_scoped_projection.ts +104 -0
  226. package/src/scripts/evaluator_umbrella.sh +34 -1
  227. package/src/scripts/generate_capability_matrix.ts +8 -0
  228. package/src/scripts/generate_router_coverage_from_matrix.ts +155 -0
  229. package/src/scripts/hook_manifest.yaml +17 -3
  230. package/src/scripts/hooks/block_config_weakening.ts +337 -0
  231. package/src/scripts/hooks/concern_registry.ts +2 -0
  232. package/src/scripts/hooks/probe_throttle.ts +75 -0
  233. package/src/scripts/install-hooks.sh +24 -2
  234. package/src/scripts/install.ts +15 -92
  235. package/src/scripts/lint_agents_md.ts +9 -3
  236. package/src/scripts/lint_archived_skills.ts +1 -1
  237. package/src/scripts/lint_artefact_frontmatter.ts +185 -37
  238. package/src/scripts/lint_command_routing.ts +21 -52
  239. package/src/scripts/lint_command_verbs.ts +47 -28
  240. package/src/scripts/lint_context_spine_usage.ts +38 -12
  241. package/src/scripts/lint_framework_leakage.ts +25 -6
  242. package/src/scripts/lint_handoffs.ts +49 -2
  243. package/src/scripts/lint_media_policy_linkage.ts +62 -30
  244. package/src/scripts/lint_namespace.ts +92 -7
  245. package/src/scripts/lint_new_skill_gate.ts +39 -31
  246. package/src/scripts/lint_originality_shingles.ts +7 -1
  247. package/src/scripts/lint_override_kernel_guard.ts +21 -3
  248. package/src/scripts/lint_pack_boundaries.ts +461 -91
  249. package/src/scripts/lint_pack_dependencies.ts +24 -57
  250. package/src/scripts/lint_persona_governance.ts +1 -1
  251. package/src/scripts/lint_role_experiences.ts +34 -5
  252. package/src/scripts/lint_trigger_precision.ts +142 -0
  253. package/src/scripts/mcp_server/consumer_tool_catalog.json +12 -201
  254. package/src/scripts/mcp_server/tools.ts +1 -1
  255. package/src/scripts/prepack-check.mjs +40 -1
  256. package/src/scripts/prepack_router_targets.d.mts +7 -0
  257. package/src/scripts/prepack_router_targets.mjs +93 -0
  258. package/src/scripts/print_required_checks.ts +72 -18
  259. package/src/scripts/profile_staleness_hook.ts +12 -0
  260. package/src/scripts/record_evaluator_measurements.mjs +105 -0
  261. package/src/scripts/recruit_preflight.sh +1 -1
  262. package/src/scripts/release.ts +105 -1
  263. package/src/scripts/router_target_paths.d.mts +4 -0
  264. package/src/scripts/router_target_paths.mjs +69 -0
  265. package/src/scripts/router_telemetry.ts +52 -1
  266. package/src/scripts/routing_doctor.ts +485 -0
  267. package/src/scripts/rule_trigger_eval.ts +403 -0
  268. package/src/scripts/schemas/rule.schema.json +5 -1
  269. package/src/scripts/session_canary_hook.ts +96 -5
  270. package/src/scripts/skill_linter.ts +90 -42
  271. package/src/scripts/smoke/skills.sh +1 -1
  272. package/src/scripts/sweep_dead_scan_roots.ts +356 -10
  273. package/src/scripts/trigger_coverage.ts +13 -2
  274. package/src/scripts/update_counts.ts +91 -12
  275. package/src/scripts/validate_telegraph_carveouts.ts +1 -1
  276. package/src/scripts/wrapper_freshness_hook.ts +14 -0
  277. package/src/server/schemas/settings.ts +19 -5
  278. package/dist/cli-delegate/chunk-TJIAM5FW.js +0 -133
  279. package/dist/ui/assets/index-BpSQt5vm.js +0 -40
  280. package/dist/ui/assets/index-BpSQt5vm.js.map +0 -1
@@ -9,7 +9,7 @@
9
9
  * historical quirks preserved (consumers pin the exact behaviour).
10
10
  *
11
11
  * Enforces caps + pointer-ratio + pointer-anatomy + emergency-triage
12
- * contract from `.agent-src.uncondensed/skills/agents-md-thin-root/SKILL.md`.
12
+ * contract from `src/skills/agents-md-thin-root/SKILL.md`.
13
13
  *
14
14
  * Exit non-zero on any (a) FAIL, (b)–(e) error. WARN is informational.
15
15
  */
@@ -87,12 +87,18 @@ function _resolve(target_str: string, template: boolean): boolean {
87
87
  return true;
88
88
  }
89
89
  const candidates = [path.join(ROOT, raw)];
90
+ // DEAD-ROOT REPAIR (road-to-renewal-foundation Phase 1): the
91
+ // retired-authoring-root fallbacks were unreachable (ADR-051 deleted that
92
+ // tree). They were harmless only while `dist/agent-src/` happened to
93
+ // be materialised; the moment it is not, a valid pointer is reported
94
+ // unresolved. `src/agent-src/` is the source of truth those paths project
95
+ // from.
90
96
  if (template && raw.startsWith('.augment/')) {
91
- candidates.push(path.join(ROOT, raw.replace('.augment/', '.agent-src.uncondensed/')));
92
97
  candidates.push(path.join(ROOT, raw.replace('.augment/', 'dist/agent-src/')));
98
+ candidates.push(path.join(ROOT, raw.replace('.augment/', 'src/agent-src/')));
93
99
  }
94
100
  if (raw.startsWith('dist/agent-src/')) {
95
- candidates.push(path.join(ROOT, raw.replace('dist/agent-src/', '.agent-src.uncondensed/')));
101
+ candidates.push(path.join(ROOT, raw.replace('dist/agent-src/', 'src/agent-src/')));
96
102
  }
97
103
  return candidates.some((c) => _exists(c));
98
104
  }
@@ -9,7 +9,7 @@
9
9
  * frontmatter parser, same `artefact_roots()` skill-root discovery.
10
10
  *
11
11
  * Enforces the contract from
12
- * .agent-src.uncondensed/templates/skill-archive-note.md:
12
+ * src/agent-src/templates/skill-archive-note.md:
13
13
  *
14
14
  * 1. Every <slug>.md under agents/evidence/archived-skills/ has the six
15
15
  * required frontmatter fields with valid values.
@@ -21,6 +21,9 @@ import * as path from 'node:path';
21
21
  import { fileURLToPath, pathToFileURL } from 'node:url';
22
22
  import { parse as parseYaml } from 'yaml';
23
23
 
24
+ import { SRC_AGENT, SRC_RULES, SRC_SKILLS, iter_commands } from './_lib/agent_src.js';
25
+ import { checkRatchet } from './_lib/gate_baseline.js';
26
+ import { DeadScopeError, assertScanned } from './_lib/scan_scope.js';
24
27
  import { parse_frontmatter } from './validate_frontmatter.js';
25
28
 
26
29
  const _HERE = fileURLToPath(import.meta.url);
@@ -31,6 +34,24 @@ let ROOT = path.resolve(path.dirname(_HERE), '..', '..');
31
34
  let SRC = path.join(ROOT, '.agent-src.uncondensed');
32
35
  let VOCAB_DIR = path.join(ROOT, 'src', 'config', 'discovery');
33
36
 
37
+ const _DEFAULT_ROOT = ROOT;
38
+ const _DEFAULT_SRC = SRC;
39
+
40
+ /**
41
+ * True while a caller has repointed the roots at a fixture tree.
42
+ *
43
+ * Stateless on purpose (compared against the values captured at import, not a
44
+ * sticky flag) so that a test restoring the real paths in `afterEach` also
45
+ * restores real-repo behaviour. Two things key off it: the scan roots (an
46
+ * injected container is scanned EXCLUSIVELY — the shared resolver still points
47
+ * at the real repo and would otherwise drag the whole corpus into a fixture
48
+ * run) and the violation ratchet, which records repo debt and must never judge
49
+ * a fixture.
50
+ */
51
+ function _paths_overridden(): boolean {
52
+ return ROOT !== _DEFAULT_ROOT || SRC !== _DEFAULT_SRC;
53
+ }
54
+
34
55
  /** Test seam: override the module-level scan roots (mirrors monkeypatch). */
35
56
  function _set_paths(opts: { root?: string; src?: string; vocabDir?: string }): void {
36
57
  if (opts.root !== undefined) {
@@ -71,25 +92,84 @@ function _load_vocab(): [Set<string>, Set<string>, Set<string>] {
71
92
  return [wsIds, packIds, quarantine];
72
93
  }
73
94
 
95
+ /**
96
+ * The four scan roots, named individually rather than via one container.
97
+ *
98
+ * ADR-051 did not move the pre-ADR-051 source container somewhere else — it
99
+ * split it. `skills/` and `rules/` became top-level `src/` siblings, `templates/` went to
100
+ * `src/agent-src/templates`, and `commands/` stopped being a directory at all
101
+ * (each command is now `src/domains/<pack>/<subpath>/command.md`). Until
102
+ * 2026-08-02 `SRC` still named the emptied container, so every walk below hit
103
+ * a missing directory and the gate printed `0 artefact(s) clean` forever.
104
+ *
105
+ * Deliberately NOT the resolver's category-append view (`artefact_roots()`
106
+ * + `/templates`): that also yields `src/templates`, an unrelated
107
+ * consumer-scaffold tree this gate never scanned. Repairing a root must
108
+ * restore the old scope, not silently widen it.
109
+ */
110
+ function _scan_roots(): Array<[root: string, pattern: string]> {
111
+ if (_paths_overridden()) {
112
+ // Fixture mode — the injected container is the whole world.
113
+ return [
114
+ [path.join(SRC, 'skills'), 'SKILL.md'],
115
+ [path.join(SRC, 'rules'), '*.md'],
116
+ [path.join(SRC, 'commands'), '*.md'],
117
+ [path.join(SRC, 'templates'), '*.md'],
118
+ ];
119
+ }
120
+ return [
121
+ [SRC_SKILLS(), 'SKILL.md'],
122
+ [SRC_RULES(), '*.md'],
123
+ [path.join(SRC_AGENT(), 'templates'), '*.md'],
124
+ ];
125
+ }
126
+
74
127
  function _iter_artefacts(): string[] {
75
128
  const out: string[] = [];
76
- for (const p of _rglobSorted(path.join(SRC, 'skills'), 'SKILL.md')) {
77
- out.push(p);
78
- }
79
- for (const p of _rglobSorted(path.join(SRC, 'rules'), '*.md')) {
80
- out.push(p);
81
- }
82
- for (const p of _rglobSorted(path.join(SRC, 'commands'), '*.md')) {
83
- out.push(p);
84
- }
85
- if (_exists(path.join(SRC, 'templates'))) {
86
- for (const p of _rglobSorted(path.join(SRC, 'templates'), '*.md')) {
129
+ const seen = new Set<string>();
130
+ const push = (p: string): void => {
131
+ if (!seen.has(p)) {
132
+ seen.add(p);
87
133
  out.push(p);
88
134
  }
135
+ };
136
+ for (const [root, pattern] of _scan_roots()) {
137
+ for (const p of _rglobSorted(root, pattern)) {
138
+ push(p);
139
+ }
140
+ }
141
+ if (!_paths_overridden()) {
142
+ for (const p of iter_commands()) {
143
+ push(p);
144
+ }
89
145
  }
90
146
  return out;
91
147
  }
92
148
 
149
+ /**
150
+ * Does this frontmatter actually declare an ADR-013 discovery block?
151
+ *
152
+ * Used only by the quarantine-collision check ("listed as unassigned AND
153
+ * declaring discovery metadata"). `workspaces`, `packs`, `lifecycle` and
154
+ * `install` are unambiguous ADR-013 names, so their mere presence counts.
155
+ *
156
+ * `trust` is NOT unambiguous: it is also a scalar field in two unrelated
157
+ * template schemas — `trust: durable` on a knowledge card and `trust: low` on a
158
+ * Class-C lesson card. A bare `'trust' in fm` test read those as discovery
159
+ * frontmatter and reported both quarantined templates as contradictions. They
160
+ * are not: ADR-013 `trust` is an OBJECT (`{level, confidence,
161
+ * human_review_required}`), a shape neither file has. So `trust` counts here
162
+ * only when it is object-shaped. Across the whole quarantine list these two
163
+ * strings were the only hits of any of the five names, so this is the complete
164
+ * fix, not a special case for two paths.
165
+ */
166
+ function _declares_discovery_frontmatter(fm: Record<string, unknown>): boolean {
167
+ if (['workspaces', 'packs', 'lifecycle', 'install'].some((k) => k in fm)) {
168
+ return true;
169
+ }
170
+ return 'trust' in fm && isPlainObject(fm['trust']);
171
+ }
172
+
93
173
  function _check_one(
94
174
  p: string,
95
175
  wsIds: Set<string>,
@@ -101,10 +181,7 @@ function _check_one(
101
181
  if (quarantine.has(rel)) {
102
182
  const text = fs.readFileSync(p, 'utf-8');
103
183
  const [fm] = parse_frontmatter(text);
104
- if (
105
- isPlainObject(fm) &&
106
- ['workspaces', 'packs', 'lifecycle', 'trust', 'install'].some((k) => k in fm)
107
- ) {
184
+ if (isPlainObject(fm) && _declares_discovery_frontmatter(fm)) {
108
185
  errs.push(
109
186
  `${rel}: quarantined in unassigned-artefacts.yml but carries` +
110
187
  ' discovery frontmatter — remove one or the other.',
@@ -120,7 +197,22 @@ function _check_one(
120
197
  return errs;
121
198
  }
122
199
 
123
- for (const key of ['workspaces', 'packs', 'lifecycle', 'trust', 'install']) {
200
+ // Only the two keys that carry irreducible information are REQUIRED.
201
+ //
202
+ // ADR-013 Phase 4 read as "all five strictly enforced", and this gate was
203
+ // written that way — but it never ran (dead scan root), while
204
+ // `validate_frontmatter` DID run, green, for months against
205
+ // skill.schema.json, whose `required` list is
206
+ // ["name","description","source","domain"] and which gives `lifecycle`,
207
+ // `trust` and `install` documented defaults ("active"; every `trust` and
208
+ // `install` sub-property defaulted). Two gates asserted contradictory
209
+ // contracts and only one was ever enforced — the enforced one is the real
210
+ // contract. The corpus agrees and splits cleanly along exactly that line:
211
+ // `workspaces`/`packs` are absent 0 times in 618 artefacts, while
212
+ // lifecycle/trust/install are absent 574/459/456 times. A default cannot
213
+ // invent which workspace or pack an artefact belongs to; it can supply the
214
+ // other three. See the 2026-08-02 amendment in ADR-013.
215
+ for (const key of ['workspaces', 'packs']) {
124
216
  if (!(key in fm)) {
125
217
  errs.push(`${rel}: missing required key \`${key}\``);
126
218
  }
@@ -156,42 +248,70 @@ function _check_one(
156
248
  }
157
249
  }
158
250
 
159
- const lc = fm['lifecycle'];
160
- if (typeof lc !== 'string' || !LIFECYCLES.has(lc)) {
161
- errs.push(`${rel}: lifecycle \`${lc}\` not in ${_sortedListRepr(LIFECYCLES)}`);
251
+ // lifecycle / trust / install: schema-defaulted, so ABSENCE is legal at
252
+ // BOTH levels — a missing key and a missing sub-key alike take the
253
+ // documented default. Anything actually written down is still fully
254
+ // validated; this narrows WHEN the checks fire, never what they accept.
255
+ //
256
+ // The sub-key half matters as much as the top-level half: an earlier pass
257
+ // defaulted an absent `trust` but still demanded all three sub-keys of a
258
+ // `trust:` block that was present, which is self-contradictory — the same
259
+ // default was honoured in one branch and refused in the other. It also had
260
+ // no basis in the corpus: across 288 skills, `trust` is complete 0 times,
261
+ // partial 120, absent 168. A required shape with 0/288 adoption was never
262
+ // the real requirement. `validate_frontmatter` — the gate that has actually
263
+ // been running — agrees: `apply_schema_defaults` fills missing sub-keys
264
+ // from the schema before validating, which is why it reports 0 failing on
265
+ // this same tree.
266
+ if ('lifecycle' in fm) {
267
+ const lc = fm['lifecycle'];
268
+ if (typeof lc !== 'string' || !LIFECYCLES.has(lc)) {
269
+ errs.push(`${rel}: lifecycle \`${lc}\` not in ${_sortedListRepr(LIFECYCLES)}`);
270
+ }
162
271
  }
163
272
 
164
273
  const trust = fm['trust'];
165
- if (!isPlainObject(trust)) {
274
+ if (!('trust' in fm)) {
275
+ // schema default: {level: core, confidence: high, human_review_required: false}
276
+ } else if (!isPlainObject(trust)) {
166
277
  errs.push(`${rel}: trust must be a mapping`);
167
278
  } else {
168
- const level = (trust as Record<string, unknown>)['level'];
169
- if (typeof level !== 'string' || !TRUST_LEVELS.has(level)) {
170
- errs.push(
171
- `${rel}: trust.level \`${_unwrap(level)}\` not in ${_sortedListRepr(TRUST_LEVELS)}`,
172
- );
279
+ const t = trust as Record<string, unknown>;
280
+ if ('level' in t) {
281
+ const level = t['level'];
282
+ if (typeof level !== 'string' || !TRUST_LEVELS.has(level)) {
283
+ errs.push(
284
+ `${rel}: trust.level \`${_unwrap(level)}\` not in ${_sortedListRepr(TRUST_LEVELS)}`,
285
+ );
286
+ }
173
287
  }
174
- const confidence = (trust as Record<string, unknown>)['confidence'];
175
- if (typeof confidence !== 'string' || !TRUST_CONFIDENCE.has(confidence)) {
176
- errs.push(
177
- `${rel}: trust.confidence \`${_unwrap(confidence)}\` not in` +
178
- ` ${_sortedListRepr(TRUST_CONFIDENCE)}`,
179
- );
288
+ if ('confidence' in t) {
289
+ const confidence = t['confidence'];
290
+ if (typeof confidence !== 'string' || !TRUST_CONFIDENCE.has(confidence)) {
291
+ errs.push(
292
+ `${rel}: trust.confidence \`${_unwrap(confidence)}\` not in` +
293
+ ` ${_sortedListRepr(TRUST_CONFIDENCE)}`,
294
+ );
295
+ }
180
296
  }
181
- const hrr = (trust as Record<string, unknown>)['human_review_required'];
182
- if (typeof hrr !== 'boolean') {
183
- errs.push(`${rel}: trust.human_review_required must be bool`);
297
+ if ('human_review_required' in t) {
298
+ if (typeof t['human_review_required'] !== 'boolean') {
299
+ errs.push(`${rel}: trust.human_review_required must be bool`);
300
+ }
184
301
  }
185
302
  }
186
303
 
187
304
  const install = fm['install'];
188
- if (!isPlainObject(install)) {
305
+ if (!('install' in fm)) {
306
+ // schema default: {default: true, removable: false}
307
+ } else if (!isPlainObject(install)) {
189
308
  errs.push(`${rel}: install must be a mapping`);
190
309
  } else {
191
- if (typeof (install as Record<string, unknown>)['default'] !== 'boolean') {
310
+ const i = install as Record<string, unknown>;
311
+ if ('default' in i && typeof i['default'] !== 'boolean') {
192
312
  errs.push(`${rel}: install.default must be bool`);
193
313
  }
194
- if (typeof (install as Record<string, unknown>)['removable'] !== 'boolean') {
314
+ if ('removable' in i && typeof i['removable'] !== 'boolean') {
195
315
  errs.push(`${rel}: install.removable must be bool`);
196
316
  }
197
317
  }
@@ -223,6 +343,22 @@ function main(argv?: readonly string[]): number {
223
343
 
224
344
  const [wsIds, packIds, quarantine] = _load_vocab();
225
345
  const artefacts = _iter_artefacts();
346
+ // Scope assertion: 0 artefacts means every root moved, not that the tree
347
+ // is clean. That was this gate's shipped state until 2026-08-02.
348
+ try {
349
+ assertScanned({
350
+ gate: 'lint_artefact_frontmatter',
351
+ scanned: artefacts.length,
352
+ units: 'artefact(s)',
353
+ roots: _scan_roots().map(([r]) => _relPosix(r) || '.'),
354
+ });
355
+ } catch (exc) {
356
+ if (!(exc instanceof DeadScopeError)) {
357
+ throw exc;
358
+ }
359
+ process.stderr.write(`❌ ${exc.message}\n`);
360
+ return 2;
361
+ }
226
362
  const allErrs: string[] = [];
227
363
  for (const p of artefacts) {
228
364
  allErrs.push(..._check_one(p, wsIds, packIds, quarantine));
@@ -235,6 +371,18 @@ function main(argv?: readonly string[]): number {
235
371
  process.stderr.write(
236
372
  `\n${allErrs.length} violation(s) across ${artefacts.length} artefact(s).\n`,
237
373
  );
374
+ if (!_paths_overridden()) {
375
+ const verdict = checkRatchet({
376
+ gate: 'lint_artefact_frontmatter',
377
+ actual: allErrs.length,
378
+ repoRoot: ROOT,
379
+ });
380
+ if (verdict.ok) {
381
+ process.stdout.write(`⚠️ ${verdict.message}\n`);
382
+ return 0;
383
+ }
384
+ process.stderr.write(`❌ ${verdict.message}\n`);
385
+ }
238
386
  return 1;
239
387
  }
240
388
  if (!args.quiet) {
@@ -25,6 +25,8 @@ import * as path from 'node:path';
25
25
  import { fileURLToPath, pathToFileURL } from 'node:url';
26
26
  import { parse as parseYaml } from 'yaml';
27
27
 
28
+ import { DeadScopeError, assertScanned } from './_lib/scan_scope.js';
29
+
28
30
  // src/scripts/lint_command_routing.ts → repo root is two dirs up. Mirrors the
29
31
  // Python `Path(__file__).resolve().parent.parent.parent`.
30
32
  const _HERE = fileURLToPath(import.meta.url);
@@ -109,61 +111,18 @@ function _rglobNameSorted(dir: string, name: string): string[] {
109
111
  return out.sort();
110
112
  }
111
113
 
112
- /** Recursively list `*.md` files under `dir`, sorted. */
113
- function _rglobMdSorted(dir: string): string[] {
114
- const out: string[] = [];
115
- const walk = (current: string): void => {
116
- let entries: fs.Dirent[];
117
- try {
118
- entries = fs.readdirSync(current, { withFileTypes: true });
119
- } catch {
120
- return;
121
- }
122
- for (const entry of entries) {
123
- const full = path.join(current, entry.name);
124
- if (entry.isDirectory()) {
125
- walk(full);
126
- } else if (entry.isFile() && entry.name.endsWith('.md')) {
127
- out.push(full);
128
- }
129
- }
130
- };
131
- walk(dir);
132
- return out.sort();
133
- }
134
-
135
114
  /**
136
115
  * Discover command sources in the post-ADR-051 layout.
137
116
  *
138
- * Authoring lives at src/domains/<domain>/**\/command.md; the legacy
139
- * packages/*\/.agent-src.uncondensed/commands tree is kept as a fallback for
140
- * older checkouts.
117
+ * Authoring lives at `src/domains/<domain>/**\/command.md`. A per-package
118
+ * `commands/` fallback used to sit behind this for "older checkouts"; that
119
+ * layout no longer exists in any supported checkout, so the branch was
120
+ * unreachable — and an unreachable fallback that takes over the moment its
121
+ * directory reappears is the trap this roadmap is closing, not a safety net.
141
122
  */
142
123
  function _command_files(): string[] {
143
124
  const domains = path.join(ROOT, 'src', 'domains');
144
- if (_isDir(domains)) {
145
- return _rglobNameSorted(domains, 'command.md');
146
- }
147
- const packagesDir = path.join(ROOT, 'packages');
148
- const legacyRoots: string[] = [];
149
- let entries: fs.Dirent[];
150
- try {
151
- entries = fs.readdirSync(packagesDir, { withFileTypes: true });
152
- } catch {
153
- entries = [];
154
- }
155
- // glob("*/.agent-src.uncondensed/commands") — sorted, dirs only.
156
- for (const entry of entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
157
- const d = path.join(packagesDir, entry.name, '.agent-src.uncondensed', 'commands');
158
- if (_isDir(d)) {
159
- legacyRoots.push(d);
160
- }
161
- }
162
- const out: string[] = [];
163
- for (const root of legacyRoots) {
164
- out.push(..._rglobMdSorted(root));
165
- }
166
- return out;
125
+ return _isDir(domains) ? _rglobNameSorted(domains, 'command.md') : [];
167
126
  }
168
127
 
169
128
  // Central eval store in the post-ADR-051 layout. Eval stems use the
@@ -324,8 +283,18 @@ function main(): number {
324
283
  const quiet = _quiet;
325
284
 
326
285
  const files = _command_files();
327
- if (files.length === 0) {
328
- process.stderr.write('❌ No command roots found.\n');
286
+ try {
287
+ assertScanned({
288
+ gate: 'lint_command_routing',
289
+ scanned: files.length,
290
+ units: 'command file(s)',
291
+ roots: ['src/domains/**/command.md'],
292
+ });
293
+ } catch (exc) {
294
+ if (!(exc instanceof DeadScopeError)) {
295
+ throw exc;
296
+ }
297
+ process.stderr.write(`❌ ${exc.message}\n`);
329
298
  return 3;
330
299
  }
331
300
 
@@ -364,7 +333,7 @@ function main(): number {
364
333
  if (!quiet) {
365
334
  process.stdout.write(
366
335
  `✅ ${visible} visible command(s) carry intent/routes_to/replaces ` +
367
- '+ a routing eval.\n',
336
+ `+ a routing eval (${files.length} command file(s) scanned).\n`,
368
337
  );
369
338
  }
370
339
  return 0;
@@ -25,12 +25,24 @@ import * as path from 'node:path';
25
25
  import { fileURLToPath, pathToFileURL } from 'node:url';
26
26
  import { parse as parseYaml } from 'yaml';
27
27
 
28
+ import { iter_commands } from './_lib/agent_src.js';
29
+ import { DeadScopeError, assertScanned } from './_lib/scan_scope.js';
30
+
28
31
  const _HERE = fileURLToPath(import.meta.url);
29
32
  // src/scripts/lint_command_verbs.ts → three dirs up is the repo root.
30
33
  // Mirrors Python `Path(__file__).resolve().parent.parent.parent`.
31
34
  const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
32
35
  const VERBS_YML = path.join(ROOT, 'src', 'config', 'discovery', 'command-verbs.yml');
33
- const _CMD_PATH_RE = /\.agent-src\.uncondensed\/commands\/.+\.md$/;
36
+ /**
37
+ * Diff-path filter for "is this changed file a command?".
38
+ *
39
+ * ADR-051 replaced the flat `commands/` directory with one `command.md` leaf
40
+ * per verb under `src/domains/<pack>/<subpath>/`. Until 2026-08-02 this regex
41
+ * still matched the retired layout, so `changed_command_files()` matched
42
+ * nothing on any diff and the gate printed "No new/changed commands" no matter
43
+ * what landed — leaving the ADR-041 verb contract unenforced.
44
+ */
45
+ const _CMD_PATH_RE = /^src\/domains\/.+\/command\.md$/;
34
46
  const NAME_RE = /^name:\s*(.*)$/m;
35
47
  const TIER_RE = /^tier:\s*(\d+)/m;
36
48
  const VISIBILITY_RE = /^visibility:\s*['"]?([A-Za-z-]+)/m;
@@ -196,34 +208,21 @@ function _rglobMd(dir: string): string[] {
196
208
  return out;
197
209
  }
198
210
 
211
+ /**
212
+ * Every command in the tree, as `--all` sees it.
213
+ *
214
+ * Was an enumeration of per-package `commands/` directories, a layout ADR-051
215
+ * retired; it returned {} on every checkout after that. Now the shared
216
+ * resolver's command iterator, which knows the `src/domains` leaf shape.
217
+ */
199
218
  function all_visible_command_files(): Record<string, string> {
200
219
  const out: Record<string, string> = {};
201
- const pkgs = path.join(ROOT, 'packages');
202
- let roots: string[] = [];
203
- if (_exists(pkgs) && fs.statSync(pkgs).isDirectory()) {
204
- // packages/*/.agent-src.uncondensed/commands directories.
205
- let pkgDirs: fs.Dirent[];
206
- try {
207
- pkgDirs = fs.readdirSync(pkgs, { withFileTypes: true });
208
- } catch {
209
- pkgDirs = [];
210
- }
211
- for (const d of pkgDirs) {
212
- const cand = path.join(pkgs, d.name, '.agent-src.uncondensed', 'commands');
213
- if (_exists(cand) && fs.statSync(cand).isDirectory()) {
214
- roots.push(cand);
215
- }
216
- }
217
- roots = roots.sort();
218
- }
219
- for (const root of roots) {
220
- for (const md of _rglobMd(root)) {
221
- const parts = _relTo(md, ROOT).split('/');
222
- if (path.basename(md) === 'AGENTS.md' || parts.includes('_archive')) {
223
- continue;
224
- }
225
- out[_relTo(md, ROOT)] = 'A';
220
+ for (const md of iter_commands()) {
221
+ const rel = _relTo(md, ROOT);
222
+ if (path.basename(md) === 'AGENTS.md' || rel.split('/').includes('_archive')) {
223
+ continue;
226
224
  }
225
+ out[rel] = 'A';
227
226
  }
228
227
  return out;
229
228
  }
@@ -357,11 +356,31 @@ function main(): number {
357
356
  const args = parse_args(process.argv.slice(2));
358
357
 
359
358
  const { approved, banned, grandfathered } = load_config();
360
- const targets = args.all ? all_visible_command_files() : changed_command_files(args.baseline);
359
+ // Scope assertion: this is a diff gate, so an empty TARGET set is normal
360
+ // (nothing changed). What is never normal is an empty COMMAND CORPUS — that
361
+ // means the path filter and the walk root no longer describe the tree, and
362
+ // the diff can never match. Assert on the corpus, not on the diff.
363
+ const corpus = all_visible_command_files();
364
+ try {
365
+ assertScanned({
366
+ gate: 'lint_command_verbs',
367
+ scanned: Object.keys(corpus).length,
368
+ units: 'command file(s)',
369
+ roots: ['src/domains/**/command.md'],
370
+ });
371
+ } catch (exc) {
372
+ if (!(exc instanceof DeadScopeError)) {
373
+ throw exc;
374
+ }
375
+ process.stderr.write(`❌ ${exc.message}\n`);
376
+ return 2;
377
+ }
378
+ const targets = args.all ? corpus : changed_command_files(args.baseline);
361
379
  if (Object.keys(targets).length === 0) {
362
380
  if (!args.quiet) {
363
381
  process.stdout.write(
364
- `✅ No new/changed commands under commands/ (baseline: ${args.baseline}).\n`,
382
+ `✅ No new/changed commands under src/domains/ (baseline: ${args.baseline}; ` +
383
+ `${Object.keys(corpus).length} command(s) in scope).\n`,
365
384
  );
366
385
  }
367
386
  return 0;
@@ -18,16 +18,24 @@ import * as fs from 'node:fs';
18
18
  import * as path from 'node:path';
19
19
  import { fileURLToPath, pathToFileURL } from 'node:url';
20
20
 
21
+ import { DeadScopeError, assertScanned } from './_lib/scan_scope.js';
22
+
21
23
  const _HERE = fileURLToPath(import.meta.url);
22
24
  const QUIET = process.argv.slice(2).includes('--quiet');
23
25
 
24
26
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
25
- // Mirror the Python glob order: each pattern is rglob'd + sorted, then the
26
- // patterns are concatenated in declaration order.
27
- const SKILL_GLOBS = [
28
- '.agent-src.uncondensed/skills',
29
- 'dist/agent-src/skills',
30
- ] as const;
27
+ /**
28
+ * Skill roots in precedence order — FIRST match wins; they are not unioned.
29
+ *
30
+ * The inherited list concatenated its patterns, which was safe only because
31
+ * the first one — the pre-ADR-051 source container — never matched anything
32
+ * after the move. So this gate has been linting the `dist/` projection rather
33
+ * than source. Repointing the first entry at `src/skills` while keeping the
34
+ * concatenation would double-count: both roots hold the same 288 skills.
35
+ * Hence first-wins — `src/skills` is the source of truth, `dist/` stays as the
36
+ * fallback for an installed tree that has no `src/`.
37
+ */
38
+ const SKILL_GLOBS = ['src/skills', 'dist/agent-src/skills'] as const;
31
39
  const VALID_SLOTS = [
32
40
  'product',
33
41
  'team',
@@ -136,13 +144,30 @@ function _relPosix(target: string, root: string): string {
136
144
  }
137
145
 
138
146
  function main(): number {
139
- const skills: string[] = [];
147
+ let skills: string[] = [];
148
+ let usedRoot = SKILL_GLOBS[0] as string;
140
149
  for (const pattern of SKILL_GLOBS) {
141
- skills.push(..._rglobSkillSorted(path.join(REPO_ROOT, pattern)));
150
+ const found = _rglobSkillSorted(path.join(REPO_ROOT, pattern));
151
+ if (found.length > 0) {
152
+ skills = found;
153
+ usedRoot = pattern;
154
+ break;
155
+ }
142
156
  }
143
- if (skills.length === 0) {
144
- process.stderr.write('❌ no SKILL.md files matched\n');
145
- return 1;
157
+ // Scope assertion: no SKILL.md under ANY root means every root moved.
158
+ try {
159
+ assertScanned({
160
+ gate: 'lint_context_spine_usage',
161
+ scanned: skills.length,
162
+ units: 'SKILL.md file(s)',
163
+ roots: SKILL_GLOBS as readonly string[],
164
+ });
165
+ } catch (exc) {
166
+ if (!(exc instanceof DeadScopeError)) {
167
+ throw exc;
168
+ }
169
+ process.stderr.write(`❌ ${exc.message}\n`);
170
+ return 2;
146
171
  }
147
172
  let failed = 0;
148
173
  let declared = 0;
@@ -172,7 +197,8 @@ function main(): number {
172
197
  if (!QUIET) {
173
198
  process.stdout.write(
174
199
  `✅ ${declared} skill(s) declare context_spine; ` +
175
- `all declared slots are cited in the body\n`,
200
+ `all declared slots are cited in the body ` +
201
+ `(${skills.length} SKILL.md scanned under ${usedRoot})\n`,
176
202
  );
177
203
  }
178
204
  return 0;