session-orchestrator 3.22.0 → 3.24.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 (316) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/commands/autopilot-multi.md +14 -0
  5. package/.cursor/commands/autopilot.md +14 -0
  6. package/.cursor/commands/bootstrap.md +14 -0
  7. package/.cursor/commands/brainstorm.md +14 -0
  8. package/.cursor/commands/close.md +13 -0
  9. package/.cursor/commands/contract-version-bump.md +14 -0
  10. package/.cursor/commands/debug.md +14 -0
  11. package/.cursor/commands/discovery.md +14 -0
  12. package/.cursor/commands/dispatcher.md +14 -0
  13. package/.cursor/commands/eli5.md +14 -0
  14. package/.cursor/commands/eval.md +14 -0
  15. package/.cursor/commands/evolve.md +14 -0
  16. package/.cursor/commands/go.md +14 -0
  17. package/.cursor/commands/grill.md +14 -0
  18. package/.cursor/commands/harness-audit.md +13 -0
  19. package/.cursor/commands/journey-audit.md +14 -0
  20. package/.cursor/commands/memory-cleanup.md +14 -0
  21. package/.cursor/commands/persona-panel.md +14 -0
  22. package/.cursor/commands/plan.md +14 -0
  23. package/.cursor/commands/portfolio.md +14 -0
  24. package/.cursor/commands/reconcile.md +14 -0
  25. package/.cursor/commands/release.md +14 -0
  26. package/.cursor/commands/repo-audit.md +13 -0
  27. package/.cursor/commands/session.md +14 -0
  28. package/.cursor/commands/spinout.md +14 -0
  29. package/.cursor/commands/sunset-review.md +14 -0
  30. package/.cursor/commands/templates-ack.md +14 -0
  31. package/.cursor/commands/test.md +14 -0
  32. package/.cursor/hooks.json +60 -0
  33. package/.cursor/rules/000-session-orchestrator.mdc +8 -0
  34. package/.cursor/rules/010-session-workflow.mdc +9 -1
  35. package/.cursor/rules/020-quality-gates.mdc +1 -1
  36. package/.cursor/rules/030-wave-execution.mdc +1 -1
  37. package/.cursor/rules/050-plan.mdc +2 -2
  38. package/.cursor/rules/070-gitlab-ops.mdc +73 -57
  39. package/.cursor/rules/080-ecosystem-health.mdc +7 -7
  40. package/.cursor/skills/architecture/SKILL.md +13 -0
  41. package/.cursor/skills/autopilot/SKILL.md +12 -0
  42. package/.cursor/skills/bootstrap/SKILL.md +12 -0
  43. package/.cursor/skills/brainstorm/SKILL.md +13 -0
  44. package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
  45. package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
  46. package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
  47. package/.cursor/skills/daily/SKILL.md +12 -0
  48. package/.cursor/skills/debug/SKILL.md +13 -0
  49. package/.cursor/skills/discovery/SKILL.md +13 -0
  50. package/.cursor/skills/dispatcher/SKILL.md +13 -0
  51. package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
  52. package/.cursor/skills/domain-model/SKILL.md +13 -0
  53. package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
  54. package/.cursor/skills/eli5/SKILL.md +13 -0
  55. package/.cursor/skills/eval/SKILL.md +12 -0
  56. package/.cursor/skills/evolve/SKILL.md +13 -0
  57. package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
  58. package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
  59. package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
  60. package/.cursor/skills/grill/SKILL.md +13 -0
  61. package/.cursor/skills/hook-development/SKILL.md +13 -0
  62. package/.cursor/skills/journey-audit/SKILL.md +13 -0
  63. package/.cursor/skills/mcp-builder/SKILL.md +13 -0
  64. package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
  65. package/.cursor/skills/mode-selector/SKILL.md +13 -0
  66. package/.cursor/skills/npm-publish/SKILL.md +12 -0
  67. package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
  68. package/.cursor/skills/persona-panel/SKILL.md +12 -0
  69. package/.cursor/skills/plan/SKILL.md +13 -0
  70. package/.cursor/skills/playwright-driver/SKILL.md +13 -0
  71. package/.cursor/skills/quality-gates/SKILL.md +13 -0
  72. package/.cursor/skills/reconcile/SKILL.md +12 -0
  73. package/.cursor/skills/remote-offload/SKILL.md +13 -0
  74. package/.cursor/skills/repo-audit/SKILL.md +13 -0
  75. package/.cursor/skills/session-end/SKILL.md +13 -0
  76. package/.cursor/skills/session-plan/SKILL.md +13 -0
  77. package/.cursor/skills/session-start/SKILL.md +13 -0
  78. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  79. package/.cursor/skills/spinout/SKILL.md +12 -0
  80. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  81. package/.cursor/skills/test-runner/SKILL.md +13 -0
  82. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  83. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  84. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  85. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  86. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  87. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  88. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  89. package/.mcp.json +4 -1
  90. package/CHANGELOG.md +446 -0
  91. package/README.md +22 -17
  92. package/agents/AGENTS.md +23 -4
  93. package/agents/code-implementer.md +2 -1
  94. package/agents/db-specialist.md +2 -2
  95. package/agents/docs-writer.md +3 -1
  96. package/agents/eval-judge.md +1 -1
  97. package/agents/session-reviewer.md +7 -1
  98. package/agents/test-writer.md +2 -1
  99. package/agents/ui-developer.md +2 -1
  100. package/commands/bootstrap.md +2 -2
  101. package/commands/close.md +3 -1
  102. package/commands/go.md +1 -1
  103. package/commands/journey-audit.md +43 -0
  104. package/docs/USER-GUIDE.md +2 -2
  105. package/docs/ci-setup.md +194 -25
  106. package/docs/codex-setup.md +64 -0
  107. package/docs/components.md +7 -7
  108. package/docs/cursor-setup.md +26 -47
  109. package/docs/events-schema.md +120 -10
  110. package/docs/github-mirror-protection.md +197 -0
  111. package/docs/pi-setup.md +2 -0
  112. package/docs/rule-authoring.md +3 -1
  113. package/docs/scope-collision-guard.md +49 -2
  114. package/docs/session-config-reference.md +89 -9
  115. package/docs/session-config-template.md +38 -7
  116. package/docs/telemetry/telemetry-claims.md +11 -10
  117. package/docs/telemetry.md +52 -1
  118. package/hooks/_lib/atomic-json.mjs +111 -0
  119. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  120. package/hooks/_lib/subagent-paths.mjs +143 -0
  121. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  122. package/hooks/cwd-change-restore.mjs +9 -29
  123. package/hooks/enforce-scope.mjs +93 -0
  124. package/hooks/hooks-codex.json +1 -1
  125. package/hooks/hooks-cursor.json +201 -20
  126. package/hooks/hooks-pi.json +1 -1
  127. package/hooks/hooks.json +2 -2
  128. package/hooks/on-session-end.mjs +486 -19
  129. package/hooks/on-session-start.mjs +263 -12
  130. package/hooks/on-stop.mjs +392 -24
  131. package/hooks/post-bash-write-verify.mjs +104 -4
  132. package/hooks/post-subagent-discovery-validator.mjs +182 -21
  133. package/hooks/post-tool-batch-wave-signal.mjs +165 -42
  134. package/hooks/post-tool-failure-corrective-context.mjs +9 -32
  135. package/hooks/pre-bash-issue-budget.mjs +117 -4
  136. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  138. package/hooks/pre-bash-staging-fence.mjs +4 -0
  139. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  140. package/hooks/skill-invocation-telemetry.mjs +21 -10
  141. package/hooks/subagent-telemetry.mjs +11 -26
  142. package/monitors/monitors.json +6 -0
  143. package/package.json +1 -1
  144. package/pi/prompts/journey-audit.md +12 -0
  145. package/rules/_index.md +9 -1
  146. package/rules/always-on/ask-via-tool.md +62 -0
  147. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  148. package/rules/always-on/build-value.md +47 -0
  149. package/rules/always-on/cross-session-messaging.md +59 -0
  150. package/rules/always-on/loop-and-monitor.md +221 -0
  151. package/rules/always-on/parallel-sessions.md +142 -12
  152. package/rules/always-on/receiving-review.md +108 -0
  153. package/rules/always-on/test-value.md +40 -0
  154. package/rules/always-on/verification-before-completion.md +77 -0
  155. package/scripts/archive-closed-prds.mjs +258 -18
  156. package/scripts/autopilot.mjs +31 -12
  157. package/scripts/backfill-abandoned-sessions.mjs +80 -11
  158. package/scripts/backfill-evidence-digest.mjs +376 -0
  159. package/scripts/cursor-install.mjs +89 -48
  160. package/scripts/emit-event.mjs +10 -2
  161. package/scripts/export-hw-learnings.mjs +143 -2
  162. package/scripts/express-path.mjs +299 -0
  163. package/scripts/generate-cursor-adapter.mjs +253 -0
  164. package/scripts/github-protection-audit.mjs +358 -0
  165. package/scripts/lib/auq/parse.mjs +5 -29
  166. package/scripts/lib/auto-dialectic.mjs +68 -0
  167. package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
  168. package/scripts/lib/build-live-signals.mjs +49 -27
  169. package/scripts/lib/ci-status-banner.mjs +158 -11
  170. package/scripts/lib/cold-start-detector.mjs +23 -14
  171. package/scripts/lib/command-blocker.mjs +70 -0
  172. package/scripts/lib/config/block-header.mjs +55 -0
  173. package/scripts/lib/config/discovery-validator.mjs +7 -2
  174. package/scripts/lib/config/health-endpoints.mjs +383 -0
  175. package/scripts/lib/config/reconcile.mjs +79 -4
  176. package/scripts/lib/config/remote-hosts.mjs +233 -0
  177. package/scripts/lib/config/section-extractor.mjs +235 -36
  178. package/scripts/lib/config-schema.mjs +9 -1
  179. package/scripts/lib/config.mjs +87 -8
  180. package/scripts/lib/convergence-monitor.mjs +13 -2
  181. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  182. package/scripts/lib/dispatcher/cli.mjs +2 -2
  183. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  184. package/scripts/lib/events-schema.mjs +48 -0
  185. package/scripts/lib/events.mjs +238 -5
  186. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  187. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  188. package/scripts/lib/express-path.mjs +327 -0
  189. package/scripts/lib/file-lock.mjs +22 -4
  190. package/scripts/lib/gates/gate-full.mjs +81 -8
  191. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  192. package/scripts/lib/git-config-drift.mjs +134 -5
  193. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  194. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  195. package/scripts/lib/host-identity.mjs +247 -2
  196. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  197. package/scripts/lib/issue-budget.mjs +229 -30
  198. package/scripts/lib/learnings/io.mjs +55 -10
  199. package/scripts/lib/learnings/schema.mjs +95 -28
  200. package/scripts/lib/lock-reaper.mjs +7 -1
  201. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  202. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  203. package/scripts/lib/memory-banner.mjs +25 -10
  204. package/scripts/lib/memory-paths.mjs +15 -6
  205. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  206. package/scripts/lib/peer-discovery.mjs +20 -2
  207. package/scripts/lib/platform.mjs +72 -9
  208. package/scripts/lib/plugin-root.mjs +143 -19
  209. package/scripts/lib/project-hygiene.mjs +43 -3
  210. package/scripts/lib/quality-gate.mjs +271 -13
  211. package/scripts/lib/reconcile/emitter.mjs +87 -19
  212. package/scripts/lib/reconcile/engine.mjs +517 -18
  213. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  214. package/scripts/lib/reconcile/renderer.mjs +148 -3
  215. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  216. package/scripts/lib/reconcile/writer.mjs +415 -84
  217. package/scripts/lib/rule-loader.mjs +37 -2
  218. package/scripts/lib/rules-sync.mjs +51 -8
  219. package/scripts/lib/scope-gate.mjs +126 -0
  220. package/scripts/lib/session-close-backfill.mjs +427 -37
  221. package/scripts/lib/session-discovery.mjs +69 -5
  222. package/scripts/lib/session-end/phase-skip.mjs +38 -5
  223. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  224. package/scripts/lib/session-id.mjs +30 -14
  225. package/scripts/lib/session-identity/own-session.mjs +220 -0
  226. package/scripts/lib/session-lock.mjs +85 -30
  227. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  228. package/scripts/lib/session-schema/validator.mjs +40 -0
  229. package/scripts/lib/session-start-probes.mjs +608 -0
  230. package/scripts/lib/session-transition.mjs +277 -0
  231. package/scripts/lib/sessions-canonical.mjs +446 -0
  232. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  233. package/scripts/lib/spiral-carryover.mjs +90 -9
  234. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  235. package/scripts/lib/state-md/mission-status.mjs +350 -52
  236. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  237. package/scripts/lib/state-md.mjs +12 -2
  238. package/scripts/lib/telemetry/schema.mjs +74 -8
  239. package/scripts/lib/telemetry/sync.mjs +91 -16
  240. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  241. package/scripts/lib/validate/check-agents.mjs +66 -0
  242. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  243. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  244. package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
  245. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  246. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  247. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  248. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  249. package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
  250. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  251. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  252. package/scripts/lib/validate/check-unwired-features.mjs +333 -32
  253. package/scripts/lib/validate/check-validator-registration.mjs +248 -0
  254. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  255. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  256. package/scripts/lib/validate/repo-files.mjs +275 -0
  257. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  258. package/scripts/lib/vault-mirror/process.mjs +99 -43
  259. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  260. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  261. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  262. package/scripts/lib/vault-status/board-writer.mjs +381 -141
  263. package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
  264. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  265. package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
  266. package/scripts/lib/wave-resource-gate.mjs +127 -7
  267. package/scripts/lib/wave-transcript-tail.mjs +889 -0
  268. package/scripts/materialize-wave-scope.mjs +228 -15
  269. package/scripts/mcp-server.sh +11 -2
  270. package/scripts/memory-propose.mjs +132 -8
  271. package/scripts/parse-config.mjs +65 -0
  272. package/scripts/promote-vault-strict.mjs +4 -15
  273. package/scripts/site-numbers.mjs +36 -4
  274. package/scripts/token-audit.sh +9 -2
  275. package/scripts/validate-plugin.mjs +29 -0
  276. package/scripts/validate-wave-scope.mjs +67 -0
  277. package/scripts/vault-consolidate.mjs +3 -11
  278. package/scripts/vault-integration-watcher.mjs +2 -4
  279. package/scripts/vault-mirror.mjs +305 -51
  280. package/skills/_shared/monitor-patterns.md +31 -5
  281. package/skills/_shared/parallel-aware-auq.md +31 -2
  282. package/skills/_shared/parallel-aware-preamble.md +19 -4
  283. package/skills/_shared/platform-tools.md +11 -5
  284. package/skills/_shared/state-ownership.md +29 -2
  285. package/skills/autopilot/SKILL.md +5 -1
  286. package/skills/bootstrap/SKILL.md +3 -3
  287. package/skills/bootstrap/_shared-template.md +18 -10
  288. package/skills/bootstrap/deep-template.md +10 -6
  289. package/skills/bootstrap/fast-template.md +15 -8
  290. package/skills/bootstrap/standard-template.md +10 -6
  291. package/skills/claude-md-drift-check/checker.mjs +39 -11
  292. package/skills/contract-version-bump/SKILL.md +1 -1
  293. package/skills/dispatcher/SKILL.md +1 -1
  294. package/skills/ecosystem-health/SKILL.md +4 -1
  295. package/skills/ecosystem-health/wizard.md +5 -0
  296. package/skills/evolve/SKILL.md +38 -1
  297. package/skills/journey-audit/SKILL.md +270 -0
  298. package/skills/peekaboo-driver/SKILL.md +15 -3
  299. package/skills/persona-panel/SKILL.md +1 -1
  300. package/skills/reconcile/SKILL.md +46 -3
  301. package/skills/remote-offload/SKILL.md +89 -0
  302. package/skills/session-end/SKILL.md +17 -4
  303. package/skills/session-end/metrics-collection.md +7 -4
  304. package/skills/session-end/phase-3-6-tail.md +20 -9
  305. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  306. package/skills/session-plan/SKILL.md +6 -1
  307. package/skills/session-plan/wave-template.md +1 -0
  308. package/skills/session-start/SKILL.md +54 -17
  309. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  310. package/skills/session-start/phase-8-5-express-path.md +77 -12
  311. package/skills/vault-sync/validator.mjs +31 -0
  312. package/skills/wave-executor/SKILL.md +5 -3
  313. package/skills/wave-executor/circuit-breaker.md +34 -9
  314. package/skills/wave-executor/wave-loop.md +143 -22
  315. package/templates/_shared/journey-manifest.md +110 -0
  316. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -0,0 +1,111 @@
