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
@@ -0,0 +1,446 @@
1
+ /**
2
+ * sessions-canonical.mjs — one record per physical session (#1167).
3
+ *
4
+ * `.orchestrator/metrics/sessions.jsonl` is APPEND-ONLY by design: nothing is
5
+ * ever rewritten in place, so the same physical session can appear more than
6
+ * once. A consumer that treats "one line = one session" therefore over-counts,
7
+ * and every duration/effectiveness aggregate computed from the raw file is
8
+ * silently wrong by however many duplicates happen to sit in its window.
9
+ *
10
+ * This module is the READ-side collapse. It never repairs the file (see § 3).
11
+ *
12
+ * ── THE THREE RULES, AND WHAT MEASURED THEM ─────────────────────────────────
13
+ *
14
+ * (1) NEWEST-WINS PER `session_id`.
15
+ * File order is chronological, so the LAST record carrying an id is the
16
+ * current one. This is the same reading rule
17
+ * `session-close-backfill.mjs::classifyExisting()` already applies (it
18
+ * takes `matches[matches.length - 1]`); this module generalises it to the
19
+ * whole file. Measured 2026-09-02 @ c3ab480 over 286 records: one id
20
+ * (2026-05-10) carries a byte-identical duplicate LINE — an older
21
+ * collision class than (3), and rule (1) alone resolves it.
22
+ *
23
+ * (2) NARROW COLLAPSE OF THE SYSTEMIC DOUBLE-STUB CLASS.
24
+ * Two `abandoned` records with an EXACT `started_at` + `completed_at`
25
+ * tuple match are one physical session recorded twice by the two backfill
26
+ * writers: `hooks/on-session-end.mjs` resolves the semantic id from
27
+ * `current-session.json` and writes `main-YYYY-MM-DD-session-N`, while
28
+ * `scripts/backfill-abandoned-sessions.mjs` could resolve a semantic id
29
+ * ONLY via `orchestrator.session.lock.acquired` — a session that lost the
30
+ * lock-acquire race has no such event, so it fell through to the synthetic
31
+ * mint (`<branch>-<date>-abandoned-<sha8>`, `_synthetic_session_id: true`)
32
+ * and wrote a SECOND stub for the same session. The join-back was
33
+ * impossible because `raw_session_id` is null on 286/286 records
34
+ * (`jq -s '[.[]|select(.raw_session_id != null)]|length'` → 0, measured
35
+ * 2026-09-02 @ c3ab480), so the two records share no key at all — only
36
+ * their millisecond-identical timestamps.
37
+ * Measured population: 8 such pairs over 6 weeks (16 records), via
38
+ * `jq -r '[.started_at,.completed_at,.status]|@tsv' … | sort | uniq -d`.
39
+ * The NON-synthetic record survives; the synthetic mint is the artefact.
40
+ *
41
+ * Deliberately narrow. The collapse requires BOTH records to be
42
+ * `status: 'abandoned'` and BOTH timestamps to be present and equal.
43
+ * `started_at` alone is NOT enough (two real sessions can start in the
44
+ * same millisecond of a re-fire), and `completed` records are never
45
+ * collapsed (an authoritative record is a truth claim about itself, never
46
+ * an artefact of a second writer).
47
+ *
48
+ * (3) AN ATTESTABLE `supersedes: X` REMOVES record X.
49
+ * The #1068 AC3/AC4 supersede path appends an authoritative `completed`
50
+ * record carrying a forward pointer to the backfilled `abandoned` stub it
51
+ * refutes. The stub is kept on disk verbatim (AC4 — forensic provenance);
52
+ * a canonical READER must drop it, or the same session is counted as both
53
+ * abandoned and completed.
54
+ *
55
+ * Two constraints, both measured defects of the first implementation:
56
+ * - ORDER-INDEPENDENT. A record is dropped iff some SURVIVING record
57
+ * supersedes it (a fixpoint over the supersede graph; cycles broken by
58
+ * keeping the newest member). Deleting in file order made a chain
59
+ * `C → B → A` resolve to `{C, A}` or `{C}` depending on the
60
+ * permutation the appends happened to land in, and a mutual pair
61
+ * resolved by insertion order.
62
+ * - ATTESTABLE ONLY. The marker is honoured only when the target is not
63
+ * authoritative (`status` `abandoned`, or absent on a legacy stub —
64
+ * never `completed`) AND the two records share a join key (equal
65
+ * `raw_session_id`, or byte-equal `started_at` — the shape
66
+ * `session-close-backfill.mjs::synthesizeRecord()` emits, since stub
67
+ * and superseder are synthesized from the same gathered events).
68
+ * Without that constraint ONE appended line could delete ANY id from
69
+ * EVERY reader of this module, the armed autonomy verdict included. A
70
+ * refused marker keeps both records and is reported (never logged)
71
+ * via `canonicalizeSessionsDetailed().ignoredSupersedes`.
72
+ *
73
+ * RULE ORDER: (1) → (2) → (3). The double-stub collapse must run BEFORE
74
+ * supersede removal: with the reverse order a `supersedes` append deleted the
75
+ * authentic stub first, shrank the tuple group to a single member, and the
76
+ * synthetic phantom then survived the very session that refuted it.
77
+ *
78
+ * ── WHAT THIS MODULE DOES NOT DO ────────────────────────────────────────────
79
+ * - It never writes. The 8 historical pairs stay on disk; the ledger is
80
+ * append-only and the duplicates are their own provenance.
81
+ * - It is not a phantom filter. `status: 'abandoned'` records SURVIVE here —
82
+ * dropping them is `session-schema/filters.mjs`'s job
83
+ * (`isRealSession` / `filterRealSessions` / `tailRealSessions`), and the
84
+ * two compose: canonicalize first, then filter.
85
+ *
86
+ * Plain Node ESM. Named exports. `canonicalizeSessions` is pure; only
87
+ * `readCanonicalSessions` touches the filesystem (sync, `readFileSync`).
88
+ */
89
+
90
+ import fs from 'node:fs';
91
+ import path from 'node:path';
92
+
93
+ const SESSIONS_REL = ['.orchestrator', 'metrics', 'sessions.jsonl'];
94
+
95
+ /**
96
+ * True when the value is a usable record object (not null, not an array).
97
+ * @param {unknown} v
98
+ * @returns {boolean}
99
+ */
100
+ function isRecordObject(v) {
101
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
102
+ }
103
+
104
+ /** Non-empty string guard — `''` is never a usable id or timestamp. */
105
+ function isNonEmptyString(v) {
106
+ return typeof v === 'string' && v.length > 0;
107
+ }
108
+
109
+ /**
110
+ * The timestamp a record is ordered by when a supersede CYCLE has to be broken:
111
+ * `completed_at` when present, else `started_at`, else `''` (sorts last).
112
+ * ISO-8601 strings compare lexicographically, so no Date parsing is needed.
113
+ * @param {object} rec
114
+ * @returns {string}
115
+ */
116
+ function cycleOrderTimestamp(rec) {
117
+ if (isNonEmptyString(rec.completed_at)) return rec.completed_at;
118
+ if (isNonEmptyString(rec.started_at)) return rec.started_at;
119
+ return '';
120
+ }
121
+
122
+ /**
123
+ * True when `superseder`'s `supersedes` marker is ATTESTABLE against `target`.
124
+ *
125
+ * `supersedes` is a forward pointer inside an append-only file that anyone (or
126
+ * any buggy writer) can append a line to, and a reader that obeys it blindly
127
+ * lets a single appended line delete ANY id from EVERY consumer — including the
128
+ * armed autonomy verdict. So the marker is honoured only for the shape the
129
+ * #1068 writer actually produces: the stub it refutes is never an AUTHORITATIVE
130
+ * record — its `status` is `abandoned`, or absent/null on a legacy stub, but
131
+ * never any other declared status (a `completed` record is a truth claim about
132
+ * itself and can never be deleted by an appended pointer) — and both records
133
+ * were synthesized from the SAME gathered events, hence share an attestable
134
+ * join key —
135
+ * - equal non-empty `raw_session_id` (the #1167 harness-uuid join), or
136
+ * - byte-equal non-empty `started_at` (`session-close-backfill.mjs`
137
+ * `synthesizeRecord()` derives `startedIso` from the same event set for the
138
+ * stub and for the record that supersedes it).
139
+ * Anything else is a data-integrity anomaly: BOTH records are kept and the
140
+ * marker is reported via `canonicalizeSessionsDetailed().ignoredSupersedes`.
141
+ *
142
+ * @param {object} superseder
143
+ * @param {object} target
144
+ * @returns {string|null} null when the marker is valid, else the reject reason
145
+ */
146
+ function supersedeRejectReason(superseder, target) {
147
+ // Absent/null `status` is a legacy stub, not an authoritative record; any
148
+ // OTHER declared status (`completed` above all) is untouchable.
149
+ if (isNonEmptyString(target.status) && target.status !== 'abandoned') {
150
+ return 'target-not-abandoned';
151
+ }
152
+ const a = superseder.raw_session_id;
153
+ const b = target.raw_session_id;
154
+ if (isNonEmptyString(a) && isNonEmptyString(b) && a === b) return null;
155
+ if (isNonEmptyString(superseder.started_at) && superseder.started_at === target.started_at) {
156
+ return null;
157
+ }
158
+ return 'no-shared-join-key';
159
+ }
160
+
161
+ /**
162
+ * Rule (3) — collapse the systemic two-writer double stub. Mutates `byId` by
163
+ * deleting the synthetic twin of each qualifying pair.
164
+ * @param {Map<string, object>} byId
165
+ * @returns {void}
166
+ */
167
+ function collapseAbandonedTuples(byId) {
168
+ // Group ONLY the records eligible for the systemic double-stub class; every
169
+ // other record bypasses this pass entirely and can never be dropped by it.
170
+ const byTuple = new Map();
171
+ for (const rec of byId.values()) {
172
+ if (rec.status !== 'abandoned') continue;
173
+ if (!isNonEmptyString(rec.started_at) || !isNonEmptyString(rec.completed_at)) continue;
174
+ const key = `${rec.started_at} ${rec.completed_at}`;
175
+ const group = byTuple.get(key);
176
+ if (group) group.push(rec);
177
+ else byTuple.set(key, [rec]);
178
+ }
179
+ for (const group of byTuple.values()) {
180
+ if (group.length < 2) continue;
181
+ const authentic = group.filter((r) => r._synthetic_session_id !== true);
182
+ // All-synthetic (or all-authentic) groups are left intact: with no
183
+ // non-synthetic record to prefer there is no evidence about WHICH one is
184
+ // the artefact, and guessing would delete a session nobody can recover.
185
+ if (authentic.length === 0 || authentic.length === group.length) continue;
186
+ for (const rec of group) {
187
+ if (rec._synthetic_session_id === true) byId.delete(rec.session_id);
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Rule (2) — order-independent supersede resolution. Mutates `byId` by deleting
194
+ * every record that a SURVIVING record supersedes, and appends every rejected
195
+ * marker to `ignored`.
196
+ *
197
+ * A record is dropped iff some record that itself survives supersedes it; the
198
+ * marking is a fixpoint over the supersede graph, so it depends on the EDGES
199
+ * only, never on the order the records appear in the file. A chain
200
+ * `C → B → A` therefore always resolves to `{C, A}` (B is dropped by the
201
+ * surviving C, so B's own marker no longer removes A).
202
+ *
203
+ * A cycle (`X → Y`, `Y → X`) has no fixpoint; it is broken deterministically by
204
+ * keeping the NEWEST member (`completed_at ?? started_at`, ties by ascending
205
+ * `session_id`) and re-running the propagation.
206
+ *
207
+ * @param {Map<string, object>} byId
208
+ * @param {Array<{by: string, target: string, reason: string}>} ignored
209
+ * @returns {void}
210
+ */
211
+ function resolveSupersedes(byId, ignored) {
212
+ /** targetId → Set of ids of records that validly supersede it. */
213
+ const supersededBy = new Map();
214
+ for (const rec of byId.values()) {
215
+ const target = rec.supersedes;
216
+ if (!isNonEmptyString(target) || target === rec.session_id) continue;
217
+ const targetRec = byId.get(target);
218
+ // A marker pointing at an id that is not present removes nothing; it is not
219
+ // an anomaly either (the target may legitimately have been collapsed by
220
+ // rule 3 first, or simply predate this window of the ledger).
221
+ if (!targetRec) continue;
222
+ const reason = supersedeRejectReason(rec, targetRec);
223
+ if (reason !== null) {
224
+ ignored.push({ by: rec.session_id, target, reason });
225
+ continue;
226
+ }
227
+ const set = supersededBy.get(target);
228
+ if (set) set.add(rec.session_id);
229
+ else supersededBy.set(target, new Set([rec.session_id]));
230
+ }
231
+ if (supersededBy.size === 0) return;
232
+
233
+ const ids = [...byId.keys()];
234
+ /** id → 'alive' | 'dead'; absent = not yet decided. */
235
+ const state = new Map();
236
+ for (;;) {
237
+ let changed = true;
238
+ while (changed) {
239
+ changed = false;
240
+ for (const id of ids) {
241
+ if (state.has(id)) continue;
242
+ const sup = supersededBy.get(id);
243
+ if (!sup || sup.size === 0) {
244
+ state.set(id, 'alive');
245
+ changed = true;
246
+ continue;
247
+ }
248
+ let anyAlive = false;
249
+ let allDead = true;
250
+ for (const s of sup) {
251
+ const st = state.get(s);
252
+ if (st === 'alive') anyAlive = true;
253
+ if (st !== 'dead') allDead = false;
254
+ }
255
+ if (anyAlive) {
256
+ state.set(id, 'dead');
257
+ changed = true;
258
+ } else if (allDead) {
259
+ state.set(id, 'alive');
260
+ changed = true;
261
+ }
262
+ }
263
+ }
264
+ const undecided = ids.filter((id) => !state.has(id));
265
+ if (undecided.length === 0) break;
266
+ // Cycle: keep the newest member, then let propagation settle the rest.
267
+ undecided.sort((a, b) => {
268
+ const ta = cycleOrderTimestamp(byId.get(a));
269
+ const tb = cycleOrderTimestamp(byId.get(b));
270
+ if (ta !== tb) return ta < tb ? 1 : -1;
271
+ return a < b ? -1 : 1;
272
+ });
273
+ state.set(undecided[0], 'alive');
274
+ }
275
+
276
+ for (const [id, st] of state) {
277
+ if (st === 'dead') byId.delete(id);
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Collapse a raw sessions.jsonl record array to one record per physical
283
+ * session AND report the supersede markers that were refused. Pure — the input
284
+ * array is never mutated.
285
+ *
286
+ * Rules, applied in this order (see the module header for the measured
287
+ * justification of each):
288
+ * 1. newest-wins per `session_id` (file order is chronological);
289
+ * 2. two `abandoned` records with an exact, both-present
290
+ * `started_at` + `completed_at` tuple collapse to the non-synthetic one;
291
+ * 3. a surviving record's ATTESTABLE `supersedes: X` removes record `X`.
292
+ *
293
+ * The double-stub collapse runs BEFORE supersede removal so that a stub which
294
+ * is itself about to be superseded still shadows its synthetic twin — with the
295
+ * old order the twin outlived the record it duplicated (a `supersedes` append
296
+ * shrank the tuple group to one member, and the phantom survived the session
297
+ * that refuted it).
298
+ *
299
+ * Records without a usable `session_id` are dropped, unless
300
+ * `keepUnidentified: true` (they cannot be deduplicated; a COUNT-style or
301
+ * effectiveness-style consumer would rather keep them than shrink its `n`).
302
+ *
303
+ * Output order follows FIRST appearance of each surviving id in the input; kept
304
+ * unidentified records are appended after them, in their original order.
305
+ *
306
+ * @param {Array<unknown>} records
307
+ * @param {object} [opts]
308
+ * @param {boolean} [opts.keepUnidentified=false] pass id-less record objects
309
+ * through untouched instead of dropping them.
310
+ * @returns {{records: Array<object>, ignoredSupersedes: Array<{by: string,
311
+ * target: string, reason: string}>}}
312
+ */
313
+ export function canonicalizeSessionsDetailed(records, { keepUnidentified = false } = {}) {
314
+ if (!Array.isArray(records)) return { records: [], ignoredSupersedes: [] };
315
+
316
+ // -- (1) newest-wins per id ------------------------------------------------
317
+ // Map insertion order = FIRST appearance of the id; the stored value is the
318
+ // LAST record carrying it, so a superseding append wins without reordering
319
+ // the ledger's chronology.
320
+ const byId = new Map();
321
+ const unidentified = [];
322
+ for (const rec of records) {
323
+ if (!isRecordObject(rec)) continue;
324
+ if (!isNonEmptyString(rec.session_id)) {
325
+ if (keepUnidentified) unidentified.push(rec);
326
+ continue;
327
+ }
328
+ byId.set(rec.session_id, rec);
329
+ }
330
+
331
+ // -- (2) narrow abandoned-tuple collapse -----------------------------------
332
+ collapseAbandonedTuples(byId);
333
+
334
+ // -- (3) supersede removal (order-independent, join-key constrained) -------
335
+ const ignoredSupersedes = [];
336
+ resolveSupersedes(byId, ignoredSupersedes);
337
+
338
+ return { records: [...byId.values(), ...unidentified], ignoredSupersedes };
339
+ }
340
+
341
+ /**
342
+ * Collapse a raw sessions.jsonl record array to one record per physical
343
+ * session. Pure — the input array is never mutated. Thin wrapper over
344
+ * `canonicalizeSessionsDetailed`, returning only the records (the array shape
345
+ * every consumer reads).
346
+ *
347
+ * @param {Array<unknown>} records
348
+ * @param {object} [opts] — see `canonicalizeSessionsDetailed`.
349
+ * @param {boolean} [opts.keepUnidentified=false]
350
+ * @returns {Array<object>} canonical records
351
+ */
352
+ export function canonicalizeSessions(records, opts) {
353
+ return canonicalizeSessionsDetailed(records, opts).records;
354
+ }
355
+
356
+ /**
357
+ * Count DISTINCT physical sessions in RAW `sessions.jsonl` text. Pure — no fs,
358
+ * so an async reader keeps its own `readFile` and only the counting rule is
359
+ * shared (the two async consumers, `memory-banner.mjs` and
360
+ * `cold-start-detector.mjs`, carried byte-identical copies of this body).
361
+ *
362
+ * Blank lines (incl. the trailing newline) are skipped. A line that does not
363
+ * PARSE is not counted at all — it cannot be attributed to any session (this
364
+ * replaces the pre-#1167 "count every non-empty line, never parse" rule).
365
+ *
366
+ * `canonicalizeSessions` DROPS records without a `session_id` (they cannot be
367
+ * deduplicated). For a COUNT that would under-report rather than de-duplicate,
368
+ * so id-less records are counted as-is and only the id-bearing ones go through
369
+ * the identity collapse.
370
+ *
371
+ * @param {string} raw — full file contents.
372
+ * @returns {number}
373
+ */
374
+ export function countSessionsInJsonl(raw) {
375
+ if (typeof raw !== 'string' || raw.length === 0) return 0;
376
+ const parsed = [];
377
+ for (const line of raw.split('\n')) {
378
+ const trimmed = line.trim();
379
+ if (!trimmed) continue;
380
+ try {
381
+ parsed.push(JSON.parse(trimmed));
382
+ } catch {
383
+ /* skip malformed line */
384
+ }
385
+ }
386
+ const identified = parsed.filter((r) => isRecordObject(r) && isNonEmptyString(r.session_id));
387
+ const anonymous = parsed.length - identified.length;
388
+ return canonicalizeSessions(identified).length + anonymous;
389
+ }
390
+
391
+ /**
392
+ * Read `sessions.jsonl` and return its canonical records (see
393
+ * `canonicalizeSessions` for the three collapse rules).
394
+ *
395
+ * Synchronous by design — every consumer of the ledger in this repo reads it
396
+ * with `readFileSync`, and the file is small (286 records / ~0.5 MB at the
397
+ * time of writing). A MISSING file (ENOENT) yields `[]` silently; an UNREADABLE
398
+ * one (EACCES/EISDIR/…) yields `[]` with a stderr WARN (#1188); each malformed line
399
+ * is skipped rather than aborting the whole read (same posture as the readers
400
+ * in `session-close-backfill.mjs` and `backfill-abandoned-sessions.mjs`).
401
+ *
402
+ * @param {object} [args]
403
+ * @param {string} [args.repoRoot] project root; the ledger is resolved as
404
+ * `<repoRoot>/.orchestrator/metrics/sessions.jsonl`. Defaults to
405
+ * `process.cwd()` when neither this nor `filePath` is given.
406
+ * @param {string} [args.filePath] explicit ledger path (wins over `repoRoot`).
407
+ * @returns {Array<object>} canonical records
408
+ */
409
+ export function readCanonicalSessions({ repoRoot, filePath } = {}) {
410
+ const resolved = isNonEmptyString(filePath)
411
+ ? filePath
412
+ : path.join(isNonEmptyString(repoRoot) ? repoRoot : process.cwd(), ...SESSIONS_REL);
413
+
414
+ let raw;
415
+ try {
416
+ raw = fs.readFileSync(resolved, 'utf8');
417
+ } catch (err) {
418
+ // #1188 — ENOENT and EACCES/EISDIR are different facts: a missing ledger is
419
+ // the ordinary fresh-repo case; an UNREADABLE one previously read as "no
420
+ // sessions" and made every downstream count silently wrong. Same split as
421
+ // readLockDetailed (session-lock.mjs § absent vs unreadable).
422
+ if (!err || err.code !== 'ENOENT') {
423
+ process.stderr.write(
424
+ `⚠ readCanonicalSessions: cannot read ${resolved} ` +
425
+ `(${err?.code ?? '?'}: ${err?.message ?? String(err)}) — ` +
426
+ 'treating as EMPTY, counts below are floors\n',
427
+ );
428
+ }
429
+ // CEILING (BV-004): still [] rather than throw — SessionStart callers must
430
+ // not crash on a transient permissions fault. REVISIT if the warn rate in
431
+ // events.jsonl shows masked corruption.
432
+ return [];
433
+ }
434
+
435
+ const parsed = [];
436
+ for (const line of raw.split('\n')) {
437
+ const trimmed = line.trim();
438
+ if (!trimmed) continue;
439
+ try {
440
+ parsed.push(JSON.parse(trimmed));
441
+ } catch {
442
+ /* skip malformed line */
443
+ }
444
+ }
445
+ return canonicalizeSessions(parsed);
446
+ }
@@ -132,6 +132,7 @@ import path from 'node:path';
132
132
 
133
133
  import { readLock, DEFAULT_TTL_HOURS } from './session-lock.mjs';
134
134
  import { isRealSession } from './session-schema/filters.mjs';
135
+ import { readCanonicalSessions } from './sessions-canonical.mjs';
135
136
 
136
137
  /** Repo-relative path to the session ledger (one record per closed session). */
137
138
  const SESSIONS_PATH = '.orchestrator/metrics/sessions.jsonl';
@@ -240,20 +241,21 @@ function keepNewer(current, candidate) {
240
241
  * result so the caller can say the anchor is a stub start, not a measured close;
241
242
  * the key is omitted (`undefined`) on the genuine path.
242
243
  *
243
- * @param {string[]} lines
244
+ * `records` is the CANONICAL (#1209b) record set — `readCanonicalSessions()`
245
+ * has already collapsed a duplicated `session_id` to its newest occurrence, so
246
+ * a since-corrected raw LINE for the same identity (e.g. one later marked
247
+ * `status: 'abandoned'`, or a stale `completed_at` a later record for the same
248
+ * id superseded) can no longer independently skew this max-reduce the way a
249
+ * raw-line scan over every append could.
250
+ *
251
+ * @param {object[]} records
244
252
  * @returns {{iso: string, ms: number, stubFallback?: true}|null}
245
253
  */
246
- function lastLedgerEntry(lines) {
254
+ function lastLedgerEntry(records) {
247
255
  let newestGenuine = null; // newest genuine completed_at (or started_at floor)
248
256
  let newestStub = null; // newest stub started_at
249
257
 
250
- for (const line of lines) {
251
- let record;
252
- try {
253
- record = JSON.parse(line);
254
- } catch {
255
- continue;
256
- }
258
+ for (const record of records) {
257
259
  if (!record || typeof record !== 'object') continue;
258
260
 
259
261
  if (isBackfillStub(record)) {
@@ -370,10 +372,15 @@ export function checkSessionsStaleness({ repoRoot, now = Date.now() } = {}) {
370
372
 
371
373
  const nowMs = typeof now === 'number' && Number.isFinite(now) ? now : Date.now();
372
374
 
373
- const sessionLines = readJsonlLines(path.join(repoRoot, SESSIONS_PATH));
375
+ const sessionsPath = path.join(repoRoot, SESSIONS_PATH);
376
+ const sessionLines = readJsonlLines(sessionsPath);
374
377
  if (sessionLines === null || sessionLines.length === 0) return null;
375
378
 
376
- const ledger = lastLedgerEntry(sessionLines);
379
+ // CANONICAL (#1209b) record set — readJsonlLines() above only decides
380
+ // missing-vs-empty (readCanonicalSessions() cannot tell those apart, see
381
+ // its own doc); the anchor scan itself now runs over the collapsed set.
382
+ const sessionRecords = readCanonicalSessions({ filePath: sessionsPath });
383
+ const ledger = lastLedgerEntry(sessionRecords);
377
384
  if (ledger === null) return null;
378
385
 
379
386
  const eventLines = readJsonlLines(path.join(repoRoot, EVENTS_PATH));
@@ -35,6 +35,7 @@ import path from 'node:path';
35
35
  import { fileURLToPath } from 'node:url';
36
36
 
37
37
  import { isRealSession } from '../session-schema/filters.mjs';
38
+ import { readCanonicalSessions } from '../sessions-canonical.mjs';
38
39
 
39
40
  const DEFAULT_INVOCATIONS_PATH = path.resolve(
40
41
  fileURLToPath(import.meta.url),
@@ -81,6 +82,15 @@ async function readJsonl(filePath) {
81
82
  * it to route the join to the `abandoned` outcome bucket instead of counting
82
83
  * a zero-signal join as `sessionsJoined`.
83
84
  *
85
+ * Callers pass `sessionRecords` from `readCanonicalSessions()` (#1209b) — one
86
+ * record per physical session (newest-wins per `session_id`, systemic
87
+ * double-stub twin dropped, superseded stubs removed). Without that upstream
88
+ * collapse, the two-writer double-stub class (a SEPARATE synthetic
89
+ * `session_id` for the SAME physical session) would enter this map as a
90
+ * second, independent `abandoned` entry — this function's own `map.set()`
91
+ * overwrite only dedupes an EXACT `session_id` repeat, never two different
92
+ * ids for one session.
93
+ *
84
94
  * @param {object[]} sessionRecords
85
95
  * @returns {Map<string, { agentSummary: { complete: number, partial: number, failed: number, spiral: number }, real: boolean }>}
86
96
  */
@@ -121,10 +131,13 @@ export async function joinSkillOutcomes({
121
131
  invocationsPath = DEFAULT_INVOCATIONS_PATH,
122
132
  sessionsPath = DEFAULT_SESSIONS_PATH,
123
133
  } = {}) {
124
- const [invocations, sessionRecords] = await Promise.all([
125
- readJsonl(invocationsPath),
126
- readJsonl(sessionsPath),
127
- ]);
134
+ // Invocations stay on the raw async reader (no identity-collapse concept
135
+ // applies to a selection-event stream). Sessions move to the CANONICAL
136
+ // (#1209b) reader — synchronous by contract (see sessions-canonical.mjs) —
137
+ // so a session_id counted twice under the two-writer double-stub bug no
138
+ // longer inflates `sessionsAbandoned` (see buildSessionMap doc above).
139
+ const invocations = await readJsonl(invocationsPath);
140
+ const sessionRecords = readCanonicalSessions({ filePath: sessionsPath });
128
141
 
129
142
  const sessionMap = buildSessionMap(sessionRecords);
130
143
 
@@ -9,8 +9,15 @@
9
9
  * @see scripts/lib/state-md/body-sections.mjs readCurrentTask, appendDeviation, markExpressPathComplete, appendWhatNotToRetry, readWhatNotToRetry, readOpenQuestions, appendOpenQuestion, markOpenQuestionAnswered
10
10
  * @see scripts/lib/state-md/mission-status.mjs parseMissionStatus, parseMissionStatusStrict, MISSION_STATUS_VALUES, writeMissionStatus, setMissionStatus, setMissionStatusDetailed, readMissionStatus, recoverFrontmatterMissionStatusDetailed, writeMissionStatusOnDisk, setMissionStatusOnDisk
11
11
  * @see scripts/lib/state-md/recommendations.mjs parseRecommendations
12
+ *
13
+ * Plus ONE small non-re-export surface: the `session-profile` frontmatter
14
+ * accessors at the bottom of this file (see their docblock for why they are
15
+ * composed here rather than added as a fourth mutator module).
12
16
  */
13
17
 
18
+ import { parseStateMd as _parseStateMd } from './state-md/yaml-parser.mjs';
19
+ import { updateFrontmatterFields as _updateFrontmatterFields } from './state-md/frontmatter-mutators.mjs';
20
+
14
21
  export { parseStateMd, serializeStateMd } from './state-md/yaml-parser.mjs';
15
22
 
16
23
  export {
@@ -61,3 +68,74 @@ export {
61
68
  } from './state-md/mission-status.mjs';
62
69
 
63
70
  export { parseRecommendations } from './state-md/recommendations.mjs';
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Session profile (PRD docs/prd/2026-09-06-ultradeep-session-profile.md)
74
+ // ---------------------------------------------------------------------------
75
+ //
76
+ // `session-profile` is an OPTIONAL STATE.md frontmatter scalar that names a
77
+ // wave-shape variant on top of an unchanged `session-type`. Today exactly one
78
+ // value is defined — `ultradeep` (7 waves, coordinator-direct Synthesis-Gate at
79
+ // wave 2) — resolved from the `/session ultradeep` argument alias in
80
+ // `commands/session.md`.
81
+ //
82
+ // The vocabulary is deliberately NOT a closed set here. A profile changes only
83
+ // how the coordinator shapes waves; unlike `session_type` (a closed set in
84
+ // scripts/lib/session-schema/constants.mjs, telemetry and the close-backfill),
85
+ // no consumer branches on the value, so an unknown one degrades to "a profile
86
+ // this reader does not recognise" rather than to a silent mislabel. Revisit
87
+ // trigger: the first consumer that BRANCHES on a specific profile value — at
88
+ // that point the set becomes load-bearing and belongs in a shared constant.
89
+ //
90
+ // Composed from the two existing helpers above rather than reaching into the
91
+ // frontmatter with a second parser: `parseStateMd` for the read,
92
+ // `updateFrontmatterFields` for the write (whose null/undefined semantics
93
+ // already mean DELETE, which is exactly "no profile").
94
+
95
+ /** Frontmatter key holding the optional session profile. */
96
+ export const SESSION_PROFILE_FIELD = 'session-profile';
97
+
98
+ /**
99
+ * Read the session profile from STATE.md contents.
100
+ *
101
+ * ABSENCE IS NEVER COERCED. Returns `null` — not `''`, not `'none'` — when the
102
+ * document has no frontmatter, no `session-profile` key, or a value that is not
103
+ * a non-empty string. Callers test `=== null` for "no profile"; they must not
104
+ * test truthiness of a string they assumed was always present.
105
+ *
106
+ * @param {string} contents Full STATE.md text.
107
+ * @returns {string|null} The profile name, or null when no profile is set.
108
+ */
109
+ export function readSessionProfile(contents) {
110
+ if (typeof contents !== 'string') return null;
111
+ const parsed = _parseStateMd(contents);
112
+ if (parsed === null) return null;
113
+ const value = parsed.frontmatter[SESSION_PROFILE_FIELD];
114
+ if (typeof value !== 'string') return null;
115
+ const trimmed = value.trim();
116
+ return trimmed.length > 0 ? trimmed : null;
117
+ }
118
+
119
+ /**
120
+ * Set or clear the session profile in STATE.md contents.
121
+ *
122
+ * Passing `null` DELETES the key, restoring the absent (= no profile) state —
123
+ * it never writes a placeholder value. Every other frontmatter key, including
124
+ * unknown extensions, is preserved verbatim by `updateFrontmatterFields`.
125
+ * No-ops (returns the input unchanged) when `contents` has no frontmatter.
126
+ *
127
+ * @param {string} contents Full STATE.md text.
128
+ * @param {string|null} profile Profile name, or null to clear.
129
+ * @returns {string} The new STATE.md text.
130
+ * @throws {TypeError} when `profile` is neither a non-empty string nor null.
131
+ */
132
+ export function setSessionProfile(contents, profile) {
133
+ if (profile !== null && (typeof profile !== 'string' || profile.trim().length === 0)) {
134
+ throw new TypeError(
135
+ `setSessionProfile: profile must be a non-empty string or null, got: ${JSON.stringify(profile)}`
136
+ );
137
+ }
138
+ return _updateFrontmatterFields(contents, {
139
+ [SESSION_PROFILE_FIELD]: profile === null ? null : profile.trim(),
140
+ });
141
+ }
@@ -471,6 +471,12 @@ function collectFiles(dir) {
471
471
  function isBoilerplateSite(relPath, kind, name) {
472
472
  if (kind === 'agent') {
473
473
  if (relPath === `agents/${name}.md`) return true;
474
+ // Kept for repoRoots where an authoring-spec file still lives at this path
475
+ // (pre-4.0.0 checkouts, template/consumer repos) — see
476
+ // tests/lib/sunset-walker.test.mjs "boilerplate exclusion (agents)". The
477
+ // session-orchestrator repo itself moved the spec to docs/agent-authoring.md,
478
+ // which is outside SCAN_DIRS and can never appear as a relPath here, so
479
+ // there is no live path to repoint this exclusion to.
474
480
  if (relPath === 'agents/AGENTS.md') return true;
475
481
  if (relPath === `agents/schemas/${name}.schema.json`) return true;
476
482
  // Routing-table / validator boilerplate.