session-orchestrator 3.24.0 → 4.0.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 (350) hide show
  1. package/.agents/skills/architecture/SKILL.md +18 -0
  2. package/.agents/skills/autopilot/SKILL.md +17 -0
  3. package/.agents/skills/bootstrap/SKILL.md +20 -0
  4. package/.agents/skills/brainstorm/SKILL.md +22 -0
  5. package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
  6. package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
  7. package/.agents/skills/debug/SKILL.md +22 -0
  8. package/.agents/skills/discovery/SKILL.md +20 -0
  9. package/.agents/skills/dispatcher/SKILL.md +15 -0
  10. package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
  11. package/.agents/skills/ecosystem-health/SKILL.md +20 -0
  12. package/.agents/skills/eli5/SKILL.md +20 -0
  13. package/.agents/skills/eval/SKILL.md +21 -0
  14. package/.agents/skills/evolve/SKILL.md +21 -0
  15. package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
  16. package/.agents/skills/gitlab-ops/SKILL.md +20 -0
  17. package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
  18. package/.agents/skills/grill/SKILL.md +22 -0
  19. package/.agents/skills/hook-development/SKILL.md +15 -0
  20. package/.agents/skills/mcp-builder/SKILL.md +15 -0
  21. package/.agents/skills/memory-cleanup/SKILL.md +21 -0
  22. package/.agents/skills/mode-selector/SKILL.md +17 -0
  23. package/.agents/skills/npm-publish/SKILL.md +16 -0
  24. package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
  25. package/.agents/skills/persona-panel/SKILL.md +17 -0
  26. package/.agents/skills/plan/SKILL.md +20 -0
  27. package/.agents/skills/playwright-driver/SKILL.md +20 -0
  28. package/.agents/skills/quality-gates/SKILL.md +20 -0
  29. package/.agents/skills/reconcile/SKILL.md +21 -0
  30. package/.agents/skills/remote-offload/SKILL.md +20 -0
  31. package/.agents/skills/repo-audit/SKILL.md +16 -0
  32. package/.agents/skills/session-end/SKILL.md +20 -0
  33. package/.agents/skills/session-plan/SKILL.md +20 -0
  34. package/.agents/skills/session-start/SKILL.md +20 -0
  35. package/.agents/skills/spinout/SKILL.md +16 -0
  36. package/.agents/skills/sunset-review/SKILL.md +16 -0
  37. package/.agents/skills/test-runner/SKILL.md +20 -0
  38. package/.agents/skills/tmux-layout/SKILL.md +21 -0
  39. package/.agents/skills/using-orchestrator/SKILL.md +17 -0
  40. package/.agents/skills/vault-mirror/SKILL.md +15 -0
  41. package/.agents/skills/vault-sync/SKILL.md +15 -0
  42. package/.agents/skills/wave-executor/SKILL.md +20 -0
  43. package/.agents/skills/write-executable-plan/SKILL.md +22 -0
  44. package/.claude-plugin/marketplace.json +1 -1
  45. package/.claude-plugin/plugin.json +1 -1
  46. package/.codex-plugin/plugin.json +1 -1
  47. package/.cursor/commands/autopilot.md +2 -2
  48. package/.cursor/commands/bootstrap.md +1 -1
  49. package/.cursor/commands/brainstorm.md +1 -1
  50. package/.cursor/commands/debug.md +1 -1
  51. package/.cursor/commands/discovery.md +1 -1
  52. package/.cursor/commands/dispatcher.md +2 -2
  53. package/.cursor/commands/eli5.md +2 -2
  54. package/.cursor/commands/eval.md +2 -2
  55. package/.cursor/commands/evolve.md +1 -1
  56. package/.cursor/commands/go.md +1 -1
  57. package/.cursor/commands/grill.md +2 -2
  58. package/.cursor/commands/memory-cleanup.md +2 -2
  59. package/.cursor/commands/persona-panel.md +1 -1
  60. package/.cursor/commands/plan.md +1 -1
  61. package/.cursor/commands/portfolio.md +1 -1
  62. package/.cursor/commands/reconcile.md +2 -2
  63. package/.cursor/commands/release.md +2 -2
  64. package/.cursor/commands/session.md +2 -2
  65. package/.cursor/commands/spinout.md +2 -2
  66. package/.cursor/commands/sunset-review.md +2 -2
  67. package/.cursor/commands/templates-ack.md +2 -2
  68. package/.cursor/commands/test.md +2 -2
  69. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  70. package/.cursor/skills/eval/SKILL.md +1 -1
  71. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  72. package/.cursor/skills/remote-offload/SKILL.md +1 -1
  73. package/.orchestrator/policy/blocked-commands.json +121 -0
  74. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  75. package/.orchestrator/policy/quality-gates.example.json +16 -0
  76. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  77. package/.orchestrator/policy/templates-policy.json +27 -0
  78. package/.orchestrator/policy/test-profiles.json +47 -0
  79. package/AGENTS.md +225 -0
  80. package/CHANGELOG.md +1125 -2
  81. package/NOTICE +11 -6
  82. package/README.md +127 -94
  83. package/agents/eval-judge.md +1 -1
  84. package/agents/skill-applied-judge.md +1 -1
  85. package/assets/wave-lifecycle.svg +98 -0
  86. package/commands/release.md +6 -3
  87. package/commands/session.md +18 -3
  88. package/docs/README.md +4 -0
  89. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  90. package/docs/baseline.md +67 -0
  91. package/docs/ci-setup.md +108 -62
  92. package/docs/codex-setup.md +65 -21
  93. package/docs/components.md +36 -15
  94. package/docs/cursor-setup.md +6 -2
  95. package/docs/events-schema.md +9 -6
  96. package/docs/instruction-delivery.md +62 -0
  97. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  98. package/docs/migration-v4.md +341 -0
  99. package/docs/pi-setup.md +6 -1
  100. package/docs/plugin-architecture-v3.md +1 -1
  101. package/docs/rule-authoring.md +85 -19
  102. package/docs/scope-collision-guard.md +5 -5
  103. package/docs/session-config-reference.md +57 -56
  104. package/docs/session-config-template.md +6 -29
  105. package/docs/telemetry.md +157 -3
  106. package/docs/vault-docs-architecture.md +50 -11
  107. package/hooks/_lib/hook-import-set.json +1487 -0
  108. package/hooks/_lib/subagent-transcript.mjs +562 -0
  109. package/hooks/config-protection.mjs +2 -2
  110. package/hooks/cwd-change-restore.mjs +2 -2
  111. package/hooks/enforce-commands.mjs +69 -0
  112. package/hooks/hooks-codex.json +1 -1
  113. package/hooks/hooks-cursor.json +10 -0
  114. package/hooks/hooks-pi.json +5 -0
  115. package/hooks/hooks.json +6 -1
  116. package/hooks/loop-guard.mjs +3 -3
  117. package/hooks/on-session-end.mjs +2 -2
  118. package/hooks/on-session-start.mjs +103 -2
  119. package/hooks/on-stop.mjs +36 -11
  120. package/hooks/operator-steer.mjs +2 -2
  121. package/hooks/post-bash-write-verify.mjs +85 -0
  122. package/hooks/post-edit-import-probe.mjs +344 -0
  123. package/hooks/post-subagent-discovery-validator.mjs +187 -431
  124. package/hooks/post-tool-batch-wave-signal.mjs +118 -4
  125. package/hooks/post-tool-failure-corrective-context.mjs +2 -2
  126. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  127. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  128. package/hooks/skill-invocation-telemetry.mjs +17 -5
  129. package/hooks/subagent-telemetry.mjs +13 -4
  130. package/monitors/monitors.json +3 -3
  131. package/package.json +9 -1
  132. package/pi/prompts/session.md +2 -2
  133. package/plugin.json +27 -0
  134. package/scripts/backfill-abandoned-sessions.mjs +50 -4
  135. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  136. package/scripts/dialectic-deriver.mjs +73 -8
  137. package/scripts/export-hw-learnings.mjs +113 -1
  138. package/scripts/generate-agents-skills.mjs +378 -0
  139. package/scripts/generate-cursor-adapter.mjs +45 -8
  140. package/scripts/generate-hook-import-set.mjs +249 -0
  141. package/scripts/lib/agent-status.mjs +13 -2
  142. package/scripts/lib/auto-dream.mjs +38 -36
  143. package/scripts/lib/autonomy/suitability.mjs +6 -0
  144. package/scripts/lib/autopilot/loop.mjs +2 -2
  145. package/scripts/lib/ci-status-banner.mjs +220 -75
  146. package/scripts/lib/codex/plugin-contract.mjs +82 -6
  147. package/scripts/lib/config/auto-dream.mjs +2 -1
  148. package/scripts/lib/config/block-header.mjs +8 -0
  149. package/scripts/lib/config/block-preprocess.mjs +177 -0
  150. package/scripts/lib/config/broken-window.mjs +2 -1
  151. package/scripts/lib/config/cold-start.mjs +2 -1
  152. package/scripts/lib/config/config-protection.mjs +22 -2
  153. package/scripts/lib/config/context-coverage.mjs +2 -1
  154. package/scripts/lib/config/cross-repo.mjs +2 -1
  155. package/scripts/lib/config/custom-phases.mjs +2 -1
  156. package/scripts/lib/config/dialectic.mjs +2 -1
  157. package/scripts/lib/config/discovery-validator.mjs +2 -1
  158. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  159. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  160. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  161. package/scripts/lib/config/docs-staleness.mjs +2 -1
  162. package/scripts/lib/config/drift-check.mjs +2 -1
  163. package/scripts/lib/config/eval.mjs +2 -1
  164. package/scripts/lib/config/events-rotation.mjs +2 -1
  165. package/scripts/lib/config/evolve.mjs +8 -2
  166. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  167. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  168. package/scripts/lib/config/handover-gate.mjs +2 -1
  169. package/scripts/lib/config/health-endpoints.mjs +7 -2
  170. package/scripts/lib/config/issue-budget.mjs +2 -1
  171. package/scripts/lib/config/loop-guard.mjs +2 -1
  172. package/scripts/lib/config/memory.mjs +2 -1
  173. package/scripts/lib/config/moc-staleness.mjs +2 -1
  174. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  175. package/scripts/lib/config/private-config-dir.mjs +67 -0
  176. package/scripts/lib/config/reconcile.mjs +2 -1
  177. package/scripts/lib/config/remote-hosts.mjs +2 -1
  178. package/scripts/lib/config/section-extractor.mjs +7 -1
  179. package/scripts/lib/config/skill-evolution.mjs +2 -1
  180. package/scripts/lib/config/slopcheck.mjs +2 -1
  181. package/scripts/lib/config/state-md-lock.mjs +2 -1
  182. package/scripts/lib/config/templates-first.mjs +2 -1
  183. package/scripts/lib/config/test.mjs +2 -1
  184. package/scripts/lib/config/vault-integration.mjs +7 -1
  185. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  186. package/scripts/lib/config/vault-staleness.mjs +2 -1
  187. package/scripts/lib/config/vault-sync.mjs +2 -1
  188. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  189. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  190. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  191. package/scripts/lib/convergence-monitor.mjs +82 -16
  192. package/scripts/lib/dispatcher/rank.mjs +124 -48
  193. package/scripts/lib/ecosystem-health.mjs +16 -2
  194. package/scripts/lib/eval/engine.mjs +9 -1
  195. package/scripts/lib/eval/session-resolve.mjs +23 -4
  196. package/scripts/lib/events.mjs +22 -6
  197. package/scripts/lib/frontmatter-guard.mjs +131 -13
  198. package/scripts/lib/gates/gate-full.mjs +26 -0
  199. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  200. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  201. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  202. package/scripts/lib/host-identity.mjs +50 -11
  203. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  204. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  205. package/scripts/lib/learnings/io.mjs +60 -6
  206. package/scripts/lib/memory-proposals/store.mjs +30 -22
  207. package/scripts/lib/owner-config-banner.mjs +43 -6
  208. package/scripts/lib/owner-config-loader.mjs +21 -10
  209. package/scripts/lib/owner-interview.mjs +3 -3
  210. package/scripts/lib/owner-yaml.mjs +207 -14
  211. package/scripts/lib/platform.mjs +108 -15
  212. package/scripts/lib/plugin-update-banner.mjs +406 -0
  213. package/scripts/lib/project-hygiene.mjs +38 -2
  214. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  215. package/scripts/lib/quality-gate.mjs +133 -44
  216. package/scripts/lib/reconcile/emitter.mjs +68 -6
  217. package/scripts/lib/reconcile/engine.mjs +13 -4
  218. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  219. package/scripts/lib/reconcile/writer.mjs +40 -18
  220. package/scripts/lib/session-close-backfill.mjs +67 -9
  221. package/scripts/lib/session-id.mjs +12 -23
  222. package/scripts/lib/session-identity/own-session.mjs +125 -10
  223. package/scripts/lib/session-lock-shape.mjs +43 -0
  224. package/scripts/lib/session-lock.mjs +5 -10
  225. package/scripts/lib/session-registry.mjs +25 -9
  226. package/scripts/lib/session-schema/constants.mjs +36 -2
  227. package/scripts/lib/session-schema/validator.mjs +38 -4
  228. package/scripts/lib/session-start-probes.mjs +18 -1
  229. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  230. package/scripts/lib/skill-health/join.mjs +17 -4
  231. package/scripts/lib/state-md.mjs +78 -0
  232. package/scripts/lib/sunset/walker.mjs +6 -0
  233. package/scripts/lib/telemetry/schema.mjs +181 -9
  234. package/scripts/lib/telemetry/sync.mjs +368 -12
  235. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  236. package/scripts/lib/validate/check-agents.mjs +3 -3
  237. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  238. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  239. package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
  240. package/scripts/lib/validate/check-skill-links.mjs +163 -0
  241. package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
  242. package/scripts/lib/validate/check-unwired-features.mjs +0 -2
  243. package/scripts/lib/validate/check-validator-registration.mjs +10 -4
  244. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  245. package/scripts/lib/vault-backfill/template.mjs +63 -6
  246. package/scripts/lib/vault-mirror/process.mjs +165 -42
  247. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  248. package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
  249. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  250. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  251. package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
  252. package/scripts/lib/wave-resource-gate.mjs +8 -2
  253. package/scripts/lib/wave-sizing.mjs +4 -1
  254. package/scripts/lib/wave-transcript-tail.mjs +118 -4
  255. package/scripts/materialize-wave-scope.mjs +12 -5
  256. package/scripts/memory-propose.mjs +19 -5
  257. package/scripts/migrate-cold-start-seed.mjs +4 -1
  258. package/scripts/parse-config.mjs +60 -3
  259. package/scripts/release.mjs +337 -29
  260. package/scripts/repair-invalid-sessions.mjs +3 -3
  261. package/scripts/run-quality-gate.mjs +128 -11
  262. package/scripts/sweep-expired-learnings.mjs +90 -0
  263. package/scripts/sync-vault-schema.mjs +3 -1
  264. package/scripts/telemetry.mjs +2 -2
  265. package/scripts/validate-plugin.mjs +161 -0
  266. package/scripts/validate-wave-scope.mjs +28 -8
  267. package/scripts/wave-scope-binding.mjs +215 -0
  268. package/skills/_shared/instruction-file-resolution.md +10 -0
  269. package/skills/_shared/parallel-aware-preamble.md +1 -0
  270. package/skills/_shared/platform-tools.md +1 -1
  271. package/skills/_shared/state-ownership.md +1 -1
  272. package/skills/architecture/SKILL.md +7 -5
  273. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  274. package/skills/autopilot/SKILL.md +4 -18
  275. package/skills/claude-md-drift-check/SKILL.md +5 -1
  276. package/skills/claude-md-drift-check/checker.mjs +62 -2
  277. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  278. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  279. package/skills/discovery/probes-arch.md +20 -18
  280. package/skills/dispatcher/SKILL.md +3 -2
  281. package/skills/evolve/SKILL.md +65 -26
  282. package/skills/frontmatter-guard/SKILL.md +11 -5
  283. package/skills/npm-publish/SKILL.md +1 -1
  284. package/skills/reconcile/SKILL.md +33 -0
  285. package/skills/remote-offload/SKILL.md +1 -1
  286. package/skills/session-end/SKILL.md +18 -905
  287. package/skills/session-end/phase-3-6-tail.md +10 -3
  288. package/skills/session-end/plan-verification.md +221 -155
  289. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  290. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  291. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  292. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  293. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  294. package/skills/session-end/references/session-summary-template.md +62 -0
  295. package/skills/session-plan/SKILL.md +49 -0
  296. package/skills/session-start/SKILL.md +22 -904
  297. package/skills/session-start/phase-8-5-express-path.md +1 -1
  298. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  299. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  300. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  301. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  302. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  303. package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
  304. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  305. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  306. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  307. package/skills/vault-sync/validator.mjs +21 -27
  308. package/skills/wave-executor/SKILL.md +15 -1
  309. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  310. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  311. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  312. package/skills/wave-executor/wave-loop.md +14 -1309
  313. package/templates/_shared/journey-manifest.md +10 -6
  314. package/.cursor/commands/autopilot-multi.md +0 -14
  315. package/.cursor/commands/contract-version-bump.md +0 -14
  316. package/.cursor/commands/journey-audit.md +0 -14
  317. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  318. package/.cursor/skills/daily/SKILL.md +0 -12
  319. package/.cursor/skills/domain-model/SKILL.md +0 -13
  320. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  321. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  322. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  323. package/commands/autopilot-multi.md +0 -74
  324. package/commands/contract-version-bump.md +0 -28
  325. package/commands/journey-audit.md +0 -43
  326. package/pi/prompts/autopilot-multi.md +0 -12
  327. package/pi/prompts/contract-version-bump.md +0 -12
  328. package/pi/prompts/journey-audit.md +0 -12
  329. package/scripts/autopilot-multi.mjs +0 -885
  330. package/scripts/backfill-learnings-expires.mjs +0 -196
  331. package/scripts/backfill-learnings.mjs +0 -203
  332. package/scripts/fleet-instruction-scan.mjs +0 -141
  333. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  334. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  335. package/scripts/lib/webhook-url.mjs +0 -105
  336. package/scripts/lifecycle-sim-v6.mjs +0 -347
  337. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  338. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  339. package/scripts/upload-social-preview.mjs +0 -316
  340. package/skills/_shared/model-selection.md +0 -64
  341. package/skills/contract-version-bump/SKILL.md +0 -219
  342. package/skills/daily/SKILL.md +0 -222
  343. package/skills/daily/generate.sh +0 -92
  344. package/skills/daily/templates/daily.md.tpl +0 -36
  345. package/skills/journey-audit/SKILL.md +0 -270
  346. package/skills/skill-creator/SKILL.md +0 -168
  347. package/skills/ubiquitous-language/SKILL.md +0 -97
  348. package/skills/vault-sync/package-lock.json +0 -40
  349. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  350. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -14,6 +14,9 @@
