@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
@@ -3,17 +3,38 @@
3
3
  * check_evaluator_budgets — enforce the outside-in evaluator budgets
4
4
  * (road-to-credible-install Phase 6) against a measurements JSON.
5
5
  *
6
- * Two failure modes per metric:
6
+ * Three failure modes per metric:
7
7
  * 1. absolute: measured > budgets[metric].max
8
8
  * 2. regression: measured > last_measured * (1 + regression_pct/100)
9
9
  * — fails EVEN WHEN still under the absolute budget (the >10%
10
10
  * creep rule), so slow rot cannot hide under a generous cap.
11
+ * 3. drift: a DETERMINISTIC metric disagrees with the committed
12
+ * measurement record the nightly publishes. `cli_help_command_count`
13
+ * drifted 74 → 80 against a value frozen at 79 with nobody seeing it;
14
+ * the record makes the day a number moves the day it shows in a diff
15
+ * (road-to-gates-that-can-fail Phase 5).
16
+ *
17
+ * ## Posture — warn on main, fail on release
18
+ *
19
+ * A hard fail on `main` turns every legitimate command addition into a
20
+ * blocked merge, which is how budgets get quietly raised with a cushion
21
+ * instead of consciously. So budget / regression / drift findings are
22
+ * ADVISORY outside a release context and BLOCKING inside one. This is not a
23
+ * weakening: before the Phase-4 path triggers, the umbrella ran on release
24
+ * PRs and the nightly only — the surfaces that now warn never saw the gate
25
+ * at all. The posture IS the delivery route the council adopted for nightly
26
+ * visibility, not a second mechanism beside it.
27
+ *
28
+ * Structural findings (a metric with NO measurement supplied) are ALWAYS
29
+ * blocking, in either posture: that is a harness reporting green on nothing,
30
+ * which is the whole class this roadmap exists to end.
11
31
  *
12
32
  * Usage:
13
33
  * ./scripts-run src/scripts/check_evaluator_budgets --measurements <file.json>
34
+ * [--recorded <file.json>] [--posture warn|fail]
14
35
  * (measurements: flat { metric: number } — produced by evaluator_umbrella.sh)
15
36
  *
16
- * Exit codes: 0 green · 1 budget/regression exceeded · 2 internal error.
37
+ * Exit codes: 0 green (or advisory findings only) · 1 blocking finding · 2 internal error.
17
38
  */
18
39
  import * as fs from 'node:fs';
19
40
  import * as path from 'node:path';
@@ -22,6 +43,9 @@ import { fileURLToPath } from 'node:url';
22
43
  const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
23
44
  const BUDGETS_PATH = path.join(REPO_ROOT, 'src', 'config', 'evaluator-budgets.json');
24
45
 
