session-orchestrator 5.12.0 → 5.13.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 (385) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +4 -1
  3. package/.codex-plugin/agents/explorer.toml +1 -1
  4. package/.codex-plugin/agents/session-reviewer.toml +2 -2
  5. package/.codex-plugin/agents/wave-worker.toml +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.cursor-plugin/plugin.json +1 -1
  8. package/.orchestrator/policy/blocked-commands.json +8 -0
  9. package/.orchestrator/policy/public-export.json +30 -0
  10. package/CHANGELOG.md +162 -4
  11. package/README.md +126 -127
  12. package/agents/analyst.md +7 -2
  13. package/agents/architect-reviewer.md +7 -2
  14. package/agents/code-implementer.md +4 -2
  15. package/agents/db-specialist.md +9 -2
  16. package/agents/dialectic-deriver.md +9 -3
  17. package/agents/docs-writer.md +7 -2
  18. package/agents/eval-judge.md +7 -1
  19. package/agents/qa-strategist.md +7 -2
  20. package/agents/security-reviewer.md +9 -2
  21. package/agents/session-reviewer.md +18 -2
  22. package/agents/skill-applied-judge.md +7 -1
  23. package/agents/test-writer.md +9 -2
  24. package/agents/ui-developer.md +9 -2
  25. package/agents/ux-evaluator.md +6 -1
  26. package/commands/session.md +1 -1
  27. package/commands/templates-ack.md +89 -18
  28. package/docs/agent-authoring.md +30 -11
  29. package/docs/ci-setup.md +38 -2
  30. package/docs/codex-setup.md +33 -9
  31. package/docs/components.md +13 -2
  32. package/docs/cursor-setup.md +3 -3
  33. package/docs/events-schema.md +5 -4
  34. package/docs/evidence-candidates.md +63 -0
  35. package/docs/fleet-checkin-audit.md +7 -4
  36. package/docs/github-mirror-protection.md +150 -3
  37. package/docs/install.md +90 -17
  38. package/docs/instruction-delivery.md +1 -1
  39. package/docs/migration-v4.md +2 -2
  40. package/docs/mods.md +118 -0
  41. package/docs/owner-config-schema.md +1 -0
  42. package/docs/plugin-architecture-v3.md +1 -1
  43. package/docs/pm-skills-marketplace.md +4 -3
  44. package/docs/rule-authoring.md +24 -13
  45. package/docs/scope-collision-guard.md +2 -1
  46. package/docs/session-config-reference.md +49 -15
  47. package/docs/session-config-template.md +30 -22
  48. package/docs/sessions-ledger-repair.md +137 -0
  49. package/docs/vault-docs-architecture.md +1 -1
  50. package/hooks/_lib/hook-import-set.json +406 -73
  51. package/hooks/_lib/hook-root.mjs +301 -0
  52. package/hooks/_lib/lock-bootstrap.mjs +434 -4
  53. package/hooks/_lib/orchestrator-ignore.mjs +3 -0
  54. package/hooks/_lib/subagent-transcript.mjs +2 -2
  55. package/hooks/_lib/vcs-create-matcher.mjs +13 -17
  56. package/hooks/cwd-change-restore.mjs +5 -1
  57. package/hooks/enforce-commands.mjs +4 -3
  58. package/hooks/enforce-scope.mjs +11 -1
  59. package/hooks/hooks-codex.json +48 -2
  60. package/hooks/hooks.json +31 -31
  61. package/hooks/on-session-end.mjs +38 -15
  62. package/hooks/on-session-start.mjs +139 -49
  63. package/hooks/on-stop.mjs +11 -6
  64. package/hooks/post-bash-issue-budget-refund.mjs +6 -1
  65. package/hooks/post-bash-write-verify.mjs +8 -4
  66. package/hooks/post-edit-validate.mjs +45 -52
  67. package/hooks/post-subagent-discovery-validator.mjs +2 -2
  68. package/hooks/post-tool-batch-wave-signal.mjs +8 -4
  69. package/hooks/post-tool-failure-corrective-context.mjs +5 -1
  70. package/hooks/pre-bash-destructive-guard.mjs +220 -8
  71. package/hooks/pre-bash-issue-budget.mjs +6 -1
  72. package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
  73. package/hooks/pre-bash-staging-fence.mjs +258 -14
  74. package/hooks/pre-bash-templates-first.mjs +28 -27
  75. package/hooks/run-node.sh +1 -1
  76. package/hooks/skill-invocation-telemetry.mjs +15 -8
  77. package/hooks/subagent-telemetry.mjs +77 -14
  78. package/hooks/wave-scope-commit-guard.mjs +143 -14
  79. package/mods/so-hud/.claude-plugin/plugin.json +11 -0
  80. package/mods/so-hud/README.md +57 -0
  81. package/mods/so-hud/hooks/beacon-io.mjs +101 -0
  82. package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
  83. package/mods/so-hud/hooks/hooks.json +1 -0
  84. package/mods/so-hud/hooks/logic.ts +182 -0
  85. package/mods/so-hud/hooks/register.tsx +670 -0
  86. package/mods/so-hud/hooks/rules.ts +2998 -0
  87. package/mods/so-hud/tsconfig.json +8 -0
  88. package/mods/so-hud/types/index.d.ts +36 -0
  89. package/package.json +4 -2
  90. package/rules/_index.md +1 -1
  91. package/rules/always-on/ask-via-tool.md +1 -1
  92. package/rules/always-on/loop-and-monitor.md +17 -194
  93. package/rules/always-on/parallel-sessions.md +3 -1
  94. package/rules/opt-in-stack/backend-data.md +1 -1
  95. package/rules/opt-in-stack/backend.md +1 -1
  96. package/rules/opt-in-stack/security-web.md +5 -1
  97. package/scripts/agent-usage-census.mjs +79 -0
  98. package/scripts/apply-session-learnings.mjs +34 -6
  99. package/scripts/archive-closed-prds.mjs +21 -5
  100. package/scripts/autopilot.mjs +11 -5
  101. package/scripts/backfill-abandoned-sessions.mjs +13 -3
  102. package/scripts/backfill-evidence-digest.mjs +2 -1
  103. package/scripts/backfill-learnings-from-vault.mjs +61 -6
  104. package/scripts/backfill-sessions.mjs +16 -2
  105. package/scripts/check-package-manager.mjs +26 -8
  106. package/scripts/check-sessions-integrity.mjs +4 -1
  107. package/scripts/ci/assert-vitest-green.mjs +3 -0
  108. package/scripts/ci/windows-smoke.mjs +201 -0
  109. package/scripts/close-precheck.mjs +55 -0
  110. package/scripts/dialectic-deriver.mjs +20 -12
  111. package/scripts/emit-event.mjs +2 -2
  112. package/scripts/emit-session.mjs +239 -20
  113. package/scripts/eval-session.mjs +17 -3
  114. package/scripts/export-hw-learnings.mjs +29 -2
  115. package/scripts/fleet-checkin-audit.mjs +8 -4
  116. package/scripts/fleet-checkin.mjs +39 -27
  117. package/scripts/gc-stale-worktrees.mjs +6 -1
  118. package/scripts/github-export.mjs +128 -0
  119. package/scripts/github-protection-audit.mjs +3 -1
  120. package/scripts/handover-queue-check.mjs +209 -0
  121. package/scripts/harness-audit.mjs +6 -4
  122. package/scripts/lib/agent-frontmatter.mjs +57 -10
  123. package/scripts/lib/agent-status.mjs +125 -7
  124. package/scripts/lib/agent-usage-census.mjs +394 -0
  125. package/scripts/lib/auto-dialectic.mjs +17 -10
  126. package/scripts/lib/auto-dream.mjs +22 -14
  127. package/scripts/lib/autopilot/loop.mjs +3 -1
  128. package/scripts/lib/autopilot/recent-runs.mjs +4 -2
  129. package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
  130. package/scripts/lib/autopilot/telemetry.mjs +5 -1
  131. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  132. package/scripts/lib/baseline-archetypes.mjs +10 -2
  133. package/scripts/lib/blocked-commands-policy.mjs +4 -3
  134. package/scripts/lib/build-live-signals.mjs +13 -9
  135. package/scripts/lib/ci-status-banner.mjs +329 -24
  136. package/scripts/lib/codex/plugin-contract.mjs +8 -0
  137. package/scripts/lib/cold-start-detector.mjs +5 -2
  138. package/scripts/lib/command-blocker.mjs +603 -43
  139. package/scripts/lib/common.mjs +3 -2
  140. package/scripts/lib/config/cross-repo.mjs +74 -7
  141. package/scripts/lib/config/host-paths.mjs +6 -2
  142. package/scripts/lib/config/section-extractor.mjs +65 -11
  143. package/scripts/lib/config/template-keys.mjs +153 -0
  144. package/scripts/lib/config/vault-integration.mjs +1 -0
  145. package/scripts/lib/config.mjs +2 -2
  146. package/scripts/lib/coordinator-snapshot.mjs +1 -1
  147. package/scripts/lib/discovery/triage-state.mjs +2 -1
  148. package/scripts/lib/dispatcher/rank.mjs +5 -7
  149. package/scripts/lib/ecosystem-health.mjs +2 -2
  150. package/scripts/lib/eval/engine.mjs +3 -2
  151. package/scripts/lib/eval/session-resolve.mjs +6 -2
  152. package/scripts/lib/eval/sink.mjs +4 -3
  153. package/scripts/lib/events-retention-banner.mjs +3 -2
  154. package/scripts/lib/events-schema.mjs +10 -0
  155. package/scripts/lib/events.mjs +23 -12
  156. package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
  157. package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
  158. package/scripts/lib/fleet-protocol.mjs +4 -0
  159. package/scripts/lib/gates/gate-full.mjs +172 -11
  160. package/scripts/lib/gates/gate-helpers.mjs +236 -23
  161. package/scripts/lib/git-config-drift.mjs +141 -7
  162. package/scripts/lib/github-export.mjs +447 -0
  163. package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
  164. package/scripts/lib/handover-queue-marks.mjs +159 -0
  165. package/scripts/lib/hardware-pattern-detector.mjs +103 -13
  166. package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
  167. package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
  168. package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
  169. package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
  170. package/scripts/lib/host-identity.mjs +9 -5
  171. package/scripts/lib/http-request.mjs +64 -0
  172. package/scripts/lib/instruction-budget-guard.mjs +30 -22
  173. package/scripts/lib/io.mjs +6 -3
  174. package/scripts/lib/issue-budget-reconcile.mjs +3 -2
  175. package/scripts/lib/issue-budget.mjs +36 -7
  176. package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
  177. package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
  178. package/scripts/lib/learnings/io.mjs +289 -15
  179. package/scripts/lib/learnings/select.mjs +53 -4
  180. package/scripts/lib/learnings/store-role.mjs +209 -0
  181. package/scripts/lib/learnings/surface.mjs +46 -7
  182. package/scripts/lib/ledger-path.mjs +113 -0
  183. package/scripts/lib/maintenance-due-banner.mjs +63 -12
  184. package/scripts/lib/memory-banner.mjs +5 -6
  185. package/scripts/lib/memory-proposals/collector.mjs +5 -2
  186. package/scripts/lib/memory-proposals/sink.mjs +7 -4
  187. package/scripts/lib/memory-proposals/store.mjs +22 -5
  188. package/scripts/lib/mode-selector-accuracy.mjs +2 -1
  189. package/scripts/lib/no-follow-open.mjs +57 -0
  190. package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
  191. package/scripts/lib/own-lock-root.mjs +95 -0
  192. package/scripts/lib/owner-interview.mjs +2 -1
  193. package/scripts/lib/owner-yaml.mjs +70 -7
  194. package/scripts/lib/platform.mjs +161 -9
  195. package/scripts/lib/plugin-update-banner.mjs +15 -11
  196. package/scripts/lib/pre-push-local-partial.mjs +117 -0
  197. package/scripts/lib/pre-push-remote-gate.mjs +151 -25
  198. package/scripts/lib/process-group.mjs +270 -34
  199. package/scripts/lib/project-hygiene.mjs +28 -0
  200. package/scripts/lib/quality-gate.mjs +33 -9
  201. package/scripts/lib/reconcile/backlog.mjs +29 -15
  202. package/scripts/lib/reconcile/engine.mjs +26 -2
  203. package/scripts/lib/reconcile/idempotency.mjs +65 -3
  204. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
  205. package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
  206. package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
  207. package/scripts/lib/resource-probe/evaluate.mjs +32 -9
  208. package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
  209. package/scripts/lib/rule-loader.mjs +20 -10
  210. package/scripts/lib/scope-baseline.mjs +123 -8
  211. package/scripts/lib/scope-echo.mjs +5 -2
  212. package/scripts/lib/scope-gate.mjs +55 -20
  213. package/scripts/lib/session-close-backfill.mjs +75 -7
  214. package/scripts/lib/session-end/close-precheck.mjs +151 -0
  215. package/scripts/lib/session-end/phase-skip.mjs +8 -7
  216. package/scripts/lib/session-end/tail-runner.mjs +7 -2
  217. package/scripts/lib/session-id.mjs +9 -1
  218. package/scripts/lib/session-identity/own-session.mjs +63 -0
  219. package/scripts/lib/session-lock.mjs +114 -0
  220. package/scripts/lib/session-record-repair.mjs +245 -35
  221. package/scripts/lib/session-registry.mjs +72 -9
  222. package/scripts/lib/session-schema/constants.mjs +9 -0
  223. package/scripts/lib/session-schema/filters.mjs +56 -1
  224. package/scripts/lib/session-schema/validator.mjs +15 -5
  225. package/scripts/lib/session-start-probes.mjs +149 -22
  226. package/scripts/lib/session-token-rollup.mjs +2 -1
  227. package/scripts/lib/session-work-trees.mjs +166 -0
  228. package/scripts/lib/sessions-canonical.mjs +61 -3
  229. package/scripts/lib/sessions-integrity-banner.mjs +17 -39
  230. package/scripts/lib/sessions-staleness-banner.mjs +8 -5
  231. package/scripts/lib/sh-quote.mjs +1 -0
  232. package/scripts/lib/skill-evidence-window.mjs +5 -1
  233. package/scripts/lib/skill-health/join.mjs +10 -9
  234. package/scripts/lib/skill-invocations-schema.mjs +13 -0
  235. package/scripts/lib/skill-judgments-schema.mjs +6 -2
  236. package/scripts/lib/snapshot-probe.mjs +414 -0
  237. package/scripts/lib/so-hud/collect.mjs +49 -5
  238. package/scripts/lib/so-hud/runtime.mjs +15 -4
  239. package/scripts/lib/spiral-carryover.mjs +8 -1
  240. package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
  241. package/scripts/lib/state-md/idle-reset.mjs +14 -5
  242. package/scripts/lib/state-md/mission-status.mjs +5 -1
  243. package/scripts/lib/state-md-peer-guard.mjs +2 -0
  244. package/scripts/lib/state-md.mjs +31 -8
  245. package/scripts/lib/sunset/walker.mjs +4 -3
  246. package/scripts/lib/telemetry/consent.mjs +4 -2
  247. package/scripts/lib/telemetry/queue.mjs +3 -3
  248. package/scripts/lib/telemetry/sync.mjs +45 -9
  249. package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
  250. package/scripts/lib/tmux-layout/layouts.mjs +20 -3
  251. package/scripts/lib/ux-grill/paths.mjs +5 -1
  252. package/scripts/lib/validate/check-agents.mjs +37 -6
  253. package/scripts/lib/validate/check-component-paths.mjs +25 -1
  254. package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
  255. package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
  256. package/scripts/lib/validate/check-json-files.mjs +3 -1
  257. package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
  258. package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
  259. package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
  260. package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
  261. package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
  262. package/scripts/lib/validate/check-unwired-features.mjs +4 -2
  263. package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
  264. package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
  265. package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
  266. package/scripts/lib/validate/confidential-names.mjs +3 -1
  267. package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
  268. package/scripts/lib/vault-mirror/namespace.mjs +19 -7
  269. package/scripts/lib/vault-mirror/process.mjs +56 -27
  270. package/scripts/lib/vault-mirror/prune.mjs +5 -2
  271. package/scripts/lib/vault-mirror/render-learnings.mjs +3 -3
  272. package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
  273. package/scripts/lib/vault-mirror/retention.mjs +2 -2
  274. package/scripts/lib/vault-mirror/utils.mjs +79 -0
  275. package/scripts/lib/vault-staleness-banner.mjs +4 -7
  276. package/scripts/lib/vault-status/board-writer.mjs +2 -1
  277. package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
  278. package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
  279. package/scripts/lib/win-process-tree.mjs +137 -0
  280. package/scripts/lib/workspace.mjs +1 -1
  281. package/scripts/lib/worktree/constants.mjs +1 -1
  282. package/scripts/lib/worktree/lifecycle.mjs +1 -1
  283. package/scripts/lib/worktree/listing.mjs +1 -1
  284. package/scripts/lib/zx.mjs +50 -0
  285. package/scripts/mcp-server.mjs +16 -7
  286. package/scripts/mcp-server.sh +13 -83
  287. package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
  288. package/scripts/memory-propose.mjs +10 -3
  289. package/scripts/migrate-cold-start-seed.mjs +7 -0
  290. package/scripts/migrate-legacy-learnings.sh +22 -0
  291. package/scripts/migrate-sessions-jsonl.mjs +16 -2
  292. package/scripts/migrate-vault-paths.mjs +29 -13
  293. package/scripts/parse-config.mjs +71 -14
  294. package/scripts/print-learnings-index.mjs +25 -2
  295. package/scripts/release-session-lock.mjs +94 -7
  296. package/scripts/release.mjs +451 -24
  297. package/scripts/relocate-vault-corpus.mjs +11 -7
  298. package/scripts/repair-invalid-sessions.mjs +68 -3
  299. package/scripts/resolve-session-invocation.mjs +10 -17
  300. package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
  301. package/scripts/run-quality-gate.mjs +33 -7
  302. package/scripts/self-update.mjs +56 -7
  303. package/scripts/session-commit-files.mjs +133 -0
  304. package/scripts/site-numbers.mjs +52 -9
  305. package/scripts/snapshot-probe.mjs +156 -0
  306. package/scripts/so-hud.mjs +29 -6
  307. package/scripts/state-md-idle-reset.mjs +2 -1
  308. package/scripts/sweep-expired-learnings.mjs +54 -6
  309. package/scripts/sweep-expired-rules.mjs +4 -1
  310. package/scripts/validate-plugin.mjs +22 -0
  311. package/scripts/vault-integration-watcher.mjs +54 -6
  312. package/scripts/vault-mirror.mjs +9 -0
  313. package/skills/_shared/fleet-protocol.md +142 -1
  314. package/skills/_shared/instruction-file-resolution.md +1 -1
  315. package/skills/_shared/monitor-patterns.md +44 -25
  316. package/skills/_shared/platform-tools.md +7 -0
  317. package/skills/_shared/state-ownership.md +3 -2
  318. package/skills/architecture/references/domain-model.md +1 -1
  319. package/skills/bootstrap/SKILL.md +3 -1
  320. package/skills/bootstrap/_shared-template.md +4 -4
  321. package/skills/bootstrap/fast-template.md +1 -1
  322. package/skills/bootstrap/private-contract.md +1 -1
  323. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
  324. package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
  325. package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
  326. package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
  327. package/skills/claude-md-drift-check/SKILL.md +3 -3
  328. package/skills/claude-md-drift-check/checker.mjs +61 -101
  329. package/skills/close/SKILL.md +20 -7
  330. package/skills/discovery/probes/docs-staleness.mjs +7 -5
  331. package/skills/discovery/probes/frontend-slop.mjs +7 -3
  332. package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
  333. package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
  334. package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
  335. package/skills/discovery/probes/vault-staleness.mjs +5 -3
  336. package/skills/discovery/probes-session.md +3 -0
  337. package/skills/discovery/probes-vault.md +2 -0
  338. package/skills/dispatcher/SKILL.md +1 -1
  339. package/skills/eval/SKILL.md +3 -1
  340. package/skills/evolve/SKILL.md +2 -0
  341. package/skills/evolve/references/evolve-analyze-mode.md +3 -3
  342. package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
  343. package/skills/gitlab-ops/SKILL.md +7 -6
  344. package/skills/gitlab-portfolio/SKILL.md +2 -2
  345. package/skills/go/SKILL.md +13 -1
  346. package/skills/npm-publish/SKILL.md +5 -2
  347. package/skills/plan/SKILL.md +23 -10
  348. package/skills/plan/mode-feature.md +13 -8
  349. package/skills/plan/mode-new.md +6 -14
  350. package/skills/plan/prd-feature-template.md +19 -0
  351. package/skills/plan/prd-full-template.md +12 -5
  352. package/skills/plan/prd-premortem-panel.md +48 -0
  353. package/skills/plan/prd-reviewer-prompt.md +11 -3
  354. package/skills/plan/soul.md +1 -1
  355. package/skills/portfolio/SKILL.md +5 -6
  356. package/skills/quality-gates/SKILL.md +2 -4
  357. package/skills/reconcile/SKILL.md +27 -4
  358. package/skills/release/SKILL.md +16 -8
  359. package/skills/session-end/SKILL.md +94 -11
  360. package/skills/session-end/phase-3-2-docs-verification.md +1 -0
  361. package/skills/session-end/phase-3-6-tail.md +11 -7
  362. package/skills/session-end/phase-3-7a-recommendations.md +9 -2
  363. package/skills/session-end/plan-verification.md +16 -1
  364. package/skills/session-end/references/phase-3-documentation-updates.md +14 -12
  365. package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
  366. package/skills/session-end/references/session-summary-template.md +4 -0
  367. package/skills/session-end/session-metrics-write.md +7 -4
  368. package/skills/session-plan/SKILL.md +5 -3
  369. package/skills/session-start/SKILL.md +9 -3
  370. package/skills/session-start/phase-4-5-resource-health.md +11 -5
  371. package/skills/session-start/phase-8-5-express-path.md +4 -3
  372. package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
  373. package/skills/session-start/references/phase-1-5-session-continuity.md +2 -2
  374. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  375. package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
  376. package/skills/tmux-layout/SKILL.md +2 -2
  377. package/skills/vault-sync/validator.mjs +3 -2
  378. package/skills/wave-executor/SKILL.md +5 -4
  379. package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
  380. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
  381. package/skills/wave-executor/references/wave-loop-review.md +13 -5
  382. package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
  383. package/assets/og-card.png +0 -0
  384. package/assets/og-card.svg +0 -47
  385. package/assets/wave-lifecycle.svg +0 -98