14
14
  * defaults from the policy file or built-in defaults apply.
15
15
  * --files <f1,f2,...> Comma-separated file list (incremental + per-file).
16
16
  * --session-start-ref <r> Git ref for diff base (incremental, to find changed files).
17
+ * --ledger-root <path> Repo root the telemetry event is pinned to (and the
18
+ * root session attribution is read from). Only the
19
+ * pre-push hook passes it; see the emission block below.
17
20
  * -h, --help Show this help and exit.
18
21
  *
19
22
  * Exit codes:
@@ -31,8 +34,8 @@
31
34
  * scripts/lib/gates/gate-{baseline,incremental,full,per-file}.mjs
32
35
  */
33
36
 
34
- import { existsSync, readFileSync } from 'node:fs';
35
- import { join, dirname } from 'node:path';
37
+ import { existsSync, readFileSync, statSync } from 'node:fs';
38
+ import { join, dirname, resolve } from 'node:path';
36
39
  import { fileURLToPath } from 'node:url';
37
40
  import { spawnSync } from 'node:child_process';
38
41
 
@@ -80,7 +83,7 @@ const argv = process.argv.slice(2);
80
83
  if (argv.includes('-h') || argv.includes('--help')) {
81
84
  process.stdout.write(
82
85
  'Usage: run-quality-gate.mjs --variant <variant> [--config <json-or-file>] ' +
83
- '[--files <file1,file2,...>] [--session-start-ref <ref>]\n\n' +
86
+ '[--files <file1,file2,...>] [--session-start-ref <ref>] [--ledger-root <path>]\n\n' +
84
87
  'Variants: baseline, incremental, full-gate, per-file\n\n' +
85
88
  'Exit codes:\n' +
86
89
  ' 0 — pass (non-blocking variants always exit 0)\n' +
@@ -94,6 +97,7 @@ let variant = '';
94
97
  let config = '';
95
98
  let files = '';
96
99
  let sessionStartRef = '';
100
+ let ledgerRootArg = '';
97
101
 
98
102
  for (let i = 0; i < argv.length; i++) {
99
103
  const arg = argv[i];
@@ -114,6 +118,10 @@ for (let i = 0; i < argv.length; i++) {
114
118
  if (i + 1 >= argv.length) die('Missing value for --session-start-ref');
115
119
  sessionStartRef = argv[++i];
116
120
  break;
121
+ case '--ledger-root':
122
+ if (i + 1 >= argv.length) die('Missing value for --ledger-root');
123
+ ledgerRootArg = argv[++i];
124
+ break;
117
125
  default:
118
126
  die(`Unknown argument: ${arg}`);
119
127
  }
@@ -208,6 +216,76 @@ function suiteCountsFromGateStdout(stdout) {
208
216
  return admitSuiteCounts(test);
209
217
  }
210
218
 
219
+ /**
220
+ * Lift `test.failed_files` out of a gate sub-script's JSON stdout envelope.
221
+ *
222
+ * Envelope adapter, same posture as {@link suiteCountsFromGateStdout}: it
223
+ * decides only whether a NAMED-FILE measurement exists, never what the names
224
+ * mean. `null` — never `[]` — for every non-measurement, so the caller OMITS
225
+ * the key instead of publishing an empty array that reads as "no file failed".
226
+ *
227
+ * Only `gate-full.mjs` publishes the key, and only when the runner printed a
228
+ * file-level summary; every other variant emits a bare status string.
229
+ *
230
+ * Never throws.
231
+ *
232
+ * @param {string} stdout — the gate sub-script's captured stdout.
233
+ * @returns {string[]|null}
234
+ */
235
+ function failedFilesFromGateStdout(stdout) {
236
+ if (typeof stdout !== 'string' || !stdout.trim()) return null;
237
+ let parsed;
238
+ try {
239
+ parsed = JSON.parse(stdout);
240
+ } catch {
241
+ return null;
242
+ }
243
+ const files = parsed?.test?.failed_files;
244
+ if (!Array.isArray(files) || files.length === 0) return null;
245
+ const named = files.filter((f) => typeof f === 'string' && f.trim());
246
+ return named.length > 0 ? named : null;
247
+ }
248
+
249
+ /**
250
+ * Validate and resolve the `--ledger-root` flag (see the telemetry block below).
251
+ *
252
+ * Only the pre-push hook passes this, and it hands over a path the gate then
253
+ * WRITES to — so a typo must not silently create an `.orchestrator/metrics/`
254
+ * tree somewhere arbitrary. The check is therefore two-part: the value must be
255
+ * an existing DIRECTORY, and it must already contain an `.orchestrator/`
256
+ * directory — the marker of a root this harness has already been initialised in.
257
+ *
258
+ * The alternative check (`git rev-parse --show-toplevel` with cwd = that path,
259
+ * compared against the value) is rejected on cost: it spawns a process on every
260
+ * gate run to prove a property two `statSync` calls already prove. The one case
261
+ * it accepts and this one rejects — a git root that has never run the
262
+ * orchestrator — is precisely the case with no ledger to pin to.
263
+ *
264
+ * A bad value NEVER crashes the gate: it warns once on stderr and returns
265
+ * `null`, which restores the previous resolution
266
+ * (`CLAUDE_PROJECT_DIR ?? CODEX_PROJECT_DIR ?? repoRoot`) at the call sites.
267
+ * The gate's exit code is the authoritative output; telemetry is best-effort.
268
+ *
269
+ * @param {string} value — raw flag value (`''` when the flag was not passed).
270
+ * @returns {string|null} absolute, validated root — or `null` to fall back.
271
+ */
272
+ function resolveLedgerRoot(value) {
273
+ const raw = (value || '').trim();
274
+ if (!raw) return null;
275
+ const abs = resolve(raw);
276
+ try {
277
+ if (statSync(abs).isDirectory() && statSync(join(abs, '.orchestrator')).isDirectory()) {
278
+ return abs;
279
+ }
280
+ } catch { /* falls through to the warn below */ }
281
+ warn(
282
+ `--ledger-root '${raw}' is not an initialised project root ` +
283
+ '(existing directory containing .orchestrator/) — falling back to the default ' +
284
+ 'telemetry destination.',
285
+ );
286
+ return null;
287
+ }
288
+
211
289
  /**
212
290
  * Resolve the active wave number from the wave-scope sidecar (#966 step 1).
213
291
  *
@@ -325,24 +403,63 @@ if (result.error && typeof result.status !== 'number') {
325
403
  // (single emission path). `sessionAttribution` is the shared helper in
326
404
  // events.mjs (#941); this CLI wrapper runs against the CWD `repoRoot`, so the
327
405
  // bare emitEvent destination (SO_PROJECT_DIR default) is correct here.
406
+ //
407
+ // EXCEPT under the pre-push hook, which is the one caller that runs the gate in
408
+ // a tree that is about to be DELETED. `.husky/pre-push` materialises the tracked
409
+ // tree into a temp dir and deliberately scrubs every `*PROJECT_DIR` name before
410
+ // invoking the gate there, so `getProjectDir()` resolves to that temp tree (it
411
+ // carries both a CLAUDE.md (or AGENTS.md) and a .git) and the record lands in
412
+ // `<tmp>/.orchestrator/metrics/events.jsonl`, which the hook's EXIT trap then
413
+ // removes. Measured 2026-09-06: a pre-push run that BLOCKED a push left no
414
+ // `orchestrator.quality_gate.failed` line in this repo's ledger at all — the
415
+ // gate failure was, by construction, the one event that could never be recorded.
416
+ //
417
+ // `--ledger-root` is that hook's channel for handing back the root it already
418
+ // knows (`git rev-parse --show-toplevel`, read BEFORE it cds). It pins ONLY the
419
+ // telemetry destination and the attribution root — every other path the gate
420
+ // resolves stays inside the tree actually under test, which is the whole point
421
+ // of the materialisation. Absent (every other caller) → unchanged behaviour:
422
+ // `emitEvent`'s own default resolution.
423
+ //
424
+ // It is an ARGV FLAG and not an env var, and that is load-bearing. Measured
425
+ // 2026-09-06 with the env-var form: `SO_GATE_LEDGER_ROOT=$tmp npx vitest run
426
+ // tests/scripts/run-quality-gate.test.mjs -t "telemetry emission"` → `8 failed |
427
+ // 1 passed`. The chain was: hook exports the var → `npm run quality-gate` →
428
+ // `gate-full.mjs` spawns `npm test` → every vitest worker inherits it → the
429
+ // suite's own gate spawns spread `...process.env`, so the pinned root outranked
430
+ // their per-test project dir and the gate's telemetry tests wrote to the hook's
431
+ // root. The gate that releases 4.0.0 would have blocked on itself. An env var is
432
+ // inherited by every descendant; a flag reaches exactly one process.
433
+ //
328
434
  // Best-effort: a telemetry failure must NEVER alter the gate's authoritative
329
435
  // exit code — which is why the counts parse also lives inside this try.
330
436
  const exitCode = result.status ?? 1;
437
+ const ledgerRoot = resolveLedgerRoot(ledgerRootArg);
331
438
  try {
332
439
  const counts = suiteCountsFromGateStdout(gateStdout);
440
+ // The names behind `counts.failed`. Absent, never `[]` — see
441
+ // `failedFilesFromGateStdout`.
442
+ const failedFiles = failedFilesFromGateStdout(gateStdout);
333
443
  // Wave-scope sidecar is read from the SAME project dir the event lands in
334
444
  // (emitEvent's own destination precedence), so a tmp-scoped run cannot pick
335
445
  // up the host repo's live wave. Mirrors the hook's projectDir resolution.
336
446
  const waveNumber = resolveWaveNumber(
337
- process.env.CLAUDE_PROJECT_DIR ?? process.env.CODEX_PROJECT_DIR ?? repoRoot,
447
+ ledgerRoot ?? process.env.CLAUDE_PROJECT_DIR ?? process.env.CODEX_PROJECT_DIR ?? repoRoot,
448
+ );
449
+ await emitEvent(
450
+ `orchestrator.quality_gate.${exitCode === 0 ? 'passed' : 'failed'}`,
451
+ {
452
+ variant,
453
+ exit_code: exitCode,
454
+ ...(counts ? { counts } : {}),
455
+ ...(failedFiles ? { failed_files: failedFiles } : {}),
456
+ ...(waveNumber !== null ? { wave_number: waveNumber } : {}),
457
+ ...sessionAttribution(ledgerRoot ?? repoRoot),
458
+ },
459
+ // `{}` is byte-identical to omitting the argument (`opts.repoRoot ??
460
+ // getProjectDir()`), so the default path is untouched.
461
+ ledgerRoot ? { repoRoot: ledgerRoot } : {},
338
462
  );
339
- await emitEvent(`orchestrator.quality_gate.${exitCode === 0 ? 'passed' : 'failed'}`, {
340
- variant,
341
- exit_code: exitCode,
342
- ...(counts ? { counts } : {}),
343
- ...(waveNumber !== null ? { wave_number: waveNumber } : {}),
344
- ...sessionAttribution(repoRoot),
345
- });
346
463
  } catch { /* best-effort telemetry — gate result is authoritative */ }
347
464
 
348
465
  process.exit(exitCode);
@@ -23,6 +23,7 @@
23
23
  * Usage:
24
24
  * node scripts/sweep-expired-learnings.mjs [--prune] [--dry-run|--apply] [--json]
25
25
  * [--grace-days N] [--entries PATH] [--file PATH] [--archive PATH]
26
+ * [--appended N] [--boosted M] [--duration-ms D] [--skipped a,b] [--repo-root PATH]
26
27
  *
27
28
  * Flags:
28
29
  * --prune Decision-driven prune+consolidate+rewrite instead of the
@@ -40,6 +41,23 @@
40
41
  * store.
41
42
  * --file PATH Learnings store (default: .orchestrator/metrics/learnings.jsonl)
42
43
  * --archive PATH Archive sidecar (default: .orchestrator/metrics/learnings-archive.jsonl)
44
+ * --appended N PRUNE ONLY (#1206). New learnings written this `/evolve`
45
+ * run (Step 3.5(4)); folded into the mechanical
46
+ * `orchestrator.evolve.completed` emit alongside this
47
+ * call's own `pruned` count. Default 0.
48
+ * --boosted M PRUNE ONLY (#1206). Existing learnings reinforced this
49
+ * run (Step 3.5(2)). Default 0.
50
+ * --duration-ms D PRUNE ONLY (#1206). Elapsed ms since the run's telemetry
51
+ * start marker. Default 0.
52
+ * --skipped a,b PRUNE ONLY (#1206). Comma-separated list of optional
53
+ * steps that ran but were themselves skipped this run
54
+ * (HR-105 — e.g. `skill-evolution-off`). Default none.
55
+ * --repo-root PATH PRUNE ONLY (#1206). Repo root the
56
+ * `orchestrator.evolve.completed` record is pinned to.
57
+ * No default and NO process.cwd() fallback (#1119) — when
58
+ * omitted, no event is emitted at all (stderr WARN, exit
59
+ * code unaffected). Emission is best-effort and never
60
+ * changes this command's exit code or stdout contract.
43
61
  *
44
62
  * Exit codes:
45
63
  * 0 Success (including no-op when nothing is archive-eligible)
@@ -51,6 +69,7 @@
51
69
  import { existsSync } from 'node:fs';
52
70
  import { sweepExpiredLearnings, pruneLearnings } from './lib/learnings/expiry-sweep.mjs';
53
71
  import { readLearnings } from './lib/learnings/io.mjs';
72
+ import { emitEvolveCompleted } from './lib/learnings/evolve-telemetry.mjs';
54
73
 
55
74
  const DEFAULT_FILE = '.orchestrator/metrics/learnings.jsonl';
56
75
  const DEFAULT_ARCHIVE = '.orchestrator/metrics/learnings-archive.jsonl';
@@ -97,6 +116,20 @@ function parseArgs(argv) {
97
116
  entries: null,
98
117
  file: DEFAULT_FILE,
99
118
  archive: DEFAULT_ARCHIVE,
119
+ appended: 0,
120
+ boosted: 0,
121
+ durationMs: 0,
122
+ skipped: [],
123
+ // #1119 — NO process.cwd() fallback. Most CLI callers (every test in this
124
+ // file except the SKILL.md-extraction fixture, which sets its own tmp
125
+ // `cwd`) invoke this script from the repo root without `--repo-root`; a
126
+ // cwd fallback would silently append synthetic `orchestrator.evolve.
127
+ // completed` records to the operator's REAL fleet ledger on every
128
+ // `--prune --apply` run in `npm test`. `emitEvolveCompleted()` already
129
+ // refuses to emit (stderr WARN, no throw) when repoRoot is absent — same
130
+ // fail-closed contract as `emitReconcileCompleted` / `express-path.mjs`.
131
+ repoRoot: null,
132
+ telemetryExplicit: false,
100
133
  };
101
134
  for (let i = 0; i < argv.length; i++) {
102
135
  const a = argv[i];
@@ -122,6 +155,37 @@ function parseArgs(argv) {
122
155
  args.file = argv[++i];
123
156
  } else if (a === '--archive') {
124
157
  args.archive = argv[++i];
158
+ } else if (a === '--appended') {
159
+ const raw = argv[++i];
160
+ const v = Number(raw);
161
+ if (!Number.isInteger(v) || v < 0) {
162
+ usageError(`--appended requires a non-negative integer, got: ${raw}`);
163
+ }
164
+ args.appended = v;
165
+ args.telemetryExplicit = true;
166
+ } else if (a === '--boosted') {
167
+ const raw = argv[++i];
168
+ const v = Number(raw);
169
+ if (!Number.isInteger(v) || v < 0) {
170
+ usageError(`--boosted requires a non-negative integer, got: ${raw}`);
171
+ }
172
+ args.boosted = v;
173
+ args.telemetryExplicit = true;
174
+ } else if (a === '--duration-ms') {
175
+ const raw = argv[++i];
176
+ const v = Number(raw);
177
+ if (!Number.isFinite(v) || v < 0) {
178
+ usageError(`--duration-ms requires a non-negative number, got: ${raw}`);
179
+ }
180
+ args.durationMs = v;
181
+ args.telemetryExplicit = true;
182
+ } else if (a === '--skipped') {
183
+ const raw = argv[++i];
184
+ args.skipped = typeof raw === 'string' ? raw.split(',').filter((s) => s.length > 0) : [];
185
+ args.telemetryExplicit = true;
186
+ } else if (a === '--repo-root') {
187
+ args.repoRoot = argv[++i];
188
+ args.telemetryExplicit = true;
125
189
  } else if (a === '--help' || a === '-h') {
126
190
  printHelp();
127
191
  process.exit(0);
@@ -140,6 +204,12 @@ function parseArgs(argv) {
140
204
  if (!args.prune && args.entries !== null) {
141
205
  usageError('--entries is only valid with --prune');
142
206
  }
207
+ if (!args.prune && args.telemetryExplicit) {
208
+ usageError(
209
+ '--appended/--boosted/--duration-ms/--skipped/--repo-root are only valid with --prune ' +
210
+ '(the sweep path never emits orchestrator.evolve.completed)',
211
+ );
212
+ }
143
213
  return args;
144
214
  }
145
215
 
@@ -265,6 +335,26 @@ async function runPrune(args) {
265
335
  ...result,
266
336
  };
267
337
 
338
+ // #1206 — the ONE mechanical call site for `orchestrator.evolve.completed`:
339
+ // this `--prune --apply` invocation IS `/evolve analyze`'s Step 3.5(5) store
340
+ // write, so folding the emit in here (instead of a separate
341
+ // `emit-event.mjs` call in skill prose) means the event can no longer be
342
+ // forgotten independently of the write it reports on. Gated on `!dryRun` —
343
+ // a `--prune --dry-run` preview (the docs' own recommended pre-check for a
344
+ // hand-assembled `--entries` sidecar) never wrote anything, so it must not
345
+ // report a completed run either. Best-effort — never changes this
346
+ // command's exit code or stdout contract.
347
+ if (!args.dryRun) {
348
+ await emitEvolveCompleted({
349
+ repoRoot: args.repoRoot,
350
+ appended: args.appended,
351
+ boosted: args.boosted,
352
+ pruned: result.archived,
353
+ durationMs: args.durationMs,
354
+ skipped: args.skipped,
355
+ });
356
+ }
357
+
268
358
  if (args.json) {
269
359
  process.stdout.write(JSON.stringify(summary) + '\n');
270
360
  } else {
@@ -141,7 +141,9 @@ function stripCommentLine(line) {
141
141
  return null;
142
142
  }
143
143
  // Single-line doc comment
144
- if (trimmed.startsWith('// ') && !trimmed.startsWith('// ──')) {
144
+ // A bare `//` (blank line inside a canonical comment run) is a comment too —
145
+ // without this the generated block carried a stray `//` (measured 2026-09-03).
146
+ if (trimmed === '//' || (trimmed.startsWith('// ') && !trimmed.startsWith('// ──'))) {
145
147
  // Keep structural separator comments, drop narrative ones
146
148
  return null;
147
149
  }
@@ -33,7 +33,7 @@ import { queueStats } from './lib/telemetry/queue.mjs';
33
33
  import { flush, buildBatch } from './lib/telemetry/sync.mjs';
34
34
  import { loadOwnerConfig } from './lib/owner-yaml.mjs';
35
35
  import { readPluginVersionFromPackageJson } from './lib/bootstrap-lock-freshness.mjs';
36
- import { SO_PLUGIN_ROOT } from './lib/platform.mjs';
36
+ import { getPluginRoot } from './lib/platform.mjs';
37
37
 
38
38
  const EXIT_OK = 0;
39
39
  const EXIT_USER = 1;
@@ -69,7 +69,7 @@ ENV KILL-SWITCHES
69
69
 
70
70
  /** Plugin version for `--version` — single-sourced via readPluginVersionFromPackageJson (null → 'unknown'). */
71
71
  function readPkgVersion() {
72
- return readPluginVersionFromPackageJson(SO_PLUGIN_ROOT) ?? 'unknown';
72
+ return readPluginVersionFromPackageJson(getPluginRoot()) ?? 'unknown';
73
73
  }
74
74
 
75
75
  // ---------------------------------------------------------------------------
@@ -94,6 +94,42 @@ function runCheck(script) {
94
94
  return result.status ?? 1;
95
95
  }
96
96
 
97
+ /**
98
+ * Advisory second pass for `check-skill-script-paths.mjs` (#1208): run it a
99
+ * second time with `--strict-sh --json` so the flag has a run surface inside
100
+ * validate-plugin, without promoting `.sh` findings to blocking here — the
101
+ * BLOCKING `.mjs`-only call above stays exactly as it is. Same non-blocking
102
+ * shape as the `check-unwired-features.mjs` / `check-learning-provenance.mjs`
103
+ * / `check-vcs-repo-flag.mjs` calls further down (exit code deliberately
104
+ * ignored; `checkFailed` is never set from this call). `runCheck()` cannot be
105
+ * reused here because it always spawns `[script, PLUGIN_ROOT]` with no room
106
+ * for extra flags, so this is a small dedicated spawn instead.
107
+ *
108
+ * @returns {void}
109
+ */
110
+ function runStrictShAdvisory() {
111
+ const result = spawnSync(
112
+ 'node',
113
+ [path.join(VALIDATE_DIR, 'check-skill-script-paths.mjs'), PLUGIN_ROOT, '--strict-sh', '--json'],
114
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
115
+ );
116
+ let parsed;
117
+ try {
118
+ parsed = JSON.parse(result.stdout || '');
119
+ } catch {
120
+ process.stdout.write(
121
+ ' WARN: check-skill-script-paths --strict-sh: could not parse JSON output (advisory)\n',
122
+ );
123
+ return;
124
+ }
125
+ const shDead = (parsed.findings || []).filter(
126
+ (f) => f.kind === 'missing-path' && f.path.endsWith('.sh'),
127
+ ).length;
128
+ process.stdout.write(
129
+ ` check-skill-script-paths --strict-sh: ${shDead} dead .sh citation(s) (advisory)\n`,
130
+ );
131
+ }
132
+
97
133
  /**
98
134
  * Run the surface-count drift family (issue #663) from the claude-md-drift-check
99
135
  * checker over the doc surfaces that carry artifact counts (README.md +
@@ -175,6 +211,105 @@ function runDriftCheck() {
175
211
  return 0;
176
212
  }
177
213
 
214
+ /**
215
+ * BLOCKING (#1224): the committed hook-reachable allowlist
216
+ * (`hooks/_lib/hook-import-set.json`) must match a fresh crawl of the four hook
217
+ * manifests. The allowlist is what gates `hooks/post-edit-import-probe.mjs`, so
218
+ * a stale set silently NARROWS that probe — a newly hook-reachable module would
219
+ * be edited without ever being checked, which is precisely the 2026-09-04
220
+ * host-wide-block incident class the probe exists to catch. A rotting allowlist
221
+ * is therefore a real defect, not a cosmetic drift, hence FAIL rather than WARN.
222
+ *
223
+ * `scripts/generate-hook-import-set.mjs --check` owns the comparison (it exits 1
224
+ * on drift); this adapter only translates it into validate-plugin's vocabulary.
225
+ * `runCheck()` cannot be reused — it spawns from `scripts/lib/validate/` and
226
+ * passes PLUGIN_ROOT positionally, while the generator takes `--plugin-root`.
227
+ *
228
+ * @returns {number} 0 when in sync, 1 on drift or a generator error.
229
+ */
230
+ function runHookImportSetCheck() {
231
+ console.log('--- Check: hook-import-set drift (#1224) ---');
232
+ const script = path.join(SCRIPT_DIR, 'generate-hook-import-set.mjs');
233
+ const result = spawnSync(
234
+ process.execPath,
235
+ [script, '--plugin-root', PLUGIN_ROOT, '--check'],
236
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
237
+ );
238
+ const combined = ((result.stdout ?? '') + (result.stderr ?? '')).trim();
239
+
240
+ // A silent exit 0 is NOT a pass. The generator's CLI entry is gated on
241
+ // `import.meta.url === file://${process.argv[1]}`, which does not hold when
242
+ // the spawned path traverses a symlink (measured: `/tmp` → `/private/tmp` on
243
+ // macOS makes the whole CLI body a no-op that still exits 0). Demanding the
244
+ // in-sync marker turns that failure mode from a silent pass into a finding.
245
+ const inSync = combined.match(/(\d+) modules, in sync/);
246
+ if (result.status === 0 && inSync) {
247
+ console.log(` PASS: hook-import-set: in sync (${inSync[1]} entries)`);
248
+ totalPass += 1;
249
+ return 0;
250
+ }
251
+ if (result.status === 0) {
252
+ console.log(' FAIL: hook-import-set: generator exited 0 without reporting a comparison '
253
+ + `(no in-sync marker in output: ${JSON.stringify(combined.slice(0, 120))})`);
254
+ totalFail += 1;
255
+ return 1;
256
+ }
257
+ const detail = combined.split('\n').filter(Boolean).pop()
258
+ ?? `generator exited ${result.status}`;
259
+ console.log(` FAIL: hook-import-set: ${detail.replace(/^✗\s*hook-import-set:\s*/, '')}`);
260
+ console.log(' Remedy: node scripts/generate-hook-import-set.mjs && stage hooks/_lib/hook-import-set.json');
261
+ totalFail += 1;
262
+ return 1;
263
+ }
264
+
265
+ /**
266
+ * BLOCKING: the cross-harness portable surface (`AGENTS.md` +
267
+ * `.agents/skills/`) must match a fresh generation from `CLAUDE.md` +
268
+ * `skills/`. 7 of 8 surveyed harnesses read `AGENTS.md` and never `CLAUDE.md`,
269
+ * so a stale or absent copy means those harnesses silently see NO Session
270
+ * Config — a failure that is invisible from inside Claude Code, which is
271
+ * exactly why it needs a mechanical gate rather than a convention.
272
+ *
273
+ * `scripts/generate-agents-skills.mjs --check` owns the comparison (exit 1 on
274
+ * drift); this adapter only translates it. `runCheck()` cannot be reused — it
275
+ * spawns from `scripts/lib/validate/` and passes PLUGIN_ROOT positionally,
276
+ * while the generator takes `--plugin-root`. The in-sync marker is demanded
277
+ * for the same reason `runHookImportSetCheck()` demands one: a spawn whose
278
+ * path traverses a symlink can make the generator's CLI body a no-op that
279
+ * still exits 0.
280
+ *
281
+ * @returns {number} 0 when in sync, 1 on drift or a generator error.
282
+ */
283
+ function runAgentsSurfaceCheck() {
284
+ console.log('--- Check: cross-harness portable surface (AGENTS.md + .agents/skills/) ---');
285
+ const script = path.join(SCRIPT_DIR, 'generate-agents-skills.mjs');
286
+ const result = spawnSync(
287
+ process.execPath,
288
+ [script, '--plugin-root', PLUGIN_ROOT, '--check'],
289
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
290
+ );
291
+ const combined = ((result.stdout ?? '') + (result.stderr ?? '')).trim();
292
+
293
+ const inSync = combined.match(/agents-surface: (\d+) artefact\(s\), in sync/);
294
+ if (result.status === 0 && inSync) {
295
+ console.log(` PASS: agents-surface: in sync (${inSync[1]} artefacts)`);
296
+ totalPass += 1;
297
+ return 0;
298
+ }
299
+ if (result.status === 0) {
300
+ console.log(' FAIL: agents-surface: generator exited 0 without reporting a comparison '
301
+ + `(no in-sync marker in output: ${JSON.stringify(combined.slice(0, 120))})`);
302
+ totalFail += 1;
303
+ return 1;
304
+ }
305
+ for (const line of combined.split('\n').filter((l) => l.startsWith('✗'))) {
306
+ console.log(` FAIL: ${line.replace(/^✗\s*/, '')}`);
307
+ totalFail += 1;
308
+ }
309
+ console.log(' Remedy: node scripts/generate-agents-skills.mjs');
310
+ return 1;
311
+ }
312
+
178
313
  // ---------------------------------------------------------------------------
179
314
  // Run all checks — same order as validate-plugin.sh
180
315
  // plugin.json checks are prerequisite; abort early if they fail.
@@ -220,6 +355,9 @@ if (runDriftCheck() !== 0) checkFailed = 1;
220
355
  process.stdout.write('\n');
221
356
  if (runCheck('check-hooks-symmetry.mjs') !== 0) checkFailed = 1;
222
357
 
358
+ process.stdout.write('\n');
359
+ if (runHookImportSetCheck() !== 0) checkFailed = 1;
360
+
223
361
  process.stdout.write('\n');
224
362
  if (runCheck('check-guard-requires-parity.mjs') !== 0) checkFailed = 1;
225
363
 
@@ -247,6 +385,18 @@ if (runCheck('check-pi-prompts.mjs') !== 0) checkFailed = 1;
247
385
  process.stdout.write('\n');
248
386
  if (runCheck('check-cursor-adapter.mjs') !== 0) checkFailed = 1;
249
387
 
388
+ // BLOCKING: the vendor-neutral portable surface — root AGENTS.md ↔ CLAUDE.md
389
+ // alias, root plugin.json (agent-plugins.org 1.0.0) manifest-version parity
390
+ // against package.json, and the .agents/skills/ mirror against the
391
+ // agentskills.io field list. The generator's own --check runs first (below);
392
+ // this checker is the INDEPENDENT oracle over the same artefacts, so a
393
+ // generator bug cannot vouch for its own output.
394
+ process.stdout.write('\n');
395
+ if (runCheck('check-agents-skills.mjs') !== 0) checkFailed = 1;
396
+
397
+ process.stdout.write('\n');
398
+ if (runAgentsSurfaceCheck() !== 0) checkFailed = 1;
399
+
250
400
  process.stdout.write('\n');
251
401
  if (runCheck('check-session-plan-routing.mjs') !== 0) checkFailed = 1;
252
402
 
@@ -327,6 +477,17 @@ runCheck('check-doc-cli-commands.mjs');
327
477
  // citation's own line or the line immediately above.
328
478
  process.stdout.write('\n');
329
479
  if (runCheck('check-skill-script-paths.mjs') !== 0) checkFailed = 1;
480
+ runStrictShAdvisory();
481
+
482
+ // BLOCKING (#1157 follow-up): a relative markdown link under skills/, commands/,
483
+ // agents/ or .claude/rules/ that does not resolve from the LINKING file's own
484
+ // directory. The check above judges cited SCRIPT paths; this one judges links
485
+ // between the instruction files themselves — the defect class a `references/`
486
+ // split creates, where the moved text is byte-identical and every `./sibling.md`
487
+ // in it now points one directory too deep. Three such links shipped broken on
488
+ // 2026-09-06 and no gate saw them; the worst silently detached the six
489
+ // session-end tail phases from their dispatcher.
490
+ if (runCheck('check-skill-links.mjs') !== 0) checkFailed = 1;
330
491
 
331
492
  // WARN-only: a state-mutating `git` call in tests/ that names no target resolves
332
493
  // its destination from the ambient cwd (or an inherited GIT_DIR) — the 2026-08-19
@@ -73,6 +73,7 @@ import path from 'node:path';
73
73
  import { readFileSync, existsSync, statSync } from 'node:fs';
74
74
  import { execFileSync } from 'node:child_process';
75
75
  import { warn } from './lib/common.mjs';
76
+ import { MANIFEST_SESSION_KEYS } from './lib/session-identity/own-session.mjs';
76
77
  import {
77
78
  assertFileScopeSubset,
78
79
  assertTestSiblingCoverage,
@@ -289,10 +290,13 @@ function validateOptionalSessionId(obj, key, errors) {
289
290
  }
290
291
 
291
292
  /**
292
- * Validate the OPTIONAL session binding (#1123): `session` (the raw
293
- * `session_id` of the session that WROTE this manifest) and its human-readable
294
- * twin `semantic_session`. Both come from one `sessionAttribution(repoRoot)`
295
- * call — see `skills/wave-executor/wave-loop.md` § Scope Manifest.
293
+ * Validate the OPTIONAL session binding (#1123): `session_id` (the raw session
294
+ * id of the session that WROTE this manifest) and its human-readable twin
295
+ * `semantic_session_id`. Both come from one `sessionAttribution(repoRoot)`
296
+ * call — see `skills/wave-executor/wave-loop.md` § Scope Manifest. The
297
+ * pre-#1153 spellings `session` / `semantic_session` are still ACCEPTED here
298
+ * (read side only, until the next minor release) — key names come from
299
+ * `MANIFEST_SESSION_KEYS` so the writer and every reader share one list.
296
300
  *
297
301
  * Deliberately NOT part of {@link validateRequired}, and that is a compatibility
298
302
  * constraint rather than a preference: `wave-scope.json` is a shared
@@ -306,9 +310,25 @@ function validateOptionalSessionId(obj, key, errors) {
306
310
  * @param {string[]} errors
307
311
  * @param {string[]} warnings
308
312
  */
309
- function validateSession(obj, errors, warnings) {
310
- const present = validateOptionalSessionId(obj, 'session', errors);
311
- validateOptionalSessionId(obj, 'semantic_session', errors);
313
+ function validateSessionBinding(obj, errors, warnings) {
314
+ let present = false;
315
+ MANIFEST_SESSION_KEYS.current.forEach((key, i) => {
316
+ const legacyKey = MANIFEST_SESSION_KEYS.legacy[i];
317
+ const hasCurrent = validateOptionalSessionId(obj, key, errors);
318
+ const hasLegacy = validateOptionalSessionId(obj, legacyKey, errors);
319
+ // Both spellings of the SAME slot, disagreeing, is the one case the reader
320
+ // cannot resolve honestly: it silently prefers `key` and drops the other
321
+ // id, so a manifest that names two different sessions would classify as
322
+ // `own` for one of them. Name it here rather than let the preference decide.
323
+ if (hasCurrent && hasLegacy && obj[key] !== obj[legacyKey]) {
324
+ errors.push(
325
+ `${key} and legacy ${legacyKey} are both present with DIFFERENT values — ` +
326
+ 'a manifest binds to exactly one session; drop the legacy key (accepted on ' +
327
+ 'the read side only, until the next minor release, #1153)',
328
+ );
329
+ }
330
+ if (i === 0) present = hasCurrent || hasLegacy;
331
+ });
312
332
  if (!present) {
313
333
  warnings.push(
314
334
  'no session field — manifest is not session-bound (legacy, #1123), so every session sharing this ' +
@@ -749,7 +769,7 @@ function validate(
749
769
  const warnings = [];
750
770
 
751
771
  validateRequired(obj, errors);
752
- validateSession(obj, errors, warnings);
772
+ validateSessionBinding(obj, errors, warnings);
753
773
  validateAllowedPaths(obj, errors, warnings);
754
774
  validateBlockedCommands(obj, errors);
755
775
  validateGates(obj, errors);