session-orchestrator 3.23.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (393) hide show
  1. package/.agents/skills/architecture/SKILL.md +18 -0
  2. package/.agents/skills/autopilot/SKILL.md +17 -0
  3. package/.agents/skills/bootstrap/SKILL.md +20 -0
  4. package/.agents/skills/brainstorm/SKILL.md +22 -0
  5. package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
  6. package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
  7. package/.agents/skills/debug/SKILL.md +22 -0
  8. package/.agents/skills/discovery/SKILL.md +20 -0
  9. package/.agents/skills/dispatcher/SKILL.md +15 -0
  10. package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
  11. package/.agents/skills/ecosystem-health/SKILL.md +20 -0
  12. package/.agents/skills/eli5/SKILL.md +20 -0
  13. package/.agents/skills/eval/SKILL.md +21 -0
  14. package/.agents/skills/evolve/SKILL.md +21 -0
  15. package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
  16. package/.agents/skills/gitlab-ops/SKILL.md +20 -0
  17. package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
  18. package/.agents/skills/grill/SKILL.md +22 -0
  19. package/.agents/skills/hook-development/SKILL.md +15 -0
  20. package/.agents/skills/mcp-builder/SKILL.md +15 -0
  21. package/.agents/skills/memory-cleanup/SKILL.md +21 -0
  22. package/.agents/skills/mode-selector/SKILL.md +17 -0
  23. package/.agents/skills/npm-publish/SKILL.md +16 -0
  24. package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
  25. package/.agents/skills/persona-panel/SKILL.md +17 -0
  26. package/.agents/skills/plan/SKILL.md +20 -0
  27. package/.agents/skills/playwright-driver/SKILL.md +20 -0
  28. package/.agents/skills/quality-gates/SKILL.md +20 -0
  29. package/.agents/skills/reconcile/SKILL.md +21 -0
  30. package/.agents/skills/remote-offload/SKILL.md +20 -0
  31. package/.agents/skills/repo-audit/SKILL.md +16 -0
  32. package/.agents/skills/session-end/SKILL.md +20 -0
  33. package/.agents/skills/session-plan/SKILL.md +20 -0
  34. package/.agents/skills/session-start/SKILL.md +20 -0
  35. package/.agents/skills/spinout/SKILL.md +16 -0
  36. package/.agents/skills/sunset-review/SKILL.md +16 -0
  37. package/.agents/skills/test-runner/SKILL.md +20 -0
  38. package/.agents/skills/tmux-layout/SKILL.md +21 -0
  39. package/.agents/skills/using-orchestrator/SKILL.md +17 -0
  40. package/.agents/skills/vault-mirror/SKILL.md +15 -0
  41. package/.agents/skills/vault-sync/SKILL.md +15 -0
  42. package/.agents/skills/wave-executor/SKILL.md +20 -0
  43. package/.agents/skills/write-executable-plan/SKILL.md +22 -0
  44. package/.claude-plugin/marketplace.json +1 -1
  45. package/.claude-plugin/plugin.json +1 -1
  46. package/.codex-plugin/plugin.json +1 -1
  47. package/.cursor/commands/autopilot.md +2 -2
  48. package/.cursor/commands/bootstrap.md +1 -1
  49. package/.cursor/commands/brainstorm.md +1 -1
  50. package/.cursor/commands/debug.md +1 -1
  51. package/.cursor/commands/discovery.md +1 -1
  52. package/.cursor/commands/dispatcher.md +2 -2
  53. package/.cursor/commands/eli5.md +2 -2
  54. package/.cursor/commands/eval.md +2 -2
  55. package/.cursor/commands/evolve.md +1 -1
  56. package/.cursor/commands/go.md +1 -1
  57. package/.cursor/commands/grill.md +2 -2
  58. package/.cursor/commands/memory-cleanup.md +2 -2
  59. package/.cursor/commands/persona-panel.md +1 -1
  60. package/.cursor/commands/plan.md +1 -1
  61. package/.cursor/commands/portfolio.md +1 -1
  62. package/.cursor/commands/reconcile.md +2 -2
  63. package/.cursor/commands/release.md +2 -2
  64. package/.cursor/commands/session.md +2 -2
  65. package/.cursor/commands/spinout.md +2 -2
  66. package/.cursor/commands/sunset-review.md +2 -2
  67. package/.cursor/commands/templates-ack.md +2 -2
  68. package/.cursor/commands/test.md +2 -2
  69. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  70. package/.cursor/skills/eval/SKILL.md +1 -1
  71. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  72. package/.cursor/skills/remote-offload/SKILL.md +13 -0
  73. package/.orchestrator/policy/blocked-commands.json +121 -0
  74. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  75. package/.orchestrator/policy/quality-gates.example.json +16 -0
  76. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  77. package/.orchestrator/policy/templates-policy.json +27 -0
  78. package/.orchestrator/policy/test-profiles.json +47 -0
  79. package/AGENTS.md +225 -0
  80. package/CHANGELOG.md +1401 -0
  81. package/NOTICE +11 -6
  82. package/README.md +127 -92
  83. package/agents/db-specialist.md +0 -1
  84. package/agents/eval-judge.md +1 -1
  85. package/agents/skill-applied-judge.md +1 -1
  86. package/assets/wave-lifecycle.svg +98 -0
  87. package/commands/release.md +6 -3
  88. package/commands/session.md +18 -3
  89. package/docs/README.md +4 -0
  90. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  91. package/docs/baseline.md +67 -0
  92. package/docs/ci-setup.md +249 -48
  93. package/docs/codex-setup.md +66 -22
  94. package/docs/components.md +37 -16
  95. package/docs/cursor-setup.md +6 -2
  96. package/docs/events-schema.md +51 -10
  97. package/docs/instruction-delivery.md +62 -0
  98. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  99. package/docs/migration-v4.md +341 -0
  100. package/docs/pi-setup.md +6 -1
  101. package/docs/plugin-architecture-v3.md +1 -1
  102. package/docs/rule-authoring.md +85 -19
  103. package/docs/scope-collision-guard.md +8 -8
  104. package/docs/session-config-reference.md +120 -61
  105. package/docs/session-config-template.md +40 -33
  106. package/docs/telemetry/telemetry-claims.md +11 -10
  107. package/docs/telemetry.md +187 -4
  108. package/docs/vault-docs-architecture.md +50 -11
  109. package/hooks/_lib/atomic-json.mjs +111 -0
  110. package/hooks/_lib/hook-import-set.json +1487 -0
  111. package/hooks/_lib/subagent-paths.mjs +143 -0
  112. package/hooks/_lib/subagent-transcript.mjs +562 -0
  113. package/hooks/config-protection.mjs +2 -2
  114. package/hooks/cwd-change-restore.mjs +11 -31
  115. package/hooks/enforce-commands.mjs +69 -0
  116. package/hooks/enforce-scope.mjs +35 -6
  117. package/hooks/hooks-codex.json +1 -1
  118. package/hooks/hooks-cursor.json +10 -0
  119. package/hooks/hooks-pi.json +5 -0
  120. package/hooks/hooks.json +6 -1
  121. package/hooks/loop-guard.mjs +3 -3
  122. package/hooks/on-session-end.mjs +280 -14
  123. package/hooks/on-session-start.mjs +153 -4
  124. package/hooks/on-stop.mjs +371 -17
  125. package/hooks/operator-steer.mjs +2 -2
  126. package/hooks/post-bash-write-verify.mjs +189 -4
  127. package/hooks/post-edit-import-probe.mjs +344 -0
  128. package/hooks/post-subagent-discovery-validator.mjs +278 -392
  129. package/hooks/post-tool-batch-wave-signal.mjs +272 -44
  130. package/hooks/post-tool-failure-corrective-context.mjs +11 -34
  131. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  132. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  133. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  134. package/hooks/skill-invocation-telemetry.mjs +17 -5
  135. package/hooks/subagent-telemetry.mjs +24 -30
  136. package/monitors/monitors.json +3 -3
  137. package/package.json +9 -1
  138. package/pi/prompts/session.md +2 -2
  139. package/plugin.json +27 -0
  140. package/scripts/autopilot.mjs +26 -12
  141. package/scripts/backfill-abandoned-sessions.mjs +130 -15
  142. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  143. package/scripts/dialectic-deriver.mjs +73 -8
  144. package/scripts/emit-event.mjs +10 -2
  145. package/scripts/export-hw-learnings.mjs +113 -1
  146. package/scripts/generate-agents-skills.mjs +378 -0
  147. package/scripts/generate-cursor-adapter.mjs +45 -8
  148. package/scripts/generate-hook-import-set.mjs +249 -0
  149. package/scripts/lib/agent-status.mjs +13 -2
  150. package/scripts/lib/auq/parse.mjs +5 -29
  151. package/scripts/lib/auto-dialectic.mjs +68 -0
  152. package/scripts/lib/auto-dream.mjs +38 -36
  153. package/scripts/lib/autonomy/suitability.mjs +6 -0
  154. package/scripts/lib/autopilot/loop.mjs +2 -2
  155. package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
  156. package/scripts/lib/build-live-signals.mjs +25 -22
  157. package/scripts/lib/ci-status-banner.mjs +220 -75
  158. package/scripts/lib/codex/plugin-contract.mjs +82 -6
  159. package/scripts/lib/cold-start-detector.mjs +23 -14
  160. package/scripts/lib/config/auto-dream.mjs +2 -1
  161. package/scripts/lib/config/block-header.mjs +63 -0
  162. package/scripts/lib/config/block-preprocess.mjs +177 -0
  163. package/scripts/lib/config/broken-window.mjs +2 -1
  164. package/scripts/lib/config/cold-start.mjs +2 -1
  165. package/scripts/lib/config/config-protection.mjs +22 -2
  166. package/scripts/lib/config/context-coverage.mjs +2 -1
  167. package/scripts/lib/config/cross-repo.mjs +2 -1
  168. package/scripts/lib/config/custom-phases.mjs +2 -1
  169. package/scripts/lib/config/dialectic.mjs +2 -1
  170. package/scripts/lib/config/discovery-validator.mjs +9 -3
  171. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  172. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  173. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  174. package/scripts/lib/config/docs-staleness.mjs +2 -1
  175. package/scripts/lib/config/drift-check.mjs +2 -1
  176. package/scripts/lib/config/eval.mjs +2 -1
  177. package/scripts/lib/config/events-rotation.mjs +2 -1
  178. package/scripts/lib/config/evolve.mjs +8 -2
  179. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  180. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  181. package/scripts/lib/config/handover-gate.mjs +2 -1
  182. package/scripts/lib/config/health-endpoints.mjs +388 -0
  183. package/scripts/lib/config/issue-budget.mjs +2 -1
  184. package/scripts/lib/config/loop-guard.mjs +2 -1
  185. package/scripts/lib/config/memory.mjs +2 -1
  186. package/scripts/lib/config/moc-staleness.mjs +2 -1
  187. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  188. package/scripts/lib/config/private-config-dir.mjs +67 -0
  189. package/scripts/lib/config/reconcile.mjs +2 -1
  190. package/scripts/lib/config/remote-hosts.mjs +234 -0
  191. package/scripts/lib/config/section-extractor.mjs +7 -1
  192. package/scripts/lib/config/skill-evolution.mjs +2 -1
  193. package/scripts/lib/config/slopcheck.mjs +2 -1
  194. package/scripts/lib/config/state-md-lock.mjs +2 -1
  195. package/scripts/lib/config/templates-first.mjs +2 -1
  196. package/scripts/lib/config/test.mjs +2 -1
  197. package/scripts/lib/config/vault-integration.mjs +7 -1
  198. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  199. package/scripts/lib/config/vault-staleness.mjs +2 -1
  200. package/scripts/lib/config/vault-sync.mjs +2 -1
  201. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  202. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  203. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  204. package/scripts/lib/config.mjs +31 -3
  205. package/scripts/lib/convergence-monitor.mjs +82 -16
  206. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  207. package/scripts/lib/dispatcher/rank.mjs +124 -48
  208. package/scripts/lib/ecosystem-health.mjs +16 -2
  209. package/scripts/lib/eval/engine.mjs +9 -1
  210. package/scripts/lib/eval/session-resolve.mjs +23 -4
  211. package/scripts/lib/events-schema.mjs +48 -0
  212. package/scripts/lib/events.mjs +256 -7
  213. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  214. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  215. package/scripts/lib/frontmatter-guard.mjs +131 -13
  216. package/scripts/lib/gates/gate-full.mjs +26 -0
  217. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  218. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  219. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  220. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  221. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  222. package/scripts/lib/host-identity.mjs +50 -11
  223. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  224. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  225. package/scripts/lib/learnings/io.mjs +60 -6
  226. package/scripts/lib/memory-banner.mjs +20 -8
  227. package/scripts/lib/memory-proposals/store.mjs +30 -22
  228. package/scripts/lib/owner-config-banner.mjs +43 -6
  229. package/scripts/lib/owner-config-loader.mjs +21 -10
  230. package/scripts/lib/owner-interview.mjs +3 -3
  231. package/scripts/lib/owner-yaml.mjs +207 -14
  232. package/scripts/lib/peer-discovery.mjs +20 -2
  233. package/scripts/lib/platform.mjs +108 -15
  234. package/scripts/lib/plugin-update-banner.mjs +406 -0
  235. package/scripts/lib/project-hygiene.mjs +38 -2
  236. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  237. package/scripts/lib/quality-gate.mjs +133 -44
  238. package/scripts/lib/reconcile/emitter.mjs +68 -6
  239. package/scripts/lib/reconcile/engine.mjs +249 -9
  240. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  241. package/scripts/lib/reconcile/writer.mjs +40 -18
  242. package/scripts/lib/scope-gate.mjs +36 -0
  243. package/scripts/lib/session-close-backfill.mjs +125 -18
  244. package/scripts/lib/session-discovery.mjs +57 -3
  245. package/scripts/lib/session-end/phase-skip.mjs +2 -2
  246. package/scripts/lib/session-id.mjs +12 -23
  247. package/scripts/lib/session-identity/own-session.mjs +187 -11
  248. package/scripts/lib/session-lock-shape.mjs +43 -0
  249. package/scripts/lib/session-lock.mjs +5 -10
  250. package/scripts/lib/session-registry.mjs +25 -9
  251. package/scripts/lib/session-schema/constants.mjs +36 -2
  252. package/scripts/lib/session-schema/validator.mjs +38 -4
  253. package/scripts/lib/session-start-probes.mjs +18 -1
  254. package/scripts/lib/session-transition.mjs +1 -1
  255. package/scripts/lib/sessions-canonical.mjs +446 -0
  256. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  257. package/scripts/lib/skill-health/join.mjs +17 -4
  258. package/scripts/lib/state-md.mjs +78 -0
  259. package/scripts/lib/sunset/walker.mjs +6 -0
  260. package/scripts/lib/telemetry/schema.mjs +255 -17
  261. package/scripts/lib/telemetry/sync.mjs +417 -24
  262. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  263. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  264. package/scripts/lib/validate/check-agents.mjs +3 -3
  265. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  266. package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
  267. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  268. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  269. package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
  270. package/scripts/lib/validate/check-skill-links.mjs +163 -0
  271. package/scripts/lib/validate/check-skill-script-paths.mjs +455 -0
  272. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  273. package/scripts/lib/validate/check-unwired-features.mjs +0 -9
  274. package/scripts/lib/validate/check-validator-registration.mjs +254 -0
  275. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  276. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  277. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  278. package/scripts/lib/vault-backfill/template.mjs +63 -6
  279. package/scripts/lib/vault-mirror/process.mjs +165 -42
  280. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  281. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  282. package/scripts/lib/vault-status/board-writer.mjs +174 -135
  283. package/scripts/lib/vault-status/narrative-mirror.mjs +129 -37
  284. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  285. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  286. package/scripts/lib/wave-executor/remote-dispatch.mjs +502 -0
  287. package/scripts/lib/wave-resource-gate.mjs +133 -7
  288. package/scripts/lib/wave-sizing.mjs +4 -1
  289. package/scripts/lib/wave-transcript-tail.mjs +142 -8
  290. package/scripts/materialize-wave-scope.mjs +32 -9
  291. package/scripts/memory-propose.mjs +146 -8
  292. package/scripts/migrate-cold-start-seed.mjs +4 -1
  293. package/scripts/parse-config.mjs +60 -3
  294. package/scripts/promote-vault-strict.mjs +4 -15
  295. package/scripts/release.mjs +337 -29
  296. package/scripts/repair-invalid-sessions.mjs +3 -3
  297. package/scripts/run-quality-gate.mjs +128 -11
  298. package/scripts/site-numbers.mjs +36 -4
  299. package/scripts/sweep-expired-learnings.mjs +90 -0
  300. package/scripts/sync-vault-schema.mjs +3 -1
  301. package/scripts/telemetry.mjs +2 -2
  302. package/scripts/validate-plugin.mjs +187 -0
  303. package/scripts/validate-wave-scope.mjs +28 -8
  304. package/scripts/vault-consolidate.mjs +3 -11
  305. package/scripts/vault-integration-watcher.mjs +2 -4
  306. package/scripts/vault-mirror.mjs +111 -26
  307. package/scripts/wave-scope-binding.mjs +215 -0
  308. package/skills/_shared/instruction-file-resolution.md +10 -0
  309. package/skills/_shared/parallel-aware-auq.md +31 -2
  310. package/skills/_shared/parallel-aware-preamble.md +18 -4
  311. package/skills/_shared/platform-tools.md +1 -1
  312. package/skills/_shared/state-ownership.md +1 -1
  313. package/skills/architecture/SKILL.md +7 -5
  314. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  315. package/skills/autopilot/SKILL.md +4 -18
  316. package/skills/claude-md-drift-check/SKILL.md +5 -1
  317. package/skills/claude-md-drift-check/checker.mjs +62 -2
  318. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  319. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  320. package/skills/discovery/probes-arch.md +20 -18
  321. package/skills/dispatcher/SKILL.md +3 -2
  322. package/skills/ecosystem-health/SKILL.md +4 -1
  323. package/skills/ecosystem-health/wizard.md +5 -0
  324. package/skills/evolve/SKILL.md +87 -11
  325. package/skills/frontmatter-guard/SKILL.md +11 -5
  326. package/skills/npm-publish/SKILL.md +1 -1
  327. package/skills/reconcile/SKILL.md +38 -2
  328. package/skills/remote-offload/SKILL.md +89 -0
  329. package/skills/session-end/SKILL.md +18 -905
  330. package/skills/session-end/phase-3-6-tail.md +19 -9
  331. package/skills/session-end/plan-verification.md +221 -155
  332. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  333. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  334. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  335. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  336. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  337. package/skills/session-end/references/session-summary-template.md +62 -0
  338. package/skills/session-plan/SKILL.md +49 -0
  339. package/skills/session-start/SKILL.md +41 -900
  340. package/skills/session-start/phase-8-5-express-path.md +1 -1
  341. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  342. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  343. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  344. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  345. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  346. package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
  347. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  348. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  349. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  350. package/skills/vault-sync/validator.mjs +21 -27
  351. package/skills/wave-executor/SKILL.md +16 -2
  352. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  353. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  354. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  355. package/skills/wave-executor/wave-loop.md +14 -1271
  356. package/templates/_shared/journey-manifest.md +10 -6
  357. package/.cursor/commands/autopilot-multi.md +0 -14
  358. package/.cursor/commands/contract-version-bump.md +0 -14
  359. package/.cursor/commands/journey-audit.md +0 -14
  360. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  361. package/.cursor/skills/daily/SKILL.md +0 -12
  362. package/.cursor/skills/domain-model/SKILL.md +0 -13
  363. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  364. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  365. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  366. package/commands/autopilot-multi.md +0 -74
  367. package/commands/contract-version-bump.md +0 -28
  368. package/commands/journey-audit.md +0 -43
  369. package/pi/prompts/autopilot-multi.md +0 -12
  370. package/pi/prompts/contract-version-bump.md +0 -12
  371. package/pi/prompts/journey-audit.md +0 -12
  372. package/scripts/autopilot-multi.mjs +0 -885
  373. package/scripts/backfill-learnings-expires.mjs +0 -196
  374. package/scripts/backfill-learnings.mjs +0 -203
  375. package/scripts/fleet-instruction-scan.mjs +0 -141
  376. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  377. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  378. package/scripts/lib/webhook-url.mjs +0 -105
  379. package/scripts/lifecycle-sim-v6.mjs +0 -347
  380. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  381. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  382. package/scripts/upload-social-preview.mjs +0 -316
  383. package/skills/_shared/model-selection.md +0 -64
  384. package/skills/contract-version-bump/SKILL.md +0 -219
  385. package/skills/daily/SKILL.md +0 -222
  386. package/skills/daily/generate.sh +0 -92
  387. package/skills/daily/templates/daily.md.tpl +0 -36
  388. package/skills/journey-audit/SKILL.md +0 -269
  389. package/skills/skill-creator/SKILL.md +0 -168
  390. package/skills/ubiquitous-language/SKILL.md +0 -97
  391. package/skills/vault-sync/package-lock.json +0 -40
  392. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  393. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -391,7 +391,14 @@ export function mergeCandidates({ candidates, repoRoot, storePath } = {}) {
391
391
  * @param {number} [params.fallbackConfidence] - used only when no existing record is found.
392
392
  * @param {string} [params.repoRoot]
393
393
  * @param {string} [params.storePath]
394
- * @returns {{ written: boolean, stamped: ReconcileCandidate|null }}
394
+ * @returns {{ written: boolean, stamped: ReconcileCandidate|null, alreadyProcessed?: boolean }}
395
+ * `stamped` is the record as PERSISTED (read back out of `mergeCandidates`'s
396
+ * merged array), never the one this call merely proposed — when a prior
397
+ * terminal verdict wins the dedupe, the two differ and only the former is
398
+ * true. `alreadyProcessed: true` marks exactly that case; `written` stays
399
+ * `true` there, because the store holds the intended terminal state and
400
+ * `written: false` means a WRITE FAILURE to the one production caller
401
+ * (`writer.mjs`, which turns it into an operator-visible error).
395
402
  */
396
403
  export function markCandidateProcessed({
397
404
  learningKey,
@@ -419,6 +426,7 @@ export function markCandidateProcessed({
419
426
  stamped = { ...found, processed_at: stampAt, outcome: typeof outcome === 'string' ? outcome : (found.outcome ?? null) };
420
427
  } else {
421
428
  const slug = typeof fallbackSlug === 'string' ? fallbackSlug : '';
429
+ const finalOutcome = typeof outcome === 'string' ? outcome : null;
422
430
  stamped = buildCandidate({
423
431
  id:
424
432
  typeof fallbackCandidateId === 'string' && fallbackCandidateId.length > 0
@@ -426,15 +434,40 @@ export function markCandidateProcessed({
426
434
  : makeCandidateId(learningKey, slug),
427
435
  learningKey,
428
436
  slug,
429
- status: 'proposed',
437
+ // A minted record is TERMINAL from birth (`processed_at` is set two lines
438
+ // below), so `status` must agree with `outcome` instead of always saying
439
+ // `'proposed'` — a record reading `status:'proposed'` while carrying
440
+ // `outcome:'rejected'` is self-contradictory and misreads as a live
441
+ // proposal to anything that renders `status`. The vocabulary is exactly
442
+ // the two members of the ReconcileCandidate typedef; nothing new is
443
+ // invented here: `'rejected'` for the operator-rejection outcome
444
+ // (writer.mjs, issue #1042), `'proposed'` for the accepted outcomes
445
+ // (`'written'` / `'already-on-disk'`), where the proposal stood.
446
+ status: finalOutcome === 'rejected' ? 'rejected' : 'proposed',
430
447
  reason: 'stamped without a prior sidecar record',
431
448
  confidence: typeof fallbackConfidence === 'number' ? fallbackConfidence : 0,
432
449
  createdAt: stampAt,
433
450
  });
434
451
  stamped.processed_at = stampAt;
435
- stamped.outcome = typeof outcome === 'string' ? outcome : null;
452
+ stamped.outcome = finalOutcome;
436
453
  }
437
454
 
438
455
  const result = mergeCandidates({ candidates: [stamped], repoRoot, storePath });
439
- return { written: result.written === true, stamped };
456
+
457
+ // Report what is PERSISTED, not what we asked for. `mergeCandidates`'s dedupe
458
+ // rule keeps an already-terminal existing record and DROPS the incoming one,
459
+ // so when `found` was already processed the store still holds the OLD
460
+ // `processed_at`/`outcome` — returning our freshly-built `stamped` would tell
461
+ // the caller a verdict that is nowhere on disk. `merged` IS the array that was
462
+ // just written, so reading the record back out of it is the honest answer.
463
+ const persisted = result.merged.find((rec) => rec && rec.learning_key === learningKey);
464
+ return {
465
+ written: result.written === true,
466
+ stamped: persisted ?? stamped,
467
+ // `true` iff a prior terminal verdict won and this call changed nothing on
468
+ // disk. `written` deliberately stays `true` in that case: the store IS in
469
+ // the intended terminal state, and writer.mjs treats `written:false` as a
470
+ // stamp FAILURE worth an operator-visible error.
471
+ alreadyProcessed: isTerminal(found),
472
+ };
440
473
  }
@@ -457,26 +457,37 @@ function frontmatterRefusalReason(content) {
457
457
  * not yet mature enough — silently, and with no way back short of editing the
458
458
  * sidecar by hand.
459
459
  *
460
- * The discriminator is the rendered `content`: `engine.mjs` pushes its own
461
- * rejections as `{learningKey, type, reason, status:'rejected'}` they never
462
- * reach the renderer, so they never carry `content`/`slug`/`path` while the
463
- * proposals the operator declines are full `ReconcileProposal` records whose
464
- * `content` is the very rule text the AUQ showed him.
465
- *
466
- * CEILING (BV-004): this reads an implicit signal, not an explicit marker,
467
- * because the one production caller (`skills/session-end/phase-3-6-tail.md`
468
- * step 6/7) concatenates engine rejections and operator-declined proposals into
469
- * ONE `rejected` array and marks neither. Revisit if that caller starts passing
470
- * rendered content on engine-side rejections, or if it gains an explicit
471
- * operator-rejection flag then key on the flag instead.
472
- *
473
- * @param {WriterRejectedItem & {content?: unknown, learningKey?: unknown}} item
460
+ * PRIMARY discriminator (#1153 P15): the explicit `operatorRejected: true`
461
+ * flag that `skills/session-end/phase-3-6-tail.md` step 6 stamps on every
462
+ * proposal the operator left unselected before concatenating it into the one
463
+ * `rejected` array. `engine.mjs` pushes its own rejections as
464
+ * `{learningKey, type, reason, status:'rejected'}` and never sets the flag, so
465
+ * the two shapes are now distinguishable by a marker rather than by inference.
466
+ *
467
+ * FALLBACK (@deprecated remove once no supported skill body predates the P15
468
+ * stamp): when the flag is absent, fall back to the old implicit signal — the
469
+ * rendered `content`. Engine rejections never reach the renderer, so they never
470
+ * carry `content`/`slug`/`path`, while an operator-declined proposal is a full
471
+ * `ReconcileProposal` whose `content` is the rule text the AUQ showed him. The
472
+ * fallback exists because the skill prose and this module ship together but
473
+ * consumer repos may pin an OLDER `phase-3-6-tail.md` that emits no flag;
474
+ * dropping it immediately would silently stop stamping their operator
475
+ * rejections terminal (the exact #1042 bug). REMOVAL TRIGGER: the next major
476
+ * release in which no supported consumer ships a pre-P15 skill body.
477
+ *
478
+ * Either way the decision stays conservative in the same direction: a false
479
+ * negative costs nothing (the learning is re-proposed next run); a false
480
+ * positive would permanently suppress a learning that was only capped.
481
+ *
482
+ * @param {WriterRejectedItem & {content?: unknown, learningKey?: unknown, operatorRejected?: unknown}} item
474
483
  * @returns {boolean}
475
484
  */
476
485
  function isOperatorRejection(item) {
477
486
  if (!item || typeof item !== 'object') return false;
478
- if (typeof item.content !== 'string' || item.content.length === 0) return false;
479
- return typeof item.learningKey === 'string' && item.learningKey.length > 0;
487
+ if (typeof item.learningKey !== 'string' || item.learningKey.length === 0) return false;
488
+ if (item.operatorRejected === true) return true;
489
+ // @deprecated fallback — pre-P15 skill bodies emit no flag; see above.
490
+ return typeof item.content === 'string' && item.content.length > 0;
480
491
  }
481
492
 
482
493
  // ---------------------------------------------------------------------------
@@ -663,8 +674,12 @@ export async function writeApprovedRules({
663
674
  repoRoot,
664
675
  });
665
676
  if (!stampResult.written) {
677
+ // Same discrimination as the rejected path below (identical shape,
678
+ // identical reader): an already-terminal record is not a lost stamp.
666
679
  errors.push(
667
- `sidecar-stamp failed for "${item.path ?? item.slug}" (learningKey=${item.learningKey}) — rule file was written but the idempotency sidecar was not updated`,
680
+ stampResult.alreadyProcessed
681
+ ? `sidecar-stamp for "${item.path ?? item.slug}" (learningKey=${item.learningKey}) wrote nothing — a prior terminal verdict is already on disk, so the rule file landed and the candidate stays terminal`
682
+ : `sidecar-stamp failed for "${item.path ?? item.slug}" (learningKey=${item.learningKey}) — rule file was written but the idempotency sidecar was not updated`,
668
683
  );
669
684
  }
670
685
  }
@@ -724,8 +739,15 @@ export async function writeApprovedRules({
724
739
  repoRoot,
725
740
  });
726
741
  if (!stampResult.written) {
742
+ // `alreadyProcessed` distinguishes the two failures that look
743
+ // identical from `written:false` alone. When a PRIOR terminal
744
+ // verdict is already on disk, the re-proposal warning is
745
+ // simply false — the sidecar holds the state we wanted — and
746
+ // telling the operator otherwise sends him after a non-bug.
727
747
  errors.push(
728
- `sidecar-stamp failed for rejected "${item.learningKey}" — the rejection was archived but the idempotency sidecar was not updated, so a later run may re-propose it`,
748
+ stampResult.alreadyProcessed
749
+ ? `sidecar-stamp for rejected "${item.learningKey}" wrote nothing — a prior terminal verdict is already on disk, so the rejection is recorded and it will NOT be re-proposed`
750
+ : `sidecar-stamp failed for rejected "${item.learningKey}" — the rejection was archived but the idempotency sidecar was not updated, so a later run may re-propose it`,
729
751
  );
730
752
  }
731
753
  }
@@ -918,6 +918,28 @@ export function suggestForScopeViolation(relPath, allowedCsv) {
918
918
  );
919
919
  }
920
920
 
921
+ /**
922
+ * Prefix that marks an aggregate-sidecar record as a PEER SESSION's declared
923
+ * scope rather than one of this wave's own agents (#1195).
924
+ *
925
+ * SSOT: this constant lives here — in the module both the `--union` helper and
926
+ * `hooks/post-bash-write-verify.mjs` already import — so the union exclusion
927
+ * below and the hook's peer-write notice can never disagree about what a peer
928
+ * record IS.
929
+ */
930
+ export const PEER_RECORD_PREFIX = 'peer-session-';
931
+
932
+ /**
933
+ * Is this record id a PEER SESSION's record (see {@link PEER_RECORD_PREFIX})?
934
+ * Fail-closed: a non-string id is not a peer record.
935
+ *
936
+ * @param {unknown} id
937
+ * @returns {boolean}
938
+ */
939
+ export function isPeerRecordId(id) {
940
+ return typeof id === 'string' && id.startsWith(PEER_RECORD_PREFIX);
941
+ }
942
+
921
943
  /**
922
944
  * Merge many agents' declared file scopes into ONE deduplicated, order-stable
923
945
  * list — the mechanical form of "allowedPaths is the UNION of all agent file
@@ -943,6 +965,19 @@ export function suggestForScopeViolation(relPath, allowedCsv) {
943
965
  * non-array / non-object members and non-string, empty entries are skipped.
944
966
  * Pure, sync, no I/O — hook-safe per the module header.
945
967
  *
968
+ * PEER RECORDS ARE EXCLUDED (#1195 follow-through). A record whose id starts
969
+ * with `peer-session-` declares a territory NO agent of this wave may write —
970
+ * it exists so a peer's paths take part in the DISJOINTNESS check and so
971
+ * `hooks/post-bash-write-verify.mjs` can name a peer write instead of alarming
972
+ * about it. Unioning it into `allowedPaths` would do the exact inverse: Gate 7
973
+ * (`hooks/enforce-scope.mjs`) would GRANT every agent of the wave write access
974
+ * to the peer's files, and the hook's peer branch would become dead code (a
975
+ * peer path can only reach it while it is OUTSIDE `allowedPaths`). The
976
+ * exclusion lives HERE — the one helper `--union` runs — rather than in the
977
+ * CLI, so every consumer of the union inherits it.
978
+ * `findScopeCollisions` deliberately does NOT filter: a peer/agent path
979
+ * collision is a real collision and must surface.
980
+ *
946
981
  * @param {Array<string[]|{id?: string, files?: string[]}>} scopes
947
982
  * @returns {string[]} deduplicated union in first-seen order
948
983
  */
@@ -954,6 +989,7 @@ export function unionFileScopes(scopes) {
954
989
  let files = null;
955
990
  if (Array.isArray(scope)) files = scope;
956
991
  else if (scope !== null && typeof scope === 'object' && Array.isArray(scope.files)) {
992
+ if (isPeerRecordId(scope.id)) continue;
957
993
  files = scope.files;
958
994
  }
959
995
  if (files === null) continue;
@@ -67,12 +67,31 @@ import { parseStateMd as defaultParseStateMd } from './state-md/yaml-parser.mjs'
67
67
  // Constants
68
68
  // ---------------------------------------------------------------------------
69
69
 
70
- /** session_type enum accepted by the schema — lock.mode is coerced against it. */
71
- const VALID_SESSION_TYPES = new Set(['feature', 'deep', 'housekeeping']);
70
+ /**
71
+ * The three real session MODES — deliberately NOT the schema's VALID_SESSION_TYPES,
72
+ * which since GitLab #1234 also carries `unknown`. This set answers a narrower
73
+ * question: "is `gathered.mode` a MEASUREMENT?". `unknown` must never pass it, or
74
+ * an events record carrying `mode: 'unknown'` would be recorded as a measured type
75
+ * (`_session_type_inferred` absent) when it is the opposite.
76
+ */
77
+ const MEASURED_SESSION_MODES = new Set(['feature', 'deep', 'housekeeping']);
78
+
79
+ /**
80
+ * session_type written when nothing in events.jsonl measured the mode
81
+ * (GitLab #1234). Always paired with `_session_type_inferred` + `_synthetic`.
82
+ */
83
+ const UNMEASURED_SESSION_TYPE = 'unknown';
72
84
 
73
85
  const EVENT_STARTED = 'orchestrator.session.started';
74
86
  const EVENT_LOCK_ACQUIRED = 'orchestrator.session.lock.acquired';
87
+ // Both names for one generation (GitLab #1234): `hooks/on-stop.mjs` now emits
88
+ // `orchestrator.turn.stopped` as the canonical name and keeps the legacy
89
+ // `orchestrator.session.stopped` (with `deprecated: true`) beside it until
90
+ // 2027-03-06. A terminal-event probe must accept EITHER, or every session that
91
+ // closes after the legacy name is dropped silently loses its attested end and
92
+ // falls back to the flagged `lastEventMs` estimate.
75
93
  const EVENT_STOPPED = 'orchestrator.session.stopped';
94
+ const EVENT_TURN_STOPPED = 'orchestrator.turn.stopped';
76
95
  const EVENT_ENDED = 'orchestrator.session.ended';
77
96
 
78
97
  const EVENTS_REL = ['.orchestrator', 'metrics', 'events.jsonl'];
@@ -108,14 +127,24 @@ function markerName(id) {
108
127
  }
109
128
 
110
129
  /**
111
- * Read a JSONL file into an array of parsed objects. Missing file → []; each
112
- * malformed line is skipped rather than aborting the whole read. Never throws.
130
+ * Read a JSONL file into an array of parsed objects. Missing file (ENOENT)
131
+ * [] silently; an unreadable one (EACCES/EISDIR/…) [] with a stderr WARN
132
+ * (#1210 — ENOENT and other read failures are different facts, same split as
133
+ * `sessions-canonical.mjs` `readCanonicalSessions`). Each malformed line is
134
+ * skipped rather than aborting the whole read. Never throws.
113
135
  */
114
136
  function readJsonlSafe(readFileSync, filePath) {
115
137
  let raw;
116
138
  try {
117
139
  raw = readFileSync(filePath, 'utf8');
118
- } catch {
140
+ } catch (err) {
141
+ if (!err || err.code !== 'ENOENT') {
142
+ process.stderr.write(
143
+ `⚠ readJsonlSafe: cannot read ${filePath} ` +
144
+ `(${err?.code ?? '?'}: ${err?.message ?? String(err)}) — ` +
145
+ 'treating as EMPTY, counts below are floors\n',
146
+ );
147
+ }
119
148
  return [];
120
149
  }
121
150
  const out = [];
@@ -153,18 +182,33 @@ function collectSessionEvents(events, { sessionId, semanticSessionId }) {
153
182
 
154
183
  let mode = null;
155
184
  let semanticFromLock = null;
185
+ // #1167 — the SECOND semantic bridge. `orchestrator.session.ended` carries
186
+ // `semantic_session_id` alongside the raw UUID since #1068 AC1, but nothing
187
+ // read it: a session that LOST the lock-acquire race emits no lock.acquired,
188
+ // so the lock bridge above resolved null and the caller fell through to the
189
+ // synthetic-id mint — writing a SECOND `abandoned` stub for a session the
190
+ // SessionEnd hook had already recorded under its semantic id. Measured
191
+ // 2026-09-02 @ c3ab480: 8 such duplicate pairs in sessions.jsonl.
192
+ let semanticFromEvents = null;
156
193
 
157
- // First pass — lock.acquired bridges the UUID set + carries mode + semantic.
194
+ // First pass — bridge the UUID set + carry mode + semantic id. lock.acquired
195
+ // is the original bridge; session.ended is the #1167 addition.
158
196
  for (const ev of events) {
159
- if (ev.event !== EVENT_LOCK_ACQUIRED) continue;
197
+ const isLock = ev.event === EVENT_LOCK_ACQUIRED;
198
+ const isEnded = ev.event === EVENT_ENDED && typeof ev.semantic_session_id === 'string';
199
+ if (!isLock && !isEnded) continue;
160
200
  const matchesUuid = isUuid(sessionId) && ev.session_id === sessionId;
161
201
  const matchesSemantic =
162
202
  (semanticSessionId && ev.semantic_session_id === semanticSessionId) ||
163
203
  (!isUuid(sessionId) && sessionId && ev.semantic_session_id === sessionId);
164
204
  if (!matchesUuid && !matchesSemantic) continue;
165
205
  if (typeof ev.session_id === 'string') uuids.add(ev.session_id);
166
- if (typeof ev.mode === 'string') mode = ev.mode;
167
- if (typeof ev.semantic_session_id === 'string') semanticFromLock = ev.semantic_session_id;
206
+ if (isLock) {
207
+ if (typeof ev.mode === 'string') mode = ev.mode;
208
+ if (typeof ev.semantic_session_id === 'string') semanticFromLock = ev.semantic_session_id;
209
+ } else {
210
+ semanticFromEvents = ev.semantic_session_id;
211
+ }
168
212
  }
169
213
 
170
214
  // Second pass — started + terminal timestamps from every matched UUID.
@@ -192,7 +236,7 @@ function collectSessionEvents(events, { sessionId, semanticSessionId }) {
192
236
  if (typeof ev.timestamp === 'string') startedAt = ev.timestamp;
193
237
  if (typeof ev.branch === 'string' && ev.branch.length > 0) branch = ev.branch;
194
238
  if (typeof ev.project === 'string') project = ev.project;
195
- } else if (ev.event === EVENT_STOPPED || ev.event === EVENT_ENDED) {
239
+ } else if (ev.event === EVENT_STOPPED || ev.event === EVENT_TURN_STOPPED || ev.event === EVENT_ENDED) {
196
240
  if (!Number.isNaN(ts)) {
197
241
  lastTerminalMs = lastTerminalMs === null ? ts : Math.max(lastTerminalMs, ts);
198
242
  }
@@ -200,7 +244,18 @@ function collectSessionEvents(events, { sessionId, semanticSessionId }) {
200
244
  }
201
245
  }
202
246
 
203
- return { uuids, mode, semanticFromLock, startedAt, branch, project, lastTerminalMs, earliestMs, lastEventMs };
247
+ return {
248
+ uuids,
249
+ mode,
250
+ semanticFromLock,
251
+ semanticFromEvents,
252
+ startedAt,
253
+ branch,
254
+ project,
255
+ lastTerminalMs,
256
+ earliestMs,
257
+ lastEventMs,
258
+ };
204
259
  }
205
260
 
206
261
  /**
@@ -256,7 +311,7 @@ function isCandidateDeadByAge({ relaxDeadByAge, assumeDeadBeforeMs, lastEventMs,
256
311
  * this record replaces. It is emitted only when non-null, so every existing
257
312
  * record shape is byte-identical to before.
258
313
  */
259
- function synthesizeRecord({ recordId, synthetic, gathered, nowMs, status = 'abandoned', backfillSource = 'events-jsonl', supersedes = null }) {
314
+ function synthesizeRecord({ recordId, synthetic, gathered, nowMs, status = 'abandoned', backfillSource = 'events-jsonl', supersedes = null, rawSessionId = null }) {
260
315
  const startedIso = canonicalIso(gathered.startedAt, gathered.earliestMs ?? nowMs);
261
316
  const startedMs = Date.parse(startedIso);
262
317
  // completed_at is events-attested, never the backfill-run wall-clock (#914 R1).
@@ -281,9 +336,9 @@ function synthesizeRecord({ recordId, synthetic, gathered, nowMs, status = 'aban
281
336
  // Guard the same monotonic invariant as before: never earlier than started_at.
282
337
  const completedIso = new Date(Math.max(startedMs, completedMs)).toISOString();
283
338
 
284
- let sessionType = 'housekeeping';
339
+ let sessionType = UNMEASURED_SESSION_TYPE;
285
340
  let inferred = true;
286
- if (gathered.mode && VALID_SESSION_TYPES.has(gathered.mode)) {
341
+ if (gathered.mode && MEASURED_SESSION_MODES.has(gathered.mode)) {
287
342
  sessionType = gathered.mode;
288
343
  inferred = false;
289
344
  }
@@ -320,7 +375,36 @@ function synthesizeRecord({ recordId, synthetic, gathered, nowMs, status = 'aban
320
375
  _backfill_incomplete_fields: incomplete,
321
376
  };
322
377
  if (branchFound) record.branch = gathered.branch;
323
- if (inferred) record._session_type_inferred = true;
378
+ if (inferred) {
379
+ record._session_type_inferred = true;
380
+ // GitLab #1234 — BACKFILLER HONESTY, half landed 2026-09-06.
381
+ //
382
+ // `session_type` above is now `'unknown'`, not the old `'housekeeping'`
383
+ // DEFAULT that nothing in events.jsonl ever said. Measured 2026-09-06, all
384
+ // 1.656 `abandoned` records in the 90-day fleet window carry
385
+ // `_session_type_inferred: true` + `total_waves: 0`, and NO organically
386
+ // written `abandoned` record exists anywhere — that `housekeeping` guess is
387
+ // what produced the "27 % close rate" figure that turned out to be an
388
+ // artefact (the real rate is 21,3 %). VALID_SESSION_TYPES was widened with
389
+ // `unknown` in `scripts/lib/session-schema/constants.mjs` to make this
390
+ // sayable; historical records keep their `housekeeping` label verbatim
391
+ // (sessions.jsonl is append-only), so a reader wanting the honest
392
+ // population still filters on `_synthetic !== true` rather than on the type.
393
+ //
394
+ // The OTHER half is deliberately NOT landed: `status` stays `'abandoned'`
395
+ // even though `'unresolved'` is the honest word and the schema now accepts
396
+ // it. Six executable phantom-stub filters key on the literal `abandoned`
397
+ // (census + revisit trigger in `scripts/lib/session-schema/validator.mjs`
398
+ // § SESSION_STATUS) and none of them is in this change's file scope —
399
+ // flipping the emitter first would make every new stub invisible to all six
400
+ // and re-open the #834 phantom-in-signal class fleet-wide. Repoint those
401
+ // filters onto one both-accepting predicate, then flip this one line.
402
+ //
403
+ // `_synthetic: true` still carries the claim no enum can: this record was
404
+ // COMPOSED. A consumer that filters on `_synthetic !== true` gets only
405
+ // measured records without needing to know either enum.
406
+ record._synthetic = true;
407
+ }
324
408
  if (synthetic) record._synthetic_session_id = true;
325
409
  if (completedEstimated) record._completed_at_estimated = true;
326
410
  // #1068 AC3/AC4 — forensic supersede marker. sessions.jsonl is append-only,
@@ -329,6 +413,13 @@ function synthesizeRecord({ recordId, synthetic, gathered, nowMs, status = 'aban
329
413
  // "historische Stub-Provenance bleibt erhalten"). Readers resolve one
330
414
  // canonical state per id by taking the NEWEST record for that id.
331
415
  if (typeof supersedes === 'string' && supersedes.length > 0) record.supersedes = supersedes;
416
+ // #1167 — the harness UUID this record was reconstructed from, when known.
417
+ // Additive and optional (the schema validates unknown keys pass-through, see
418
+ // session-schema/validator.mjs `_validateOptionalFields`): it is the ONLY key
419
+ // that lets a reader join a semantic record back to its raw uuid. Measured
420
+ // 2026-09-02 @ c3ab480: 0 of 286 existing records carry it, which is exactly
421
+ // why the two backfill writers could not see each other's work.
422
+ if (typeof rawSessionId === 'string' && rawSessionId.length > 0) record.raw_session_id = rawSessionId;
332
423
  return record;
333
424
  }
334
425
 
@@ -535,8 +626,11 @@ export async function backfillAbandonedSession({
535
626
 
536
627
  // -- Resolve a deferred id from the lock bridge or a synthetic mint ------
537
628
  if (recordId === null) {
538
- if (gathered.semanticFromLock) {
539
- recordId = gathered.semanticFromLock;
629
+ // Prefer the lock bridge (it also carries `mode`), then the #1167
630
+ // session.ended bridge. Only when NEITHER attests a semantic id do we
631
+ // mint a synthetic one — that fallback was the duplicate-stub source.
632
+ if (gathered.semanticFromLock || gathered.semanticFromEvents) {
633
+ recordId = gathered.semanticFromLock || gathered.semanticFromEvents;
540
634
  } else {
541
635
  // No semantic bridge — mint a synthetic id. Both components are STABLE
542
636
  // across re-runs so dedupe/marker suppress a double write (idempotency
@@ -619,7 +713,13 @@ export async function backfillAbandonedSession({
619
713
  }
620
714
 
621
715
  // -- Synthesize + validate (round-trip gate) BEFORE any disk mutation ---
622
- const record = synthesizeRecord({ recordId, synthetic, gathered, nowMs });
716
+ const record = synthesizeRecord({
717
+ recordId,
718
+ synthetic,
719
+ gathered,
720
+ nowMs,
721
+ rawSessionId: isUuid(sessionId) ? sessionId : null,
722
+ });
623
723
  let validated;
624
724
  try {
625
725
  validated = validateSession(record);
@@ -845,6 +945,13 @@ export async function backfillCompletedFromStateMd({
845
945
  status: 'completed',
846
946
  backfillSource: 'state-md-completed',
847
947
  supersedes,
948
+ // #1167 — the abandoned path stamps this; so must the authoritative
949
+ // one, or the join key exists on exactly the weaker half of the pair.
950
+ // `gathered.uuids` is bridged from lock.acquired / session.ended, so it
951
+ // normally holds EXACTLY the one uuid this semantic id ran under. Two
952
+ // (or zero) means the bridge is ambiguous — omit rather than guess, the
953
+ // same fail-quiet posture as `isUuid(sessionId) ? sessionId : null`.
954
+ rawSessionId: gathered.uuids?.size === 1 ? [...gathered.uuids][0] : null,
848
955
  });
849
956
  let validated;
850
957
  try {
@@ -32,6 +32,28 @@
32
32
  * - Lock takes precedence over registry (more detail per session); registry
33
33
  * supplements missing per-worktree entries (e.g., when the hook ran but
34
34
  * the prose Phase 1.2 acquire was skipped).
35
+ * - A live local session.lock does NOT supersede same-repo registry entries
36
+ * with a different raw session_id (GH#67 candidate fix, measured 2026-09-02
37
+ * and NOT adopted): the session lock is advisory, so a second session can
38
+ * and demonstrably does run in the same working copy without holding it.
39
+ * Filtering those entries out hides genuine same-working-copy peers — the
40
+ * #1085 semantic-alias boundary (tests/integration/session-identity-
41
+ * boundaries.test.mjs) and 7 peer-discovery cases pin exactly that
42
+ * visibility. Any GH#67 fix must discriminate finished-but-fresh entries
43
+ * some other way; do not re-attempt lock-ownership supersession here.
44
+ * See ADR-0014 (docs/adr/0014-peer-visibility-under-advisory-lock.md) for
45
+ * the annotate-never-filter decision and the 9-test refutation of the
46
+ * filtering alternative.
47
+ * - The adopted GH#67 shape is therefore ADDITIVE ANNOTATION, never a filter.
48
+ * Registry-sourced sessions carry `registryOnly: true` plus
49
+ * `lockSuperseded` / `lockOwnerId`. `lockSuperseded: true` means: a LIVE
50
+ * lock at this repoRoot is owned by a different raw session_id than this
51
+ * registry entry. It is a HINT, not a verdict — the lock is advisory, so
52
+ * the entry may still be a live session that lost the acquire race (#1085
53
+ * contract). Consumers deciding a worktree-PROMOTION_OFFER downgrade such
54
+ * a peer to an advisory line (GH#67); consumers counting or displaying
55
+ * peers keep it. Lock-sourced sessions carry none of the three fields, so
56
+ * their objects stay byte-identical to the pre-GH#67 shape.
35
57
  *
36
58
  * Timeout + A1 fallback:
37
59
  * - listWorktrees() is raced against DEFAULT_DISCOVERY_TIMEOUT_MS (2 s).
@@ -93,10 +115,24 @@ function sessionFromLock(lock, worktreePath, branch = '') {
93
115
  * does not record per-worktree paths — only repo-path-hashes. Branch is
94
116
  * passed through from the registry entry when available.
95
117
  *
118
+ * GH#67 annotation (additive, never a filter — see the module header): every
119
+ * registry-sourced session carries `registryOnly: true` and the pair
120
+ * `lockSuperseded` / `lockOwnerId`. `lockSuperseded: true` says a LIVE lock at
121
+ * this repoRoot is owned by a DIFFERENT raw session_id than this entry — a
122
+ * HINT that the entry may be a finished-but-still-fresh task (the GH#67 case:
123
+ * Codex emits no SessionEnd, so the registry entry outlives the task by up to
124
+ * `freshnessMin`). It is not a verdict: the lock is advisory, so the entry may
125
+ * equally be a live session that never acquired it (#1085). Consumers deciding
126
+ * a worktree PROMOTION_OFFER downgrade such a peer to an advisory line;
127
+ * consumers counting or displaying peers keep it unchanged.
128
+ *
96
129
  * @param {object} entry Registry entry.
97
130
  * @param {string} repoRoot Discovery repoRoot (used as worktreePath fallback).
131
+ * @param {object} [ctx]
132
+ * @param {string|null} [ctx.lockOwnerId] Raw session_id owning a LIVE lock at
133
+ * repoRoot, or null when there is no live local lock.
98
134
  */
99
- function sessionFromRegistryEntry(entry, repoRoot) {
135
+ function sessionFromRegistryEntry(entry, repoRoot, { lockOwnerId = null } = {}) {
100
136
  return {
101
137
  worktreePath: repoRoot,
102
138
  sessionId: entry.session_id,
@@ -111,6 +147,11 @@ function sessionFromRegistryEntry(entry, repoRoot) {
111
147
  // single machine, so consumers comparing hosts need the normalised form.
112
148
  host_id: stableHostname(),
113
149
  branch: typeof entry.branch === 'string' ? entry.branch : '',
150
+ // GH#67 additive annotation (see the JSDoc above). Lock-sourced sessions
151
+ // deliberately carry none of these three fields.
152
+ registryOnly: true,
153
+ lockSuperseded: lockOwnerId !== null && entry.session_id !== lockOwnerId,
154
+ lockOwnerId,
114
155
  };
115
156
  }
116
157
 
@@ -177,7 +218,10 @@ function dedupeBySessionId(sessions) {
177
218
  * @param {Function} [opts.registryReader] DI hook replacing readRegistry() for tests.
178
219
  * @param {number} [opts.freshnessMin=15] Registry-entry freshness threshold in minutes.
179
220
  * @param {number} [opts.now] ms-since-epoch (test seam for heartbeat freshness).
180
- * @returns {Promise<Array<{worktreePath:string,sessionId:string,mode:string,startedAt:string,pid:number,host:string,host_id:string,branch:string}>>}
221
+ * @returns {Promise<Array<{worktreePath:string,sessionId:string,mode:string,startedAt:string,pid:number,host:string,host_id:string,branch:string,registryOnly?:boolean,lockSuperseded?:boolean,lockOwnerId?:string|null}>>}
222
+ * The last three fields appear ONLY on registry-sourced sessions (GH#67
223
+ * annotation — see `sessionFromRegistryEntry`); lock-sourced sessions omit
224
+ * them entirely.
181
225
  */
182
226
  export async function discoverActiveSessions(repoRoot, opts = {}) {
183
227
  const listWorktreesFn = opts.listWorktreesImpl ?? listWorktrees;
@@ -232,10 +276,20 @@ export async function discoverActiveSessions(repoRoot, opts = {}) {
232
276
  try {
233
277
  const allEntries = await registryReaderFn();
234
278
  const myRepoHash = repoPathHash(repoRoot);
279
+ // GH#67: who (if anyone) holds a LIVE lock at this repoRoot right now.
280
+ // Read once, outside the map — it is a property of the repo, not of an
281
+ // entry. A stale lock proves nothing, so it yields null.
282
+ const localLock = readLock({ repoRoot });
283
+ const lockOwnerId = (
284
+ localLock
285
+ && isLockLive(localLock, nowMs)
286
+ && typeof localLock.session_id === 'string'
287
+ && localLock.session_id
288
+ ) ? localLock.session_id : null;
235
289
  registrySessions = allEntries
236
290
  .filter((e) => e && e.repo_path_hash === myRepoHash)
237
291
  .filter((e) => isRegistryEntryFresh(e, { freshnessMin, now: nowMs }))
238
- .map((e) => sessionFromRegistryEntry(e, repoRoot));
292
+ .map((e) => sessionFromRegistryEntry(e, repoRoot, { lockOwnerId }));
239
293
  } catch {
240
294
  // Registry read failure → keep going with lock-only results.
241
295
  registrySessions = [];
@@ -55,7 +55,7 @@ import { sweepExpiredLearnings } from '../learnings/expiry-sweep.mjs';
55
55
  import { shouldDispatchAutoDream } from '../auto-dream.mjs';
56
56
  import { shouldDispatchAutoDialectic } from '../auto-dialectic.mjs';
57
57
  import { readSkillInvocations } from '../skill-invocations-schema.mjs';
58
- import { runReconcile, resolveEffectiveTargets } from '../reconcile/engine.mjs';
58
+ import { runReconcileFromPhaseSkip, resolveEffectiveTargets } from '../reconcile/engine.mjs';
59
59
  import { resolveMemoryDir } from '../memory-paths.mjs';
60
60
 
61
61
  // ---------------------------------------------------------------------------
@@ -285,7 +285,7 @@ async function decideReconcile({ repoRoot, cfg }) {
285
285
  if (!existsSync(learningsPath)) {
286
286
  return mkSkip(phase, 'learnings.jsonl absent', 'learnings.jsonl');
287
287
  }
288
- const { proposals, summary, error } = await runReconcile({
288
+ const { proposals, summary, error } = await runReconcileFromPhaseSkip({
289
289
  repoRoot,
290
290
  ruleExpiryDays: cfg?.reconcile?.['rule-expiry-days'] ?? undefined,
291
291
  minRuleDays: cfg?.reconcile?.['min-rule-days'] ?? undefined,
@@ -42,6 +42,7 @@
42
42
  import { open, readFile } from 'node:fs/promises';
43
43
  import path from 'node:path';
44
44
 
45
+ import { readCanonicalSessions } from './sessions-canonical.mjs';
45
46
  import { withStateMdLock } from './session-lock.mjs';
46
47
  import { resolveStateMdPath } from './state-md/frontmatter-mutators.mjs';
47
48
  import { parseStateMd } from './state-md/yaml-parser.mjs';
@@ -127,34 +128,22 @@ function isValidMode(mode) {
127
128
  * - Malformed JSONL line → silently skipped (per-line try/catch).
128
129
  * - Lines without a string `session_id` field → filtered out.
129
130
  *
130
- * Performance note: sessions.jsonl is line-oriented but typically <100 KB.
131
- * A single readFile is faster than line-streaming at this size. Should the
132
- * file grow past ~5 MB a future change can swap to a `readline` stream with
133
- * early-exit; not a launch blocker.
131
+ * #1209: migrated to `readCanonicalSessions` (dedup per `session_id`, drop
132
+ * id-less lines same three rules `sessions-canonical.mjs` documents).
133
+ * Number-neutral for THIS reader: the only consumer of its output
134
+ * (`resolveSemanticSessionId`) folds `candidateIds` through `Math.max` over
135
+ * the parsed `n` — "Duplicates are fine — Math.max handles them" (see that
136
+ * function's body) — so collapsing a duplicate `session_id` line to one
137
+ * record changes nothing observable here. Migrated anyway for one collapse
138
+ * implementation across all `sessions.jsonl` raw-line-scan readers (#1209).
134
139
  *
135
140
  * @param {string} repoRoot
136
- * @returns {Promise<string[]>} Array of session_id strings (may include duplicates).
141
+ * @returns {Promise<string[]>} Array of session_id strings (may include duplicates
142
+ * only across DIFFERENT ids — never the same id twice, see above).
137
143
  */
138
144
  async function readSessionIdsFromHistory(repoRoot) {
139
145
  const filePath = path.join(repoRoot, '.orchestrator', 'metrics', 'sessions.jsonl');
140
- let raw;
141
- try {
142
- raw = await readFile(filePath, 'utf8');
143
- } catch {
144
- return [];
145
- }
146
- const ids = [];
147
- for (const line of raw.split(/\r?\n/)) {
148
- const trimmed = line.trim();
149
- if (trimmed === '') continue;
150
- try {
151
- const parsed = JSON.parse(trimmed);
152
- if (typeof parsed?.session_id === 'string') ids.push(parsed.session_id);
153
- } catch {
154
- // Malformed line — skip silently (audit §3.1 robustness contract).
155
- }
156
- }
157
- return ids;
146
+ return readCanonicalSessions({ filePath }).map((rec) => rec.session_id);
158
147
  }
159
148
 
160
149
  /**