@event4u/agent-config 9.14.0 → 9.16.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 (287) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +118 -99
  3. package/README.md +8 -0
  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 +5 -2
  15. package/dist/agent-src/commands/optimize/skills.md +5 -5
  16. package/dist/agent-src/commands/override.md +1 -0
  17. package/dist/agent-src/commands/prediction-pool.md +4 -4
  18. package/dist/agent-src/commands/research/deep.md +1 -1
  19. package/dist/agent-src/commands/roadmap.md +2 -1
  20. package/dist/agent-src/commands/rule-compliance-audit.md +1 -1
  21. package/dist/agent-src/commands/tdd.md +1 -0
  22. package/dist/agent-src/commands/tests.md +1 -0
  23. package/dist/agent-src/commands/upstream-contribute.md +5 -5
  24. package/dist/agent-src/commands/video/from-script.md +1 -1
  25. package/dist/agent-src/commands/video/from-song.md +1 -1
  26. package/dist/agent-src/commands/video/scene.md +1 -1
  27. package/dist/agent-src/commands/video/stitch.md +1 -1
  28. package/dist/agent-src/commands/video/storyboard.md +1 -1
  29. package/dist/agent-src/commands/worktree/create.md +2 -1
  30. package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
  31. package/dist/agent-src/contexts/contracts/artifact-engagement-flow.md +5 -5
  32. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +3 -3
  33. package/dist/agent-src/contexts/contracts/emergency-triage-block.md +2 -2
  34. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +4 -3
  35. package/dist/agent-src/contexts/execution/subagent-routing.md +19 -0
  36. package/dist/agent-src/contexts/override-system.md +1 -1
  37. package/dist/agent-src/ghostwriter/fictional-fixture-v1.md +1 -1
  38. package/dist/agent-src/rules/augment-edit-discipline.md +1 -1
  39. package/dist/agent-src/rules/autonomous-execution.md +2 -1
  40. package/dist/agent-src/rules/brand-consistency.md +1 -0
  41. package/dist/agent-src/rules/brand-source-of-truth.md +1 -0
  42. package/dist/agent-src/rules/code-comment-discipline.md +2 -0
  43. package/dist/agent-src/rules/communication-through-line.md +1 -0
  44. package/dist/agent-src/rules/content-quoting-floor.md +1 -0
  45. package/dist/agent-src/rules/cross-source-consistency.md +0 -1
  46. package/dist/agent-src/rules/downstream-changes.md +1 -0
  47. package/dist/agent-src/rules/external-code-graph-interop.md +1 -0
  48. package/dist/agent-src/rules/external-reference-deep-dive.md +1 -0
  49. package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
  50. package/dist/agent-src/rules/finance-safety-floor.md +0 -1
  51. package/dist/agent-src/rules/icon-consistency.md +1 -0
  52. package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
  53. package/dist/agent-src/rules/improve-before-implement.md +1 -1
  54. package/dist/agent-src/rules/invite-challenge.md +1 -0
  55. package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
  56. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  57. package/dist/agent-src/rules/markdown-safe-codeblocks.md +1 -0
  58. package/dist/agent-src/rules/media-governance-routing.md +4 -3
  59. package/dist/agent-src/rules/media-sync-ground-truth.md +1 -0
  60. package/dist/agent-src/rules/no-attribution-footers.md +1 -0
  61. package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +1 -0
  62. package/dist/agent-src/rules/no-pr-progress-comments.md +1 -0
  63. package/dist/agent-src/rules/onboarding-gate.md +1 -0
  64. package/dist/agent-src/rules/output-discipline.md +1 -0
  65. package/dist/agent-src/rules/prefer-enums-over-literals.md +1 -0
  66. package/dist/agent-src/rules/preservation-guard.md +1 -0
  67. package/dist/agent-src/rules/question-not-instruction.md +1 -0
  68. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +1 -0
  69. package/dist/agent-src/rules/role-mode-adherence.md +1 -0
  70. package/dist/agent-src/rules/runtime-safety.md +1 -0
  71. package/dist/agent-src/rules/security-sensitive-stop.md +1 -0
  72. package/dist/agent-src/rules/session-canary.md +9 -2
  73. package/dist/agent-src/rules/source-confidentiality.md +1 -0
  74. package/dist/agent-src/rules/source-of-truth.md +1 -0
  75. package/dist/agent-src/rules/spreadsheet-source-quality.md +1 -0
  76. package/dist/agent-src/rules/think-before-action.md +1 -0
  77. package/dist/agent-src/rules/token-budget-discipline.md +1 -0
  78. package/dist/agent-src/rules/token-efficiency.md +1 -0
  79. package/dist/agent-src/rules/token-optimizer-maintenance.md +5 -5
  80. package/dist/agent-src/rules/tool-safety.md +1 -0
  81. package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
  82. package/dist/agent-src/rules/user-interaction.md +1 -0
  83. package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
  84. package/dist/agent-src/skills/adr-create/SKILL.md +1 -1
  85. package/dist/agent-src/skills/check-refs/SKILL.md +1 -1
  86. package/dist/agent-src/skills/competitive-positioning/SKILL.md +1 -1
  87. package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
  88. package/dist/agent-src/skills/context-authoring/SKILL.md +1 -1
  89. package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +1 -1
  90. package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
  91. package/dist/agent-src/skills/github-ci/SKILL.md +1 -0
  92. package/dist/agent-src/skills/guideline-writing/SKILL.md +1 -1
  93. package/dist/agent-src/skills/lint-skills/SKILL.md +1 -1
  94. package/dist/agent-src/skills/readme-reviewer/SKILL.md +1 -1
  95. package/dist/agent-src/skills/roadmap-management/SKILL.md +8 -1
  96. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  97. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  98. package/dist/agent-src/skills/skill-writing/SKILL.md +4 -4
  99. package/dist/agent-src/skills/token-optimizer/SKILL.md +4 -4
  100. package/dist/agent-src/skills/upstream-contribute/SKILL.md +6 -6
  101. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -0
  102. package/dist/agent-src/templates/AGENTS.md +1 -1
  103. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  104. package/dist/agent-src/templates/command.md +1 -1
  105. package/dist/agent-src/templates/copilot-instructions.md +3 -3
  106. package/dist/agent-src/templates/rule.md +2 -2
  107. package/dist/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
  108. package/dist/agent-src/templates/skill-archive-note.md +5 -5
  109. package/dist/agent-src/templates/skill.md +1 -1
  110. package/dist/cli/agent-config.js +66 -19
  111. package/dist/cli/agent-config.js.map +1 -1
  112. package/dist/cli/main.js +9 -4
  113. package/dist/cli/main.js.map +1 -1
  114. package/dist/cli/mcp/content.js +1 -1
  115. package/dist/cli/mcp/content.js.map +1 -1
  116. package/dist/cli/python/workspace_drive.js +2 -0
  117. package/dist/cli/python/workspace_drive.js.map +1 -1
  118. package/dist/cli/python/workspace_drive_health.js +2 -0
  119. package/dist/cli/python/workspace_drive_health.js.map +1 -1
  120. package/dist/cli/python/workspace_hosts.js +2 -0
  121. package/dist/cli/python/workspace_hosts.js.map +1 -1
  122. package/dist/cli/python/workspace_sessions.js +2 -0
  123. package/dist/cli/python/workspace_sessions.js.map +1 -1
  124. package/dist/cli/registry.js +1 -0
  125. package/dist/cli/registry.js.map +1 -1
  126. package/dist/cli-delegate/{chunk-A3HJ6NTI.js → chunk-63WSP6YZ.js} +2 -1
  127. package/dist/cli-delegate/{chunk-EJYIGZ3T.js → chunk-BKO2AZ7Z.js} +1 -1
  128. package/dist/cli-delegate/{chunk-GCVFLBP2.js → chunk-H574KOUH.js} +338 -494
  129. package/dist/cli-delegate/chunk-NXFWFAYV.js +339 -0
  130. package/dist/cli-delegate/{chunk-TEGJU2JX.js → chunk-ZXXJLL5K.js} +2 -2
  131. package/dist/cli-delegate/cmd_conformance.js +29 -23
  132. package/dist/cli-delegate/cmd_doctor.js +3 -3
  133. package/dist/cli-delegate/cmd_export.js +4 -4
  134. package/dist/cli-delegate/cmd_preflight.js +2 -2
  135. package/dist/cli-delegate/cmd_prune.js +4 -4
  136. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  137. package/dist/cli-delegate/cmd_sync.js +4 -4
  138. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  139. package/dist/cli-delegate/cmd_validate.js +4 -4
  140. package/dist/discovery/deprecation-report.md +1 -1
  141. package/dist/discovery/discovery-manifest.json +127 -95
  142. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  143. package/dist/discovery/discovery-manifest.summary.md +1 -1
  144. package/dist/discovery/orphan-report.md +1 -1
  145. package/dist/discovery/packs.json +2 -2
  146. package/dist/discovery/trust-report.md +1 -1
  147. package/dist/discovery/workspaces.json +2 -2
  148. package/dist/hooks/dispatch.js +729 -430
  149. package/dist/install/install.mjs +52 -11
  150. package/dist/install/rule_scope.js +18 -2
  151. package/dist/install/rule_scope.js.map +1 -1
  152. package/dist/mcp/registry-manifest.json +2 -2
  153. package/dist/mcp/server.json +2 -2
  154. package/dist/mcp/server.mjs +518 -411
  155. package/dist/router.json +1 -1
  156. package/dist/scripts/_lib/claude_settings_hooks.js +30 -5
  157. package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
  158. package/dist/scripts/_lib/scoped_projection.js +223 -0
  159. package/dist/scripts/_lib/scoped_projection.js.map +1 -0
  160. package/dist/scripts/_lib/surface_tiers.js +125 -0
  161. package/dist/scripts/_lib/surface_tiers.js.map +1 -0
  162. package/dist/scripts/_lib/token_count.js +166 -0
  163. package/dist/scripts/_lib/token_count.js.map +1 -0
  164. package/dist/scripts/_lib/trigger_eval_floors.js +8 -0
  165. package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -1
  166. package/dist/scripts/condense.js +139 -9
  167. package/dist/scripts/condense.js.map +1 -1
  168. package/dist/scripts/project_thin_rules.js +450 -0
  169. package/dist/scripts/project_thin_rules.js.map +1 -0
  170. package/dist/server/routes/workspace.js +3 -1
  171. package/dist/server/routes/workspace.js.map +1 -1
  172. package/dist/server/schemas/settings.js +9 -3
  173. package/dist/server/schemas/settings.js.map +1 -1
  174. package/dist/shared/userMd/utils.js +1 -1
  175. package/dist/shared/userMd/utils.js.map +1 -1
  176. package/dist/ui/assets/index-DmR0mbk4.js +26 -0
  177. package/dist/ui/assets/index-DmR0mbk4.js.map +1 -0
  178. package/dist/ui/index.html +1 -1
  179. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  180. package/package.json +14 -14
  181. package/src/agent-src/templates/AGENTS.md +1 -1
  182. package/src/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
  183. package/src/config/agent-settings.template.yml +61 -11
  184. package/src/config/budget-routing.json +7 -0
  185. package/src/config/ci-local-parity.yml +8 -0
  186. package/src/config/discovery/unassigned-artefacts.yml +23 -25
  187. package/src/config/evaluator-budgets.json +35 -5
  188. package/src/config/gate-coverage.yml +147 -0
  189. package/src/config/gate-violation-baselines.json +25 -0
  190. package/src/install/rule_scope.ts +27 -2
  191. package/src/scripts/_cli/cmd_conformance.ts +13 -24
  192. package/src/scripts/_dispatch.bash +10 -0
  193. package/src/scripts/_lib/claude_settings_hooks.ts +30 -4
  194. package/src/scripts/_lib/gate_baseline.ts +213 -0
  195. package/src/scripts/_lib/scoped_projection.ts +50 -0
  196. package/src/scripts/_lib/tier_budget_routing.ts +360 -0
  197. package/src/scripts/_lib/tool_adapter_registry.ts +8 -1
  198. package/src/scripts/_lib/trigger_eval_floors.ts +9 -0
  199. package/src/scripts/_lib/trigger_routers.ts +17 -5
  200. package/src/scripts/adr/regenerate_index.ts +4 -3
  201. package/src/scripts/ai-video/lib/parse-blueprint.sh +1 -1
  202. package/src/scripts/ai_council/compile_corpus.ts +1 -1
  203. package/src/scripts/anchor_scoring_run.ts +1 -1
  204. package/src/scripts/audit_cloud_compatibility.ts +37 -2
  205. package/src/scripts/audit_initial_context.ts +53 -19
  206. package/src/scripts/audit_likelihood.ts +31 -2
  207. package/src/scripts/audit_user_type_axis.ts +58 -11
  208. package/src/scripts/bench_hook_latency.ts +172 -41
  209. package/src/scripts/bench_persona_placebo.ts +1 -1
  210. package/src/scripts/bench_quality_run.ts +1 -1
  211. package/src/scripts/build_mcp_catalog.ts +12 -1
  212. package/src/scripts/check_always_budget.ts +122 -11
  213. package/src/scripts/check_augment_description_cap.ts +24 -3
  214. package/src/scripts/check_ci_strict_superset.ts +150 -0
  215. package/src/scripts/check_cli_registry_budget_sync.ts +168 -0
  216. package/src/scripts/check_cluster_patterns.ts +100 -0
  217. package/src/scripts/check_consumer_scope_flip.ts +26 -4
  218. package/src/scripts/check_evaluator_budgets.ts +198 -19
  219. package/src/scripts/check_gate_coverage.ts +338 -2
  220. package/src/scripts/check_generator_output_coverage.ts +2 -1
  221. package/src/scripts/check_iron_law_prominence.ts +1 -1
  222. package/src/scripts/check_kernel_rule_bundle.ts +1 -1
  223. package/src/scripts/check_no_new_legacy_path.ts +167 -3
  224. package/src/scripts/check_pack_size.ts +18 -4
  225. package/src/scripts/check_portability.ts +37 -7
  226. package/src/scripts/check_references.ts +19 -3
  227. package/src/scripts/check_release_pr_shape.ts +13 -0
  228. package/src/scripts/check_token_quality_golden.ts +1 -1
  229. package/src/scripts/check_token_regression.ts +9 -2
  230. package/src/scripts/condense.ts +155 -12
  231. package/src/scripts/cost/budget.mjs +77 -2
  232. package/src/scripts/evaluator_umbrella.sh +34 -1
  233. package/src/scripts/generate_capability_matrix.ts +8 -0
  234. package/src/scripts/generate_router_coverage_from_matrix.ts +155 -0
  235. package/src/scripts/hook_manifest.yaml +17 -3
  236. package/src/scripts/hooks/block_config_weakening.ts +337 -0
  237. package/src/scripts/hooks/concern_registry.ts +2 -0
  238. package/src/scripts/hooks/probe_throttle.ts +75 -0
  239. package/src/scripts/install-hooks.sh +24 -2
  240. package/src/scripts/install.ts +2 -2
  241. package/src/scripts/lint_agents_md.ts +9 -3
  242. package/src/scripts/lint_archived_skills.ts +1 -1
  243. package/src/scripts/lint_artefact_frontmatter.ts +185 -37
  244. package/src/scripts/lint_command_routing.ts +21 -52
  245. package/src/scripts/lint_command_verbs.ts +47 -28
  246. package/src/scripts/lint_context_spine_usage.ts +38 -12
  247. package/src/scripts/lint_framework_leakage.ts +25 -6
  248. package/src/scripts/lint_handoffs.ts +49 -2
  249. package/src/scripts/lint_media_policy_linkage.ts +62 -30
  250. package/src/scripts/lint_namespace.ts +92 -7
  251. package/src/scripts/lint_new_skill_gate.ts +39 -31
  252. package/src/scripts/lint_originality_shingles.ts +7 -1
  253. package/src/scripts/lint_override_kernel_guard.ts +21 -3
  254. package/src/scripts/lint_pack_boundaries.ts +461 -91
  255. package/src/scripts/lint_pack_dependencies.ts +24 -57
  256. package/src/scripts/lint_persona_governance.ts +1 -1
  257. package/src/scripts/lint_role_experiences.ts +34 -5
  258. package/src/scripts/lint_trigger_precision.ts +142 -0
  259. package/src/scripts/mcp_server/consumer_tool_catalog.json +12 -201
  260. package/src/scripts/mcp_server/tools.ts +1 -1
  261. package/src/scripts/prepack-check.mjs +40 -1
  262. package/src/scripts/prepack_router_targets.d.mts +7 -0
  263. package/src/scripts/prepack_router_targets.mjs +93 -0
  264. package/src/scripts/print_required_checks.ts +72 -18
  265. package/src/scripts/profile_staleness_hook.ts +12 -0
  266. package/src/scripts/record_evaluator_measurements.mjs +105 -0
  267. package/src/scripts/recruit_preflight.sh +1 -1
  268. package/src/scripts/release.ts +214 -7
  269. package/src/scripts/router_target_paths.d.mts +4 -0
  270. package/src/scripts/router_target_paths.mjs +69 -0
  271. package/src/scripts/router_telemetry.ts +52 -1
  272. package/src/scripts/routing_doctor.ts +560 -0
  273. package/src/scripts/rule_trigger_eval.ts +403 -0
  274. package/src/scripts/schemas/rule.schema.json +5 -1
  275. package/src/scripts/session_canary_hook.ts +96 -5
  276. package/src/scripts/skill_linter.ts +9 -28
  277. package/src/scripts/smoke/skills.sh +1 -1
  278. package/src/scripts/sweep_dead_scan_roots.ts +356 -10
  279. package/src/scripts/trigger_coverage.ts +13 -2
  280. package/src/scripts/update_counts.ts +1 -1
  281. package/src/scripts/validate_telegraph_carveouts.ts +1 -1
  282. package/src/scripts/wrapper_freshness_hook.ts +14 -0
  283. package/src/server/schemas/settings.ts +19 -5
  284. package/src/shared/userMd/utils.ts +1 -1
  285. package/dist/cli-delegate/chunk-TJIAM5FW.js +0 -133
  286. package/dist/ui/assets/index-BpSQt5vm.js +0 -40
  287. package/dist/ui/assets/index-BpSQt5vm.js.map +0 -1
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Budget-aware tier routing (road-to-tested-routing Phase 7).
3
+ *
4
+ * Deterministic decision layer for `docs/contracts/budget-routing.md`:
5
+ * pick the cheapest CLASSIFIER-ADEQUATE tier that has available budget;
6
+ * cheap exhausted but a stronger tier funded → route up, never block;
7
+ * everything exhausted or unreadable → session model + notice (fail-open).
8
+ *
9
+ * Two hard mechanism requirements from the contract:
10
+ * - `acquireBudgetPermit` is an ATOMIC reserve — O_EXCL lock file, then
11
+ * sum + append-pending in one critical section. Check-then-spend races
12
+ * two concurrent dispatches past the ceiling (council finding
13
+ * 2026-08-03).
14
+ * - `tripCooldown` / cool-down state — a 429/quota error pauses a tier
15
+ * (default 60 min) instead of retry-looping; `pickTier` skips cooling
16
+ * tiers via the same relation.
17
+ *
18
+ * Pure decision logic is I/O-free; only the permit/cool-down helpers
19
+ * touch the filesystem (ledger + state under agents/cost-tracking/).
20
+ */
21
+ import * as fs from 'node:fs';
22
+ import * as path from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ export type BudgetTier = 'cheap' | 'medium' | 'strong';
26
+
27
+ /** Ordered cheapest-first — the relation walks this order upward. */
28
+ export const TIER_ORDER: readonly BudgetTier[] = ['cheap', 'medium', 'strong'];
29
+
30
+ export interface TierBudgetState {
31
+ /** USD ceiling from cost.budgets.per_tier.<tier>; null = no tier cap. */
32
+ ceiling_usd: number | null;
33
+ /** Ledger-summed spend for this tier in the ceiling's window (USD). */
34
+ spent_usd: number;
35
+ /** Epoch ms until which this tier is cooling after a quota error; 0 = live. */
36
+ cooldown_until_ms: number;
37
+ }
38
+
39
+ export type BudgetRoutingSwitch = 'ask' | 'auto' | 'off';
40
+
41
+ export interface PickTierInputs {
42
+ /** Cheapest tier the classifier marked adequate (quality floor). */
43
+ adequate_tier: BudgetTier;
44
+ /** Per-tier budget state, complete over TIER_ORDER. */
45
+ budgets: Record<BudgetTier, TierBudgetState>;
46
+ /** subagents.budget_routing setting. */
47
+ routing_switch: BudgetRoutingSwitch;
48
+ /** Estimated cost of the dispatch (USD) for the availability check. */
49
+ estimated_cost_usd: number;
50
+ /** Clock injection — never Date.now() in the decision path. */
51
+ now_ms: number;
52
+ }
53
+
54
+ export interface PickTierDecision {
55
+ /** 'tier' → dispatch on `tier`; 'session' → run on the session model. */
56
+ route: 'tier' | 'session';
57
+ tier: BudgetTier | null;
58
+ /** 'proceed' | 'ask' (switch=ask and a budget-motivated downshift) */
59
+ action: 'proceed' | 'ask';
60
+ /** Non-empty when the relation degraded (exhausted / cooling / off). */
61
+ notice: string;
62
+ reason: string;
63
+ }
64
+
65
+ function tierAvailable(
66
+ s: TierBudgetState,
67
+ est: number,
68
+ now: number,
69
+ ): { ok: boolean; why: string } {
70
+ if (s.cooldown_until_ms > now) {
71
+ return { ok: false, why: 'cooling after quota error' };
72
+ }
73
+ if (s.ceiling_usd === null) {
74
+ return { ok: true, why: 'no tier cap' };
75
+ }
76
+ if (s.spent_usd + est > s.ceiling_usd) {
77
+ return { ok: false, why: `ceiling ${s.ceiling_usd} reached (spent ${s.spent_usd})` };
78
+ }
79
+ return { ok: true, why: 'within ceiling' };
80
+ }
81
+
82
+ /** The binding relation from docs/contracts/budget-routing.md. */
83
+ export function pickTier(inp: PickTierInputs): PickTierDecision {
84
+ if (inp.routing_switch === 'off') {
85
+ return {
86
+ route: 'session',
87
+ tier: null,
88
+ action: 'proceed',
89
+ notice: '',
90
+ reason: 'budget_routing off — session model, today\'s behavior',
91
+ };
92
+ }
93
+ const startIdx = TIER_ORDER.indexOf(inp.adequate_tier);
94
+ const skipped: string[] = [];
95
+ for (let i = startIdx; i < TIER_ORDER.length; i += 1) {
96
+ const tier = TIER_ORDER[i] as BudgetTier;
97
+ const avail = tierAvailable(inp.budgets[tier], inp.estimated_cost_usd, inp.now_ms);
98
+ if (avail.ok) {
99
+ const degraded = i > startIdx;
100
+ return {
101
+ route: 'tier',
102
+ tier,
103
+ action: inp.routing_switch === 'ask' ? 'ask' : 'proceed',
104
+ notice: degraded
105
+ ? `cheapest adequate tier unavailable (${skipped.join('; ')}) — routed up to ${tier}`
106
+ : '',
107
+ reason: `${tier}: ${avail.why}${degraded ? '' : ' — cheapest adequate tier'}`,
108
+ };
109
+ }
110
+ skipped.push(`${tier}: ${avail.why}`);
111
+ }
112
+ return {
113
+ route: 'session',
114
+ tier: null,
115
+ action: 'proceed',
116
+ notice: `all tier budgets unavailable (${skipped.join('; ')}) — session model, work not blocked`,
117
+ reason: 'fail-open: budget machinery degrades savings, never work',
118
+ };
119
+ }
120
+
121
+ // ── Atomic permit + cool-down (filesystem side) ────────────────────────────
122
+
123
+ export const RESERVE_FILE = 'tier-reserves.jsonl';
124
+ export const COOLDOWN_FILE = 'tier-cooldowns.json';
125
+ export const DEFAULT_COOLDOWN_MS = 60 * 60 * 1000;
126
+
127
+ // Reserve lifecycle constants — single source of truth shared with
128
+ // budget.mjs (the second reserve reader) via src/config/budget-routing.json.
129
+ // Duplicating the literal here was the exact two-truths defect the external
130
+ // review flagged; both readers now load the same file.
131
+ const _LIFECYCLE_CONFIG = path.join(
132
+ path.dirname(fileURLToPath(import.meta.url)),
133
+ '..',
134
+ '..',
135
+ 'config',
136
+ 'budget-routing.json',
137
+ );
138
+
139
+ function _lifecycle(): { reserve_ttl_ms: number; lock_break_ms: number } {
140
+ try {
141
+ const raw = JSON.parse(fs.readFileSync(_LIFECYCLE_CONFIG, 'utf-8')) as Record<string, unknown>;
142
+ const ttl = typeof raw['reserve_ttl_ms'] === 'number' ? raw['reserve_ttl_ms'] : 600_000;
143
+ const brk = typeof raw['lock_break_ms'] === 'number' ? raw['lock_break_ms'] : 30_000;
144
+ return { reserve_ttl_ms: ttl, lock_break_ms: brk };
145
+ } catch {
146
+ return { reserve_ttl_ms: 600_000, lock_break_ms: 30_000 };
147
+ }
148
+ }
149
+
150
+ export function reserveTtlMs(): number {
151
+ return _lifecycle().reserve_ttl_ms;
152
+ }
153
+
154
+ interface ReserveEntry {
155
+ ts_ms: number;
156
+ tier: BudgetTier;
157
+ est_usd: number;
158
+ status: 'pending' | 'settled';
159
+ }
160
+
161
+ function _readReserves(reservePath: string): ReserveEntry[] {
162
+ if (!fs.existsSync(reservePath)) {
163
+ return [];
164
+ }
165
+ const out: ReserveEntry[] = [];
166
+ for (const line of fs.readFileSync(reservePath, 'utf-8').split('\n')) {
167
+ if (!line.trim()) continue;
168
+ try {
169
+ out.push(JSON.parse(line) as ReserveEntry);
170
+ } catch {
171
+ // tolerate a torn line — reserve file is advisory spend state
172
+ }
173
+ }
174
+ return out;
175
+ }
176
+
177
+ /** Take the reserve lock; break a stale one (crash leftover) once. */
178
+ function _takeLock(lock: string, now_ms: number, lock_break_ms: number): number | null {
179
+ for (let attempt = 0; attempt < 2; attempt += 1) {
180
+ try {
181
+ return fs.openSync(lock, 'wx');
182
+ } catch {
183
+ try {
184
+ const age = now_ms - fs.statSync(lock).mtimeMs;
185
+ if (age > lock_break_ms) {
186
+ fs.rmSync(lock, { force: true });
187
+ continue; // one retry after breaking the stale lock
188
+ }
189
+ } catch {
190
+ continue; // lock vanished between open and stat — retry once
191
+ }
192
+ return null;
193
+ }
194
+ }
195
+ return null;
196
+ }
197
+
198
+ /**
199
+ * Atomic budget reserve — full lifecycle (external review 2026-08-03):
200
+ *
201
+ * acquire → expire (TTL) / settle → compact-on-write
202
+ *
203
+ * Under the lock: live reserves = pending entries younger than the shared
204
+ * TTL (reserves are RACE PROTECTION, not spend accounting — real spend is
205
+ * the ledger's job); the file is COMPACTED to exactly those live entries
206
+ * on every acquire, so it stays bounded; a lock file older than
207
+ * `lock_break_ms` is a crash leftover and is broken with one retry.
208
+ * Returns granted=false (never throws) when the ceiling would be crossed
209
+ * or a fresh lock is genuinely held — the caller falls back per the
210
+ * relation (fail-open at the DECISION layer, conservative at the SPEND
211
+ * layer).
212
+ */
213
+ export function acquireBudgetPermit(options: {
214
+ tracking_dir: string;
215
+ tier: BudgetTier;
216
+ estimated_cost_usd: number;
217
+ ceiling_usd: number | null;
218
+ spent_usd: number;
219
+ now_ms: number;
220
+ }): { granted: boolean; reason: string } {
221
+ const dir = options.tracking_dir;
222
+ fs.mkdirSync(dir, { recursive: true });
223
+ const lock = path.join(dir, `${RESERVE_FILE}.lock`);
224
+ const { reserve_ttl_ms, lock_break_ms } = _lifecycle();
225
+ const fd = _takeLock(lock, options.now_ms, lock_break_ms);
226
+ if (fd === null) {
227
+ return { granted: false, reason: 'reserve lock busy — treat tier as unavailable this instant' };
228
+ }
229
+ try {
230
+ const reservePath = path.join(dir, RESERVE_FILE);
231
+ const live = _readReserves(reservePath).filter(
232
+ (e) => e.status === 'pending' && options.now_ms - e.ts_ms < reserve_ttl_ms,
233
+ );
234
+ const reserved = live
235
+ .filter((e) => e.tier === options.tier)
236
+ .reduce((acc, e) => acc + e.est_usd, 0);
237
+ if (
238
+ options.ceiling_usd !== null &&
239
+ options.spent_usd + reserved + options.estimated_cost_usd > options.ceiling_usd
240
+ ) {
241
+ // Compact even on deny — expiry must not depend on a grant.
242
+ fs.writeFileSync(
243
+ reservePath,
244
+ live.map((e) => JSON.stringify(e)).join('\n') + (live.length ? '\n' : ''),
245
+ 'utf-8',
246
+ );
247
+ return {
248
+ granted: false,
249
+ reason: `ceiling ${options.ceiling_usd} would be crossed (spent ${options.spent_usd} + reserved ${reserved})`,
250
+ };
251
+ }
252
+ live.push({
253
+ ts_ms: options.now_ms,
254
+ tier: options.tier,
255
+ est_usd: options.estimated_cost_usd,
256
+ status: 'pending',
257
+ });
258
+ fs.writeFileSync(
259
+ reservePath,
260
+ live.map((e) => JSON.stringify(e)).join('\n') + '\n',
261
+ 'utf-8',
262
+ );
263
+ return { granted: true, reason: 'reserved' };
264
+ } finally {
265
+ fs.closeSync(fd);
266
+ fs.rmSync(lock, { force: true });
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Settle the oldest live pending reserve of a tier (call when the real
272
+ * cost lands in the ledger, so the estimate stops counting immediately
273
+ * instead of waiting out the TTL). Settled entries are dropped by the
274
+ * next compaction. Best-effort: lock busy → no-op (the TTL is the
275
+ * backstop that makes settling optional, never load-bearing).
276
+ */
277
+ export function settlePermit(options: {
278
+ tracking_dir: string;
279
+ tier: BudgetTier;
280
+ now_ms: number;
281
+ }): boolean {
282
+ const lock = path.join(options.tracking_dir, `${RESERVE_FILE}.lock`);
283
+ const { reserve_ttl_ms, lock_break_ms } = _lifecycle();
284
+ const fd = _takeLock(lock, options.now_ms, lock_break_ms);
285
+ if (fd === null) {
286
+ return false;
287
+ }
288
+ try {
289
+ const reservePath = path.join(options.tracking_dir, RESERVE_FILE);
290
+ const live = _readReserves(reservePath).filter(
291
+ (e) => e.status === 'pending' && options.now_ms - e.ts_ms < reserve_ttl_ms,
292
+ );
293
+ const idx = live.findIndex((e) => e.tier === options.tier);
294
+ if (idx === -1) {
295
+ return false;
296
+ }
297
+ live.splice(idx, 1);
298
+ fs.writeFileSync(
299
+ reservePath,
300
+ live.map((e) => JSON.stringify(e)).join('\n') + (live.length ? '\n' : ''),
301
+ 'utf-8',
302
+ );
303
+ return true;
304
+ } finally {
305
+ fs.closeSync(fd);
306
+ fs.rmSync(lock, { force: true });
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Record a quota/429 error: the tier cools down; pickTier skips it.
312
+ *
313
+ * Deliberately lock-free: last-writer-wins on a 3-key timestamp map is
314
+ * benign (two concurrent trips write near-identical "until" values), so
315
+ * the reserve lock is not reused here (accepted in the 2026-08-03 review).
316
+ * Expired entries are purged on every write so the map never grows stale.
317
+ */
318
+ export function tripCooldown(options: {
319
+ tracking_dir: string;
320
+ tier: BudgetTier;
321
+ now_ms: number;
322
+ cooldown_ms?: number;
323
+ }): number {
324
+ const until = options.now_ms + (options.cooldown_ms ?? DEFAULT_COOLDOWN_MS);
325
+ const p = path.join(options.tracking_dir, COOLDOWN_FILE);
326
+ let state: Record<string, number> = {};
327
+ try {
328
+ state = JSON.parse(fs.readFileSync(p, 'utf-8')) as Record<string, number>;
329
+ } catch {
330
+ state = {};
331
+ }
332
+ for (const [k, v] of Object.entries(state)) {
333
+ if (typeof v !== 'number' || v <= options.now_ms) {
334
+ delete state[k];
335
+ }
336
+ }
337
+ state[options.tier] = until;
338
+ fs.mkdirSync(options.tracking_dir, { recursive: true });
339
+ fs.writeFileSync(p, `${JSON.stringify(state, null, 2)}\n`, 'utf-8');
340
+ return until;
341
+ }
342
+
343
+ /** Read the cool-down map (missing/torn file → all tiers live). */
344
+ export function readCooldowns(tracking_dir: string): Record<BudgetTier, number> {
345
+ const empty: Record<BudgetTier, number> = { cheap: 0, medium: 0, strong: 0 };
346
+ try {
347
+ const raw = JSON.parse(
348
+ fs.readFileSync(path.join(tracking_dir, COOLDOWN_FILE), 'utf-8'),
349
+ ) as Record<string, unknown>;
350
+ for (const t of TIER_ORDER) {
351
+ const v = raw[t];
352
+ if (typeof v === 'number' && Number.isFinite(v)) {
353
+ empty[t] = v;
354
+ }
355
+ }
356
+ return empty;
357
+ } catch {
358
+ return empty;
359
+ }
360
+ }
@@ -31,7 +31,14 @@ export const ADAPTER_REGISTRY: readonly AdapterEntry[] = [
31
31
  {
32
32
  tool: 'Claude Code',
33
33
  kind: 'native',
34
- roots: ['.claude/rules', '.claude/skills', '.claude/agents', '.claude/personas', '.claude/user-types'],
34
+ roots: [
35
+ '.claude/rules',
36
+ '.claude/skills',
37
+ '.claude/commands',
38
+ '.claude/agents',
39
+ '.claude/personas',
40
+ '.claude/user-types',
41
+ ],
35
42
  generator_tag: 'claude-code',
36
43
  },
37
44
  { tool: 'Cursor', kind: 'native', roots: ['.cursor/rules', '.cursor/commands'], generator_tag: 'cursor' },
@@ -29,3 +29,12 @@ export const DOMAIN_FLOORS: Readonly<Record<string, TriggerEvalFloor>> = {
29
29
  export function floor_for(skill: string | null | undefined): TriggerEvalFloor {
30
30
  return (skill !== null && skill !== undefined && DOMAIN_FLOORS[skill]) || DEFAULT_FLOOR;
31
31
  }
32
+
33
+ /**
34
+ * Advisory floor for the rules-mode canary leg (`rule_trigger_eval.ts`,
35
+ * road-to-tested-routing Phase 4). Deliberately loose: it is a drift canary
36
+ * over a ~40-case derived suite spanning ~97 rules, not a per-rule quality
37
+ * gate — ratchet only after live baseline data exists. A breach fails the
38
+ * scheduled canary job only, never a PR.
39
+ */
40
+ export const RULES_MODE_FLOOR: TriggerEvalFloor = { minRecall: 0.5, minPrecision: 0.5 };
@@ -42,14 +42,20 @@ export interface FetchRouterOpts {
42
42
  model?: string;
43
43
  apiKey?: string;
44
44
  fetchImpl?: FetchImpl;
45
+ /**
46
+ * Prompt header prepended to the catalogue (default: the skill-routing
47
+ * header). The rules-mode eval (`rule_trigger_eval.ts`) injects its own
48
+ * header here so it reuses this transport code unchanged.
49
+ */
50
+ promptHeader?: string;
45
51
  }
46
52
 
47
53
  const MAX_TOKENS = 256;
48
54
 
49
55
  /** Build the system prompt the same way AnthropicRouter does. */
50
- function buildSystemPrompt(skills: SkillMeta[]): string {
56
+ function buildSystemPrompt(skills: SkillMeta[], header: string = ROUTING_PROMPT_HEADER): string {
51
57
  const catalogue = skills.map((s) => `- ${s.name} :: ${s.description}`).join('\n');
52
- return ROUTING_PROMPT_HEADER + catalogue + '\n';
58
+ return header + catalogue + '\n';
53
59
  }
54
60
 
55
61
  function requireKey(name: string, apiKey: string | undefined): string {
@@ -97,11 +103,13 @@ export class OpenAiRouter implements TriggerRouter {
97
103
  private _model: string;
98
104
  private _apiKey: string;
99
105
  private _fetch: FetchImpl;
106
+ private _promptHeader: string | undefined;
100
107
 
101
108
  constructor(opts: FetchRouterOpts = {}) {
102
109
  this._model = opts.model ?? 'gpt-4o-mini';
103
110
  this._apiKey = requireKey('OpenAiRouter', opts.apiKey);
104
111
  this._fetch = opts.fetchImpl ?? (globalThis.fetch as unknown as FetchImpl);
112
+ this._promptHeader = opts.promptHeader;
105
113
  }
106
114
 
107
115
  route(_query: string, _skills: SkillMeta[]): [string[], number, number] {
@@ -109,7 +117,7 @@ export class OpenAiRouter implements TriggerRouter {
109
117
  }
110
118
 
111
119
  async routeAsync(query: string, skills: SkillMeta[]): Promise<[string[], number, number]> {
112
- const systemPrompt = buildSystemPrompt(skills);
120
+ const systemPrompt = buildSystemPrompt(skills, this._promptHeader);
113
121
  const body = JSON.stringify({
114
122
  model: this._model,
115
123
  max_tokens: MAX_TOKENS,
@@ -156,12 +164,14 @@ export class GeminiRouter implements TriggerRouter {
156
164
  private _model: string;
157
165
  private _apiKey: string;
158
166
  private _fetch: FetchImpl;
167
+ private _promptHeader: string | undefined;
159
168
 
160
169
  constructor(opts: FetchRouterOpts = {}) {
161
170
  // flash, NOT pro: pro burns ~150 "thinking" tokens on trivial prompts.
162
171
  this._model = opts.model ?? 'gemini-2.5-flash';
163
172
  this._apiKey = requireKey('GeminiRouter', opts.apiKey);
164
173
  this._fetch = opts.fetchImpl ?? (globalThis.fetch as unknown as FetchImpl);
174
+ this._promptHeader = opts.promptHeader;
165
175
  }
166
176
 
167
177
  route(_query: string, _skills: SkillMeta[]): [string[], number, number] {
@@ -169,7 +179,7 @@ export class GeminiRouter implements TriggerRouter {
169
179
  }
170
180
 
171
181
  async routeAsync(query: string, skills: SkillMeta[]): Promise<[string[], number, number]> {
172
- const systemPrompt = buildSystemPrompt(skills);
182
+ const systemPrompt = buildSystemPrompt(skills, this._promptHeader);
173
183
  const url =
174
184
  `https://generativelanguage.googleapis.com/v1beta/models/` +
175
185
  `${this._model}:generateContent?key=${this._apiKey}`;
@@ -221,11 +231,13 @@ export class AnthropicFetchRouter implements TriggerRouter {
221
231
  private _model: string;
222
232
  private _apiKey: string;
223
233
  private _fetch: FetchImpl;
234
+ private _promptHeader: string | undefined;
224
235
 
225
236
  constructor(opts: FetchRouterOpts = {}) {
226
237
  this._model = opts.model ?? 'claude-haiku-4-5-20251001';
227
238
  this._apiKey = requireKey('AnthropicFetchRouter', opts.apiKey);
228
239
  this._fetch = opts.fetchImpl ?? (globalThis.fetch as unknown as FetchImpl);
240
+ this._promptHeader = opts.promptHeader;
229
241
  }
230
242
 
231
243
  route(_query: string, _skills: SkillMeta[]): [string[], number, number] {
@@ -233,7 +245,7 @@ export class AnthropicFetchRouter implements TriggerRouter {
233
245
  }
234
246
 
235
247
  async routeAsync(query: string, skills: SkillMeta[]): Promise<[string[], number, number]> {
236
- const systemPrompt = buildSystemPrompt(skills);
248
+ const systemPrompt = buildSystemPrompt(skills, this._promptHeader);
237
249
  const body = JSON.stringify({
238
250
  model: this._model,
239
251
  max_tokens: MAX_TOKENS,
@@ -8,8 +8,9 @@
8
8
  * Ported from the retired Python `src/scripts/adr/regenerate_index.py` (ADR-200).
9
9
  * The CLI contract is pinned — `--dir` (default `docs/adr/`),
10
10
  * `--check`, exit codes (0 / 1 / 2), the stdout/stderr split, byte-identical
11
- * messages, AND byte-identical generated `INDEX.md`. No behaviour changes —
12
- * historical quirks preserved (consumers pin the exact behaviour).
11
+ * messages. One deliberate deviation from the Python output: the INDEX.md
12
+ * header names this TypeScript generator, not the retired `.py` (the ported
13
+ * self-misdescription was itself a defect — ADR-hygiene roadmap, Phase 1).
13
14
  */
14
15
  import * as fs from 'node:fs';
15
16
  import * as path from 'node:path';
@@ -244,7 +245,7 @@ export function render(num: Row[], leg: Row[], outside: OutsideRow[] = []): stri
244
245
  const out = [
245
246
  '# ADR Index',
246
247
  '',
247
- '_Auto-generated by `scripts/adr/regenerate_index.py`. Do not edit._',
248
+ '_Auto-generated by `src/scripts/adr/regenerate_index.ts`. Do not edit._',
248
249
  '',
249
250
  ];
250
251
  if (num.length === 0 && leg.length === 0 && outside.length === 0) {
@@ -4,7 +4,7 @@
4
4
  # stdout. Pure POSIX-compatible bash (no associative arrays, runs
5
5
  # on macOS bash 3.2); jq required for JSON safety.
6
6
  #
7
- # Schema: .agent-src.uncondensed/skills/scene-expander/scene-blueprint.schema.yaml
7
+ # Schema: src/skills/scene-expander/scene-blueprint.schema.yaml
8
8
  # Contract: scripts/media/lib/adapter-contract.md
9
9
  #
10
10
  # Usage:
@@ -10,7 +10,7 @@
10
10
  * human-authored source-of-truth for PR review. This script reads it
11
11
  * through the hardened {@link parse_corpus_strict} parser and writes a YAML
12
12
  * lockfile that becomes the **runtime** source-of-truth. The pattern mirrors
13
- * `dist/agent-src/` vs `.agent-src.uncondensed/`: human edits Markdown,
13
+ * `dist/agent-src/` vs `src/`: human edits Markdown,
14
14
  * `task consistency` enforces lockfile parity via the same
15
15
  * `git diff --quiet` gate.
16
16
  *
@@ -22,7 +22,7 @@ import * as path from 'node:path';
22
22
  import process from 'node:process';
23
23
  import { fileURLToPath } from 'node:url';
24
24
 
25
- import yaml from 'js-yaml';
25
+ import * as yaml from 'js-yaml';
26
26
 
27
27
  import { build_thin, RULES_SOURCE } from './project_thin_rules.js';
28
28
  import { cohensKappa } from './check_quality_regression.js';
@@ -22,6 +22,8 @@ import * as path from 'node:path';
22
22
  import process from 'node:process';
23
23
  import { fileURLToPath, pathToFileURL } from 'node:url';
24
24
 
25
+ import { DeadScopeError, assertScanned } from './_lib/scan_scope.js';
26
+
25
27
  type Json = unknown;
26
28
  type JsonObject = Record<string, Json>;
27
29
 
@@ -29,9 +31,25 @@ const _HERE = fileURLToPath(import.meta.url);
29
31
 
30
32
  // src/scripts/audit_cloud_compatibility.ts → parents[2] of the .py file = repo root.
31
33
  export const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
32
- export const SOURCE = path.join(ROOT, '.agent-src.uncondensed');
33
34
  export const SCAN_DIRS = ['skills', 'rules', 'commands', 'guidelines'];
34
35
 
36
+ /**
37
+ * Where each artefact kind actually lives.
38
+ *
39
+ * Every kind used to be `SOURCE/<kind>` under one pre-ADR-051 container. All
40
+ * four resolved to a missing directory after the move, so `scan()` returned []
41
+ * and the audit emitted `"total": 0` with every histogram empty — a cloud-
42
+ * compatibility report over zero artefacts. The four kinds now have four
43
+ * unrelated homes (commands are not a directory at all but one `command.md`
44
+ * leaf per verb), so the mapping is explicit rather than a join.
45
+ */
46
+ export const KIND_ROOTS: Readonly<Record<string, string>> = {
47
+ skills: path.join(ROOT, 'src', 'skills'),
48
+ rules: path.join(ROOT, 'src', 'rules'),
49
+ commands: path.join(ROOT, 'src', 'domains'),
50
+ guidelines: path.join(ROOT, 'docs', 'guidelines'),
51
+ };
52
+
35
53
  // --- patterns (global so findall returns every non-overlapping match) ---------
36
54
  const SCRIPT_RE = /scripts\/[a-z_]+\.(?:py|sh)|python3\s+scripts\/|bash\s+scripts\/|\.\/scripts\//g;
37
55
  const TASK_RE = /`task\s+[a-z][a-z0-9-]+`/g;
@@ -357,7 +375,7 @@ interface Row extends ClassifyEvidence {
357
375
  export function scan(): Row[] {
358
376
  const rows: Row[] = [];
359
377
  for (const sub of SCAN_DIRS) {
360
- const base = path.join(SOURCE, sub);
378
+ const base = KIND_ROOTS[sub] as string;
361
379
  if (!_isDir(base)) {
362
380
  continue;
363
381
  }
@@ -565,6 +583,23 @@ export function main(argv: string[] | null = null): number {
565
583
  }
566
584
 
567
585
  const rows = scan();
586
+ // Scope assertion: a cloud-compat report over an empty corpus is not "no
587
+ // cloud-incompatible artefacts", it is a report about nothing — the exact
588
+ // `"total": 0` output this audit produced while its roots were dead.
589
+ try {
590
+ assertScanned({
591
+ gate: 'audit_cloud_compatibility',
592
+ scanned: rows.length,
593
+ units: 'artefact(s)',
594
+ roots: SCAN_DIRS.map((k) => _relativeToPosix(KIND_ROOTS[k] as string, ROOT)),
595
+ });
596
+ } catch (exc) {
597
+ if (!(exc instanceof DeadScopeError)) {
598
+ throw exc;
599
+ }
600
+ process.stderr.write(`❌ ${exc.message}\n`);
601
+ return 2;
602
+ }
568
603
  const summary = summarize(rows);
569
604
 
570
605
  if (args.details) {