46
+ /** Committed measurement record the nightly refreshes (Phase 5 step 1). */
47
+ export const RECORD_REL = path.join('agents', 'evidence', 'metrics', 'evaluator-measurements.json');
48
+
25
49
  interface BudgetEntry {
26
50
  max: number;
27
51
  last_measured: number;
@@ -30,51 +54,174 @@ interface BudgetEntry {
30
54
  * (absolute cap is their real gate); sizes/counts keep the tight
31
55
  * global default. */
32
56
  regression_pct?: number;
57
+ /** `false` for wall-clock metrics: they cannot be compared run-to-run,
58
+ * so they are excluded from the drift check. Missing = deterministic. */
59
+ deterministic?: boolean;
33
60
  }
34
61
  interface BudgetsDoc {
35
62
  regression_pct: number;
36
63
  budgets: Record<string, BudgetEntry>;
37
64
  }
38
65
 
39
- export function evaluate(
66
+ /** Shape of the committed measurement record. */
67
+ export interface MeasurementRecord {
68
+ schema_version: number;
69
+ recorded_at: string;
70
+ git_sha?: string;
71
+ run_url?: string;
72
+ conditions?: string;
73
+ measurements: Record<string, number | string>;
74
+ }
75
+
76
+ export type Severity =
77
+ /** Always blocks — the harness itself is broken. */
78
+ | 'structural'
79
+ /** Blocks only under the `fail` posture. */
80
+ | 'advisory';
81
+
82
+ export interface Finding {
83
+ metric: string;
84
+ severity: Severity;
85
+ message: string;
86
+ }
87
+
88
+ export function evaluateFindings(
40
89
  budgetsDoc: BudgetsDoc,
41
90
  measurements: Record<string, number>,
42
- ): string[] {
43
- const errors: string[] = [];
91
+ ): Finding[] {
92
+ const findings: Finding[] = [];
44
93
  for (const [metric, entry] of Object.entries(budgetsDoc.budgets)) {
45
94
  const measured = measurements[metric];
46
95
  if (measured === undefined || Number.isNaN(measured)) {
47
- errors.push(`${metric}: no measurement supplied (method: ${entry.method})`);
96
+ findings.push({
97
+ metric,
98
+ severity: 'structural',
99
+ message: `${metric}: no measurement supplied (method: ${entry.method})`,
100
+ });
48
101
  continue;
49
102
  }
50
103
  if (measured > entry.max) {
51
- errors.push(`${metric}: measured ${measured} exceeds budget ${entry.max}`);
104
+ findings.push({
105
+ metric,
106
+ severity: 'advisory',
107
+ message: `${metric}: measured ${measured} exceeds budget ${entry.max}`,
108
+ });
52
109
  continue;
53
110
  }
54
111
  const pct = entry.regression_pct ?? budgetsDoc.regression_pct;
55
112
  const ceiling = entry.last_measured * (1 + pct / 100);
56
113
  if (entry.last_measured > 0 && measured > ceiling) {
57
- errors.push(
58
- `${metric}: measured ${measured} regressed >${pct}% vs ` +
114
+ findings.push({
115
+ metric,
116
+ severity: 'advisory',
117
+ message:
118
+ `${metric}: measured ${measured} regressed >${pct}% vs ` +
59
119
  `last_measured ${entry.last_measured} (ceiling ${ceiling.toFixed(2)}) — ` +
60
120
  'fails even under the absolute budget',
61
- );
121
+ });
62
122
  }
63
123
  }
64
- return errors;
124
+ return findings;
125
+ }
126
+
127
+ /**
128
+ * Back-compat surface: the flat message list. Kept because the budget shape
129
+ * and the per-metric rules are pinned by tests through this function.
130
+ */
131
+ export function evaluate(budgetsDoc: BudgetsDoc, measurements: Record<string, number>): string[] {
132
+ return evaluateFindings(budgetsDoc, measurements).map((f) => f.message);
133
+ }
134
+
135
+ /**
136
+ * Compare a fresh measurement set against the committed record.
137
+ *
138
+ * Only DETERMINISTIC metrics participate: comparing wall-clock timings
139
+ * run-to-run produces noise, and a noisy comparison gets ignored — which
140
+ * reproduces the failure the record exists to fix. A metric absent from
141
+ * either side is skipped (a missing measurement is already a structural
142
+ * finding above; a metric absent from the record is simply not recorded yet).
143
+ */
144
+ export function driftFindings(
145
+ budgetsDoc: BudgetsDoc,
146
+ measurements: Record<string, number>,
147
+ record: MeasurementRecord | null,
148
+ ): Finding[] {
149
+ if (record === null) return [];
150
+ const findings: Finding[] = [];
151
+ for (const [metric, entry] of Object.entries(budgetsDoc.budgets)) {
152
+ if (entry.deterministic === false) continue;
153
+ const fresh = measurements[metric];
154
+ const recorded = record.measurements[metric];
155
+ if (fresh === undefined || recorded === undefined) continue;
156
+ if (typeof recorded !== 'number') continue;
157
+ if (fresh === recorded) continue;
158
+ findings.push({
159
+ metric,
160
+ severity: 'advisory',
161
+ message:
162
+ `${metric}: measured ${fresh} but the committed record says ${recorded} ` +
163
+ `(recorded ${record.recorded_at}` +
164
+ `${record.git_sha ? ` @ ${record.git_sha.slice(0, 8)}` : ''}) — refresh ` +
165
+ `${RECORD_REL} in the PR that moves the number, or explain the move`,
166
+ });
167
+ }
168
+ return findings;
169
+ }
170
+
171
+ export type Posture = 'warn' | 'fail';
172
+
173
+ /**
174
+ * Where are we, and does a budget finding block?
175
+ *
176
+ * Release context → `fail`. Local (no CI env) → `fail`, deliberately: a
177
+ * maintainer running this by hand — including `release.ts` — wants the strict
178
+ * answer, and a local run blocks nobody else. Everything else (main, the
179
+ * nightly, a feature PR) → `warn`.
180
+ */
181
+ export function detectPosture(
182
+ env: NodeJS.ProcessEnv = process.env,
183
+ override?: string,
184
+ ): { posture: Posture; reason: string } {
185
+ if (override === 'warn' || override === 'fail') {
186
+ return { posture: override, reason: `--posture ${override}` };
187
+ }
188
+ const head = env['GITHUB_HEAD_REF'] ?? '';
189
+ const base = env['GITHUB_BASE_REF'] ?? '';
190
+ const ref = env['GITHUB_REF'] ?? '';
191
+ if (head.startsWith('release/')) {
192
+ return { posture: 'fail', reason: `release PR (head ${head})` };
193
+ }
194
+ if (base.startsWith('release/')) {
195
+ return { posture: 'fail', reason: `PR into a release branch (base ${base})` };
196
+ }
197
+ if (ref.startsWith('refs/heads/release/') || ref.startsWith('refs/tags/')) {
198
+ return { posture: 'fail', reason: `release ref (${ref})` };
199
+ }
200
+ if (!env['GITHUB_ACTIONS']) {
201
+ return { posture: 'fail', reason: 'local run (no CI env) — strict by default' };
202
+ }
203
+ return { posture: 'warn', reason: `non-release CI context (${ref || 'unknown ref'})` };
204
+ }
205
+
206
+ function argValue(argv: string[], flag: string): string | undefined {
207
+ const i = argv.indexOf(flag);
208
+ return i >= 0 ? argv[i + 1] : undefined;
65
209
  }
66
210
 
67
211
  export function main(argv: string[] = process.argv.slice(2)): number {
68
- const idx = argv.indexOf('--measurements');
69
- if (idx < 0 || argv[idx + 1] === undefined) {
70
- process.stderr.write('usage: check_evaluator_budgets --measurements <file.json>\n');
212
+ const measurementsPath = argValue(argv, '--measurements');
213
+ if (measurementsPath === undefined) {
214
+ process.stderr.write(
215
+ 'usage: check_evaluator_budgets --measurements <file.json> ' +
216
+ '[--recorded <file.json>] [--posture warn|fail]\n',
217
+ );
71
218
  return 2;
72
219
  }
73
220
  let budgetsDoc: BudgetsDoc;
74
221
  let measurements: Record<string, number>;
75
222
  try {
76
223
  budgetsDoc = JSON.parse(fs.readFileSync(BUDGETS_PATH, 'utf-8')) as BudgetsDoc;
77
- measurements = JSON.parse(fs.readFileSync(argv[idx + 1] as string, 'utf-8')) as Record<
224
+ measurements = JSON.parse(fs.readFileSync(measurementsPath, 'utf-8')) as Record<
78
225
  string,
79
226
  number
80
227
  >;
@@ -82,13 +229,45 @@ export function main(argv: string[] = process.argv.slice(2)): number {
82
229
  process.stderr.write(`check_evaluator_budgets: ${(e as Error).message}\n`);
83
230
  return 2;
84
231
  }
85
- const errors = evaluate(budgetsDoc, measurements);
86
- if (errors.length > 0) {
87
- for (const e of errors) process.stderr.write(`❌ evaluator budget: ${e}\n`);
232
+
233
+ // The committed record is optional: a fresh checkout before the first
234
+ // nightly has none, and a missing record must not fake a green drift check.
235
+ const recordPath = argValue(argv, '--recorded') ?? path.join(REPO_ROOT, RECORD_REL);
236
+ let record: MeasurementRecord | null = null;
237
+ try {
238
+ record = JSON.parse(fs.readFileSync(recordPath, 'utf-8')) as MeasurementRecord;
239
+ } catch {
240
+ process.stdout.write(
241
+ `ℹ️ no measurement record at ${recordPath} — drift check skipped ` +
242
+ '(the nightly publishes it)\n',
243
+ );
244
+ }
245
+
246
+ const { posture, reason } = detectPosture(process.env, argValue(argv, '--posture'));
247
+ const findings = [
248
+ ...evaluateFindings(budgetsDoc, measurements),
249
+ ...driftFindings(budgetsDoc, measurements, record),
250
+ ];
251
+ const blocking = findings.filter((f) => f.severity === 'structural' || posture === 'fail');
252
+ const advisory = findings.filter((f) => !blocking.includes(f));
253
+
254
+ for (const f of advisory) {
255
+ process.stderr.write(`⚠️ evaluator budget (advisory, posture=${posture}): ${f.message}\n`);
256
+ }
257
+ for (const f of blocking) {
258
+ process.stderr.write(`❌ evaluator budget: ${f.message}\n`);
259
+ }
260
+ if (blocking.length > 0) {
261
+ process.stderr.write(
262
+ `check_evaluator_budgets: ${blocking.length} blocking finding(s) — ` +
263
+ `posture ${posture} (${reason})\n`,
264
+ );
88
265
  return 1;
89
266
  }
90
267
  process.stdout.write(
91
- `✅ evaluator budgets met (${Object.keys(budgetsDoc.budgets).length} metrics)\n`,
268
+ `✅ evaluator budgets met (${Object.keys(budgetsDoc.budgets).length} metrics` +
269
+ `${advisory.length > 0 ? `, ${advisory.length} advisory finding(s)` : ''}) — ` +
270
+ `posture ${posture} (${reason})\n`,
92
271
  );
93
272
  return 0;
94
273
  }
@@ -23,13 +23,25 @@
23
23
  * the manifest is empty or unreadable, and it REPORTS `pending` gates (listed but
24
24
  * not yet emitting a count) instead of skipping them silently.
25
25
  *
26
+ * MUTATION CANARY (`--canary`, road-to-gates-that-can-fail Phase 7). Coverage
27
+ * proves a gate READ something; it cannot prove the gate can still FAIL. The
28
+ * canary plants one declared violation per gate, runs the gate, and records
29
+ * red/green — a gate that stays green over a real planted defect is dead by
30
+ * definition. It runs the same contract as the review-side canary in
31
+ * `docs/contracts/adversarial-review-protocol.md` § 6 (rotating class, sealed
32
+ * record, NEVER SHIPS) and is deliberately kept OFF the default CI path: it
33
+ * mutates the tree, so it is an operator-invoked biannual experiment, not a
34
+ * per-PR gate. Every plant is reverted in a `finally`.
35
+ *
26
36
  * CLI:
27
37
  * ./scripts-run src/scripts/check_gate_coverage # enforce
28
38
  * ./scripts-run src/scripts/check_gate_coverage --list # show the manifest
29
39
  * ./scripts-run src/scripts/check_gate_coverage --format json
40
+ * ./scripts-run src/scripts/check_gate_coverage --canary [--ledger <file>]
30
41
  *
31
42
  * Exit codes: 0 all enforced gates cleared their floor · 1 a gate is blind,
32
- * collapsed, or silent · 2 the manifest itself is missing/empty/malformed.
43
+ * collapsed, or silent (in `--canary`: a gate stayed green, or the ledger
44
+ * disagrees with the census) · 2 the manifest itself is missing/empty/malformed.
33
45
  */
34
46
  import * as fs from 'node:fs';
35
47
  import * as path from 'node:path';
@@ -42,16 +54,36 @@ import { runCountedProbe } from './_lib/counted_probe.js';
42
54
  const _HERE = fileURLToPath(import.meta.url);
43
55
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
44
56
  const MANIFEST = path.join(REPO_ROOT, 'src/config/gate-coverage.yml');
57
+ const CENSUS = path.join(REPO_ROOT, 'agents/evidence/reports/gate-scope-census.md');
45
58
 
46
59
  /** The one line a gate must emit. Deliberately narrow: no format guessing. */
47
60
  const SCANNED_RE = /^\s*scanned:\s*(\d+)\s*$/m;
48
61
 
62
+ /**
63
+ * A declared, revertible violation for one gate (Phase 7).
64
+ *
65
+ * Only ONE operation shape exists on purpose: create a file that does not exist,
66
+ * delete it afterwards. An in-place edit would need byte-exact restoration of a
67
+ * tracked file, and a canary that can corrupt the tree is worse than no canary —
68
+ * the contract's "never ships" rule binds this implementation too.
69
+ */
70
+ export interface CanarySpec {
71
+ /** Rotating-class label, per adversarial-review-protocol § 6. */
72
+ class: string;
73
+ /** Repo-relative path to create. MUST NOT already exist. */
74
+ path: string;
75
+ /** File body planted at that path. */
76
+ content: string;
77
+ }
78
+
49
79
  export interface GateSpec {
50
80
  id: string;
51
81
  argv: string[];
52
82
  min_scanned: number;
53
83
  corpus: string;
54
84
  status: 'enforced' | 'pending';
85
+ /** Absent = this gate has no canary recipe; reported, never hidden. */
86
+ canary?: CanarySpec;
55
87
  /**
56
88
  * Exit code meaning "my prerequisite is absent, so I inspected nothing" — e.g.
57
89
  * check_site_links exits 2 when the site is unbuilt or stale. Without this the
@@ -117,10 +149,27 @@ export function load_manifest(file = MANIFEST): GateSpec[] {
117
149
  ? {}
118
150
  : { unavailable_exit: _require_int(e['unavailable_exit'], id, i) }),
119
151
  ...(e['note'] === undefined ? {} : { note: String(e['note']) }),
152
+ ...(e['canary'] === undefined ? {} : { canary: _require_canary(e['canary'], id, i) }),
120
153
  };
121
154
  });
122
155
  }
123
156
 
157
+ function _require_canary(v: unknown, id: string, i: number): CanarySpec {
158
+ const c = v as Record<string, unknown>;
159
+ const cls = String(c?.['class'] ?? '');
160
+ const p = String(c?.['path'] ?? '');
161
+ const content = String(c?.['content'] ?? '');
162
+ // A half-declared recipe would plant nothing and read as a passing canary —
163
+ // the exact false green this whole file exists to prevent.
164
+ if (cls === '' || p === '' || content === '') {
165
+ throw new Error(`gates[${String(i)}] (${id}): canary needs class, path and content`);
166
+ }
167
+ if (path.isAbsolute(p) || p.split('/').includes('..')) {
168
+ throw new Error(`gates[${String(i)}] (${id}): canary path must be repo-relative and contain no '..'`);
169
+ }
170
+ return { class: cls, path: p, content };
171
+ }
172
+
124
173
  /** Extract the contract line. Returns null when the gate emitted no count. */
125
174
  export function parse_scanned(output: string): number | null {
126
175
  const m = SCANNED_RE.exec(output);
@@ -215,6 +264,10 @@ export function main(argv: readonly string[]): number {
215
264
  return 2;
216
265
  }
217
266
 
267
+ if (argv.includes('--canary')) {
268
+ return run_canary_mode(specs, argv);
269
+ }
270
+
218
271
  if (listOnly) {
219
272
  for (const s of specs) {
220
273
  process.stdout.write(
@@ -283,6 +336,289 @@ function probe_pending(s: GateSpec): { scanned: number | null; crashed: boolean
283
336
  return run_gate(s);
284
337
  }
285
338
 
339
+ // ── Mutation canary (Phase 7) ──────────────────────────────────────────────
340
+
341
+ export type CanaryVerdict = 'red' | 'green' | 'no_recipe' | 'crashed';
342
+
343
+ export interface CanaryResult {
344
+ id: string;
345
+ verdict: CanaryVerdict;
346
+ class: string | null;
347
+ exit_code: number | null;
348
+ message: string;
349
+ }
350
+
351
+ /**
352
+ * Plant one declared violation, run the gate, revert.
353
+ *
354
+ * The revert is in a `finally` and deletes exactly the file this function
355
+ * created — never a pre-existing one, which is why an occupied path aborts
356
+ * instead of overwriting.
357
+ */
358
+ export function run_canary(spec: GateSpec, repoRoot = REPO_ROOT): CanaryResult {
359
+ const c = spec.canary;
360
+ if (c === undefined) {
361
+ return {
362
+ id: spec.id,
363
+ verdict: 'no_recipe',
364
+ class: null,
365
+ exit_code: null,
366
+ message: 'no canary recipe declared — this gate is UNPROVEN, not proven working',
367
+ };
368
+ }
369
+ const abs = path.join(repoRoot, c.path);
370
+ if (fs.existsSync(abs)) {
371
+ return {
372
+ id: spec.id,
373
+ verdict: 'crashed',
374
+ class: c.class,
375
+ exit_code: null,
376
+ message: `canary path ${c.path} already exists — refusing to overwrite a real file`,
377
+ };
378
+ }
379
+ let planted = false;
380
+ // Directories the plant had to create. An empty `src/skills/<x>/` left behind
381
+ // is invisible to `git status` but is still residue, and a skill directory
382
+ // with no SKILL.md is itself a defect the next gate run would report.
383
+ const madeDirs: string[] = [];
384
+ for (let d = path.dirname(abs); !fs.existsSync(d) && d.startsWith(repoRoot); d = path.dirname(d)) {
385
+ madeDirs.push(d);
386
+ }
387
+ try {
388
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
389
+ fs.writeFileSync(abs, c.content, 'utf8');
390
+ planted = true;
391
+ const runner = path.join(repoRoot, 'scripts-run');
392
+ const r = runCountedProbe(runner, [`src/scripts/${spec.id}`, ...spec.argv], { cwd: repoRoot });
393
+ if (r.failure !== null && r.status === null) {
394
+ return {
395
+ id: spec.id,
396
+ verdict: 'crashed',
397
+ class: c.class,
398
+ exit_code: null,
399
+ message: `gate could not be executed under the plant: ${r.failure}`,
400
+ };
401
+ }
402
+ const code = r.status ?? 0;
403
+ return code !== 0
404
+ ? {
405
+ id: spec.id,
406
+ verdict: 'red',
407
+ class: c.class,
408
+ exit_code: code,
409
+ message: `caught the planted ${c.class} defect (exit ${String(code)})`,
410
+ }
411
+ : {
412
+ id: spec.id,
413
+ verdict: 'green',
414
+ class: c.class,
415
+ exit_code: 0,
416
+ message:
417
+ `stayed GREEN over a planted ${c.class} defect at ${c.path} — ` +
418
+ 'the gate is dead by definition. This row is a defect ticket',
419
+ };
420
+ } finally {
421
+ // NEVER SHIPS (adversarial-review-protocol § 6): the plant is removed even
422
+ // when the gate crashes or the process is interrupted mid-run.
423
+ if (planted) {
424
+ try {
425
+ fs.rmSync(abs, { force: true });
426
+ // Innermost first — rmdir only succeeds while the directory is empty,
427
+ // so a dir that gained other content is left alone.
428
+ for (const d of madeDirs) {
429
+ try {
430
+ fs.rmdirSync(d);
431
+ } catch {
432
+ break;
433
+ }
434
+ }
435
+ } catch {
436
+ process.stderr.write(`❌ canary could not remove its plant at ${c.path} — REMOVE IT BY HAND\n`);
437
+ }
438
+ }
439
+ }
440
+ }
441
+
442
+ export interface Disagreement {
443
+ gate: string;
444
+ kind: 'dead_gate' | 'census_stale';
445
+ detail: string;
446
+ }
447
+
448
+ /**
449
+ * Read the scan-scope census into `gate -> units`.
450
+ *
451
+ * Column-position-agnostic: it locates the gate column and the units column by
452
+ * header text, so a census regenerated with a different column set still
453
+ * cross-checks. `null` = the census recorded no measurable count.
454
+ */
455
+ export function parse_census(md: string): Map<string, number | null> {
456
+ const out = new Map<string, number | null>();
457
+ const rows = md.split('\n').filter((l) => l.trim().startsWith('|'));
458
+ const cells = (l: string): string[] =>
459
+ l.split('|').slice(1, -1).map((c) => c.trim());
460
+ let gateCol = -1;
461
+ let unitCol = -1;
462
+ for (const line of rows) {
463
+ const cs = cells(line);
464
+ const head = cs.map((c) => c.toLowerCase());
465
+ const g = head.findIndex((c) => c === 'gate');
466
+ const u = head.findIndex((c) => c.startsWith('unit'));
467
+ if (g >= 0 && u >= 0) {
468
+ gateCol = g;
469
+ unitCol = u;
470
+ continue;
471
+ }
472
+ if (gateCol < 0 || cs.length <= Math.max(gateCol, unitCol)) continue;
473
+ const idm = /`([A-Za-z0-9_.-]+)`/.exec(cs[gateCol] as string);
474
+ if (idm === null) continue;
475
+ const raw = cs[unitCol] as string;
476
+ const num = /(\d[\d,]*)/.exec(raw.replace(/\*/g, ''));
477
+ out.set(idm[1] as string, num === null ? null : Number((num[1] as string).replace(/,/g, '')));
478
+ }
479
+ return out;
480
+ }
481
+
482
+ /**
483
+ * Make the two artefacts disagree loudly (Phase 7 step 2).
484
+ *
485
+ * The census says what a gate READS; the canary says whether anything it reads
486
+ * can make it FAIL. Each alone is satisfiable by a broken gate; together they
487
+ * are not.
488
+ */
489
+ export function cross_check(
490
+ results: readonly CanaryResult[],
491
+ census: ReadonlyMap<string, number | null>,
492
+ ): Disagreement[] {
493
+ const out: Disagreement[] = [];
494
+ for (const r of results) {
495
+ const units = census.get(r.id);
496
+ if (units === undefined) continue; // not censused — nothing to disagree with
497
+ if (r.verdict === 'green' && units !== null && units > 0) {
498
+ out.push({
499
+ gate: r.id,
500
+ kind: 'dead_gate',
501
+ detail:
502
+ `census records ${String(units)} unit(s) read, but the canary could not make it fail — ` +
503
+ 'a gate that reads a live corpus and cannot go red is dead',
504
+ });
505
+ }
506
+ if (r.verdict === 'red' && (units === null || units === 0)) {
507
+ out.push({
508
+ gate: r.id,
509
+ kind: 'census_stale',
510
+ detail:
511
+ 'the canary made it fail, so it reads a live corpus, but the census records no units — ' +
512
+ 're-run the census; its row is stale',
513
+ });
514
+ }
515
+ }
516
+ return out;
517
+ }
518
+
519
+ const CANARY_ICON: Record<CanaryVerdict, string> = {
520
+ red: '✅',
521
+ green: '❌',
522
+ no_recipe: '⚠️',
523
+ crashed: '❌',
524
+ };
525
+
526
+ export function render_ledger(
527
+ results: readonly CanaryResult[],
528
+ disagreements: readonly Disagreement[],
529
+ totalGateScripts: number,
530
+ cycleId: string,
531
+ ): string {
532
+ const recipes = results.filter((r) => r.verdict !== 'no_recipe');
533
+ const red = results.filter((r) => r.verdict === 'red');
534
+ const green = results.filter((r) => r.verdict === 'green');
535
+ const lines = [
536
+ `# Gate-surface canary ledger — ${cycleId}`,
537
+ '',
538
+ 'Produced by `./scripts-run src/scripts/check_gate_coverage --canary`. Governed by',
539
+ '[`adversarial-review-protocol`](../../../../docs/contracts/adversarial-review-protocol.md)',
540
+ '§ 6 — biannual cadence, rotating class, sealed record, never ships.',
541
+ '',
542
+ '## Coverage (stated, not implied)',
543
+ '',
544
+ `- Gate scripts in \`src/scripts/\`: **${String(totalGateScripts)}**`,
545
+ `- Listed in \`src/config/gate-coverage.yml\`: **${String(results.length)}**`,
546
+ `- Carrying a canary recipe: **${String(recipes.length)}**`,
547
+ `- RED (caught the plant): **${String(red.length)}** · GREEN (dead): **${String(green.length)}**`,
548
+ '',
549
+ 'Every gate outside the recipe count is UNPROVEN by this experiment. That is a',
550
+ 'gap, not a pass.',
551
+ '',
552
+ '## Per-gate ledger',
553
+ '',
554
+ '| Gate | Class | Verdict | Exit | Detail |',
555
+ '|---|---|---|---:|---|',
556
+ ];
557
+ for (const r of results) {
558
+ lines.push(
559
+ `| \`${r.id}\` | ${r.class ?? '—'} | ${CANARY_ICON[r.verdict]} ${r.verdict.toUpperCase()} ` +
560
+ `| ${r.exit_code === null ? '—' : String(r.exit_code)} | ${r.message} |`,
561
+ );
562
+ }
563
+ lines.push('', '## Cross-check against the scan-scope census', '');
564
+ if (disagreements.length === 0) {
565
+ lines.push('No disagreement: every censused gate with a recipe went red, and every red gate');
566
+ lines.push('has a non-zero census count.');
567
+ } else {
568
+ lines.push('| Gate | Kind | Detail |', '|---|---|---|');
569
+ for (const d of disagreements) lines.push(`| \`${d.gate}\` | **${d.kind}** | ${d.detail} |`);
570
+ }
571
+ lines.push('');
572
+ return lines.join('\n');
573
+ }
574
+
575
+ /** Count of gate-shaped scripts, for the honest denominator in the ledger. */
576
+ export function count_gate_scripts(dir = path.join(REPO_ROOT, 'src/scripts')): number {
577
+ try {
578
+ return fs
579
+ .readdirSync(dir)
580
+ .filter((f) => /^(lint|check|audit)_.*\.ts$/.test(f) && !f.endsWith('.d.ts')).length;
581
+ } catch {
582
+ return 0;
583
+ }
584
+ }
585
+
586
+ function run_canary_mode(specs: readonly GateSpec[], argv: readonly string[]): number {
587
+ const results = specs.map((s) => run_canary(s));
588
+ const censusMd = fs.existsSync(CENSUS) ? fs.readFileSync(CENSUS, 'utf8') : '';
589
+ const disagreements = cross_check(results, parse_census(censusMd));
590
+
591
+ for (const r of results) {
592
+ process.stdout.write(` ${CANARY_ICON[r.verdict]} ${r.id}: ${r.message}\n`);
593
+ }
594
+ for (const d of disagreements) {
595
+ process.stdout.write(` ❌ DISAGREEMENT [${d.kind}] ${d.gate}: ${d.detail}\n`);
596
+ }
597
+
598
+ const cycleId = `gate-surface-${new Date().toISOString().slice(0, 10)}`;
599
+ const li = argv.indexOf('--ledger');
600
+ if (li >= 0 && argv[li + 1] !== undefined) {
601
+ const out = path.resolve(REPO_ROOT, argv[li + 1] as string);
602
+ fs.mkdirSync(path.dirname(out), { recursive: true });
603
+ fs.writeFileSync(out, render_ledger(results, disagreements, count_gate_scripts(), cycleId), 'utf8');
604
+ process.stdout.write(`\nledger: ${path.relative(REPO_ROOT, out)}\n`);
605
+ }
606
+
607
+ const recipes = results.filter((r) => r.verdict !== 'no_recipe').length;
608
+ const green = results.filter((r) => r.verdict === 'green' || r.verdict === 'crashed').length;
609
+ process.stdout.write(
610
+ `\nscanned: ${String(results.length)}\n` +
611
+ ` recipes ${String(recipes)} · unproven ${String(results.length - recipes)} · ` +
612
+ `green(dead) ${String(green)} · disagreements ${String(disagreements.length)}\n`,
613
+ );
614
+ if (green > 0 || disagreements.length > 0) {
615
+ process.stdout.write('❌ canary found dead gate(s) or a census disagreement — each row is a defect ticket.\n');
616
+ return 1;
617
+ }
618
+ process.stdout.write('✅ every gate with a recipe went red over its planted defect.\n');
619
+ return 0;
620
+ }
621
+
286
622
  function _isCliEntry(): boolean {
287
623
  if (process.argv[1] === undefined) return false;
288
624
  const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
@@ -35,7 +35,8 @@ const GENERATOR_OUTPUT_ROOTS = [
35
35
  { root: '.claude/rules', generator: 'generate_rule_symlinks', tool: 'claude-code' },
36
36
  { root: '.claude/personas', generator: 'generate_persona_symlinks', tool: 'claude-code' },
37
37
  { root: '.claude/user-types', generator: 'generate_user_type_symlinks', tool: 'claude-code' },
38
- { root: '.claude/skills', generator: 'generate_claude_skills + generate_plugin_command_skills', tool: 'claude-code' },
38
+ { root: '.claude/skills', generator: 'generate_claude_skills + generate_plugin_command_skills + generate_claude_commands', tool: 'claude-code' },
39
+ { root: '.claude/commands', generator: 'generate_claude_project_commands', tool: 'claude-code' },
39
40
  { root: '.claude/agents', generator: 'generate_claude_subagents', tool: 'claude-code' },
40
41
  { root: '.augment', generator: 'sync_augment (condense main)', tool: 'augment' },
41
42
  { root: '.cursor/rules', generator: 'generate_cursor_mdc_rules', tool: 'cursor' },