1
+ /**
2
+ * atomic-json.mjs — shared atomic read-modify-write helper for JSON hook state.
3
+ *
4
+ * Extracted (issue #1197) from four byte-identical private copies of
5
+ * `atomicMutateJson` that had accumulated independently in
6
+ * `hooks/cwd-change-restore.mjs`, `hooks/on-session-end.mjs`,
7
+ * `hooks/post-tool-batch-wave-signal.mjs`, and
8
+ * `hooks/post-tool-failure-corrective-context.mjs` (measured 2026-09-02 @
9
+ * a019d5a4 via `rg -n "function atomicMutateJson" hooks/` — 4 hits, one per
10
+ * file, logic byte-identical modulo the per-file tmp-suffix and the
11
+ * `fs.`-namespace-vs-named-import style `on-session-end.mjs` uses).
12
+ *
13
+ * F-H fix (W3-reviewer finding, #1197): the pre-extraction copies treated
14
+ * EVERY read/parse failure as "file absent" and silently fell back to
15
+ * `defaultValue` — so an unparsable file, `EISDIR`, `EACCES`, or a file
16
+ * truncated mid-write by a concurrent writer was overwritten with
17
+ * `defaultValue`-derived content instead of being left alone. Only `ENOENT`
18
+ * (file genuinely does not exist yet) is a legitimate "start fresh" case;
19
+ * every other failure now aborts BEFORE the tmp-write/rename stage and
20
+ * reports `{ ok: false, reason }` — the original file is never touched.
21
+ *
22
+ * Return-object over throw (deliberate — see #1197 task note): all four
23
+ * callers already run under `main().catch(() => {}).finally(() =>
24
+ * process.exit(0))` — informational, never-deny hooks (verified 2026-09-02:
25
+ * none of the four match a deny/block pattern) — so a thrown error would be
26
+ * swallowed safely too. But in `post-tool-batch-wave-signal.mjs` a throw at
27
+ * the FIRST call site (line ~278, the `last_batch` write) would abort
28
+ * `main()` before it reaches the independent heartbeat-refresh block that
29
+ * follows — a real behavioural loss the thrown-error path would introduce
30
+ * silently. A result object lets every caller decide locally whether an RMW
31
+ * failure should short-circuit the rest of `main()` or just get logged and
32
+ * ignored, so no caller loses unrelated post-call behaviour by construction.
33
+ *
34
+ * NAMED CEILING (BV-004): tmp-file + rename makes each individual write
35
+ * atomic, but not the full read-modify-write — two concurrent callers can
36
+ * still interleave (both read the same `current`, both compute an update
37
+ * from it, the second rename wins and silently drops the first mutation).
38
+ * This module does not defend against that race. Revisit with a per-file
39
+ * lock (e.g. an flock-style sidecar or `session-lock.mjs`'s lease pattern)
40
+ * if `.orchestrator/current-session.json` writes start dropping fields
41
+ * under concurrent-session load — no such loss has been measured yet.
42
+ *
43
+ * @module hooks/_lib/atomic-json
44
+ */
45
+
46
+ import { readFile, writeFile, rename, mkdir, unlink } from 'node:fs/promises';
47
+ import path from 'node:path';
48
+
49
+ /**
50
+ * Atomic read-modify-write of a JSON file via temp-file + rename.
51
+ *
52
+ * Reads the existing file and parses it as JSON. When the file does not
53
+ * exist (`ENOENT`), starts from `defaultValue` — the only case in which
54
+ * "file absent" is legitimate. Any OTHER read or parse failure (directory
55
+ * at `filePath`, permission denied, truncated/corrupt JSON, …) aborts
56
+ * WITHOUT calling `mutate` and WITHOUT writing anything — the original file
57
+ * (if any) is left exactly as it was.
58
+ *
59
+ * On success, applies the synchronous `mutate` transformer, writes the
60
+ * result to a `${filePath}.tmp-<suffix>-<pid>-<ts>` sibling, then renames it
61
+ * over `filePath` (atomic on POSIX same-filesystem rename; best-effort on
62
+ * Windows). If the write/rename stage itself fails, the tmp file is
63
+ * best-effort unlinked so a failure never leaves an orphaned `.tmp-*`
64
+ * artifact behind.
65
+ *
66
+ * @param {string} filePath — absolute path to the JSON file.
67
+ * @param {object} defaultValue — starting value used ONLY when the file does
68
+ * not exist yet (`ENOENT`). Never applied on top of an unreadable-but-
69
+ * present file.
70
+ * @param {function(object): object} mutate — pure synchronous transformer;
71
+ * receives the parsed current value (or `defaultValue`), returns the next
72
+ * value to persist.
73
+ * @param {string} [tmpTag] — short tag folded into the tmp filename so
74
+ * concurrent callers targeting the same `filePath` from different hooks
75
+ * don't collide on the same tmp path (mirrors the per-caller suffixes the
76
+ * four pre-extraction copies used: `-cwd-`, `-ose-`, `-ptb-`, `-ptf-`).
77
+ * @returns {Promise<{ ok: true, value: object } | { ok: false, reason: string }>}
78
+ */
79
+ export async function atomicMutateJson(filePath, defaultValue, mutate, tmpTag = 'ajs') {
80
+ let current = defaultValue;
81
+ try {
82
+ const raw = await readFile(filePath, 'utf8');
83
+ current = JSON.parse(raw);
84
+ } catch (err) {
85
+ if (err && err.code === 'ENOENT') {
86
+ // File genuinely does not exist yet — the only legitimate "fresh
87
+ // file" case. `current` already holds `defaultValue`.
88
+ } else {
89
+ // EISDIR, EACCES, a JSON.parse SyntaxError (unparsable/truncated
90
+ // content), or anything else — never silently treat as "absent".
91
+ // Abort before mutate/write; the original file is untouched.
92
+ return { ok: false, reason: (err && err.code) || 'parse-error' };
93
+ }
94
+ }
95
+
96
+ const updated = mutate(current);
97
+ const tmp = `${filePath}.tmp-${tmpTag}-${process.pid}-${Date.now()}`;
98
+ try {
99
+ await mkdir(path.dirname(filePath), { recursive: true });
100
+ await writeFile(tmp, JSON.stringify(updated, null, 2) + '\n', 'utf8');
101
+ await rename(tmp, filePath);
102
+ } catch (err) {
103
+ try {
104
+ await unlink(tmp);
105
+ } catch {
106
+ // tmp was never created, or is already gone — nothing to clean up.
107
+ }
108
+ return { ok: false, reason: (err && err.code) || 'write-error' };
109
+ }
110
+ return { ok: true, value: updated };
111
+ }
@@ -89,8 +89,9 @@ export async function bootstrapLock({
89
89
  }
90
90
 
91
91
  // Step 1: try to acquire. If a fresh acquire succeeds, we are done.
92
- // If a stale-PID-dead/-alive lock exists, force-overwrite it (the prior
93
- // session has died; the current raw owner can take the worktree).
92
+ // If a stale-heartbeat lock exists, force-overwrite it (the prior session
93
+ // stopped heartbeating past its ttl; the current raw owner can take the
94
+ // worktree).
94
95
  // Only an exact match of the existing physical raw sessionId permits the
95
96
  // same-session force-refresh. semantic_session_id, STATE.md `session`, and
96
97
  // an owner proof never make a different raw id the same owner.
@@ -107,8 +108,11 @@ export async function bootstrapLock({
107
108
 
108
109
  const shouldForce =
109
110
  acquireResult.ok !== true && (
110
- acquireResult.reason === 'stale-pid-dead' ||
111
- acquireResult.reason === 'stale-pid-alive' ||
111
+ // #1137: 'stale-heartbeat' replaced the former 'stale-pid-dead' /
112
+ // 'stale-pid-alive' pair. Both legacy spellings are gone from
113
+ // session-lock.mjs; matching only the new one keeps this force-branch
114
+ // reachable.
115
+ acquireResult.reason === 'stale-heartbeat' ||
112
116
  (acquireResult.reason === 'active' &&
113
117
  acquireResult.existingLock &&
114
118
  acquireResult.existingLock.session_id === sessionId)
@@ -0,0 +1,143 @@
1
+ /**
2
+ * subagent-paths.mjs — consolidated subagent sidecar-path derivation (#1196).
3
+ *
4
+ * Four call sites independently derived the SAME shape — the stopping
5
+ * subagent's own `agent-<id>.jsonl` transcript + `agent-<id>.meta.json`
6
+ * sidecar, from the PARENT transcript path the harness sends on stdin — and
7
+ * diverged on strictness. Measured 2026-09-02 @ a019d5a4 (this consolidation):
8
+ *
9
+ * | copy (file:line) | honours `agent_transcript_path` | id bound | rejects `'unknown'` | containment check |
10
+ * |------------------------------------------------------------|:---:|:---:|:---:|:---:|
11
+ * | hooks/subagent-telemetry.mjs:513 `resolveSubagentTranscriptPath` | no | unbounded (`+`) | yes (:517) | n/a (no override) |
12
+ * | hooks/on-stop.mjs:666 `resolveSidecarBase` | no | `{1,64}` (:566) | **no** | n/a (no override) |
13
+ * | hooks/post-subagent-discovery-validator.mjs:538 `resolveAgentTranscriptPath` | yes (:539) | `{1,64}` (:547) | yes (:549) | **no** — `explicit` returned unchecked (:539) |
14
+ * | scripts/lib/wave-transcript-tail.mjs:610 `readAgentType` | no | **none** | no | n/a (no override) |
15
+ *
16
+ * `resolveSubagentSidecar()` below applies the STRICTEST rule found in ANY
17
+ * copy to every caller: the `{1,64}` bound, the `'unknown'` rejection (closes
18
+ * the on-stop.mjs / wave-transcript-tail.mjs gap), AND a path-containment
19
+ * check on the `agentTranscriptPath` override that NONE of the four copies
20
+ * performed — post-subagent-discovery-validator.mjs returned the harness
21
+ * override completely unvalidated. That containment check is a NEW rule this
22
+ * consolidation introduces, not a preserved behaviour.
23
+ *
24
+ * @module hooks/_lib/subagent-paths
25
+ */
26
+
27
+ import path from 'node:path';
28
+
29
+ /**
30
+ * Real agent ids are hex-ish tokens (e.g. `a60348a01ca982b4c`); anything else
31
+ * is rejected, not sanitised, so no payload value can traverse out of the
32
+ * `subagents/` directory. `{1,64}` is the strictest bound found across the
33
+ * four prior copies (hooks/subagent-telemetry.mjs used an unbounded `+`;
34
+ * scripts/lib/wave-transcript-tail.mjs applied no charset check at all).
35
+ */
36
+ export const AGENT_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
37
+
38
+ /**
39
+ * Validate a candidate agent id against the strictest rule of the four prior
40
+ * copies: the `{1,64}` charset+length bound, AND explicit rejection of the
41
+ * literal `'unknown'` — the no-usable-id fallback value that several callers
42
+ * (`firstNonEmptyString(..., 'unknown')`) substitute when the harness sends no
43
+ * id at all. `'unknown'` matches `AGENT_ID_RE` (it's plain lowercase letters),
44
+ * so a charset check alone does not catch it — hooks/on-stop.mjs's
45
+ * `resolveSidecarBase()` had exactly this gap (AGENT_ID_RE-only, no explicit
46
+ * `'unknown'` rejection) before this consolidation.
47
+ *
48
+ * @param {unknown} agentId
49
+ * @returns {agentId is string}
50
+ */
51
+ export function isValidAgentId(agentId) {
52
+ return typeof agentId === 'string' && AGENT_ID_RE.test(agentId) && agentId !== 'unknown';
53
+ }
54
+
55
+ /**
56
+ * Resolve the sidecar pair (`.jsonl` transcript + `.meta.json`) for a stopping
57
+ * subagent (#949, #1190, #1191, #1196).
58
+ *
59
+ * Two paths to the same result:
60
+ * - **Override.** `agentTranscriptPath` (the harness's `agent_transcript_path`,
61
+ * honoured today only by hooks/post-subagent-discovery-validator.mjs:539)
62
+ * wins when present — but ONLY when, after `path.resolve`, it sits inside
63
+ * `dirname(transcriptPath)`. An override outside that tree is REJECTED
64
+ * (returns null), never silently accepted and never falls through to
65
+ * derivation — a bad override must not resolve to an unrelated agent's
66
+ * sidecar.
67
+ * - **Derivation.** `<dir>/<base>/subagents/agent-<agentId>` from the PARENT
68
+ * transcript path — the shape all four prior copies agree on
69
+ * (subagent-telemetry.mjs:513, on-stop.mjs:666,
70
+ * post-subagent-discovery-validator.mjs:552-553, and the subagentsDir
71
+ * scripts/lib/wave-transcript-tail.mjs's tailLoop() already builds by hand).
72
+ *
73
+ * Returns null — never a guessed or partial path — on ANY invalid input: an
74
+ * empty/non-string `transcriptPath`, an `agentId` failing `isValidAgentId()`
75
+ * (only checked in the derivation branch — the override branch does not need
76
+ * a valid agentId, matching all four prior copies' precedence), or an override
77
+ * that fails containment.
78
+ *
79
+ * NAMED CEILING (BV-004): the meta path is always derived as
80
+ * `transcript.replace(/\.jsonl$/i, '.meta.json')` (or `${transcript}.meta.json`
81
+ * when the transcript has no `.jsonl` suffix to replace — an override the
82
+ * harness contract says never happens; see the header comments of
83
+ * hooks/post-subagent-discovery-validator.mjs). REVISIT TRIGGER: if a real
84
+ * override without a `.jsonl` suffix is ever observed, this needs its own
85
+ * validation branch instead of the current always-well-formed assumption.
86
+ *
87
+ * NAMED CEILING (BV-004, LOW, qa review): the override's containment check
88
+ * (`path.relative(resolvedDir, resolvedOverride)` below) is LEXICAL, not
89
+ * filesystem-real — it runs on `path.resolve()`'d strings, never on
90
+ * `fs.realpathSync()`'d ones. A symlink SITTING INSIDE `dirname(transcriptPath)`
91
+ * whose TARGET points outside that tree therefore passes containment: the
92
+ * lexical path looks contained, the file it actually resolves to is not.
93
+ * Deliberately not fixed with a realpath call here — every current consumer
94
+ * of this override branch (hooks/post-subagent-discovery-validator.mjs) only
95
+ * READS the resulting sidecar files, and `agentTranscriptPath` is a value the
96
+ * HARNESS itself supplies on stdin, not one an untrusted external actor can
97
+ * inject — so the realpath syscalls would buy nothing against the live
98
+ * threat model. REVISIT TRIGGER: if `agentTranscriptPath` (or any future
99
+ * override this module accepts) is ever sourced from a producer this repo
100
+ * does NOT trust as much as the harness's own stdin payload, resolve both
101
+ * `resolvedOverride` and `resolvedDir` through `fs.realpathSync()` before the
102
+ * `path.relative()` comparison, so a symlink cannot smuggle the effective
103
+ * path outside the allowed tree.
104
+ *
105
+ * @param {object} args
106
+ * @param {string|undefined|null} args.transcriptPath — parent transcript path
107
+ * (stdin `transcript_path`, or an equivalent parent-transcript path)
108
+ * @param {string|undefined|null} args.agentId
109
+ * @param {string|undefined|null} [args.agentTranscriptPath] — harness override
110
+ * (`agent_transcript_path`); takes precedence over derivation when present
111
+ * and contained under `dirname(transcriptPath)`
112
+ * @returns {{base: string, transcript: string, meta: string}|null}
113
+ */
114
+ export function resolveSubagentSidecar({ transcriptPath, agentId, agentTranscriptPath = null }) {
115
+ if (typeof transcriptPath !== 'string' || !transcriptPath.trim()) return null;
116
+
117
+ const dir = path.dirname(transcriptPath);
118
+ const base = path.basename(transcriptPath).replace(/\.jsonl$/i, '');
119
+ if (!base || base === '.' || base === '..') return null;
120
+
121
+ let transcript;
122
+ if (typeof agentTranscriptPath === 'string' && agentTranscriptPath.trim()) {
123
+ const resolvedOverride = path.resolve(agentTranscriptPath);
124
+ const resolvedDir = path.resolve(dir);
125
+ const rel = path.relative(resolvedDir, resolvedOverride);
126
+ const contained = rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
127
+ if (!contained) return null;
128
+ transcript = resolvedOverride;
129
+ } else {
130
+ if (!isValidAgentId(agentId)) return null;
131
+ transcript = path.join(dir, base, 'subagents', `agent-${agentId}.jsonl`);
132
+ }
133
+
134
+ const meta = /\.jsonl$/i.test(transcript)
135
+ ? transcript.replace(/\.jsonl$/i, '.meta.json')
136
+ : `${transcript}.meta.json`;
137
+
138
+ return {
139
+ base: transcript.replace(/\.jsonl$/i, ''),
140
+ transcript,
141
+ meta,
142
+ };
143
+ }