@@ -15,7 +15,7 @@
15
15
  * via `.passthrough()`, and it is NOT rewritten retroactively by this change.
16
16
  */
17
17
 
18
- import { toDate, buildTag, slugifyIdSafe } from './utils.mjs';
18
+ import { toDate, buildTag, slugifyIdSafe, noteIdFor, yamlNoteId } from './utils.mjs';
19
19
  import { stableHostname } from '../host-identity.mjs';
20
20
  import { sourceRecordValue, sourceRecordLine } from './render-learnings.mjs';
21
21
  import { sessionNarrativeText } from './retention.mjs';
@@ -118,38 +118,17 @@ function waveCount(value) {
118
118
  }
119
119
 
120
120
  /**
121
- * ── RENDERABLE ⊋ SCHEMA-VALID (#964) ───────────────────────────────────────
122
- *
123
- * Each generator gates on its own required-field list. Before #964 all three
124
- * lists were function-local `const`s with no stated relationship to
125
- * `REQUIRED_FIELDS` (scripts/lib/session-schema/constants.mjs) — the write-path
126
- * schema — which made five independent notions of "a valid session record" in
127
- * one codebase, drifting silently. They are lifted here, exported, and pinned
128
- * against `REQUIRED_FIELDS` by a mechanical superset test in
129
- * tests/lib/vault-mirror/render-sessions.test.mjs.
130
- *
131
- * The relationship is deliberately asymmetric, not a duplicate:
132
- *
133
- * `REQUIRED_FIELDS` — "this record is well-formed". The writer
134
- * (scripts/emit-session.mjs) REFUSES a record that fails it.
135
- * these lists — "this record renders into a note a human can read".
136
- * vault-mirror SKIPS a record that fails them; the record stays valid.
137
- *
138
- * Collapsing the two would destroy that distinction. Hence `effectiveness`:
139
- * optional to the writer (a record lacking it is fine), required by the v1
140
- * renderer (a note reading `planned=n/a, completed=n/a, carryover=n/a,
141
- * rate=n/a` is worse than no note — indistinguishable from a real session whose
142
- * metrics happened to be absent).
143
- *
144
- * The predicate here is VALUE presence (`null`/`undefined` both fail); the
145
- * validator's is KEY presence. See `_validateRequiredFields` for why.
121
+ * ── RENDERABLE AND SCHEMA-VALID (#1575) ───────────────────────────────────
122
+ *
123
+ * The generators retain route-specific required-field lists. V1 requires the
124
+ * canonical REQUIRED_FIELDS; legacy V2/V3 derive aggregates from other shapes
125
+ * and cannot be emitted by the canonical writer. Effectiveness is optional
126
+ * and nullable in every route, matching the writer. Unknown metrics render
127
+ * n/a; a measured zero remains zero. Malformed non-object metrics still fail.
128
+ * Required fields use VALUE presence; the validator separately checks types.
146
129
  */
