session-orchestrator 5.2.0 → 5.4.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 (427) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +5 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +5 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +5 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +4 -2
  9. package/.agents/skills/close/SKILL.md +5 -1
  10. package/.agents/skills/close/agents/openai.yaml +5 -0
  11. package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
  12. package/.agents/skills/debug/SKILL.md +5 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +5 -1
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +5 -1
  17. package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
  18. package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
  19. package/.agents/skills/ecosystem-health/SKILL.md +3 -1
  20. package/.agents/skills/eli5/SKILL.md +5 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +6 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +6 -2
  25. package/.agents/skills/evolve/agents/openai.yaml +5 -0
  26. package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
  27. package/.agents/skills/gitlab-ops/SKILL.md +3 -1
  28. package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
  29. package/.agents/skills/go/SKILL.md +5 -1
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +5 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +5 -1
  34. package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
  35. package/.agents/skills/hook-development/SKILL.md +3 -1
  36. package/.agents/skills/mcp-builder/SKILL.md +3 -1
  37. package/.agents/skills/memory-cleanup/SKILL.md +5 -1
  38. package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
  39. package/.agents/skills/mode-selector/SKILL.md +3 -1
  40. package/.agents/skills/navigator/SKILL.md +22 -0
  41. package/.agents/skills/navigator/agents/openai.yaml +5 -0
  42. package/.agents/skills/npm-publish/SKILL.md +4 -2
  43. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  44. package/.agents/skills/persona-panel/SKILL.md +5 -1
  45. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  46. package/.agents/skills/plan/SKILL.md +5 -1
  47. package/.agents/skills/plan/agents/openai.yaml +5 -0
  48. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  49. package/.agents/skills/portfolio/SKILL.md +5 -1
  50. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  51. package/.agents/skills/quality-gates/SKILL.md +3 -1
  52. package/.agents/skills/reconcile/SKILL.md +5 -1
  53. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  54. package/.agents/skills/release/SKILL.md +5 -1
  55. package/.agents/skills/release/agents/openai.yaml +5 -0
  56. package/.agents/skills/remote-offload/SKILL.md +3 -1
  57. package/.agents/skills/repo-audit/SKILL.md +5 -1
  58. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  59. package/.agents/skills/session/SKILL.md +21 -0
  60. package/.agents/skills/session/agents/openai.yaml +5 -0
  61. package/.agents/skills/session-end/SKILL.md +3 -1
  62. package/.agents/skills/session-plan/SKILL.md +3 -1
  63. package/.agents/skills/session-start/SKILL.md +3 -1
  64. package/.agents/skills/spinout/SKILL.md +5 -1
  65. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  66. package/.agents/skills/sunset-review/SKILL.md +5 -1
  67. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  68. package/.agents/skills/templates-ack/SKILL.md +21 -0
  69. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  70. package/.agents/skills/test/SKILL.md +5 -1
  71. package/.agents/skills/test/agents/openai.yaml +5 -0
  72. package/.agents/skills/test-audit/SKILL.md +21 -0
  73. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  74. package/.agents/skills/test-runner/SKILL.md +3 -1
  75. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  76. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  77. package/.agents/skills/ux-grill/SKILL.md +5 -1
  78. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  79. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  80. package/.agents/skills/vault-sync/SKILL.md +3 -1
  81. package/.agents/skills/wave-executor/SKILL.md +3 -1
  82. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  83. package/.claude-plugin/marketplace.json +1 -1
  84. package/.claude-plugin/plugin.json +1 -1
  85. package/.codex-plugin/plugin.json +4 -4
  86. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  87. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  88. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  89. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  90. package/.codex-plugin/skills/navigator/SKILL.md +22 -0
  91. package/.codex-plugin/skills/{convergence-monitoring → navigator}/agents/openai.yaml +2 -2
  92. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  93. package/.codex-plugin/skills/session/SKILL.md +1 -1
  94. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  95. package/.codex-plugin/skills/{npm-publish → test-audit}/agents/openai.yaml +1 -1
  96. package/.cursor/commands/eval.md +1 -1
  97. package/.cursor/commands/navigator.md +15 -0
  98. package/.cursor/commands/session.md +1 -1
  99. package/.cursor/commands/test-audit.md +14 -0
  100. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  101. package/.cursor/rules/050-plan.mdc +1 -1
  102. package/.cursor/rules/060-evolve.mdc +27 -12
  103. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  104. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  105. package/.cursor/skills/eval/SKILL.md +1 -1
  106. package/.cursor/skills/navigator/SKILL.md +13 -0
  107. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  108. package/.cursor/skills/test-audit/SKILL.md +12 -0
  109. package/.cursor-plugin/plugin.json +1 -1
  110. package/.orchestrator/policy/blocked-commands.json +12 -3
  111. package/AGENTS.md +9 -30
  112. package/CHANGELOG.md +206 -0
  113. package/README.md +13 -9
  114. package/SECURITY.md +12 -0
  115. package/agents/dialectic-deriver.md +13 -10
  116. package/agents/eval-judge.md +67 -45
  117. package/agents/skill-applied-judge.md +34 -19
  118. package/commands/session.md +7 -3
  119. package/docs/agent-authoring.md +2 -2
  120. package/docs/baseline.md +22 -7
  121. package/docs/ci-setup.md +57 -4
  122. package/docs/codex-setup.md +14 -2
  123. package/docs/components.md +12 -10
  124. package/docs/events-schema.md +65 -15
  125. package/docs/persona-panel.md +5 -4
  126. package/docs/rule-authoring.md +84 -17
  127. package/docs/scope-collision-guard.md +26 -4
  128. package/docs/session-config-reference.md +154 -12
  129. package/docs/session-config-template.md +41 -3
  130. package/docs/telemetry.md +2 -0
  131. package/hooks/_lib/hook-import-set.json +87 -6
  132. package/hooks/_lib/subagent-transcript.mjs +582 -31
  133. package/hooks/config-protection.mjs +11 -3
  134. package/hooks/cwd-change-restore.mjs +11 -3
  135. package/hooks/enforce-commands.mjs +70 -23
  136. package/hooks/enforce-scope.mjs +143 -33
  137. package/hooks/hooks-codex.json +1 -1
  138. package/hooks/hooks-cursor.json +5 -0
  139. package/hooks/hooks-pi.json +5 -0
  140. package/hooks/hooks.json +6 -1
  141. package/hooks/loop-guard.mjs +11 -3
  142. package/hooks/on-session-end.mjs +58 -23
  143. package/hooks/on-session-start.mjs +101 -11
  144. package/hooks/on-stop.mjs +170 -22
  145. package/hooks/operator-steer.mjs +11 -3
  146. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  147. package/hooks/post-bash-write-verify.mjs +3 -2
  148. package/hooks/post-edit-import-probe.mjs +17 -9
  149. package/hooks/post-edit-validate.mjs +13 -5
  150. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  151. package/hooks/post-tool-batch-wave-signal.mjs +202 -38
  152. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  153. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  154. package/hooks/pre-auq-clarity.mjs +15 -2
  155. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  156. package/hooks/pre-bash-issue-budget.mjs +16 -11
  157. package/hooks/pre-bash-memory-propose-audit.mjs +137 -56
  158. package/hooks/pre-bash-sessions-ledger-guard.mjs +419 -22
  159. package/hooks/pre-bash-staging-fence.mjs +335 -31
  160. package/hooks/pre-bash-templates-first.mjs +19 -14
  161. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  162. package/hooks/pre-task-scope-disjoint.mjs +792 -74
  163. package/hooks/subagent-telemetry.mjs +111 -38
  164. package/hooks/wave-scope-commit-guard.mjs +197 -100
  165. package/monitors/monitors.json +1 -1
  166. package/output-styles/wave-summary.md +1 -1
  167. package/package.json +3 -3
  168. package/pi/prompts/eval.md +1 -1
  169. package/pi/prompts/navigator.md +12 -0
  170. package/pi/prompts/session.md +1 -1
  171. package/pi/prompts/test-audit.md +12 -0
  172. package/rules/README.md +7 -2
  173. package/rules/_index.md +3 -3
  174. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  175. package/rules/opt-in-domain/prompt-caching.md +6 -2
  176. package/rules/opt-in-stack/backend-data.md +10 -2
  177. package/rules/opt-in-stack/backend.md +10 -4
  178. package/rules/opt-in-stack/frontend.md +7 -2
  179. package/rules/opt-in-stack/security-web.md +10 -4
  180. package/rules/opt-in-stack/swift.md +6 -2
  181. package/scripts/apply-session-learnings.mjs +492 -0
  182. package/scripts/archive-closed-prds.mjs +2 -1
  183. package/scripts/autopilot.mjs +70 -53
  184. package/scripts/backfill-abandoned-sessions.mjs +118 -16
  185. package/scripts/baseline-archetypes.mjs +1 -1
  186. package/scripts/check-package-manager.mjs +62 -5
  187. package/scripts/check-sessions-integrity.mjs +300 -0
  188. package/scripts/cursor-install.mjs +1 -4
  189. package/scripts/dialectic-deriver.mjs +50 -13
  190. package/scripts/emit-session.mjs +371 -31
  191. package/scripts/eval-session.mjs +65 -3
  192. package/scripts/export-hw-learnings.mjs +22 -3
  193. package/scripts/fleet-checkin.mjs +147 -0
  194. package/scripts/generate-agents-skills.mjs +102 -29
  195. package/scripts/generate-cursor-adapter.mjs +61 -16
  196. package/scripts/lib/agent-frontmatter.mjs +3 -3
  197. package/scripts/lib/agent-status.mjs +2 -31
  198. package/scripts/lib/auq/clarity.mjs +10 -2
  199. package/scripts/lib/auq/parse.mjs +12 -31
  200. package/scripts/lib/auq/schema.mjs +56 -41
  201. package/scripts/lib/auto-dialectic.mjs +304 -15
  202. package/scripts/lib/auto-dream.mjs +7 -6
  203. package/scripts/lib/autopilot/flags.mjs +18 -2
  204. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  205. package/scripts/lib/autopilot/loop.mjs +19 -2
  206. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  207. package/scripts/lib/baseline-archetypes.mjs +41 -22
  208. package/scripts/lib/ci-status-banner.mjs +376 -16
  209. package/scripts/lib/command-blocker.mjs +275 -28
  210. package/scripts/lib/config/dialectic.mjs +12 -3
  211. package/scripts/lib/config/gate.mjs +74 -0
  212. package/scripts/lib/config/host-paths.mjs +79 -0
  213. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  214. package/scripts/lib/config/reaper.mjs +163 -0
  215. package/scripts/lib/config-schema.mjs +13 -0
  216. package/scripts/lib/config.mjs +85 -10
  217. package/scripts/lib/convergence-monitor.mjs +74 -11
  218. package/scripts/lib/ecosystem-health.mjs +11 -0
  219. package/scripts/lib/eval/engine.mjs +421 -53
  220. package/scripts/lib/eval/judge.mjs +463 -40
  221. package/scripts/lib/eval/schema.mjs +10 -1
  222. package/scripts/lib/events-rotation.mjs +221 -25
  223. package/scripts/lib/events-schema.mjs +114 -0
  224. package/scripts/lib/events.mjs +545 -7
  225. package/scripts/lib/fleet-protocol.mjs +270 -0
  226. package/scripts/lib/frontmatter-guard.mjs +21 -10
  227. package/scripts/lib/gates/gate-baseline.mjs +40 -5
  228. package/scripts/lib/gates/gate-full.mjs +35 -5
  229. package/scripts/lib/gates/gate-helpers.mjs +336 -23
  230. package/scripts/lib/gates/gate-incremental.mjs +49 -23
  231. package/scripts/lib/gates/gate-per-file.mjs +42 -8
  232. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  233. package/scripts/lib/instruction-budget-guard.mjs +208 -8
  234. package/scripts/lib/io.mjs +42 -8
  235. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  236. package/scripts/lib/js-mask.mjs +197 -0
  237. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  238. package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
  239. package/scripts/lib/learnings/io.mjs +154 -26
  240. package/scripts/lib/learnings/schema.mjs +163 -15
  241. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  242. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  243. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  244. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  245. package/scripts/lib/named-vault-resolver.mjs +104 -5
  246. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  247. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  248. package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
  249. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  250. package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
  251. package/scripts/lib/orphan-reaper/scan.mjs +506 -0
  252. package/scripts/lib/orphan-reaper.mjs +316 -0
  253. package/scripts/lib/owner-yaml.mjs +104 -6
  254. package/scripts/lib/peer-cards/merger.mjs +48 -10
  255. package/scripts/lib/peer-cards/reader.mjs +78 -2
  256. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  257. package/scripts/lib/process-group.mjs +950 -0
  258. package/scripts/lib/project-hygiene.mjs +21 -1
  259. package/scripts/lib/quality-gate.mjs +152 -33
  260. package/scripts/lib/reconcile/backlog.mjs +368 -0
  261. package/scripts/lib/reconcile/engine.mjs +55 -188
  262. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  263. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  264. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  265. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  266. package/scripts/lib/resource-probe.mjs +1 -1
  267. package/scripts/lib/rule-loader.mjs +141 -74
  268. package/scripts/lib/rules-sync.mjs +27 -12
  269. package/scripts/lib/scope-baseline.mjs +3 -4
  270. package/scripts/lib/scope-echo.mjs +39 -2
  271. package/scripts/lib/scope-gate.mjs +605 -1
  272. package/scripts/lib/session-close-backfill.mjs +33 -6
  273. package/scripts/lib/session-id.mjs +9 -20
  274. package/scripts/lib/session-invocation.mjs +20 -0
  275. package/scripts/lib/session-record-repair.mjs +183 -26
  276. package/scripts/lib/session-registry.mjs +27 -2
  277. package/scripts/lib/session-schema/constants.mjs +56 -2
  278. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  279. package/scripts/lib/session-schema/validator.mjs +28 -1
  280. package/scripts/lib/session-schema.mjs +8 -3
  281. package/scripts/lib/session-start-probes.mjs +95 -10
  282. package/scripts/lib/session-token-rollup.mjs +245 -52
  283. package/scripts/lib/sessions-canonical.mjs +118 -4
  284. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  285. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  286. package/scripts/lib/sh-quote.mjs +29 -0
  287. package/scripts/lib/skill-evidence-window.mjs +891 -0
  288. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  289. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  290. package/scripts/lib/skill-judge.mjs +45 -3
  291. package/scripts/lib/subagents-schema.mjs +30 -3
  292. package/scripts/lib/tail-window.mjs +56 -0
  293. package/scripts/lib/telemetry/pricing.mjs +118 -71
  294. package/scripts/lib/telemetry/schema.mjs +30 -0
  295. package/scripts/lib/telemetry/sync.mjs +61 -6
  296. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  297. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  298. package/scripts/lib/tests-src-ratio.mjs +140 -23
  299. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  300. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  301. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  302. package/scripts/lib/user-invocable-skills.mjs +23 -3
  303. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  304. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  305. package/scripts/lib/validate/check-agents.mjs +1 -1
  306. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  307. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  308. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  309. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  310. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  311. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  312. package/scripts/lib/validate/check-rules.mjs +7 -5
  313. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  314. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  315. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  316. package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
  317. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  318. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  319. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  320. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  321. package/scripts/lib/validate/confidential-names.mjs +10 -0
  322. package/scripts/lib/validate-vendored-rules.mjs +210 -84
  323. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  324. package/scripts/lib/vault-mirror/process.mjs +10 -3
  325. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  326. package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
  327. package/scripts/lib/vault-status/board-writer.mjs +48 -11
  328. package/scripts/lib/vault-status/narrative-mirror.mjs +71 -16
  329. package/scripts/lib/vault-yaml.mjs +118 -0
  330. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  331. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  332. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  333. package/scripts/lib/wave-resource-gate.mjs +34 -8
  334. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  335. package/scripts/memory-propose.mjs +18 -5
  336. package/scripts/release-session-lock.mjs +306 -0
  337. package/scripts/release.mjs +40 -8
  338. package/scripts/repair-invalid-sessions.mjs +37 -4
  339. package/scripts/resolve-session-invocation.mjs +59 -0
  340. package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
  341. package/scripts/run-quality-gate.mjs +357 -197
  342. package/scripts/site-numbers.mjs +163 -5
  343. package/scripts/sweep-expired-learnings.mjs +56 -5
  344. package/scripts/sweep-expired-rules.mjs +16 -4
  345. package/scripts/validate-plugin.mjs +19 -0
  346. package/scripts/validate-wave-scope.mjs +32 -105
  347. package/scripts/vault-integration-watcher.mjs +2 -1
  348. package/scripts/vault-mirror.mjs +81 -47
  349. package/scripts/wave-scope-binding.mjs +2 -1
  350. package/skills/_shared/config-reading.md +3 -3
  351. package/skills/_shared/fleet-protocol.md +131 -0
  352. package/skills/_shared/platform-tools.md +23 -11
  353. package/skills/_shared/state-ownership.md +5 -0
  354. package/skills/autopilot/SKILL.md +47 -18
  355. package/skills/bootstrap/SKILL.md +1 -1
  356. package/skills/bootstrap/_shared-template.md +1 -1
  357. package/skills/bootstrap/fast-template.md +1 -1
  358. package/skills/bootstrap/private-contract.md +2 -1
  359. package/skills/bootstrap/public-fallback.md +6 -3
  360. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  361. package/skills/claude-md-drift-check/SKILL.md +10 -10
  362. package/skills/claude-md-drift-check/checker.mjs +26 -18
  363. package/skills/convergence-monitoring/README.md +8 -1
  364. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  365. package/skills/convergence-monitoring/SKILL.md +15 -6
  366. package/skills/eval/SKILL.md +39 -24
  367. package/skills/eval/rubric-v1.md +1 -0
  368. package/skills/eval/rubric-v2.md +457 -0
  369. package/skills/evolve/SKILL.md +3 -3
  370. package/skills/evolve/references/evolve-analyze-mode.md +10 -6
  371. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  372. package/skills/gitlab-ops/SKILL.md +3 -2
  373. package/skills/memory-cleanup/SKILL.md +1 -1
  374. package/skills/navigator/SKILL.md +193 -0
  375. package/skills/navigator/references/account-check.sh +87 -0
  376. package/skills/navigator/references/fleet-decisions.mjs +231 -0
  377. package/skills/navigator/references/slot-check.sh +114 -0
  378. package/skills/npm-publish/SKILL.md +1 -1
  379. package/skills/persona-panel/SKILL.md +2 -2
  380. package/skills/persona-panel/persona-format.md +2 -2
  381. package/skills/persona-panel/presets/designer-lens.md +1 -1
  382. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  383. package/skills/persona-panel/presets/pm-lens.md +1 -1
  384. package/skills/plan/mode-retro.md +19 -4
  385. package/skills/quality-gates/SKILL.md +6 -4
  386. package/skills/reconcile/SKILL.md +11 -0
  387. package/skills/remote-offload/SKILL.md +4 -2
  388. package/skills/session-end/SKILL.md +13 -16
  389. package/skills/session-end/discovery-scan.md +1 -1
  390. package/skills/session-end/learning-patterns.md +22 -12
  391. package/skills/session-end/metrics-collection.md +5 -6
  392. package/skills/session-end/phase-3-6-tail.md +55 -9
  393. package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
  394. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  395. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  396. package/skills/session-end/references/session-summary-template.md +1 -1
  397. package/skills/session-end/session-metrics-write.md +32 -60
  398. package/skills/session-plan/SKILL.md +17 -8
  399. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  400. package/skills/session-start/SKILL.md +6 -0
  401. package/skills/session-start/references/operations-contract.md +4 -0
  402. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  403. package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
  404. package/skills/test-audit/SKILL.md +152 -0
  405. package/skills/test-audit/references/campaign.md +111 -0
  406. package/skills/test-audit/references/mutate.sh +50 -0
  407. package/skills/ux-grill/SKILL.md +1 -1
  408. package/skills/wave-executor/SKILL.md +9 -5
  409. package/skills/wave-executor/circuit-breaker.md +2 -0
  410. package/skills/wave-executor/references/wave-executor-state-init.md +8 -3
  411. package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
  412. package/skills/wave-executor/references/wave-loop-review.md +3 -5
  413. package/templates/personas/README.md +5 -5
  414. package/templates/personas/accounting-compliance.v1.md +1 -1
  415. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  416. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  417. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  418. package/templates/personas/buyer-p3-build.v1.md +1 -1
  419. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  420. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  421. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  422. package/templates/personas/klima-ai-expert.v1.md +1 -1
  423. package/templates/personas/klima-physicist.v1.md +1 -1
  424. package/.cursor/commands/convergence-monitoring.md +0 -13
  425. package/.cursor/commands/npm-publish.md +0 -13
  426. package/pi/prompts/convergence-monitoring.md +0 -11
  427. package/pi/prompts/npm-publish.md +0 -11
