session-orchestrator 3.24.0 → 4.0.1

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 (435) 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 +3 -2
  47. package/.codex-plugin/skills/architecture/SKILL.md +20 -0
  48. package/.codex-plugin/skills/autopilot/SKILL.md +21 -0
  49. package/.codex-plugin/skills/autopilot/agents/openai.yaml +5 -0
  50. package/.codex-plugin/skills/bootstrap/SKILL.md +22 -0
  51. package/.codex-plugin/skills/bootstrap/agents/openai.yaml +5 -0
  52. package/.codex-plugin/skills/brainstorm/SKILL.md +22 -0
  53. package/.codex-plugin/skills/brainstorm/agents/openai.yaml +5 -0
  54. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +17 -0
  55. package/.codex-plugin/skills/close/SKILL.md +21 -0
  56. package/.codex-plugin/skills/close/agents/openai.yaml +5 -0
  57. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +24 -0
  58. package/.codex-plugin/skills/debug/SKILL.md +21 -0
  59. package/.codex-plugin/skills/debug/agents/openai.yaml +5 -0
  60. package/.codex-plugin/skills/discovery/SKILL.md +21 -0
  61. package/.codex-plugin/skills/discovery/agents/openai.yaml +5 -0
  62. package/.codex-plugin/skills/dispatcher/SKILL.md +21 -0
  63. package/.codex-plugin/skills/dispatcher/agents/openai.yaml +5 -0
  64. package/.codex-plugin/skills/docs-orchestrator/SKILL.md +20 -0
  65. package/.codex-plugin/skills/ecosystem-health/SKILL.md +22 -0
  66. package/.codex-plugin/skills/eli5/SKILL.md +21 -0
  67. package/.codex-plugin/skills/eli5/agents/openai.yaml +5 -0
  68. package/.codex-plugin/skills/eval/SKILL.md +21 -0
  69. package/.codex-plugin/skills/eval/agents/openai.yaml +5 -0
  70. package/.codex-plugin/skills/evolve/SKILL.md +21 -0
  71. package/.codex-plugin/skills/evolve/agents/openai.yaml +5 -0
  72. package/.codex-plugin/skills/frontmatter-guard/SKILL.md +17 -0
  73. package/.codex-plugin/skills/gitlab-ops/SKILL.md +22 -0
  74. package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +17 -0
  75. package/.codex-plugin/skills/go/SKILL.md +22 -0
  76. package/.codex-plugin/skills/go/agents/openai.yaml +5 -0
  77. package/.codex-plugin/skills/grill/SKILL.md +21 -0
  78. package/.codex-plugin/skills/grill/agents/openai.yaml +5 -0
  79. package/.codex-plugin/skills/harness-audit/SKILL.md +19 -0
  80. package/.codex-plugin/skills/harness-audit/agents/openai.yaml +5 -0
  81. package/.codex-plugin/skills/hook-development/SKILL.md +17 -0
  82. package/.codex-plugin/skills/mcp-builder/SKILL.md +17 -0
  83. package/.codex-plugin/skills/memory-cleanup/SKILL.md +21 -0
  84. package/.codex-plugin/skills/memory-cleanup/agents/openai.yaml +5 -0
  85. package/.codex-plugin/skills/mode-selector/SKILL.md +19 -0
  86. package/.codex-plugin/skills/npm-publish/SKILL.md +18 -0
  87. package/.codex-plugin/skills/peekaboo-driver/SKILL.md +20 -0
  88. package/.codex-plugin/skills/persona-panel/SKILL.md +22 -0
  89. package/.codex-plugin/skills/persona-panel/agents/openai.yaml +5 -0
  90. package/.codex-plugin/skills/plan/SKILL.md +22 -0
  91. package/.codex-plugin/skills/plan/agents/openai.yaml +5 -0
  92. package/.codex-plugin/skills/playwright-driver/SKILL.md +22 -0
  93. package/.codex-plugin/skills/portfolio/SKILL.md +21 -0
  94. package/.codex-plugin/skills/portfolio/agents/openai.yaml +5 -0
  95. package/.codex-plugin/skills/quality-gates/SKILL.md +22 -0
  96. package/.codex-plugin/skills/reconcile/SKILL.md +21 -0
  97. package/.codex-plugin/skills/reconcile/agents/openai.yaml +5 -0
  98. package/.codex-plugin/skills/release/SKILL.md +22 -0
  99. package/.codex-plugin/skills/release/agents/openai.yaml +5 -0
  100. package/.codex-plugin/skills/remote-offload/SKILL.md +22 -0
  101. package/.codex-plugin/skills/repo-audit/SKILL.md +19 -0
  102. package/.codex-plugin/skills/repo-audit/agents/openai.yaml +5 -0
  103. package/.codex-plugin/skills/session/SKILL.md +21 -0
  104. package/.codex-plugin/skills/session/agents/openai.yaml +5 -0
  105. package/.codex-plugin/skills/session-end/SKILL.md +22 -0
  106. package/.codex-plugin/skills/session-plan/SKILL.md +22 -0
  107. package/.codex-plugin/skills/session-start/SKILL.md +22 -0
  108. package/.codex-plugin/skills/spinout/SKILL.md +21 -0
  109. package/.codex-plugin/skills/spinout/agents/openai.yaml +5 -0
  110. package/.codex-plugin/skills/sunset-review/SKILL.md +21 -0
  111. package/.codex-plugin/skills/sunset-review/agents/openai.yaml +5 -0
  112. package/.codex-plugin/skills/templates-ack/SKILL.md +21 -0
  113. package/.codex-plugin/skills/templates-ack/agents/openai.yaml +5 -0
  114. package/.codex-plugin/skills/test/SKILL.md +21 -0
  115. package/.codex-plugin/skills/test/agents/openai.yaml +5 -0
  116. package/.codex-plugin/skills/test-runner/SKILL.md +22 -0
  117. package/.codex-plugin/skills/tmux-layout/SKILL.md +23 -0
  118. package/.codex-plugin/skills/using-orchestrator/SKILL.md +19 -0
  119. package/.codex-plugin/skills/vault-mirror/SKILL.md +17 -0
  120. package/.codex-plugin/skills/vault-sync/SKILL.md +17 -0
  121. package/.codex-plugin/skills/wave-executor/SKILL.md +22 -0
  122. package/.codex-plugin/skills/write-executable-plan/SKILL.md +24 -0
  123. package/.cursor/commands/autopilot.md +2 -2
  124. package/.cursor/commands/bootstrap.md +1 -1
  125. package/.cursor/commands/brainstorm.md +1 -1
  126. package/.cursor/commands/debug.md +1 -1
  127. package/.cursor/commands/discovery.md +1 -1
  128. package/.cursor/commands/dispatcher.md +2 -2
  129. package/.cursor/commands/eli5.md +2 -2
  130. package/.cursor/commands/eval.md +2 -2
  131. package/.cursor/commands/evolve.md +1 -1
  132. package/.cursor/commands/go.md +1 -1
  133. package/.cursor/commands/grill.md +2 -2
  134. package/.cursor/commands/memory-cleanup.md +2 -2
  135. package/.cursor/commands/persona-panel.md +1 -1
  136. package/.cursor/commands/plan.md +1 -1
  137. package/.cursor/commands/portfolio.md +1 -1
  138. package/.cursor/commands/reconcile.md +2 -2
  139. package/.cursor/commands/release.md +2 -2
  140. package/.cursor/commands/session.md +2 -2
  141. package/.cursor/commands/spinout.md +2 -2
  142. package/.cursor/commands/sunset-review.md +2 -2
  143. package/.cursor/commands/templates-ack.md +2 -2
  144. package/.cursor/commands/test.md +2 -2
  145. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  146. package/.cursor/skills/eval/SKILL.md +1 -1
  147. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  148. package/.cursor/skills/remote-offload/SKILL.md +1 -1
  149. package/.cursor-plugin/plugin.json +30 -0
  150. package/.orchestrator/policy/blocked-commands.json +121 -0
  151. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  152. package/.orchestrator/policy/quality-gates.example.json +16 -0
  153. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  154. package/.orchestrator/policy/templates-policy.json +27 -0
  155. package/.orchestrator/policy/test-profiles.json +47 -0
  156. package/AGENTS.md +225 -0
  157. package/CHANGELOG.md +1314 -2
  158. package/NOTICE +11 -6
  159. package/README.md +135 -94
  160. package/agents/eval-judge.md +1 -1
  161. package/agents/skill-applied-judge.md +1 -1
  162. package/assets/wave-lifecycle.svg +98 -0
  163. package/commands/release.md +6 -3
  164. package/commands/session.md +18 -3
  165. package/docs/README.md +4 -0
  166. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  167. package/docs/baseline.md +67 -0
  168. package/docs/ci-setup.md +108 -62
  169. package/docs/codex-setup.md +107 -29
  170. package/docs/components.md +38 -16
  171. package/docs/cursor-setup.md +6 -2
  172. package/docs/events-schema.md +9 -6
  173. package/docs/instruction-delivery.md +69 -0
  174. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  175. package/docs/migration-v4.md +365 -0
  176. package/docs/pi-setup.md +6 -1
  177. package/docs/plugin-architecture-v3.md +1 -1
  178. package/docs/rule-authoring.md +85 -19
  179. package/docs/scope-collision-guard.md +5 -5
  180. package/docs/session-config-reference.md +57 -56
  181. package/docs/session-config-template.md +6 -29
  182. package/docs/telemetry.md +157 -3
  183. package/docs/vault-docs-architecture.md +50 -11
  184. package/hooks/_lib/hook-import-set.json +1488 -0
  185. package/hooks/_lib/subagent-transcript.mjs +562 -0
  186. package/hooks/config-protection.mjs +2 -2
  187. package/hooks/cwd-change-restore.mjs +2 -2
  188. package/hooks/enforce-commands.mjs +69 -0
  189. package/hooks/hooks-codex.json +1 -1
  190. package/hooks/hooks-cursor.json +10 -0
  191. package/hooks/hooks-pi.json +5 -0
  192. package/hooks/hooks.json +6 -1
  193. package/hooks/loop-guard.mjs +3 -3
  194. package/hooks/on-session-end.mjs +2 -2
  195. package/hooks/on-session-start.mjs +103 -2
  196. package/hooks/on-stop.mjs +60 -14
  197. package/hooks/operator-steer.mjs +2 -2
  198. package/hooks/post-bash-write-verify.mjs +85 -0
  199. package/hooks/post-edit-import-probe.mjs +344 -0
  200. package/hooks/post-subagent-discovery-validator.mjs +187 -431
  201. package/hooks/post-tool-batch-wave-signal.mjs +118 -4
  202. package/hooks/post-tool-failure-corrective-context.mjs +2 -2
  203. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  204. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  205. package/hooks/skill-invocation-telemetry.mjs +17 -5
  206. package/hooks/subagent-telemetry.mjs +13 -4
  207. package/monitors/monitors.json +3 -3
  208. package/package.json +9 -1
  209. package/pi/prompts/session.md +2 -2
  210. package/scripts/backfill-abandoned-sessions.mjs +50 -4
  211. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  212. package/scripts/dialectic-deriver.mjs +73 -8
  213. package/scripts/export-hw-learnings.mjs +113 -1
  214. package/scripts/generate-agents-skills.mjs +378 -0
  215. package/scripts/generate-codex-skills.mjs +246 -0
  216. package/scripts/generate-cursor-adapter.mjs +45 -8
  217. package/scripts/generate-hook-import-set.mjs +292 -0
  218. package/scripts/lib/agent-status.mjs +13 -2
  219. package/scripts/lib/auto-dream.mjs +38 -36
  220. package/scripts/lib/autonomy/suitability.mjs +6 -0
  221. package/scripts/lib/autopilot/loop.mjs +2 -2
  222. package/scripts/lib/ci-status-banner.mjs +220 -75
  223. package/scripts/lib/codex/plugin-contract.mjs +88 -6
  224. package/scripts/lib/config/auto-dream.mjs +2 -1
  225. package/scripts/lib/config/block-header.mjs +8 -0
  226. package/scripts/lib/config/block-preprocess.mjs +177 -0
  227. package/scripts/lib/config/broken-window.mjs +2 -1
  228. package/scripts/lib/config/cold-start.mjs +2 -1
  229. package/scripts/lib/config/config-protection.mjs +22 -2
  230. package/scripts/lib/config/context-coverage.mjs +2 -1
  231. package/scripts/lib/config/cross-repo.mjs +2 -1
  232. package/scripts/lib/config/custom-phases.mjs +2 -1
  233. package/scripts/lib/config/dialectic.mjs +2 -1
  234. package/scripts/lib/config/discovery-validator.mjs +2 -1
  235. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  236. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  237. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  238. package/scripts/lib/config/docs-staleness.mjs +2 -1
  239. package/scripts/lib/config/drift-check.mjs +2 -1
  240. package/scripts/lib/config/eval.mjs +2 -1
  241. package/scripts/lib/config/events-rotation.mjs +2 -1
  242. package/scripts/lib/config/evolve.mjs +8 -2
  243. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  244. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  245. package/scripts/lib/config/handover-gate.mjs +2 -1
  246. package/scripts/lib/config/health-endpoints.mjs +7 -2
  247. package/scripts/lib/config/host-paths.mjs +20 -4
  248. package/scripts/lib/config/issue-budget.mjs +2 -1
  249. package/scripts/lib/config/loop-guard.mjs +2 -1
  250. package/scripts/lib/config/memory.mjs +2 -1
  251. package/scripts/lib/config/moc-staleness.mjs +2 -1
  252. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  253. package/scripts/lib/config/private-config-dir.mjs +67 -0
  254. package/scripts/lib/config/reconcile.mjs +2 -1
  255. package/scripts/lib/config/remote-hosts.mjs +2 -1
  256. package/scripts/lib/config/section-extractor.mjs +7 -1
  257. package/scripts/lib/config/skill-evolution.mjs +2 -1
  258. package/scripts/lib/config/slopcheck.mjs +2 -1
  259. package/scripts/lib/config/state-md-lock.mjs +2 -1
  260. package/scripts/lib/config/templates-first.mjs +2 -1
  261. package/scripts/lib/config/test.mjs +2 -1
  262. package/scripts/lib/config/vault-integration.mjs +7 -1
  263. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  264. package/scripts/lib/config/vault-staleness.mjs +2 -1
  265. package/scripts/lib/config/vault-sync.mjs +2 -1
  266. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  267. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  268. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  269. package/scripts/lib/convergence-monitor.mjs +82 -16
  270. package/scripts/lib/dispatcher/rank.mjs +124 -48
  271. package/scripts/lib/ecosystem-health.mjs +16 -2
  272. package/scripts/lib/eval/engine.mjs +9 -1
  273. package/scripts/lib/eval/session-resolve.mjs +23 -4
  274. package/scripts/lib/events.mjs +22 -6
  275. package/scripts/lib/frontmatter-guard.mjs +131 -13
  276. package/scripts/lib/gates/gate-full.mjs +30 -0
  277. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  278. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  279. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  280. package/scripts/lib/host-identity.mjs +50 -11
  281. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  282. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  283. package/scripts/lib/learnings/io.mjs +60 -6
  284. package/scripts/lib/memory-proposals/store.mjs +30 -22
  285. package/scripts/lib/owner-config-banner.mjs +41 -6
  286. package/scripts/lib/owner-config-loader.mjs +21 -10
  287. package/scripts/lib/owner-interview.mjs +3 -3
  288. package/scripts/lib/owner-yaml.mjs +215 -15
  289. package/scripts/lib/platform.mjs +108 -15
  290. package/scripts/lib/plugin-update-banner.mjs +414 -0
  291. package/scripts/lib/project-hygiene.mjs +38 -2
  292. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  293. package/scripts/lib/quality-gate.mjs +133 -44
  294. package/scripts/lib/reconcile/emitter.mjs +68 -6
  295. package/scripts/lib/reconcile/engine.mjs +51 -11
  296. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  297. package/scripts/lib/reconcile/writer.mjs +40 -18
  298. package/scripts/lib/session-close-backfill.mjs +67 -9
  299. package/scripts/lib/session-id.mjs +12 -23
  300. package/scripts/lib/session-identity/own-session.mjs +125 -10
  301. package/scripts/lib/session-lock-shape.mjs +43 -0
  302. package/scripts/lib/session-lock.mjs +5 -10
  303. package/scripts/lib/session-registry.mjs +25 -9
  304. package/scripts/lib/session-schema/constants.mjs +64 -3
  305. package/scripts/lib/session-schema/validator.mjs +38 -4
  306. package/scripts/lib/session-start-probes.mjs +30 -1
  307. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  308. package/scripts/lib/skill-health/join.mjs +17 -4
  309. package/scripts/lib/state-md.mjs +78 -0
  310. package/scripts/lib/sunset/walker.mjs +6 -0
  311. package/scripts/lib/telemetry/schema.mjs +202 -9
  312. package/scripts/lib/telemetry/sync.mjs +368 -12
  313. package/scripts/lib/telemetry-flush-health-banner.mjs +211 -0
  314. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  315. package/scripts/lib/validate/check-agents.mjs +3 -3
  316. package/scripts/lib/validate/check-codex-skills.mjs +191 -0
  317. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  318. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  319. package/scripts/lib/validate/check-owner-leakage.mjs +319 -22
  320. package/scripts/lib/validate/check-skill-links.mjs +193 -0
  321. package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
  322. package/scripts/lib/validate/check-test-git-config-target.mjs +192 -12
  323. package/scripts/lib/validate/check-unwired-features.mjs +163 -15
  324. package/scripts/lib/validate/check-validator-registration.mjs +10 -4
  325. package/scripts/lib/validate/confidential-names.mjs +95 -30
  326. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  327. package/scripts/lib/validate/repo-files.mjs +48 -14
  328. package/scripts/lib/vault-backfill/template.mjs +63 -6
  329. package/scripts/lib/vault-mirror/process.mjs +165 -42
  330. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  331. package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
  332. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  333. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  334. package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
  335. package/scripts/lib/wave-resource-gate.mjs +8 -2
  336. package/scripts/lib/wave-sizing.mjs +4 -1
  337. package/scripts/lib/wave-transcript-tail.mjs +118 -4
  338. package/scripts/materialize-wave-scope.mjs +12 -5
  339. package/scripts/memory-propose.mjs +19 -5
  340. package/scripts/migrate-cold-start-seed.mjs +4 -1
  341. package/scripts/parse-config.mjs +60 -3
  342. package/scripts/release.mjs +430 -31
  343. package/scripts/repair-invalid-sessions.mjs +3 -3
  344. package/scripts/run-quality-gate.mjs +128 -11
  345. package/scripts/site-numbers.mjs +344 -8
  346. package/scripts/sweep-expired-learnings.mjs +90 -0
  347. package/scripts/sync-vault-schema.mjs +3 -1
  348. package/scripts/telemetry.mjs +2 -2
  349. package/scripts/validate-plugin.mjs +164 -0
  350. package/scripts/validate-wave-scope.mjs +28 -8
  351. package/scripts/wave-scope-binding.mjs +215 -0
  352. package/skills/_shared/instruction-file-resolution.md +10 -0
  353. package/skills/_shared/parallel-aware-preamble.md +1 -0
  354. package/skills/_shared/platform-tools.md +1 -1
  355. package/skills/_shared/state-ownership.md +1 -1
  356. package/skills/architecture/SKILL.md +7 -5
  357. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  358. package/skills/autopilot/SKILL.md +4 -18
  359. package/skills/claude-md-drift-check/SKILL.md +5 -1
  360. package/skills/claude-md-drift-check/checker.mjs +62 -2
  361. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  362. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  363. package/skills/discovery/probes-arch.md +20 -18
  364. package/skills/dispatcher/SKILL.md +3 -2
  365. package/skills/evolve/SKILL.md +65 -26
  366. package/skills/frontmatter-guard/SKILL.md +11 -5
  367. package/skills/npm-publish/SKILL.md +1 -1
  368. package/skills/reconcile/SKILL.md +33 -0
  369. package/skills/remote-offload/SKILL.md +1 -1
  370. package/skills/session-end/SKILL.md +18 -905
  371. package/skills/session-end/phase-3-6-tail.md +10 -3
  372. package/skills/session-end/plan-verification.md +221 -155
  373. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  374. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  375. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  376. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  377. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  378. package/skills/session-end/references/session-summary-template.md +62 -0
  379. package/skills/session-plan/SKILL.md +49 -0
  380. package/skills/session-start/SKILL.md +22 -904
  381. package/skills/session-start/phase-8-5-express-path.md +1 -1
  382. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  383. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  384. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  385. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  386. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  387. package/skills/session-start/references/phase-4-ssot-environment-check.md +160 -0
  388. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  389. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  390. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  391. package/skills/vault-sync/SKILL.md +10 -0
  392. package/skills/vault-sync/validator.mjs +21 -27
  393. package/skills/wave-executor/SKILL.md +15 -1
  394. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  395. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  396. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  397. package/skills/wave-executor/wave-loop.md +14 -1309
  398. package/templates/_shared/journey-manifest.md +10 -6
  399. package/.cursor/commands/autopilot-multi.md +0 -14
  400. package/.cursor/commands/contract-version-bump.md +0 -14
  401. package/.cursor/commands/journey-audit.md +0 -14
  402. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  403. package/.cursor/skills/daily/SKILL.md +0 -12
  404. package/.cursor/skills/domain-model/SKILL.md +0 -13
  405. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  406. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  407. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  408. package/commands/autopilot-multi.md +0 -74
  409. package/commands/contract-version-bump.md +0 -28
  410. package/commands/journey-audit.md +0 -43
  411. package/pi/prompts/autopilot-multi.md +0 -12
  412. package/pi/prompts/contract-version-bump.md +0 -12
  413. package/pi/prompts/journey-audit.md +0 -12
  414. package/scripts/autopilot-multi.mjs +0 -885
  415. package/scripts/backfill-learnings-expires.mjs +0 -196
  416. package/scripts/backfill-learnings.mjs +0 -203
  417. package/scripts/fleet-instruction-scan.mjs +0 -141
  418. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  419. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  420. package/scripts/lib/webhook-url.mjs +0 -105
  421. package/scripts/lifecycle-sim-v6.mjs +0 -347
  422. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  423. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  424. package/scripts/upload-social-preview.mjs +0 -316
  425. package/skills/_shared/model-selection.md +0 -64
  426. package/skills/contract-version-bump/SKILL.md +0 -219
  427. package/skills/daily/SKILL.md +0 -222
  428. package/skills/daily/generate.sh +0 -92
  429. package/skills/daily/templates/daily.md.tpl +0 -36
  430. package/skills/journey-audit/SKILL.md +0 -270
  431. package/skills/skill-creator/SKILL.md +0 -168
  432. package/skills/ubiquitous-language/SKILL.md +0 -97
  433. package/skills/vault-sync/package-lock.json +0 -40
  434. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  435. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -168,6 +168,18 @@ export const PROBES = [
168
168
  // vault-staleness-banner.mjs's header for the "one vocabulary, not two"
169
169
  // rationale.
170
170
  },