147
130
 
148
- /**
149
- * v1 — the only generator reachable from a write-path-valid record (measured:
150
- * 205/205 live ledger records at HEAD 730ee9d route v1). Its list is
151
- * `REQUIRED_FIELDS` plus `effectiveness`, i.e. a strict superset.
152
- */
131
+ /** V1 uses the canonical required-field set without requiring optional metrics. */
153
132
  export const RENDERABLE_SESSION_FIELDS_V1 = Object.freeze([
154
133
  'session_id',
155
134
  'session_type',
@@ -160,7 +139,6 @@ export const RENDERABLE_SESSION_FIELDS_V1 = Object.freeze([
160
139
  'total_files_changed',
161
140
  'agent_summary',
162
141
  'waves',
163
- 'effectiveness',
164
142
  ]);
165
143
 
166
144
  /**
@@ -183,7 +161,6 @@ export const RENDERABLE_SESSION_FIELDS_V2 = Object.freeze([
183
161
  'completed_at',
184
162
  'waves',
185
163
  'files_changed',
186
- 'effectiveness',
187
164
  ]);
188
165
 
189
166
  /**
@@ -200,9 +177,18 @@ export const RENDERABLE_SESSION_FIELDS_V3 = Object.freeze([
200
177
  'started_at',
201
178
  'completed_at',
202
179
  'waves',
203
- 'effectiveness',
204
180
  ]);
205
181
 
182
+ /** Optional metrics share the writer's object-or-null contract in all routes. */
183
+ function sessionEffectiveness(entry) {
184
+ const value = entry.effectiveness;
185
+ if (value === undefined || value === null) return {};
186
+ if (typeof value !== 'object' || Array.isArray(value)) {
187
+ throw new Error(`vault-mirror: session entry missing nested field 'effectiveness' (session_id=${entry.session_id})`);
188
+ }
189
+ return value;
190
+ }
191
+
206
192
  /**
207
193
  * Session-ledger status → vault-frontmatter status mapping (#909).
208
194
  *
@@ -422,12 +408,9 @@ export function generateSessionNote(entry, options = {}) {
422
408
  total_files_changed,
423
409
  agent_summary,
424
410
  waves,
425
- effectiveness,
426
411
  } = entry;
427
412
 
428
- if (typeof effectiveness !== 'object' || effectiveness === null) {
429
- throw new Error(`vault-mirror: session entry missing nested field 'effectiveness' (session_id=${session_id})`);
430
- }
413
+ const effectiveness = sessionEffectiveness(entry);
431
414
  if (typeof agent_summary !== 'object' || agent_summary === null) {
432
415
  throw new Error(`vault-mirror: session entry missing nested field 'agent_summary' (session_id=${session_id})`);
433
416
  }
@@ -463,7 +446,7 @@ export function generateSessionNote(entry, options = {}) {
463
446
  // and cap the combined tag at 64 chars so the frontmatter passes the vault
464
447
  // tagPathRegex. The `id` is likewise slugified to a kebab slug below —
465
448
  // entry.session_id may carry an ISO-timestamp uppercase `T`/`:`/`.`/`Z`.
466
- const noteId = slugifyIdSafe(session_id) ?? session_id;
449
+ const noteId = noteIdFor(options.repoNs, slugifyIdSafe(session_id) ?? session_id);
467
450
  // #909: the real ledger status, mapped into the vault enum — never hard-coded.
468
451
  const vaultStatus = vaultStatusForSession(entry);
469
452
  const tags = `[${buildTag(['session', session_type])}, ${buildTag(['status', vaultStatus])}]`;
@@ -554,7 +537,7 @@ export function generateSessionNote(entry, options = {}) {
554
537
  const { repoNs } = options;
555
538
 
556
539
  return `---
557
- id: ${noteId}
540
+ id: ${yamlNoteId(noteId)}
558
541
  type: session
559
542
  title: ${title}
560
543
  status: ${vaultStatus}
@@ -592,11 +575,9 @@ export function generateSessionNoteV2(entry, options = {}) {
592
575
  if (!Array.isArray(entry.waves)) {
593
576
  throw new Error(`vault-mirror: session entry 'waves' must be an array (session_id=${entry.session_id})`);
594
577
  }
595
- if (typeof entry.effectiveness !== 'object' || entry.effectiveness === null) {
596
- throw new Error(`vault-mirror: session entry missing nested field 'effectiveness' (session_id=${entry.session_id})`);
597
- }
578
+ const effectiveness = sessionEffectiveness(entry);
598
579
 
599
- const { session_id, session_type, started_at, completed_at, duration_seconds, branch, planned_issues, waves, files_changed, issues_closed, issues_created, effectiveness } = entry;
580
+ const { session_id, session_type, started_at, completed_at, duration_seconds, branch, planned_issues, waves, files_changed, issues_closed, issues_created } = entry;
600
581
 
601
582
  const created = toDate(started_at);
602
583
  const updated = toDate(completed_at);
@@ -620,7 +601,7 @@ export function generateSessionNoteV2(entry, options = {}) {
620
601
  // and cap the combined tag at 64 chars so the frontmatter passes the vault
621
602
  // tagPathRegex. The `id` is likewise slugified to a kebab slug below —
622
603
  // entry.session_id may carry an ISO-timestamp uppercase `T`/`:`/`.`/`Z`.
623
- const noteId = slugifyIdSafe(session_id) ?? session_id;
604
+ const noteId = noteIdFor(options.repoNs, slugifyIdSafe(session_id) ?? session_id);
624
605
  // #909: the real ledger status, mapped into the vault enum — never hard-coded.
625
606
  const vaultStatus = vaultStatusForSession(entry);
626
607
  const tags = `[${buildTag(['session', session_type])}, ${buildTag(['status', vaultStatus])}]`;
@@ -641,7 +622,7 @@ export function generateSessionNoteV2(entry, options = {}) {
641
622
  const { repoNs } = options;
642
623
 
643
624
  return `---
644
- id: ${noteId}
625
+ id: ${yamlNoteId(noteId)}
645
626
  type: session
646
627
  title: ${title}
647
628
  status: ${vaultStatus}
@@ -690,14 +671,12 @@ export function generateSessionNoteV3(entry, options = {}) {
690
671
  if (typeof entry.waves !== 'number') {
691
672
  throw new Error(`vault-mirror: session entry 'waves' must be a number (session_id=${entry.session_id})`);
692
673
  }
693
- if (typeof entry.effectiveness !== 'object' || entry.effectiveness === null) {
694
- throw new Error(`vault-mirror: session entry missing nested field 'effectiveness' (session_id=${entry.session_id})`);
695
- }
674
+ const effectiveness = sessionEffectiveness(entry);
696
675
 
697
676
  const {
698
677
  session_id, session_type, platform, branch, started_at, completed_at,
699
678
  duration_minutes, duration_seconds, waves, agents_dispatched, agent_summary,
700
- planned_issues, effectiveness, commits, issues_closed, issues_created,
679
+ planned_issues, commits, issues_closed, issues_created,
701
680
  follow_ups_filed, tests_total_pre, tests_total_post, tests_added,
702
681
  } = entry;
703
682
 
@@ -727,8 +706,12 @@ export function generateSessionNoteV3(entry, options = {}) {
727
706
  const failed = as.failed ?? MISSING_VALUE;
728
707
  const spiral = as.spiral ?? MISSING_VALUE;
729
708
 
709
+ // A qualified cross-repo reference (`group/repo#n`, #1603) already carries
710
+ // its own `#`; only a bare number gets the prefix.
730
711
  const fmtIssues = (list) =>
731
- Array.isArray(list) && list.length ? list.map((i) => `#${i}`).join(', ') : '—';
712
+ Array.isArray(list) && list.length
713
+ ? list.map((i) => (typeof i === 'number' ? `#${i}` : String(i))).join(', ')
714
+ : '—';
732
715
  const closedList = fmtIssues(issues_closed);
733
716
  const createdList = fmtIssues(issues_created);
734
717
  const followList = fmtIssues(follow_ups_filed);
@@ -752,7 +735,7 @@ export function generateSessionNoteV3(entry, options = {}) {
752
735
  // and cap the combined tag at 64 chars so the frontmatter passes the vault
753
736
  // tagPathRegex. The `id` is likewise slugified to a kebab slug below —
754
737
  // entry.session_id may carry an ISO-timestamp uppercase `T`/`:`/`.`/`Z`.
755
- const noteId = slugifyIdSafe(session_id) ?? session_id;
738
+ const noteId = noteIdFor(options.repoNs, slugifyIdSafe(session_id) ?? session_id);
756
739
  // #909: the real ledger status, mapped into the vault enum — never hard-coded.
757
740
  const vaultStatus = vaultStatusForSession(entry);
758
741
  const tags = `[${buildTag(['session', session_type])}, ${buildTag(['status', vaultStatus])}]`;
@@ -768,7 +751,7 @@ export function generateSessionNoteV3(entry, options = {}) {
768
751
  const { repoNs } = options;
769
752
 
770
753
  return `---
771
- id: ${noteId}
754
+ id: ${yamlNoteId(noteId)}
772
755
  type: session
773
756
  title: ${title}
774
757
  status: ${vaultStatus}
@@ -34,7 +34,7 @@
34
34
  import { readdirSync } from 'node:fs';
35
35
  import { join } from 'node:path';
36
36
 
37
- import { subjectToSlug } from './utils.mjs';
37
+ import { subjectToSlug, noteIdFor, yamlNoteId } from './utils.mjs';
38
38
 
39
39
  /** The generator marker every mirror-written note carries. */
40
40
  export const GENERATOR_MARKER = 'session-orchestrator-vault-mirror@1';
@@ -367,7 +367,7 @@ export function renderRollup(existing, rows, { ns, month }) {
367
367
  .map((r) => `| ${r.date} | \`${r.id}\` | ${r.type} | ${r.waves} | ${r.agents} | ${r.files} |`)
368
368
  .join('\n');
369
369
  return `---
370
- id: ${idSlug}
370
+ id: ${yamlNoteId(noteIdFor(null, idSlug))}
371
371
  type: session
372
372
  title: "Sessions ${month} (${label}, metrics only)"
373
373
  status: verified
@@ -144,6 +144,85 @@ export function slugifyIdSafe(input) {
144
144
  return slug.length >= 2 ? slug : `${slug}-x`;
145
145
  }
146
146
 
147
+ /** The vault-sync schema's `id` cap (`skills/vault-sync/validator.mjs`, `.max(128)`). */
148
+ export const NOTE_ID_MAX_LENGTH = 128;
149
+
150
+ /**
151
+ * The frontmatter `id` of a generated note (#1601, #1602). The FILENAME slug
152
+ * stays the note's identity on disk (paths never change); the `id` adds the
153
+ * repo namespace so it is unique across the vault's per-repo folders — a
154
+ * session id or learning subject recurs across repos — and is capped at
155
+ * {@link NOTE_ID_MAX_LENGTH}. An over-long id keeps a 119-char prefix plus an
156
+ * 8-hex SHA-256 suffix of the FULL id, so two long subjects sharing a prefix
157
+ * stay distinct. Deterministic: the same `(repoNs, slug)` always yields the
158
+ * same id. Without a namespace (vault-less callers, unit tests) the slug is
159
+ * only capped.
160
+ *
161
+ * BV-004 ceiling: the separator is a single `-` (the schema slug forbids
162
+ * `--`), so namespace `a` + slug `b-c` and namespace `a-b` + slug `c` render
163
+ * the same id. Revisit-Trigger: the vault validator reports a duplicate id
164
+ * whose two notes sit in different namespace folders.
165
+ *
166
+ * @param {string|null|undefined} repoNs - the resolved repo namespace folder.
167
+ * @param {string} slug - the note's filename slug (or rendered session slug).
168
+ * @returns {string}
169
+ */
170
+ export function noteIdFor(repoNs, slug) {
171
+ const base = String(slug ?? '');
172
+ const ns = typeof repoNs === 'string' && repoNs.length > 0 ? (isValidSlug(repoNs) ? repoNs : legacySubjectToSlug(repoNs)) : '';
173
+ const full = ns ? `${ns}-${base}` : base;
174
+ if (full.length <= NOTE_ID_MAX_LENGTH) return full;
175
+ const suffix = digestSha256Short(full);
176
+ return `${full.slice(0, NOTE_ID_MAX_LENGTH - suffix.length - 1).replace(/-+$/, '')}-${suffix}`;
177
+ }
178
+
179
+ /**
180
+ * Does a note's frontmatter `id` name the note `slug`? Accepts both the
181
+ * pre-#1601 id (the bare slug — a numeric one parses as the same string here,
182
+ * `parseFrontmatter` keeps scalars verbatim) and the current {@link noteIdFor}
183
+ * form, so notes written before the id change keep their ownership evidence.
184
+ *
185
+ * @param {unknown} fmId
186
+ * @param {string} slug
187
+ * @param {string|null|undefined} repoNs
188
+ * @returns {boolean}
189
+ */
190
+ export function noteIdMatches(fmId, slug, repoNs) {
191
+ return typeof fmId === 'string' && (fmId === slug || fmId === noteIdFor(repoNs, slug));
192
+ }
193
+
194
+ /**
195
+ * The `id:` frontmatter value, always a double-quoted YAML string (#1601): an
196
+ * unquoted all-digit id (`id: 433`) parses as a NUMBER under a real YAML parser
197
+ * and fails the schema's `z.string()`.
198
+ *
199
+ * @param {string} id
200
+ * @returns {string}
201
+ */
202
+ export function yamlNoteId(id) {
203
+ return JSON.stringify(String(id));
204
+ }
205
+
206
+ /**
207
+ * Replace the frontmatter `id:` line of a note — used when a displaced note
208
+ * moves to its disambiguated name (#1506.2), whose `id` must match the file,
209
+ * and by the id migration (#1601, `migrate-ids.mjs`). Only the `id:` line
210
+ * INSIDE the leading frontmatter block changes; every other byte — body,
211
+ * other fields, `[REDACTED]` spans (#1025) — is kept verbatim. A note without
212
+ * a frontmatter `id:` line is returned unchanged.
213
+ * @param {string} content
214
+ * @param {string} id
215
+ * @returns {string}
216
+ */
217
+ export function withNoteId(content, id) {
218
+ if (!content.startsWith('---')) return content;
219
+ const end = content.indexOf('\n---', 3);
220
+ if (end === -1) return content;
221
+ const head = content.slice(0, end);
222
+ const nextHead = head.replace(/^id:.*$/m, `id: ${yamlNoteId(id)}`);
223
+ return nextHead === head ? content : nextHead + content.slice(end);
224
+ }
225
+
147
226
  /** Derive the first 8 chars of a UUID (strip hyphens, take first 8 hex chars). */
148
227
  export function uuidPrefix8(id) {
149
228
  return id.replace(/-/g, '').slice(0, 8);
@@ -23,7 +23,7 @@
23
23
  */
24
24
 
25
25
  import { existsSync, readFileSync } from 'node:fs';
26
- import path from 'node:path';
26
+ import { resolveLedgerPath } from './ledger-path.mjs';
27
27
 
28
28
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
29
29
 
@@ -95,12 +95,9 @@ export function checkVaultStaleness({ repoRoot, now = Date.now() } = {}) {
95
95
  try {
96
96
  if (!repoRoot || typeof repoRoot !== 'string') return null;
97
97
 
98
- const jsonlPath = path.join(
99
- repoRoot,
100
- '.orchestrator',
101
- 'metrics',
102
- 'vault-staleness.jsonl',
103
- );
98
+ // #1722: the ledger the probe writes (skills/discovery/probes/vault-staleness.mjs)
99
+ // — the main checkout's when repoRoot is a linked worktree that ignores it.
100
+ const jsonlPath = resolveLedgerPath('.orchestrator/metrics/vault-staleness.jsonl', repoRoot);
104
101
 
105
102
  if (!existsSync(jsonlPath)) return null;
106
103
 
@@ -1339,7 +1339,8 @@ export function buildSweepRepos(candidates, { thisRepoRoot } = {}) {
1339
1339
  * @param {object} [opts]
1340
1340
  * @param {string} opts.repoRoot — the calling repo (always included in the sweep).
1341
1341
  * @param {string} [opts.startDir] — enumeration root; omitted in production so
1342
- * {@link enumerateCandidates} defaults to `getConfinementRoot()` (~/Projects).
1342
+ * {@link enumerateCandidates} defaults to `getConfinementRoot()` (~/Projects
1343
+ * unless the host sets `projects-root`, #1562).
1343
1344
  * Test seam only — do NOT compute `path.dirname(repoRoot)` here.
1344
1345
  * @param {Date|number} [opts.now] — clock seam. A `Date` is used as-is; a finite
1345
1346
  * number is treated as epoch-ms (matching {@link collectRows}'s own
@@ -31,12 +31,15 @@ import { readFile } from 'node:fs/promises';
31
31
  import path from 'node:path';
32
32
 
33
33
  import { emitEvent, sessionAttribution } from '../events.mjs';
34
- import { parseStateMd, parseMissionStatus, resolveStateMdPath } from '../state-md.mjs';
34
+ import { parseStateMd, parseMissionStatus } from '../state-md.mjs';
35
+ import { resolveStateMdWriteTarget, warnForeignStateMd } from '../state-md/frontmatter-mutators.mjs';
35
36
  import {
36
37
  parseFrontmatter,
37
38
  toDate,
38
39
  yamlQuoteIfNeeded,
39
40
  subjectToSlug,
41
+ noteIdFor,
42
+ yamlNoteId,
40
43
  } from '../vault-mirror/utils.mjs';
41
44
  import { matchesModuloRedaction } from '../vault-mirror/process.mjs';
42
45
  import { readConfigFile, parseSessionConfig } from '../config.mjs';
@@ -317,7 +320,7 @@ export function renderNarrative(opts) {
317
320
 
318
321
  // Frontmatter — `_generator` LAST (markdown-writer / render-sessions convention).
319
322
  lines.push('---');
320
- lines.push(`id: ${noteId}`);
323
+ lines.push(`id: ${yamlNoteId(noteIdFor(null, noteId))}`);
321
324
  lines.push('type: session');
322
325
  lines.push(`title: ${title}`);
323
326
  lines.push(`created: ${createdValue}`);
@@ -380,6 +383,31 @@ export function resolveNarrativePath(vaultDir, repoSlug) {
380
383
  return path.join(vaultDir, '01-projects', repoSlug, '_session-narrative.md');
381
384
  }
382
385
 
386
+ /**
387
+ * The `created` date of the narrative already at `outputPath` when THIS generator
388
+ * wrote it (#1602; same rule as board-writer.mjs). A hand-authored or foreign
389
+ * file, a missing one, or a malformed date yields `undefined` — the render then
390
+ * stamps today, exactly as on a first write. A read failure is not fatal here:
391
+ * `writeNarrative` reads the file again and owns that error contract.
392
+ *
393
+ * @param {string} outputPath
394
+ * @param {{ readFileSync?: Function, existsSync?: Function }} [injectedFs]
395
+ * @returns {string|undefined} `YYYY-MM-DD`
396
+ */
397
+ function existingNarrativeCreated(outputPath, injectedFs) {
398
+ const fsExists = injectedFs?.existsSync ?? existsSync;
399
+ const fsReadFile = injectedFs?.readFileSync ?? readFileSync;
400
+ try {
401
+ if (!fsExists(outputPath)) return undefined;
402
+ const fm = parseFrontmatter(fsReadFile(outputPath, 'utf8'));
403
+ if (fm?.['_generator'] !== GENERATOR_MARKER) return undefined;
404
+ const created = fm['created'];
405
+ return typeof created === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(created) ? created : undefined;
406
+ } catch {
407
+ return undefined;
408
+ }
409
+ }
410
+
383
411
  // ── Idempotent write ─────────────────────────────────────────────────────────────
384
412
 
385
413
  /**
@@ -393,6 +421,26 @@ function normalizeUpdated(content) {
393
421
  return content.replace(/^(updated:\s*)(.+)$/m, `$1${UPDATED_PLACEHOLDER}`);
394
422
  }
395
423
 
424
+ /**
425
+ * The comparison form of a narrative for the noop check: {@link normalizeUpdated}
426
+ * plus the `id:` line dropped to a bare `id:` (#1601, release-review HOCH 2).
427
+ * The id differs between generations of the renderer in more than one way: the
428
+ * render now quotes it (`id: "x"`) where older files carry `id: x`, and
429
+ * `noteIdFor` caps it, so a pre-cap file can carry a longer value. Any such
430
+ * difference made the whole document "changed", which skipped the `[REDACTED]`
431
+ * comparison below and rewrote the file — with the raw secret whenever it was
432
+ * missing from the current env (#1025). The id is derived from the note's
433
+ * path, not from its content, so no id difference may ever trigger a rewrite;
434
+ * a schema-breaking id on disk is the job of `vault-mirror/migrate-ids.mjs`,
435
+ * which changes that one line and nothing else.
436
+ *
437
+ * @param {string} content
438
+ * @returns {string}
439
+ */
440
+ function normalizeForNoopCompare(content) {
441
+ return normalizeUpdated(content).replace(/^id:.*$/m, 'id:');
442
+ }
443
+
396
444
  /**
397
445
  * Write the narrative file with idempotency guards (mirrors gitlab-portfolio
398
446
  * markdown-writer.mjs contract).
@@ -501,8 +549,9 @@ export function writeNarrative(opts) {
501
549
  // #1025 — identical once every `[REDACTED]` span already on disk is read
502
550
  // as a wildcard. See the guard-order note above for why the second arm is
503
551
  // load-bearing rather than a nicety.
504
- const existingNormalized = normalizeUpdated(existingContent);
505
- const candidateNormalized = normalizeUpdated(content);
552
+ // #1601: the id line is left out of the compare — see normalizeForNoopCompare.
553
+ const existingNormalized = normalizeForNoopCompare(existingContent);
554
+ const candidateNormalized = normalizeForNoopCompare(content);
506
555
  if (
507
556
  existingNormalized === candidateNormalized ||
508
557
  matchesModuloRedaction(existingNormalized, candidateNormalized)
@@ -963,8 +1012,15 @@ async function runNarrativeMirror(opts) {
963
1012
  return { result: { action: 'skipped-invalid-path', path: outputPath }, needleCount: masker.needleCount, hits: 0, dryRun };
964
1013
  }
965
1014
 
966
- // Read STATE.md (best-effort; absent STATE.md → nothing to mirror).
967
- const stateMdPath = resolveStateMdPath(repoRoot);
1015
+ // Read STATE.md (best-effort; absent STATE.md → nothing to mirror). #1622:
1016
+ // another platform's STATE.md not proven to be this process's counts as absent.
1017
+ // #1640: `rejected` is also set when the target ITSELF is foreign
1018
+ // (`blocked`) — never read and mirror that file either.
1019
+ const { path: stateMdPath, rejected } = resolveStateMdWriteTarget(repoRoot);
1020
+ if (rejected) {
1021
+ warnForeignStateMd('mirrorNarrative', rejected, stateMdPath);
1022
+ return { result: { action: 'skipped-no-statemd', path: outputPath }, needleCount: masker.needleCount, hits: 0, dryRun };
1023
+ }
968
1024
  let stateContents;
969
1025
  try {
970
1026
  stateContents = await readFile(stateMdPath, 'utf8');
@@ -976,7 +1032,10 @@ async function runNarrativeMirror(opts) {
976
1032
  // parse (so `[REDACTED]` can never break it) and before the render, the
977
1033
  // idempotency comparison and the write. See maskNarrative above.
978
1034
  const { narrative, hits } = maskNarrative(extractNarrative(stateContents), masker);
979
- const content = renderNarrative({ repo: repoName, narrative, now });
1035
+ // #1602: `created` is the day the narrative was FIRST written — carried over
1036
+ // from the file on disk, so only `updated` moves between runs.
1037
+ const createdIso = existingNarrativeCreated(outputPath, injectedFs);
1038
+ const content = renderNarrative({ repo: repoName, narrative, now, createdIso });
980
1039
 
981
1040
  // `chars` measures the document THIS call rendered — so it is present on
982
1041
  // `skipped-noop` and `dry-run` too, where the render happened but nothing
@@ -80,6 +80,25 @@ import {
80
80
  * ever observed writing outside its worktree despite this flag, the sandbox is
81
81
  * not the boundary it is assumed to be here and the `hookTampering` check below
82
82
  * becomes the only defence rather than the backstop.
83
+ *
84
+ * `--disable-codebase-ref` keeps the CLI from starting its codebase index
85
+ * (#1683). Without it every run leaves a `worker-server` and an
86
+ * `npx typescript-language-server --stdio` pair behind; they detach via setsid,
87
+ * so the timeout kill in `runChild` never reaches them. Measured 2026-10-19 on
88
+ * cursor-agent 2026.10.01-e373342: 3 orphans per run without the flag, 0 with
89
+ * it, same answer either way. The foreign agent loses semantic search and
90
+ * LSP-backed lints; read and grep stay, and the Claude diff review remains the
91
+ * quality anchor. Named ceiling (BV-004): the flag is registered in the CLI but
92
+ * missing from `--help`, so a release may drop it silently. Revisit trigger: a
93
+ * `worker-server` or `typescript-language-server` with PPID 1 after a dispatch.
94
+ *
95
+ * The hidden `--disable-indexing` flag is deliberately absent (#1687): it only
96
+ * gates the codebase sync inside that same `worker-server`, which this argv
97
+ * never starts. Measured 2026-10-20 on cursor-agent 2026.10.01-e373342 with
98
+ * per-process `nettop` against a 1.5 MB throwaway repo: 52–71 KB out per run
99
+ * with or without the flag, no worker-server, while a 1.5 MB loopback control
100
+ * read back as exactly 1,500,000 bytes. Revisit trigger: a dispatch whose
101
+ * outbound volume grows with the worktree size.
83
102
  * @type {readonly string[]}
84
103
  */
85
104
  export const FOREIGN_AGENT_BASE_ARGS = Object.freeze([
@@ -87,6 +106,7 @@ export const FOREIGN_AGENT_BASE_ARGS = Object.freeze([
87
106
  '--force',
88
107
  '--sandbox',
89
108
  'enabled',
109
+ '--disable-codebase-ref',
90
110
  ]);
91
111
 
92
112
  /**
@@ -0,0 +1,137 @@
1
+ /** Windows tree cleanup: PowerShell 5.1 CIM identities, then taskkill and readback. */
2
+ import { spawnSync } from 'node:child_process';
3
+ import path from 'node:path';
4
+
5
+ // Ceiling per native call: 5 s. Revisit if measured CIM startup exceeds this;
6
+ // a hung PowerShell must never hang gate cleanup (including the exit handler).
7
+ const OPTIONS = { encoding: 'utf8', shell: false, windowsHide: true, timeout: 5000, maxBuffer: 16 * 1024 * 1024 };
8
+ const system32 = () => path.join(process.env.SystemRoot || 'C:\\Windows', 'System32');
9
+
10
+ function readWindowsProcesses(spawnSyncFn = spawnSync) {
11
+ const result = spawnSyncFn(
12
+ path.join(system32(), 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
13
+ ['-NoProfile', '-NonInteractive', '-Command', 'Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CreationDate | ConvertTo-Json -Compress'],
14
+ OPTIONS,
15
+ );
16
+ if (result.error || result.status !== 0) throw new Error('CIM snapshot failed');
17
+ let parsed;
18
+ try {
19
+ parsed = JSON.parse(String(result.stdout));
20
+ } catch {
21
+ // A truncated or banner-polluted snapshot is "not measurable", never "no processes".
22
+ throw new Error('CIM snapshot output is not JSON');
23
+ }
24
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
25
+ if (!rows.length || rows.some(p => !p || !Number.isInteger(p.ProcessId)
26
+ || !Number.isInteger(p.ParentProcessId))) {
27
+ throw new Error('CIM snapshot has no usable process identities');
28
+ }
29
+ return rows;
30
+ }
31
+
32
+ // PowerShell 5.1 ConvertTo-Json uses /Date(epoch milliseconds)/.
33
+ // Other formats (including WMI dates) remain explicitly unmeasurable.
34
+ function creationMs(row) {
35
+ const match = /^\/Date\((-?\d+)\)\/$/.exec(row?.CreationDate);
36
+ return match && Number.isFinite(Number(match[1])) ? Number(match[1]) : null;
37
+ }
38
+
39
+ function descendantIds(rows, seeds) {
40
+ const ids = new Set(seeds);
41
+ let size;
42
+ do {
43
+ size = ids.size;
44
+ for (const p of rows) if (ids.has(p.ParentProcessId)) ids.add(p.ProcessId);
45
+ } while (ids.size !== size);
46
+ return ids;
47
+ }
48
+
49
+ /** Same synchronous primitive for timeout and process-exit cleanup.
50
+ * A live ChildProcess holds libuv's Windows handle, preventing root PID reuse.
51
+ * After exit, only dated descendants may be targeted; the root is never killed.
52
+ * null survivors means unmeasurable, never a successful empty measurement.
53
+ */
54
+ export function killWindowsProcessTree(pid, {
55
+ spawnSyncFn = spawnSync,
56
+ snapshotFn = () => readWindowsProcesses(spawnSyncFn),
57
+ child,
58
+ spawnedAtMs,
59
+ verifyTree = true,
60
+ } = {}) {
61
+ const base = { signalsSent: [], aborted: null };
62
+ const unknown = reason => ({ ...base, ok: false, survivors: null, error: `nicht messbar auf win32: ${reason}` });
63
+ const rootHeld = child?.exitCode === null && child?.signalCode === null;
64
+ let killError = null;
65
+ const kill = (target, tree = true) => {
66
+ base.signalsSent.push('taskkill');
67
+ try {
68
+ const args = tree ? ['/PID', String(target), '/T', '/F'] : ['/PID', String(target), '/F'];
69
+ const result = spawnSyncFn(path.join(system32(), 'taskkill.exe'), args, OPTIONS);
70
+ if (result.error || result.status !== 0) killError = result.error?.message ?? `taskkill exit ${result.status}`;
71
+ } catch (err) { killError = String(err); }
72
+ };
73
+ if (!verifyTree) {
74
+ if (rootHeld) kill(pid);
75
+ return unknown(`nicht gemessen (verifyTree: false)${killError ? `; ${killError}` : ''}`);
76
+ }
77
+
78
+ let before;
79
+ let measurementError;
80
+ try {
81
+ before = snapshotFn();
82
+ if (!Array.isArray(before)) throw Error('invalid snapshot');
83
+ } catch (err) { measurementError = err; }
84
+ if (measurementError) {
85
+ if (rootHeld) kill(pid);
86
+ return unknown(measurementError);
87
+ }
88
+ const ids = descendantIds(before, [pid]);
89
+ const floor = Number.isFinite(spawnedAtMs) ? spawnedAtMs - 2000 : null;
90
+ let targets = before.filter(p => ids.has(p.ProcessId));
91
+ if (!rootHeld) {
92
+ targets = targets.filter(p => p.ProcessId !== pid);
93
+ if (floor === null || targets.some(p => creationMs(p) === null)) return unknown('missing descendant CreationDate or spawnedAtMs');
94
+ targets = targets.filter(p => creationMs(p) >= floor);
95
+ for (const p of targets) kill(p.ProcessId);
96
+ } else {
97
+ // Windows never rewrites ParentProcessId when a parent dies, so a process
98
+ // older than our root can still name the root's (reused) PID as its parent
99
+ // (Opus review M2, #1612). Such a process is not ours: drop it from the
100
+ // targets, and when one exists, end ours one by one without /T so
101
+ // taskkill's own PPID walk cannot reach it.
102
+ const rootBorn = creationMs(targets.find(p => p.ProcessId === pid)) ?? floor;
103
+ const stale = rootBorn === null ? [] : targets.filter(p => p.ProcessId !== pid
104
+ && creationMs(p) !== null && creationMs(p) < rootBorn);
105
+ if (stale.length === 0) {
106
+ kill(pid);
107
+ } else {
108
+ targets = targets.filter(p => !stale.includes(p));
109
+ for (const p of targets) kill(p.ProcessId, false);
110
+ }
111
+ }
112
+
113
+ let after;
114
+ try {
115
+ after = snapshotFn();
116
+ if (!Array.isArray(after)) throw Error('invalid snapshot');
117
+ } catch (err) { return unknown(err); }
118
+ if ((rootHeld && !targets.some(p => p.ProcessId === pid))
119
+ || targets.some(p => creationMs(p) === null)
120
+ || after.some(p => targets.some(q => q.ProcessId === p.ProcessId) && creationMs(p) === null)) {
121
+ return unknown('missing tree identity');
122
+ }
123
+ const survivors = new Set(targets.filter(p => after.some(q =>
124
+ q.ProcessId === p.ProcessId && q.CreationDate === p.CreationDate)).map(p => p.ProcessId));
125
+
126
+ // Include descendants born during taskkill, following parents through both
127
+ // snapshots even when an intermediate parent has already disappeared.
128
+ const treeIds = new Set([pid, ...targets.map(p => p.ProcessId)]);
129
+ const related = descendantIds([...before, ...after], treeIds);
130
+ for (const p of after) {
131
+ if (p.ProcessId === pid || !related.has(p.ParentProcessId)) continue;
132
+ const date = creationMs(p);
133
+ if (date === null || floor === null) return unknown('missing descendant CreationDate or spawnedAtMs');
134
+ if (date >= floor) survivors.add(p.ProcessId);
135
+ }
136
+ return { ...base, ok: survivors.size === 0 && !killError, survivors: [...survivors], error: killError };
137
+ }
@@ -13,7 +13,7 @@
13
13
  import fs from 'node:fs/promises';
14
14
  import fsSync from 'node:fs';
15
15
  import path from 'node:path';
16
- import { $ } from 'zx';
16
+ import { $ } from './zx.mjs';
17
17
  import { listWorktrees } from './worktree.mjs';
18
18
 
19
19
  // Do not spam stdout/stderr with git command echoes.
@@ -7,7 +7,7 @@
7
7
  * Side effect: configures zx to suppress git command echoes on load.
8
8
  */
9
9
 
10
- import { $ } from 'zx';
10
+ import { $ } from '../zx.mjs';
11
11
 
12
12
  // Do not spam stdout/stderr with git command echoes.
13
13
  $.verbose = false;