@@ -46,6 +46,15 @@ async function _exists(p) {
46
46
  }
47
47
  }
48
48
 
49
+ /**
50
+ * The one directory session-orchestrator worktrees may live in.
51
+ * Host-shared: several sessions run concurrently against the same base.
52
+ * @returns {string}
53
+ */
54
+ function _worktreeBaseDir() {
55
+ return path.join(os.tmpdir(), 'so-worktrees');
56
+ }
57
+
49
58
  /**
50
59
  * Compute the standard branch name and tmp path for a given suffix.
51
60
  * @param {string} suffix
@@ -53,10 +62,146 @@ async function _exists(p) {
53
62
  */
54
63
  function _worktreeInfo(suffix) {
55
64
  const branch = `so-worktree-${suffix}`;
56
- const wtPath = path.join(os.tmpdir(), 'so-worktrees', branch);
65
+ const wtPath = path.join(_worktreeBaseDir(), branch);
57
66
  return { branch, wtPath };
58
67
  }
59
68
 
69
+ /**
70
+ * Resolve symlinks, returning null when the path does not exist.
71
+ * @param {string} p
72
+ * @returns {Promise<string|null>}
73
+ */
74
+ async function _realpathOrNull(p) {
75
+ try {
76
+ return await fs.realpath(p);
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * True when `wtPath` is NOT a live working tree of any repository — i.e. either
84
+ * it carries no `.git` entry at all, or its `.git` file points at an
85
+ * administrative directory that no longer exists (the registration was dropped).
86
+ *
87
+ * A directory whose registration is intact is never an orphan: `git worktree
88
+ * remove --force` handles that case, so this predicate is what keeps a live
89
+ * worktree — including one owned by a PEER session, or by another repository —
90
+ * out of the delete path below.
91
+ *
92
+ * @param {string} wtPath
93
+ * @returns {Promise<boolean>}
94
+ */
95
+ async function _isUnregisteredWorktreeDir(wtPath) {
96
+ const dotGit = path.join(wtPath, '.git');
97
+ let st;
98
+ try {
99
+ st = await fs.lstat(dotGit);
100
+ } catch {
101
+ return true; // No .git entry — not a working tree of anything.
102
+ }
103
+ // A real .git DIRECTORY means a full repository lives here; never ours to delete.
104
+ if (!st.isFile()) return false;
105
+
106
+ const content = await fs.readFile(dotGit, 'utf8').catch(() => '');
107
+ const match = /^gitdir:\s*(.+)$/m.exec(content);
108
+ if (!match) return false; // Unparseable — treat as live, refuse.
109
+
110
+ const adminDir = path.resolve(wtPath, match[1].trim());
111
+ // Admin dir still present → git knows this worktree → not an orphan.
112
+ return !(await _exists(adminDir));
113
+ }
114
+
115
+ /**
116
+ * Remove an ORPHAN worktree directory (issue #984).
117
+ *
118
+ * An interrupted `removeWorktree` drops the git registration first and can die
119
+ * before the directory is gone. `git worktree add` then fails with
120
+ * "'…' already exists" while `git worktree remove --force` fails with
121
+ * "'…' is not a working tree" (swallowed by nothrow) — the retry loops on the
122
+ * same error until someone deletes the directory by hand.
123
+ *
124
+ * SAFETY — this is a recursive delete inside library code, so every condition
125
+ * below is asserted in code before `fs.rm` runs. Any failed condition returns
126
+ * false and leaves the directory alone (the retry then fails with git's own
127
+ * error, which is the pre-#984 behaviour):
128
+ * 1. `wtPath` itself is a real directory and NOT a symlink — the LEAF is never
129
+ * followed. (A symlink one level up is covered by condition 2.)
130
+ * 2. The literal base `<os.tmpdir()>/so-worktrees` is itself a real directory
131
+ * and NOT a symlink. The base lives on a host-shared /tmp where a co-tenant
132
+ * can pre-create it; without this check the realpath of a planted base
133
+ * symlink BECOMES the delete root, so a `so-worktree-*` directory anywhere
134
+ * on the filesystem would qualify.
135
+ * 3. The REALPATH of `wtPath` is a DIRECT child of the REALPATH of that base
136
+ * — both sides resolved, because macOS resolves /tmp → /private/tmp and
137
+ * os.tmpdir() itself may be a symlinked path.
138
+ * 4. The resolved basename starts with `so-worktree-`.
139
+ * 5. It is not a live working tree of ANY repository (see above).
140
+ * Note what is NOT asserted: an intermediate component of `wtPath` other than
141
+ * the base cannot exist — the path is `<base>/so-worktree-<suffix>`, computed by
142
+ * `_worktreeInfo`, never caller-supplied.
143
+ * `git worktree prune` is deliberately NOT used: it is repo-global and would
144
+ * drop a peer session's stale registration.
145
+ *
146
+ * CEILING (BV-004): the delete is bounded to one directory directly under the
147
+ * fixed base `<os.tmpdir()>/so-worktrees` whose name carries the `so-worktree-`
148
+ * prefix. Revisit when worktree paths become caller-supplied (the
149
+ * `validateWorkspacePath` follow-up noted above) or move out of os.tmpdir() —
150
+ * at that point the containment base is no longer a constant and must be passed
151
+ * in and validated by the caller.
152
+ *
153
+ * @param {string} wtPath
154
+ * @returns {Promise<boolean>} true when a directory was actually removed.
155
+ */
156
+ async function _removeOrphanWorktreeDir(wtPath) {
157
+ let lst;
158
+ try {
159
+ lst = await fs.lstat(wtPath);
160
+ } catch {
161
+ return false; // Nothing there.
162
+ }
163
+ if (lst.isSymbolicLink() || !lst.isDirectory()) return false;
164
+
165
+ // The containment BASE must be a real directory too: on a host-shared /tmp a
166
+ // co-tenant can pre-create `<os.tmpdir()>/so-worktrees` as a symlink, and a
167
+ // realpath-resolved base would then relocate the delete root off-tmpdir.
168
+ const baseDir = _worktreeBaseDir();
169
+ let baseLst;
170
+ try {
171
+ baseLst = await fs.lstat(baseDir);
172
+ } catch {
173
+ return false; // No base directory — nothing under it to remove.
174
+ }
175
+ if (baseLst.isSymbolicLink() || !baseLst.isDirectory()) return false;
176
+
177
+ const realWt = await _realpathOrNull(wtPath);
178
+ const realBase = await _realpathOrNull(baseDir);
179
+ if (!realWt || !realBase) return false;
180
+ if (!isPathInside(realWt, realBase)) return false;
181
+ // Direct child only — matches the CEILING below; no nested path qualifies.
182
+ if (path.dirname(realWt) !== realBase) return false;
183
+ if (!path.basename(realWt).startsWith('so-worktree-')) return false;
184
+
185
+ if (!(await _isUnregisteredWorktreeDir(realWt))) return false;
186
+
187
+ // RACE WINDOW (accepted, #1390 P7): `_isUnregisteredWorktreeDir` above and
188
+ // `fs.rm` below are two separate syscalls, not one atomic check-then-act.
189
+ // Both plausible racers here are session-orchestrator sessions on the SAME
190
+ // host, contending over the same `<os.tmpdir()>/so-worktrees` base: one
191
+ // could register (`git worktree add`) or delete `realWt` in the gap between
192
+ // the two calls. No lock primitive guards this sub-millisecond window
193
+ // between our own sessions — the worst case is a benign retry (the caller's
194
+ // `git worktree add` above already retries once on failure), never data
195
+ // loss, because neither racer ever touches a path outside its own
196
+ // `so-worktree-*` name.
197
+ // REVISIT TRIGGER (BV-004): an UNRELATED process gains the ability to
198
+ // create or delete `so-worktree-*` directories under this base — at that
199
+ // point the race is no longer bounded to our own retry semantics and this
200
+ // window needs an actual lock, not an accepted gap.
201
+ await fs.rm(realWt, { recursive: true, force: true });
202
+ return true;
203
+ }
204
+
60
205
  // ---------------------------------------------------------------------------
61
206
  // validateWorkspacePath
62
207
  // ---------------------------------------------------------------------------
@@ -136,6 +281,13 @@ export async function createWorktree(suffix, baseRef = 'HEAD', options = {}) {
136
281
  await nothrow(git`git worktree remove ${wtPath} --force`);
137
282
  await nothrow(git`git branch -D ${branch}`);
138
283
 
284
+ // Still on disk after the remove attempt → git has no registration for it
285
+ // (issue #984). Delete the orphan directory under the guards documented on
286
+ // _removeOrphanWorktreeDir; a live/registered worktree never reaches here.
287
+ if (await _exists(wtPath)) {
288
+ await _removeOrphanWorktreeDir(wtPath);
289
+ }
290
+
139
291
  try {
140
292
  await git`git worktree add -b ${branch} ${wtPath} ${baseRef}`;
141
293
  } catch (secondErr) {
@@ -35,6 +35,7 @@
35
35
  * Related modules:
36
36
  * scripts/lib/memory-proposals/schema.mjs — createProposalRecord, PROPOSAL_TYPES
37
37
  * scripts/lib/memory-proposals/store.mjs — appendProposal
38
+ * scripts/lib/learnings/schema.mjs — isRepoRelativePathEntry (--file-paths)
38
39
  * scripts/lib/state-md.mjs — resolveStateMdPath, parseStateMd
39
40
  * scripts/parse-config.mjs — Session Config subprocess
40
41
  */
@@ -42,9 +43,10 @@
42
43
  import { parseArgs } from 'node:util';
43
44
  import { readFileSync, existsSync } from 'node:fs';
44
45
  import { spawnSync } from 'node:child_process';
45
- import { dirname, join, isAbsolute } from 'node:path';
46
+ import { dirname, join } from 'node:path';
46
47
  import { fileURLToPath } from 'node:url';
47
48
 
49
+ import { isRepoRelativePathEntry } from './lib/learnings/schema.mjs';
48
50
  import { isWaveAgentContext, WAVE_AGENT_ENV_VAR, WAVE_AGENT_ENV_VALUE } from './lib/wave-context.mjs';
49
51
  import { readProcessLocalSessionIds, manifestSessionBinding } from './lib/session-identity/own-session.mjs';
50
52
  import { readLockDetailed } from './lib/session-lock.mjs';
@@ -197,6 +199,14 @@ if (confidenceRaw !== undefined) {
197
199
  // (Step 6) — so a malformed --file-paths value produces the same exit-4
198
200
  // argv-error contract as every other required/optional flag, never a
199
201
  // downstream schema-validation surprise.
202
+ //
203
+ // The repo-relative rule is `isRepoRelativePathEntry` from learnings/schema.mjs
204
+ // — the predicate strict validateLearning() applies to `file_paths` at write
205
+ // time. A looser rule here (it was `path.isAbsolute`, blind to `~/x`, `C:\x`,
206
+ // UNC and URL entries) queued proposals that the operator approved and
207
+ // appendLearning() then refused, so the learning never landed. The checks below
208
+ // it (newline, glob metacharacters, count, length) are this CLI's own, stricter
209
+ // additions.
200
210
 
201
211
  const FILE_PATHS_MAX_COUNT = 20;
202
212
  const FILE_PATH_MAX_CHARS = 256;
@@ -219,10 +229,13 @@ if (filePaths !== undefined) {
219
229
  );
220
230
  }
221
231
  for (const p of filePaths) {
222
- if (isAbsolute(p)) {
223
- argErrors.push(`--file-paths must be repo-relative — absolute path rejected: "${p}"`);
224
- } else if (p.split(/[\\/]/).includes('..')) {
225
- argErrors.push(`--file-paths must not contain ".." path segments: "${p}"`);
232
+ if (!isRepoRelativePathEntry(p)) {
233
+ // The `..` probe only picks the message; the predicate decides.
234
+ argErrors.push(
235
+ p.split(/[\\/]/).includes('..')
236
+ ? `--file-paths must not contain ".." path segments: "${p}"`
237
+ : `--file-paths must be repo-relative — absolute path rejected (leading /, \\ or ~, Windows drive, UNC or URL): "${p}"`,
238
+ );
226
239
  } else if (/[\r\n]/.test(p)) {
227
240
  argErrors.push(`--file-paths entries must not contain newline characters: "${p}"`);
228
241
  } else if (/[*?[\]{}]/.test(p)) {
@@ -0,0 +1,306 @@
1
+ /**
2
+ * release-session-lock.mjs — CLI: release THIS session's `session.lock`, emit the
3
+ * terminal `orchestrator.session.lock.released` breadcrumb, and PROVE the lock is
4
+ * gone before exiting 0.
5
+ *
6
+ * ## Why a CLI and not one more `release()` call site (#1395)
7
+ *
8
+ * Measured 2026-09-19: three of the four `release()` call sites already emit
9
+ * `orchestrator.session.lock.released` — `hooks/on-session-end.mjs`,
10
+ * `scripts/lib/autopilot/worktree-pipeline.mjs` and
11
+ * `scripts/lib/session-transition.mjs`. The silent one was never code at all: it
12
+ * is the PROSE step `skills/session-end/SKILL.md` § Phase 3.8, executed by hand
13
+ * by the coordinator LLM. It deletes the lock, so the SessionEnd hook later reads
14
+ * `status: 'absent'` and emits nothing — this repo recorded 5 `lock.acquired`
15
+ * against 0 `lock.released`. A prose "call X, then check Y" step is the shape
16
+ * that gets skipped (`.claude/rules/process-contracts.md`: 339 of 351 issues
17
+ * closed without the prose-mandated pre-step); the repair is ONE command that
18
+ * verifies its own effect and exits non-zero when it cannot.
19
+ *
20
+ * ## Why the emit is not inside `release()`
21
+ *
22
+ * Three structural reasons, none of them fixable here:
23
+ * 1. Import cycle — `scripts/lib/events.mjs` imports `session-lock.mjs`
24
+ * (`sessionAttribution()` reads the lock), so `session-lock.mjs` cannot
25
+ * import `events.mjs`.
26
+ * 2. `release()` is synchronous; `emitEvent()` is async.
27
+ * 3. After the unlink the event envelope can no longer read the lock for
28
+ * correlation — which is exactly why this CLI captures `session_id` and
29
+ * `semantic_session_id` from the lock BEFORE releasing and passes both
30
+ * EXPLICITLY in the payload.
31
+ *
32
+ * ## Reuse note (BV-001.2)
33
+ *
34
+ * `leaveSourceRoot()` in `scripts/lib/session-transition.mjs` is the closest
35
+ * existing helper and is deliberately NOT reused: it also deregisters the
36
+ * host-wide registry entry, which a session closing in place must NOT do at
37
+ * Phase 3.8 (the SessionEnd hook owns deregistration, at the real end). Its
38
+ * `LOCK_RELEASED_EVENT` constant IS reused, so the event name has one writer.
39
+ *
40
+ * Exit codes: 0 verified · 1 user/input or ownership error · 2 system error.
41
+ *
42
+ * Exports (for tests): main.
43
+ */
44
+
45
+ import { parseArgs } from 'node:util';
46
+ import path from 'node:path';
47
+
48
+ import { readLockDetailed, loadOwnerProof, release } from './lib/session-lock.mjs';
49
+ import { LOCK_RELEASED_EVENT } from './lib/session-transition.mjs';
50
+ import { emitEvent } from './lib/events.mjs';
51
+ import { isMainModule } from './lib/is-main-module.mjs';
52
+
53
+ /**
54
+ * `caller` value stamped into the breadcrumb, so this fourth release site stays
55
+ * distinguishable from `on-session-end`, `session-transition` and the autopilot
56
+ * worktree pipeline in the single `orchestrator.session.lock.*` stream.
57
+ */
58
+ export const PHASE_CALLER = 'session-end-phase-3-8';
59
+
60
+ const HELP = `release-session-lock.mjs — release this session's lock and prove it is gone
61
+
62
+ USAGE
63
+ node scripts/release-session-lock.mjs --session-id <raw-id> [--repo-root <path>] [--json]
64
+
65
+ WHAT IT DOES
66
+ 1. Reads <repo-root>/.orchestrator/session.lock.
67
+ 2. Releases it IFF its raw session_id equals --session-id (owner-proof gated,
68
+ exactly as hooks/on-session-end.mjs gates it).
69
+ 3. Emits orchestrator.session.lock.released with the ids read BEFORE the
70
+ delete, plus caller="${PHASE_CALLER}".
71
+ 4. RE-READS the lock path and exits 0 only when our lock is provably gone.
72
+
73
+ FLAGS
74
+ --session-id <id> REQUIRED. The RAW (physical) session id that owns the lock
75
+ — .orchestrator/session.lock "session_id", never STATE.md
76
+ "session" and never the semantic id. A lock owned by a
77
+ different id is NEVER released.
78
+ --repo-root <path> Repo whose lock is released (default: cwd).
79
+ --json Machine-readable result on stdout.
80
+ -h, --help Show this help and exit 0.
81
+
82
+ OUTCOMES
83
+ absent no lock file — nothing released, NO event, exit 0.
84
+ deleted our lock was removed and re-read as gone.
85
+ already-gone ownership matched but the lock had already vanished; the event
86
+ is still written, because a lock removed by a third party while
87
+ we held it is the forensically interesting case.
88
+
89
+ EXIT CODES
90
+ 0 — the lock is provably absent (released now, or nothing to release)
91
+ 1 — user/input or ownership error: missing/empty --session-id, unknown flag,
92
+ or a lock owned by a different session (nothing is touched)
93
+ 2 — system error: unreadable/corrupt lock, filesystem error, owner-proof
94
+ mismatch, the lock still present after release, or the breadcrumb could
95
+ not be written
96
+ `;
97
+
98
+ /**
99
+ * Print the one-line human summary (stdout = data, stderr = diagnostics).
100
+ *
101
+ * @param {{outcome: string, verified: boolean, session_id: string, event_emitted: boolean}} result
102
+ */
103
+ function printHuman(result) {
104
+ if (result.outcome === 'absent') {
105
+ process.stdout.write('session-lock: nothing to release — no lock file (no event written).\n');
106
+ return;
107
+ }
108
+ const verified = result.verified ? 'verified gone' : 'NOT verified — lock still present';
109
+ const event = result.event_emitted ? 'breadcrumb written' : 'breadcrumb FAILED';
110
+ process.stdout.write(
111
+ `session-lock ${result.outcome} for session ${result.session_id} — ${verified}, ${event}.\n`,
112
+ );
113
+ }
114
+
115
+ /**
116
+ * Did OUR lock really vanish?
117
+ *
118
+ * Computed from a FRESH read after `release()` returned, deliberately not from
119
+ * `release()`'s own `verified` field: that field is absent on the benign
120
+ * `no-lock` branch (so a caller copying it reports `verified: false` for a lock
121
+ * that IS gone), and it was computed before this process did anything else. The
122
+ * CLI's whole contract is "exit 0 proves absence", so it re-reads.
123
+ *
124
+ * A FOREIGN lock present at the re-read counts as verified — ours is gone and a
125
+ * sibling legitimately acquired the path in the race window (same rule as
126
+ * `release()`'s post-delete verify; PSA-005: never touch a lock we do not own).
127
+ *
128
+ * @param {string} repoRoot
129
+ * @param {string} sessionId
130
+ * @returns {boolean}
131
+ */
132
+ function ourLockIsGone(repoRoot, sessionId) {
133
+ const after = readLockDetailed({ repoRoot });
134
+ if (after.status === 'absent') return true;
135
+ if (after.status === 'ok') return after.lock.session_id !== sessionId;
136
+ // unreadable / corrupt — absence cannot be confirmed, so it is not claimed.
137
+ return false;
138
+ }
139
+
140
+ /**
141
+ * CLI entry point.
142
+ *
143
+ * @param {{argv?: string[]}} [opts]
144
+ * @returns {Promise<number>} process exit code
145
+ */
146
+ export async function main({ argv = process.argv.slice(2) } = {}) {
147
+ let values;
148
+ try {
149
+ ({ values } = parseArgs({
150
+ args: argv,
151
+ options: {
152
+ 'repo-root': { type: 'string' },
153
+ 'session-id': { type: 'string' },
154
+ json: { type: 'boolean', default: false },
155
+ help: { type: 'boolean', short: 'h', default: false },
156
+ },
157
+ strict: true,
158
+ allowPositionals: false,
159
+ }));
160
+ } catch (err) {
161
+ process.stderr.write(`release-session-lock: ${err?.message ?? 'bad arguments'}\n`);
162
+ process.stderr.write('Run with --help for usage.\n');
163
+ return 1;
164
+ }
165
+
166
+ if (values.help) {
167
+ process.stdout.write(HELP);
168
+ return 0;
169
+ }
170
+
171
+ const sessionId = values['session-id'];
172
+ if (typeof sessionId !== 'string' || sessionId.length === 0) {
173
+ process.stderr.write('release-session-lock: --session-id <raw-id> is required.\n');
174
+ process.stderr.write('Run with --help for usage.\n');
175
+ return 1;
176
+ }
177
+
178
+ const repoRoot = path.resolve(values['repo-root'] ?? process.cwd());
179
+
180
+ const emitResult = async (result) => {
181
+ if (values.json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
182
+ else printHuman(result);
183
+ };
184
+
185
+ // -- Step 1: read the lock, and capture the ids BEFORE anything is deleted --
186
+ const lockDetail = readLockDetailed({ repoRoot });
187
+
188
+ if (lockDetail.status === 'absent') {
189
+ // Nothing was released, so nothing terminal happened: NO event. Writing one
190
+ // here would put a fictional lifecycle end into the stream — the idempotent
191
+ // re-run case is not a release.
192
+ const result = { ok: true, outcome: 'absent', verified: true, session_id: sessionId, event_emitted: false };
193
+ await emitResult(result);
194
+ return 0;
195
+ }
196
+
197
+ if (lockDetail.status !== 'ok') {
198
+ process.stderr.write(`release-session-lock: lock is ${lockDetail.status} — ownership cannot be established; nothing touched.\n`);
199
+ await emitResult({
200
+ ok: false, outcome: `lock-${lockDetail.status}`, verified: false, session_id: sessionId, event_emitted: false,
201
+ });
202
+ return 2;
203
+ }
204
+
205
+ const lock = lockDetail.lock;
206
+ if (lock.session_id !== sessionId) {
207
+ // A raw-id mismatch never grants release ownership (same gate as
208
+ // hooks/on-session-end.mjs). Reconciliation of a foreign lease is the
209
+ // reaper's job, not this command's.
210
+ process.stderr.write(`release-session-lock: lock is owned by a different session — not released.\n`);
211
+ await emitResult({
212
+ ok: false, outcome: 'session-mismatch', verified: false, session_id: sessionId, event_emitted: false,
213
+ });
214
+ return 1;
215
+ }
216
+
217
+ const semanticSessionId = typeof lock.semantic_session_id === 'string' && lock.semantic_session_id.length > 0
218
+ ? lock.semantic_session_id
219
+ : null;
220
+
221
+ // -- Step 2: release, owner-proof gated (#987/#989) ------------------------
222
+ // `proof` is null whenever ownership cannot be proven; release() treats null
223
+ // as absent and degrades to the session_id-only path, so it is passed
224
+ // through unguarded.
225
+ const proof = loadOwnerProof({ repoRoot });
226
+ const releaseResult = release({ sessionId, repoRoot, proof });
227
+
228
+ if (releaseResult.ok !== true) {
229
+ process.stderr.write(`release-session-lock: release failed — ${releaseResult.reason ?? 'fs-error'}\n`);
230
+ await emitResult({
231
+ ok: false, outcome: `release-${releaseResult.reason ?? 'fs-error'}`, verified: false, session_id: sessionId, event_emitted: false,
232
+ });
233
+ return 2;
234
+ }
235
+
236
+ const alreadyGone = releaseResult.deleted !== true && releaseResult.reason === 'no-lock';
237
+ if (releaseResult.deleted !== true && !alreadyGone) {
238
+ // 'proof-mismatch' (and any future non-delete reason): ownership matched on
239
+ // the raw id but the second factor contradicted it. Contradictory on-disk
240
+ // state, not a bad argument — exit 2.
241
+ process.stderr.write(`release-session-lock: lock not released — ${releaseResult.reason ?? 'not-deleted'}\n`);
242
+ await emitResult({
243
+ ok: false, outcome: `release-${releaseResult.reason ?? 'not-deleted'}`, verified: false, session_id: sessionId, event_emitted: false,
244
+ });
245
+ return 2;
246
+ }
247
+
248
+ // -- Step 3: prove it ------------------------------------------------------
249
+ const verified = ourLockIsGone(repoRoot, sessionId);
250
+ const outcome = alreadyGone ? 'already-gone' : 'deleted';
251
+
252
+ // -- Step 4: the terminal breadcrumb --------------------------------------
253
+ // The ids are passed EXPLICITLY: the lock is gone by now, so emitEvent's
254
+ // lock-derived correlation would find nothing to attribute this record to.
255
+ let eventEmitted = true;
256
+ let emitError = null;
257
+ try {
258
+ await emitEvent(LOCK_RELEASED_EVENT, {
259
+ session_id: sessionId,
260
+ ...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
261
+ caller: PHASE_CALLER,
262
+ outcome,
263
+ verified,
264
+ }, { repoRoot });
265
+ } catch (err) {
266
+ eventEmitted = false;
267
+ emitError = err?.message ?? String(err);
268
+ }
269
+
270
+ const result = {
271
+ ok: verified && eventEmitted,
272
+ outcome,
273
+ verified,
274
+ session_id: sessionId,
275
+ ...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
276
+ event_emitted: eventEmitted,
277
+ ...(emitError !== null ? { event_error: emitError } : {}),
278
+ };
279
+ await emitResult(result);
280
+
281
+ if (!verified) {
282
+ process.stderr.write('release-session-lock: the lock is STILL present after release — not verified.\n');
283
+ return 2;
284
+ }
285
+ if (!eventEmitted) {
286
+ // The release itself succeeded; what failed is the one thing this command
287
+ // exists to guarantee, so it is reported as a system error rather than
288
+ // swallowed. Phase 3.8 treats a non-zero exit as a WARN and continues.
289
+ process.stderr.write(`release-session-lock: breadcrumb could not be written — ${emitError}\n`);
290
+ return 2;
291
+ }
292
+ return 0;
293
+ }
294
+
295
+ // ---------------------------------------------------------------------------
296
+ // CLI guard — prevents execution during test-time imports.
297
+ // ---------------------------------------------------------------------------
298
+
299
+ if (isMainModule(import.meta.url)) {
300
+ main()
301
+ .then((code) => process.exit(code))
302
+ .catch((err) => {
303
+ process.stderr.write(`release-session-lock: unexpected error: ${err?.stack ?? err}\n`);
304
+ process.exit(2);
305
+ });
306
+ }
@@ -384,7 +384,10 @@ const INTENTIONAL_TEST_ASSET_PATHS = new Set([
384
384
  // September 10 campaign snapshot: inputs and receipt reproduce the dated 4.3-planned
385
385
  // render made while 4.2.0 was current. Exact files only; neighboring marketing
386
386
  // remains checked. Do not bump receipt props without a newly reviewed render.
387
- export const HISTORY_ALLOWLIST = /^(CHANGELOG\.md|README\.md|CLAUDE\.md|AGENTS\.md|CONTRIBUTING\.md|NOTICE|\.husky\/pre-push|docs\/|tests\/|skills\/npm-publish\/|skills\/architecture\/references\/domain-model\.md|skills\/autopilot\/SKILL\.md|scripts\/release\.mjs|\.orchestrator\/|site\/leaderboard\.json|site\/guide\/index\.html|site\/llms-full\.txt|skills\/release\/SKILL\.md|marketing\/remotion\/(?:README\.md|campaign\.json|render-receipt\.json|src\/ReleaseFilm\.tsx)$)/;
387
+ // `scripts/lib/locks/index.mjs` is the one-cycle deprecation shim (5.2.0 CHANGELOG, Changed): its
388
+ // runtime warning names the version it was deprecated IN, so it legitimately carries the previous
389
+ // release literal until its 6.0.0 removal — version history by construction, not drift.
390
+ export const HISTORY_ALLOWLIST = /^(CHANGELOG\.md|README\.md|scripts\/lib\/locks\/index\.mjs|CLAUDE\.md|AGENTS\.md|CONTRIBUTING\.md|NOTICE|\.husky\/pre-push|docs\/|tests\/|skills\/npm-publish\/|skills\/architecture\/references\/domain-model\.md|skills\/autopilot\/SKILL\.md|scripts\/release\.mjs|\.orchestrator\/|site\/leaderboard\.json|site\/guide\/index\.html|site\/llms-full\.txt|skills\/release\/SKILL\.md|marketing\/remotion\/(?:README\.md|campaign\.json|render-receipt\.json|src\/ReleaseFilm\.tsx)$)/;
388
391
 
389
392
  /** Pure check over packed-entry lines. Returns violations: {name, line}[]. */
390
393
  export function checkLeakage(lines) {
@@ -881,14 +884,36 @@ export function evaluateNpmAuth(whoami) {
881
884
  * never found out". Only an actual `status: 'green'` reading passes; a release
882
885
  * must never proceed on an unknown CI state.
883
886
  *
884
- * @param {null | {status?: string, failingJobName?: string, degraded?: string}} ci
887
+ * The row also carries the CAUSE the probe already knows, because the three
888
+ * ways this row goes red are indistinguishable without it (#1384 P5): a short
889
+ * `sha`, a commit the mirror has never seen, and a HEAD with no pipeline all
890
+ * printed `CI status unknown (query-failed)` / `status: unknown`. The probe's
891
+ * `detail` (degraded branch, `degradedResult`) and `details.reason` /
892
+ * `failingJobName` (read branch, `sanitizeApiText`) are clamped and
893
+ * control-byte-escaped AT THEIR GENERATION in `ci-status-banner.mjs` —
894
+ * appended here, never re-formatted. That escaping is load-bearing, not
895
+ * hygiene: the read branch interpolates a RAW API `status` value into
896
+ * `reason`, and until #1384 f-4 a status carrying `\r` + an ANSI sequence
897
+ * could repaint THIS row green while its verdict stayed `ok:false`. Do not
898
+ * append a further probe field here without checking it goes through one of
899
+ * those two. With neither present the text is byte-identical to
900
+ * before, so a plain green or a named failing job reads exactly as it did.
901
+ *
902
+ * @param {null | {status?: string, failingJobName?: string, degraded?: string,
903
+ * detail?: string, details?: {reason?: string}}} ci
885
904
  * @returns {{ok: boolean, detail: string}}
886
905
  */
887
906
  export function evaluateCiRow(ci) {
888
907
  if (ci === null || ci === undefined) return { ok: false, detail: 'CI status unavailable' };
889
- if (ci.degraded) return { ok: false, detail: `CI status unknown (${ci.degraded})` };
890
- const job = ci.failingJobName ? ` (${ci.failingJobName})` : '';
891
- return { ok: ci.status === 'green', detail: `status: ${ci.status}${job}` };
908
+ if (ci.degraded) {
909
+ const why = ci.detail ? `: ${ci.detail}` : '';
910
+ return { ok: false, detail: `CI status unknown (${ci.degraded}${why})` };
911
+ }
912
+ // A failing job NAMES the failure, so it wins over the generic reason;
913
+ // `reason` is what fills the gap when there is no job to name (`unknown`).
914
+ const reason = ci.failingJobName || ci.details?.reason || '';
915
+ const why = reason ? ` (${reason})` : '';
916
+ return { ok: ci.status === 'green', detail: `status: ${ci.status}${why}` };
892
917
  }
893
918
 
894
919
  /**
@@ -1278,14 +1303,21 @@ export function evaluatePublishReceipt(result, target) {
1278
1303
  * failure remains visible, but none invalidates the already irreversible npm
1279
1304
  * publish; callers must reconcile rather than retry `--publish`.
1280
1305
  *
1306
+ * `npm view <pkg> version` resolves the `latest` dist-tag (and `npm view` fetches
1307
+ * with `preferOnline`, so no local cache masks the answer): the poll returns
1308
+ * `verified` on the first attempt whose `dist-tags.latest` equals `target`.
1309
+ * Default budget: 12 attempts, 15 s apart = 165 s between the first and last
1310
+ * poll. npm's registry needs ~2 min to serve a fresh `latest` (#1440 C2); the
1311
+ * previous 5 x 3 s = 12 s budget reported `timeout` on every real publish.
1312
+ *
1281
1313
  * @param {string} repoRoot
1282
1314
  * @param {string} target
1283
1315
  * @param {{attempts?: number, delaySeconds?: number, runImpl?: Function, waitImpl?: Function}} [deps]
1284
1316
  * @returns {{ok: boolean, kind: 'verified'|'timeout'|'query-failed'|'wait-failed', attempts: number, detail: string}}
1285
1317
  */
1286
1318
  export function waitForRegistryPropagation(repoRoot, target, deps = {}) {
1287
- const attempts = deps.attempts ?? 5;
1288
- const delaySeconds = deps.delaySeconds ?? 3;
1319
+ const attempts = deps.attempts ?? 12;
1320
+ const delaySeconds = deps.delaySeconds ?? 15;
1289
1321
  const runImpl = deps.runImpl ?? run;
1290
1322
  const waitImpl = deps.waitImpl ?? (() => runImpl('sleep', [String(delaySeconds)], { cwd: repoRoot }));
1291
1323
 
@@ -1869,7 +1901,7 @@ async function main() {
1869
1901
  console.log(`Rewrote ${changed.length} surface file(s) to ${target}:`);
1870
1902
  for (const f of changed) console.log(` ${f}`);
1871
1903
  console.log(' package-lock.json (via npm install --package-lock-only)');
1872
- console.log(' site/index.html cells + site/_census.json re-stamped (scripts/site-numbers.mjs --write) — commit BOTH');
1904
+ console.log(' site/ page cells + site/_census.json + site/sitemap.xml lastmod re-stamped (scripts/site-numbers.mjs --write) — commit ALL of them');
1873
1905
  console.log('\nEditorial TODOs (enforced by --check):');
1874
1906
  console.log(` 1. CHANGELOG.md — write the "## [${target}] - YYYY-MM-DD" entry, fold [Unreleased].`);
1875
1907
  console.log(' 2. README.md — rewrite the "Recent highlights" section content.');