171
+ {
172
+ id: 'telemetry-flush-health',
173
+ spec: local('./telemetry-flush-health-banner.mjs'),
174
+ fn: 'checkTelemetryFlushHealth',
175
+ network: false,
176
+ args: ({ repoRoot }) => ({ repoRoot }),
177
+ // No custom render/severityOf: the probe returns `{severity:'warn',
178
+ // message}` — exactly the shape the module-level defaults below read. It
179
+ // reports the LAST `orchestrator.telemetry.flush` record when that record
180
+ // is a `sandbox:*` refusal (#1255); the refusal reached the ledger and
181
+ // nothing else before this entry existed.
182
+ },
171
183
  {
172
184
  id: 'ci-status',
173
185
  spec: local('./ci-status-banner.mjs'),
@@ -176,8 +188,16 @@ export const PROBES = [
176
188
  args: ({ repoRoot }) => ({ repoRoot }),
177
189
  // Bespoke shape: `{status, ok, details, …}` with no `message` field. The
178
190
  // banner text is prescribed by SKILL.md § Phase 4.
191
+ //
192
+ // The degraded branch is NOT decoration (#1031): this entry overrides BOTH
193
+ // `render` and `severityOf`, so the module-level defaults that already
194
+ // handle a `{severity:'warn', message, degraded}` result never run for this
195
+ // probe. Without these two lines a degraded ci-status result scored `'ok'`
196
+ // and rendered nothing — "could not read" displayed exactly like "green",
197
+ // which is the confusion the probe's own migration removed one layer down.
179
198
  render: (r) => {
180
199
  if (!r || typeof r !== 'object') return null;
200
+ if (r.degraded) return typeof r.message === 'string' && r.message ? r.message : null;
181
201
  if (r.status === 'red') {
182
202
  const pid = r.details?.currentPipelineId ?? '?';
183
203
  const green = r.lastGreen
@@ -193,7 +213,16 @@ export const PROBES = [
193
213
  return null;
194
214
  },
195
215
  // `status: 'red'` is an alert even though the probe publishes no severity.
196
- severityOf: (r) => (r?.status === 'red' ? 'alert' : r?.status === 'green' && r?.allowFailureJobs ? 'warn' : 'ok'),
216
+ // A degraded result is a finding, never clean same rule as the generic
217
+ // path in `severityOf()` below.
218
+ severityOf: (r) =>
219
+ r?.degraded
220
+ ? 'warn'
221
+ : r?.status === 'red'
222
+ ? 'alert'
223
+ : r?.status === 'green' && r?.allowFailureJobs
224
+ ? 'warn'
225
+ : 'ok',
197
226
  },
198
227
  {
199
228
  id: 'qg-command-drift',
@@ -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.
@@ -35,9 +35,10 @@
35
35
  * none and holds no mutable runtime state.
36
36
  */
37
37
 
38
- import { SO_PLATFORM, SO_PLUGIN_ROOT } from '../platform.mjs';
38
+ import { getPlatform, getPluginRoot } from '../platform.mjs';
39
39
  import { enumerateSurface } from '../sunset/walker.mjs';
40
40
  import { readPluginVersionFromPackageJson } from '../bootstrap-lock-freshness.mjs';
41
+ import { VALID_SESSION_PROFILES } from '../session-schema/constants.mjs';
41
42
 
42
43
  // ---------------------------------------------------------------------------
43
44
  // Constants
@@ -70,6 +71,52 @@ export const USAGE_PING_FIELDS = Object.freeze([
70
71
  'commands',
71
72
  ]);
72
73
 
74
+ /**
75
+ * ADDITIVE optional fields (schema v1, GitLab #1234). Deliberately a SECOND
76
+ * list rather than an extension of USAGE_PING_FIELDS above, for two reasons:
77
+ *
78
+ * 1. USAGE_PING_FIELDS is the REQUIRED v1 contract — `tests/telemetry/parity.mjs`
79
+ * asserts, field by field, that the server independently REQUIRES every
80
+ * member. An optional field is by definition not required, so putting it in
81
+ * that list would force the parity guard to be weakened for all 15 fields to
82
+ * accommodate one.
83
+ * 2. The frozen-list tripwire (`projectUsagePing` drops anything unlisted)
84
+ * stays intact: the projection whitelist is the UNION of both lists, so a
85
+ * leaky field still has to be added to a reviewed list before it can reach
86
+ * the wire.
87
+ *
88
+ * Server tolerance: `server/ingest/validate.mjs` accepts unknown top-level
89
+ * fields and preserves them verbatim in `raw_json` (see its module docblock), so
90
+ * every member here round-trips through a server that predates it.
91
+ */
92
+ export const USAGE_PING_OPTIONAL_FIELDS = Object.freeze([
93
+ 'fleet_self_declared',
94
+ 'session_record',
95
+ 'session_profile',
96
+ ]);
97
+
98
+ /**
99
+ * The full projection whitelist: required v1 fields + additive optional fields.
100
+ * `projectUsagePing` is driven from THIS, so neither list alone can leak a field.
101
+ */
102
+ const USAGE_PING_PROJECTED_FIELDS = Object.freeze([...USAGE_PING_FIELDS, ...USAGE_PING_OPTIONAL_FIELDS]);
103
+
104
+ /**
105
+ * `session_record` provenance tokens — WHICH source the session facts
106
+ * (`session_type`, `duration_bucket`) in this ping came from.
107
+ *
108
+ * - `ledger` — `.orchestrator/metrics/sessions.jsonl` had a matching record.
109
+ * - `derived` — no ledger record; the facts were reconstructed from
110
+ * `events.jsonl` (`orchestrator.session.started` + last event).
111
+ * - `absent` — neither source produced a session type. `session_type` is then
112
+ * `'unknown'` and `duration_bucket` is NOT a measurement.
113
+ *
114
+ * Measured 2026-09-06 (d8 audit): this repo has NO sessions.jsonl, so 100 % of
115
+ * its pings took the `absent` path and were indistinguishable on the wire from
116
+ * a genuinely-measured `other` / `<15m` session — 32 such pings on the server.
117
+ */
118
+ export const SESSION_RECORD_SOURCES = Object.freeze(['ledger', 'derived', 'absent']);
119
+
73
120
  /** Exact duration-bucket tokens (ASCII, stable wire values). */
74
121
  export const DURATION_BUCKETS = Object.freeze(['<15m', '15-60m', '1-3h', '>3h']);
75
122
 
@@ -78,11 +125,68 @@ const ROSTER_OTHER = 'other';
78
125
  const MAX_NAME_LENGTH = 64;
79
126
  const MAX_NAMES = 100;
80
127
 
128
+ /**
129
+ * The two list bounds that are genuinely SHARED with the ingest server
130
+ * (`server/ingest/validate.mjs` `INGEST_LIST_BOUNDS` — MAX_LIST_ITEMS /
131
+ * MAX_LIST_ITEM_LEN). Exported so `tests/telemetry/parity.test.mjs` can compare
132
+ * the two trees instead of trusting two hand-typed numbers: a client that caps
133
+ * higher than the server emits pings the server 400s.
134
+ *
135
+ * The server's other five bounds (MAX_ANON_ID, MAX_SENT_AT, MAX_PLUGIN_VERSION,
136
+ * MAX_SESSION_TYPE, MAX_SESSION_PROFILE) are SERVER-ONLY BY DESIGN — they bound
137
+ * inputs from any client, including foreign or tampered ones, and have no client
138
+ * counterpart to keep in lockstep. Do not mirror them here.
139
+ */
140
+ export const SHARED_LIST_BOUNDS = Object.freeze({
141
+ maxItems: MAX_NAMES,
142
+ maxItemLength: MAX_NAME_LENGTH,
143
+ });
144
+
81
145
  /** Enum fallbacks. */
82
146
  const VALID_PLATFORMS = Object.freeze(['claude', 'codex', 'cursor', 'pi']);
147
+ // DELIBERATE ASYMMETRY, not a drift: this is the TELEMETRY type set and omits
148
+ // `'unknown'`, which session-schema/constants.mjs VALID_SESSION_TYPES carries.
149
+ // There `unknown` is a storable ledger value; here it is a NORMALIZER FALLBACK
150
+ // (SESSION_TYPE_UNKNOWN below) that must never be reachable from the input set,
151
+ // or "we could not tell" would be indistinguishable from a measured type. Not
152
+ // unified with constants.mjs for exactly that reason.
83
153
  const VALID_SESSION_TYPES = Object.freeze(['housekeeping', 'feature', 'deep']);
84
154
  const PLATFORM_OTHER = 'other';
85
155
  const SESSION_TYPE_OTHER = 'other';
156
+ /**
157
+ * The type was never measured — distinct from `'other'`, which means "measured,
158
+ * but not one of the three known modes". Conflating the two is the defect this
159
+ * token fixes: before it existed, a ping built with NO session record silently
160
+ * reported `'other'`, so "we could not tell" and "we looked and it was unusual"
161
+ * were the same wire value.
162
+ *
163
+ * NOT an enum widening server-side: `session_type` is validated as a bounded
164
+ * STRING (`server/ingest/validate.mjs` `requireString` + `MAX_SESSION_TYPE`),
165
+ * never against a closed set, so `'unknown'` is accepted by today's server.
166
+ */
167
+ const SESSION_TYPE_UNKNOWN = 'unknown';
168
+
169
+ /**
170
+ * CLOSED whitelist of PUBLIC session-profile names that may reach the wire.
171
+ *
172
+ * RE-EXPORT, not a definition (GitLab #1252): the SSOT is
173
+ * `scripts/lib/session-schema/constants.mjs` `VALID_SESSION_PROFILES`, whose
174
+ * docblock carries the reproduced leak and the whitelist-over-regex rationale.
175
+ *
176
+ * Kept as a pure COMPATIBILITY SHIM, and measured as one: since 2026-09-07 no
177
+ * module in this repo imports the name from here (`rg -n VALID_SESSION_PROFILES
178
+ * scripts server hooks skills tests` — the two telemetry tests that used to now
179
+ * address constants.mjs). It stays because removing an export is a MAJOR change
180
+ * and this is a patch line; a deep importer outside the repo may still hold it.
181
+ * `tests/telemetry/schema.test.mjs` pins it with an IDENTITY assertion
182
+ * (`toBe`, not `toEqual`) so the shim can never quietly become a second
183
+ * definition of the whitelist.
184
+ *
185
+ * Adding a profile is a reviewed edit in constants.mjs AND in the server's
186
+ * mirror (`server/ingest/validate.mjs` `SESSION_PROFILES`) — the same two-tree
187
+ * contract `ACCEPTED_VERSIONS` already uses server-side.
188
+ */
189
+ export { VALID_SESSION_PROFILES };
86
190
 
87
191
  /**
88
192
  * Closed sets for os/arch client-side normalization. A value outside the set —
@@ -121,7 +225,8 @@ function isNonEmptyString(v) {
121
225
  // ---------------------------------------------------------------------------
122
226
 
123
227
  /**
124
- * Project an arbitrary object onto the usage-ping whitelist (USAGE_PING_FIELDS).
228
+ * Project an arbitrary object onto the usage-ping whitelist (USAGE_PING_FIELDS
229
+ * PLUS USAGE_PING_OPTIONAL_FIELDS).
125
230
  * Fully data-driven: any key not on the whitelist — paths, repo names, prompts,
126
231
  * args, hostnames, rogue extras — is dropped. Array fields (skills, commands) are
127
232
  * copied as NEW arrays so no caller reference leaks into the projection.
@@ -132,7 +237,7 @@ function isNonEmptyString(v) {
132
237
  export function projectUsagePing(input) {
133
238
  if (!isPlainObject(input)) return {};
134
239
  const out = {};
135
- for (const key of USAGE_PING_FIELDS) {
240
+ for (const key of USAGE_PING_PROJECTED_FIELDS) {
136
241
  if (key in input) {
137
242
  const v = input[key];
138
243
  out[key] = Array.isArray(v) ? [...v] : v;
@@ -163,7 +268,7 @@ export function projectUsagePing(input) {
163
268
  export function loadRoster({ pluginRoot } = {}) {
164
269
  const root = (typeof pluginRoot === 'string' && pluginRoot.trim() !== '')
165
270
  ? pluginRoot
166
- : SO_PLUGIN_ROOT;
271
+ : getPluginRoot();
167
272
 
168
273
  if (!isNonEmptyString(root)) {
169
274
  process.stderr.write(
@@ -309,8 +414,9 @@ export function deriveDurationBucket(startedAtISO, completedAtISO) {
309
414
  * whose null return (missing/unparseable package.json or non-string version) maps to 'unknown'.
310
415
  */
311
416
  function resolvePluginVersion() {
312
- if (!isNonEmptyString(SO_PLUGIN_ROOT)) return 'unknown';
313
- return readPluginVersionFromPackageJson(SO_PLUGIN_ROOT) ?? 'unknown';
417
+ const root = getPluginRoot();
418
+ if (!isNonEmptyString(root)) return 'unknown';
419
+ return readPluginVersionFromPackageJson(root) ?? 'unknown';
314
420
  }
315
421
 
316
422
  /** Normalize the detected platform to the closed enum (+ 'other' fallback). */
@@ -320,7 +426,36 @@ function normalizePlatform(platform) {
320
426
 
321
427
  /** Normalize the session type to the closed enum (+ 'other' fallback). */
322
428
  function normalizeSessionType(sessionType) {
323
- return VALID_SESSION_TYPES.includes(sessionType) ? sessionType : SESSION_TYPE_OTHER;
429
+ if (VALID_SESSION_TYPES.includes(sessionType)) return sessionType;
430
+ // ABSENT ≠ UNRECOGNISED. A missing/blank value means nothing was measured
431
+ // (`unknown`); a present-but-unlisted value means something WAS measured and
432
+ // is not one of the three modes (`other`). The old single-branch version
433
+ // returned `other` for both, which is how 394 pings from a host with no
434
+ // sessions.jsonl arrived looking like measured `other` sessions (d8, 2026-09-06).
435
+ return isNonEmptyString(sessionType) ? SESSION_TYPE_OTHER : SESSION_TYPE_UNKNOWN;
436
+ }
437
+
438
+ /**
439
+ * Normalize a session PROFILE against the closed VALID_SESSION_PROFILES
440
+ * whitelist. Anything else — absent, blank, or an unlisted repo-authored value —
441
+ * returns `null`, and the caller OMITS the field.
442
+ *
443
+ * DELIBERATELY NOT MIRRORING normalizeSessionType's two-token report. That helper
444
+ * can distinguish ABSENT (`'unknown'`) from UNRECOGNISED (`'other'`) because both
445
+ * of its answers are public constants. Here the unrecognised VALUE is precisely
446
+ * what must not travel, and there is no third token that carries the distinction
447
+ * without carrying the string: an `'other'` profile would say "this host ran a
448
+ * profile we do not ship" — a fact of no product use, bought with a new wire
449
+ * value. So both cases collapse to omission, and the wire cannot tell them apart
450
+ * BY DESIGN. The local `session-profile` key is untouched either way.
451
+ *
452
+ * @param {unknown} profile
453
+ * @returns {string|null} a whitelisted profile name, or null (⇒ field omitted).
454
+ */
455
+ export function normalizeSessionProfile(profile) {
456
+ if (!isNonEmptyString(profile)) return null;
457
+ const trimmed = profile.trim();
458
+ return VALID_SESSION_PROFILES.includes(trimmed) ? trimmed : null;
324
459
  }
325
460
 
326
461
  /**
@@ -403,6 +538,9 @@ export function buildUsagePing({
403
538
  env = process.env,
404
539
  now = new Date().toISOString(),
405
540
  roster,
541
+ consentState,
542
+ sessionRecordSource,
543
+ sessionProfile,
406
544
  } = {}) {
407
545
  const session = isPlainObject(sessionRecord) ? sessionRecord : {};
408
546
  const invocations = Array.isArray(skillInvocations) ? skillInvocations : [];
@@ -431,19 +569,74 @@ export function buildUsagePing({
431
569
  (kind === 'command' ? commandNames : skillNames).push(name);
432
570
  }
433
571
 
572
+ // ── fleet: a statement about the OPERATOR, derived from the RESOLVED consent
573
+ // state, not from a raw owner.yaml read ────────────────────────────────────
574
+ // The old expression was `ownerConfig?.telemetry?.enabled === true` — a
575
+ // statement about a FILE. Measured 2026-09-06 (d8): the operator's second Mac
576
+ // has consent granted but no `telemetry:` block in owner.yaml, so 394 of 490
577
+ // server records (80,4 %) counted the operator as an external user and every
578
+ // week's `fleet_vs_external` was wrong.
579
+ //
580
+ // `resolveConsent()` already answers this question correctly: it returns
581
+ // `enabled-fleet` for the owner.yaml opt-in AND `enabled-env` for
582
+ // `SO_TELEMETRY=1`, both of which are operator-side postures. Callers that
583
+ // pass `consentState` get that answer; the ownerConfig fallback below keeps
584
+ // the two-argument callers (and the CLI preview) behaviourally identical.
585
+ //
586
+ // CEILING, named because the client cannot close it: this is still
587
+ // SELF-DECLARED — a sandbox or a host whose owner.yaml is unreachable declares
588
+ // `false` however honest it is. The authoritative classification is
589
+ // server-side (`SO_INGEST_FLEET_ANON_IDS`, server/ingest/config.mjs).
590
+ // `session_profile` (STATE.md frontmatter `session-profile`) is bounded by the
591
+ // CLOSED VALID_SESSION_PROFILES whitelist — it was the one free-text value on
592
+ // the wire until 2026-09-06, and a repo-authored one at that (see the
593
+ // whitelist's own docblock for the reproduced leak). It is still deliberately
594
+ // NOT routed through normalizeSessionType: that helper degrades anything
595
+ // outside ['housekeeping','feature','deep'] to 'other', which is exactly the
596
+ // silent loss the profile exists to prevent. The contract is
597
+ // `session_type: "deep"` PLUS `session_profile: "ultradeep"` — never
598
+ // `session_type: "ultradeep"`, and never a profile flattened to 'other'.
599
+ //
600
+ // ABSENT IS NOT EMPTY: with no profile — and with an UNLISTED one — the KEY IS
601
+ // OMITTED, matching every other optional ping field. `projectUsagePing` copies
602
+ // only keys that are `in` the input, so an omitted key never reaches the wire
603
+ // as `null`, and an unlisted profile never reaches it at all.
604
+ //
605
+ // SOURCE PRECEDENCE is resolved BEFORE normalization (explicit argument beats
606
+ // the session record), so an unlisted explicit profile is dropped rather than
607
+ // silently replaced by the record's value.
608
+ const rawProfile =
609
+ isNonEmptyString(sessionProfile) ? sessionProfile
610
+ : isNonEmptyString(session.session_profile) ? session.session_profile
611
+ : null;
612
+ const profile = normalizeSessionProfile(rawProfile);
613
+
614
+ const fleetSelfDeclared =
615
+ consentState === undefined || consentState === null
616
+ ? ownerConfig?.telemetry?.enabled === true
617
+ : consentState === 'enabled-fleet' || consentState === 'enabled-env';
618
+
434
619
  return {
435
620
  record_kind: 'usage-ping',
436
621
  schema_version: USAGE_PING_SCHEMA_VERSION,
437
622
  sent_at: now,
438
623
  plugin_version: resolvePluginVersion(),
439
- platform: normalizePlatform(SO_PLATFORM),
624
+ platform: normalizePlatform(getPlatform()),
440
625
  os: normalizeOs(process.platform),
441
626
  arch: normalizeArch(process.arch),
442
627
  node_major: parseInt(process.versions.node, 10),
443
628
  ci: deriveCi(env),
444
- fleet: ownerConfig?.telemetry?.enabled === true,
629
+ fleet: fleetSelfDeclared,
630
+ // DEPRECATED alias of `fleet_self_declared`, kept for one schema generation
631
+ // (removal: 2027-03-06 — see docs/telemetry.md § Schema evolution) so the
632
+ // server's existing `fleet` column stays comparable across the rename.
633
+ fleet_self_declared: fleetSelfDeclared,
445
634
  session_type: normalizeSessionType(session.session_type),
635
+ ...(profile !== null ? { session_profile: profile } : {}),
446
636
  duration_bucket: deriveDurationBucket(session.started_at, session.completed_at),
637
+ session_record: SESSION_RECORD_SOURCES.includes(sessionRecordSource)
638
+ ? sessionRecordSource
639
+ : 'absent',
447
640
  skills: filterRosterNames(skillNames, rosterSkills),
448
641
  commands: filterRosterNames(commandNames, rosterCommands),
449
642
  };