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
@@ -41,13 +41,13 @@ The coordinator's **own** planned direct edits belong in `coordinator.json` in t
41
41
 
42
42
  `wave-scope.json` is written into the WORKING COPY, not into the session — so until #1123 one session's manifest governed every session sharing that checkout. Measured 2026-08-22 (#1082): a Discovery wave's `allowedPaths: []` — which `wave-loop.md` prescribes for *every* Discovery wave — denied every write of an unrelated parallel session, with a deny reason that could only tell it to fix a wave plan it does not own.
43
43
 
44
- Two OPTIONAL manifest fields close that: `session` (the raw `session_id`) and its human-readable twin `semantic_session`. Both come from ONE `sessionAttribution(repoRoot)` call (`scripts/lib/events.mjs`, reads `.orchestrator/session.lock` once) in the same coordinator step that writes the rest of the manifest — `skills/wave-executor/wave-loop.md` § Scope Manifest 1.
44
+ Two OPTIONAL manifest fields close that: `session_id` (the raw harness session id) and its human-readable twin `semantic_session_id` — the same spelling `.orchestrator/session.lock` and `current-session.json` use (renamed from `session` / `semantic_session` in #1153 P2; readers accept the legacy pair until the next minor release, see `MANIFEST_SESSION_KEYS` in `scripts/lib/session-identity/own-session.mjs`). Both come from ONE `attributionForRecord(repoRoot)` call (`scripts/lib/events.mjs`) in the same coordinator step that writes the rest of the manifest — `skills/wave-executor/wave-loop.md` § Scope Manifest 1. Unlike a raw lock read, `attributionForRecord()` reads `.orchestrator/session.lock` and then confirms the lock's raw `session_id` against `readProcessLocalSessionIds()` before returning anything — a mismatch (or no process-local id at all) yields `{}`, never a peer's ids (#1207).
45
45
 
46
46
  The reader is `hooks/enforce-scope.mjs` **Gate 3b**, between the manifest parse (G3) and the path-guard gate (G4). It resolves identity via `new Set(readProcessLocalSessionIds({ hookInput: input }))` and classifies via `classifyManifestSession(scope, ownIds)`, both from [`scripts/lib/session-identity/own-session.mjs`](../scripts/lib/session-identity/own-session.mjs):
47
47
 
48
48
  | Manifest state | `classifyManifestSession` verdict | Gate 3b disposition |
49
49
  |---|---|---|
50
- | no `session` / `semantic_session` (legacy, pre-#1123) | `unknown` | ENFORCE — falls through unchanged |
50
+ | no `session_id` / `semantic_session_id` (and no legacy `session` / `semantic_session`; pre-#1123) | `unknown` | ENFORCE — falls through unchanged |
51
51
  | an id present and matching one of our own | `own` | ENFORCE — falls through unchanged |
52
52
  | ids present, none matching, own identity resolvable | `foreign` | **ALLOW** + one `orchestrator.scope.foreign_session_ignored` event |
53
53
  | ids present, own identity unresolvable (empty id set) | `unknown` | ENFORCE |
@@ -56,7 +56,7 @@ Five properties are choices, not omissions — and every one of them points the
56
56
 
57
57
  - **Only what is PROVABLY foreign is foreign.** `readProcessLocalSessionIds()` returns the ids that are PROCESS-LOCAL — hook input (`session_id`/`sessionId`/`parent_session_id`) and `CLAUDE_CODE_SESSION_ID` — and an EMPTY set when neither yields an id, which can only produce `unknown`. The repo-global `session.lock` is deliberately NOT a tier here (#1194): it is ONE file shared by every session in the checkout, so unioning it let a peer's manifest match a peer-written lock id, classify `own`, and have Gate 7 deny the second session's legitimate writes — the exact lockout G3b exists to end. A better signal REPLACES a worse one (`.claude/rules/host-resources.md` § HR-102). A gate that guessed would turn "cannot tell" into a silent enforcement-off on every harness exporting no session id. Every value is trimmed on the way in, so a whitespace-only env var cannot enter as a phantom id that matches nothing (`.claude/rules/development.md` § env-var whitespace trap).
58
58
  - **Union across the process-local tiers, not first-tier-wins.** Both process-local tiers are read and merged; only an id in NEITHER is somebody else's. Gating them against each other made the READER's identity a strict subset of the WRITER's — the manifest's `session` comes from `sessionAttribution()` = the same repo-global lock — and two divergences inside these tiers produce the same silent failure, the OWN manifest read `foreign` and the write gate switched itself off for the whole wave, logging an event indistinguishable from correct behaviour: (a) a nested harness where payload `session_id` ≠ `CLAUDE_CODE_SESSION_ID` (measured in `hooks/pre-bash-issue-budget.mjs` `resolveSessionId`); (b) a sub-agent invocation, whose own id is the subagent's while the manifest names the coordinator. A third divergence — a session that lost the lock race and wrote a PEER's id into its own manifest — is NO longer covered here since #1194 dropped the lock tier; it is the accepted cost in limit 11, and its defense is the writer guard. The merge only ADDS ids the process actually carries, so the security direction is unchanged: a manifest whose id appears in neither tier still classifies `foreign`. Its cost is named in limit 11 below.
59
- - **The writer verifies the binding names itself.** Because the lock is repo-global, `skills/wave-executor/wave-loop.md` § Scope Manifest 1 requires the coordinator to compare `sessionAttribution()`'s ids against its own session (STATE.md `session`) and to OMIT the `session`/`semantic_session` keys when they diverge — unbound = ENFORCE. The reader's union covers the case anyway; the writer guard keeps the manifest readable as an audit record instead of publishing a foreign name.
59
+ - **The writer's binding check is mechanical, not a prose comparison (#1207).** Because the lock is repo-global, `skills/wave-executor/wave-loop.md` § Scope Manifest 1 calls `attributionForRecord(repoRoot)`, which confirms the lock's raw `session_id` against `readProcessLocalSessionIds()` internally and returns `{}` on any mismatch or absent process-local id — the coordinator writes whatever comes back, with no further comparison to perform. STATE.md is deliberately not part of this check: it is a shared working-copy artefact written by whichever session holds the lock, so under a peer-owned lock STATE.md's `session` field would agree with the lock about the same peer and "confirm" exactly the wrong id. OMIT the `session_id`/`semantic_session_id` keys whenever `attributionForRecord()` returns `{}` — unbound = ENFORCE. The reader's union covers the case anyway; the writer guard keeps the manifest readable as an audit record instead of publishing a foreign name.
60
60
  - **Gate 3b runs after the parse, never on the raw bytes.** A corrupt manifest yields `{}`, hence no ids, hence `unknown` — and keeps failing closed. A gate that peeked at the bytes first would let a truncated manifest disarm the guard.
61
61
  - **The empty string is a validator ERROR, not a third flavour of absent.** `validateSession()` → `validateOptionalSessionId()` in `scripts/validate-wave-scope.mjs` rejects `"session": ""` with *"an empty id attributes to nothing; omit the key entirely to declare the manifest unbound"*. An empty id satisfies a truthiness check while matching nobody, so every reader would classify the manifest FOREIGN where the writer meant UNBOUND — opposite dispositions, not a cosmetic ambiguity. An ABSENT key only WARNS, because the § 3.3 pre-union skeleton is itself an unbound manifest and so is every manifest written before #1123.
62
62
 
@@ -89,7 +89,7 @@ false
89
89
  true true
90
90
  ```
91
91
 
92
- Both globs match `scripts/lib/x.mjs`, yet the direct comparison says `false`. For `assertFileScopeSubset()` that inexactness is *safe*: its glob branch reduces to verbatim presence plus literal-prefix coverage and therefore **over-approximates coverage**, which at worst accepts a union it could not fully prove. For a **collision** check the sign flips — the same over-approximation becomes a **false negative**, i.e. a missed collision, i.e. the incident. That is why the two exact stages decide first and stage 3 is reached only for pairs neither can settle.
92
+ Both globs match `scripts/lib/x.mjs`, yet the direct comparison says `false`. For `assertFileScopeSubset()` that inexactness is *safe*: its glob branch reduces to verbatim presence plus literal-prefix coverage and therefore **over-approximates coverage**, which at worst accepts a union it could not fully prove. For a **collision** check the sign flips — the same over-approximation becomes a **false negative**, i.e. a missed collision, i.e. the incident. That is why the two exact stages decide first and stage 3 is reached only for pairs neither can settle. <!-- path-check: example -->
93
93
 
94
94
  ## 4. The hook
95
95
 
@@ -174,7 +174,7 @@ Complete list of what this guard does **not** see, or sees only approximately:
174
174
  8. **Lock loss reopens the race.** On lock timeout the cycle runs unlocked (row 14) — two dispatches starting together can then read the same ledger state and one record is lost. That is the pre-lock behaviour, chosen over denying on a lock-file problem.
175
175
  9. **The session binding is self-declared** (§ 2.3). `session` is a plain field in a file any process in this working copy can write, so writing a foreign id into it turns the write gate off for that manifest. Named rather than hidden: it is the SAME power `enforcement: "off"` already grants in the same file, so Gate 3b adds no new authority — the manifest is the coordinator's own artefact either way.
176
176
  10. **Only the WRITE gate is session-bound.** The dispatch ledger of § 4 takes its session component from the harness's own `input.session_id` (`waveKeyOf(projectDir, sessionId, …)`), and reads only `wave` and `role` out of `wave-scope.json` — the `session` field is not consulted there at all. So a peer session's manifest cannot bind this session's writes since #1123, but the two hooks reach that property by different routes, and a change to one does not carry to the other.
177
- 11. **A session that published a peer's id reads its OWN manifest as `foreign` (#1194).** Dropping the lock tier (§ 2.3) moved the cost to the other side of the trade, deliberately: a session that lost the `bootstrapLock()` race got the peer's id from `sessionAttribution()`, and if it writes that id into its own manifest, Gate 3b now classifies the manifest `foreign` and its own write guard stands down. The defense is on the WRITER, not the reader — `skills/wave-executor/wave-loop.md` § Scope Manifest, "Verify the binding names YOU before you write it": compare both ids against your own session and OMIT the keys when they diverge, because unbound = ENFORCE. CEILING (BV-004): on a harness that exports no session env var and puts no `session_id` in the hook payload (Codex CLI, Cursor today) both tiers are empty, so G3b is permanently `unknown` = enforce = pre-#1123 behaviour there. Revisit when Codex/Cursor hook payloads carry a session id.
177
+ 11. **A session that published a peer's id reads its OWN manifest as `foreign` (#1194) — the writer's defense is now mechanical (#1207).** Dropping the lock tier (§ 2.3) moved the cost to the other side of the trade: with a raw `sessionAttribution()` read, a session that lost the `bootstrapLock()` race could get the peer's id and write it into its own manifest, and Gate 3b would then classify the manifest `foreign`, standing its own write guard down. Since #1207 the writer calls `attributionForRecord(repoRoot)` instead — `skills/wave-executor/wave-loop.md` § Scope Manifest 1 — which confirms the lock's raw `session_id` against `readProcessLocalSessionIds()` before returning anything and yields `{}` on any mismatch, so the coordinator performs no manual comparison and STATE.md plays no part in it (a shared working-copy artefact written by whichever session holds the lock, not a process-local witness). A filled binding is therefore provably this session's own; unbound (`{}`) = ENFORCE. CEILING (BV-004): on a harness that exports no session env var and puts no `session_id` in the hook payload (Codex CLI, Cursor today) both tiers are empty, so G3b is permanently `unknown` = enforce = pre-#1123 behaviour there. Revisit when Codex/Cursor hook payloads carry a session id.
178
178
 
179
179
  ## 7. Debugging
180
180
 
@@ -61,6 +61,8 @@ Bypass via `SO_SKIP_CONFIG_VALIDATION=1`. Missing fields can be patched into an
61
61
 
62
62
  **Stale-citation note:** an older code comment on the `custom-phases:` key in this repo's own `CLAUDE.md` cites a per-key regex (`/^custom-phases:\s*$/`) as the mechanism. That citation predates the #830 generalisation — `custom-phases.mjs` (like all 37 consumers) now delegates to the shared `matchBlockHeader(line, 'custom-phases')`, which is strictly MORE tolerant than the old per-key regex (it additionally accepts the dash-bullet and bold-bullet renderings). The no-inline-comment failure mode is unchanged; only the underlying mechanism moved from a bespoke regex to the shared helper. Treat any remaining per-key regex citation in prose (including in this file, prior to this section's introduction) as documentation of the OLD mechanism — the general contract above is current.
63
63
 
64
+ **A second, orthogonal gotcha shares this section: a multi-line `<!-- … -->` comment (#1162).** Every block-shaped parser now strips commented-out lines before matching, via `scripts/lib/config/block-preprocess.mjs` — so a block commented out to disable it can no longer be read as live config, and a bold-bullet sub-key rendering (`- **enabled:** true`) is normalised before parsing instead of silently missing its regex. The one failure mode that still exists is an **unterminated** `<!--` — a stray opener with no matching `-->` anywhere in the rest of the document. `scripts/parse-config.mjs` detects this ONCE per session (not once per parser) and prints a single stderr WARN: `⚠ <file>: unterminated <!-- at line N — comment stripping disabled for the whole document`. The fail-closed direction differs by consumer: a block PARSER gets its lines back UNFILTERED (nothing may silently vanish), while the two destructive-bypass scanners (`allow-config-weakening`, `allow-destructive-ops`) treat an unterminated comment as the bypass being **NOT ARMED** — an ambiguous document must never grant an opt-in it cannot read cleanly.
65
+
64
66
  ## Policy Files
65
67
 
66
68
  Some sub-configs live in dedicated policy files under `.orchestrator/policy/`:
@@ -74,12 +76,46 @@ Some sub-configs live in dedicated policy files under `.orchestrator/policy/`:
74
76
 
75
77
  | Field | Type | Default | Description |
76
78
  |-------|------|---------|-------------|
77
- | `agents-per-wave` | integer or integer with overrides | `6` | Maximum parallel subagents per wave. Supports session-type overrides: `6 (deep: 18)` outputs `{"default": 6, "deep": 18}`. Plain integers remain plain. The override key names a session type but does **not** create one: there is no `session-type:` Session Config key — `parseSessionConfig()` emits none, so writing one into a repo's `## Session Config` block is inert prose. The session type comes from the `/session` argument (default `deep`, see `commands/session.md`) and is persisted to STATE.md frontmatter as `session-type:`, which is the only live read (`scripts/print-applicable-rules.mjs` rule mode-gating). |
79
+ | `agents-per-wave` | integer or integer with overrides | `6` | Maximum parallel subagents per wave. Supports session-type overrides: `6 (deep: 18)` outputs `{"default": 6, "deep": 18}`. The override key set is OPEN — `_coerceInteger` (`scripts/lib/config/coercers.mjs`) parses whatever keys the parentheses contain, so `6 (deep: 18, ultradeep: 18)` outputs `{"default": 6, "deep": 18, "ultradeep": 18}` with no code change (see § Session Profile below). Plain integers remain plain. The override key names a session type but does **not** create one: there is no `session-type:` Session Config key — `parseSessionConfig()` emits none, so writing one into a repo's `## Session Config` block is inert prose. The session type comes from the `/session` argument (default `deep`, see `commands/session.md`) and is persisted to STATE.md frontmatter as `session-type:`, which is the only live read (`scripts/print-applicable-rules.mjs` rule mode-gating). |
78
80
  | `agent-mapping` | object | null | Optional mapping of role keys to agent names for explicit agent binding. Keys: `impl`, `test`, `db`, `ui`, `security`, `compliance`, `docs`, `perf`. Example: `{ impl: code-editor, test: test-specialist }`. Overrides auto-discovery when present. Values may carry a channel prefix — see § `agent-mapping` values below. |
79
81
  | `waves` | integer | `5` | Number of execution waves for feature and deep sessions. |
80
82
  | `recent-commits` | integer | `20` | Number of recent commits to display during session start git analysis. |
81
83
  | `special` | string | none | Repo-specific instructions. Freeform text that the orchestrator reads and follows during sessions. |
82
84
 
85
+ ### Session Profile — `session-profile` (NOT a Session Config key)
86
+
87
+ `session-profile` names a WAVE-SHAPE variant on top of an unchanged `session-type`. It is listed here because it is easy to look for in the wrong place: **it is not a Session Config key and `parseSessionConfig()` does not emit one.** Writing `session-profile:` into a repo's `## Session Config` block is inert prose, exactly like `session-type:` (see the `agents-per-wave` row above).
88
+
89
+ | Aspect | Value |
90
+ |---|---|
91
+ | Where it lives | STATE.md frontmatter (`session-profile: ultradeep`), written per session |
92
+ | Who writes it | The `/session ultradeep` argument alias — `commands/session.md` |
93
+ | Read/write API | `readSessionProfile` / `setSessionProfile` / `SESSION_PROFILE_FIELD` in `scripts/lib/state-md.mjs` |
94
+ | Absent means | No profile. Never an empty string, never `none` — `readSessionProfile` returns `null` |
95
+ | Session record | Optional `session_profile` field (`scripts/lib/session-schema/constants.mjs` `OPTIONAL_FIELDS`); records without it validate unchanged |
96
+ | Defined values | `ultradeep` (7 waves, coordinator-direct Synthesis-Gate at wave 2) — spec: `docs/prd/2026-09-06-ultradeep-session-profile.md` |
97
+
98
+ `session-type` NEVER becomes `ultradeep`: that value is a closed set in `scripts/lib/session-schema/constants.mjs`, `scripts/lib/wave-sizing.mjs` and `scripts/lib/session-close-backfill.mjs`, and an unknown member degrades SILENTLY there (telemetry maps it to `"other"`, the close-backfill labels it `housekeeping`). The profile field exists so no closed set has to change.
99
+
100
+ **Sizing an ultradeep session** uses the open override key set:
101
+
102
+ ```yaml
103
+ agents-per-wave: 6 (deep: 18, ultradeep: 18)
104
+ waves: 5 # must be >= 7 for the ultradeep wave shape
105
+ ```
106
+
107
+ Verified against the parser (2026-09-06, `scripts/lib/config/coercers.mjs`):
108
+
109
+ ```
110
+ $ node -e "import('./scripts/lib/config/coercers.mjs').then(m => console.log(JSON.stringify(
111
+ m._coerceInteger(new Map([['agents-per-wave','6 (deep: 18, ultradeep: 18)']]), 'agents-per-wave', 6))))"
112
+ {"default":6,"deep":18,"ultradeep":18}
113
+ ```
114
+
115
+ Two consumers resolve that object to `.default` rather than to a mode key — `scripts/lib/resource-probe/evaluate.mjs` and `scripts/lib/wave-resource-gate.mjs` (see `heavy-repo` in § Environment Awareness) — so an `ultradeep: 18` override does NOT raise the resource gate's cap.
116
+
117
+ **Budgets are deliberately absent.** The PRD's `ultradeep.max-agents-total` / `max-wall-clock-hours` / `max-output-tokens` / `on-breach` block (§ 7) is NOT implemented and no key of that name is read anywhere. It stays deferred until three ultradeep runs have been measured, per `.claude/rules/host-resources.md` HR-105 — a threshold whose firing rate nothing records is unfalsifiable. Do not add one ahead of the measurement.
118
+
83
119
  ### `agent-mapping` values — channel prefixes (#1150)
84
120
 
85
121
  A mapping value has three forms, distinguished by the colon:
@@ -281,7 +317,7 @@ slopcheck:
281
317
  | `grounding-injection-max-files` | integer | `3` | Max files with recent `edit-format-friction` stagnation history to inject as line-numbered GROUNDING blocks into each agent's prompt before dispatch (wave-executor pre-dispatch step). Per-agent scope; selects top N by recency. `0` disables the feature. Gated on `persistence: true`. (#85) |
282
318
  | `isolation` | string | `auto` | Agent isolation mode: `worktree`, `none`, or `auto`. `auto` resolves per-wave via the graduated default (#194): ≤2 agents → `none`, 3–4 agents on feature/deep → `worktree`, ≥5 agents → `worktree`, housekeeping 3–4 → `none`. Explicit `worktree` or `none` overrides the graduation. See [isolation graduation](#isolation-graduation) below. |
283
319
  | `max-turns` | integer or string | `auto` | Maximum agent turns before PARTIAL. Auto: housekeeping=8, feature=15, deep=25. |
284
- | `auto-commit-per-wave` | boolean | `false` | Automatically commit each wave's work after the Quality-Lite gate passes. Checkpoint commits per wave reduce the risk of data loss from `git stash` collisions in parallel sessions (V3.3 RESCUE incident — see GitLab #214). When `false`, all work is committed at session-end via `/close`. Requires `persistence: true`; the flag is silently ignored when `persistence: false`. Trade-off: each wave produces an additional commit; git log shows N+1 commits instead of 1. Use `/simplify` or `git rebase -i --autosquash` before final close to squash if a clean history is desired. **Implementation note:** the procedural commit sequence (`scripts/lib/auto-commit.mjs`) is deferred to V3.6. Until then, setting this flag to `true` triggers a session-start warning that auto-commits are not yet active — the flag is a no-op but is validated so projects can opt in early. |
320
+ | `auto-commit-per-wave` | boolean | `false` | Automatically commit each wave's work after the Quality-Lite gate passes. Checkpoint commits per wave reduce the risk of data loss from `git stash` collisions in parallel sessions (V3.3 RESCUE incident — see GitLab #214). When `false`, all work is committed at session-end via `/close`. Requires `persistence: true`; the flag is silently ignored when `persistence: false`. Trade-off: each wave produces an additional commit; git log shows N+1 commits instead of 1. Use `/simplify` or `git rebase -i --autosquash` before final close to squash if a clean history is desired. **Implementation note:** the procedural commit sequence (`scripts/lib/auto-commit.mjs`) is deferred to V3.6. Until then, setting this flag to `true` triggers a session-start warning that auto-commits are not yet active — the flag is a no-op but is validated so projects can opt in early. <!-- path-check: historical --> |
285
321
 
286
322
  ### enforcement-gates: the five gate keys (#800/#915)
287
323
 
@@ -704,6 +740,8 @@ vault-integration:
704
740
 
705
741
  > **Host-local override (#653; extended #819).** `vault-dir` resolves host-locally with precedence: env-var (`SO_VAULT_DIR`) > `owner.yaml` `paths.vault-dir` > the committed default. `plan-baseline-path` resolves with an extra per-context tier in between: `SO_BASELINE_PATH` env > `owner.yaml` `baselines:` directory-prefix match against cwd > `owner.yaml` `paths.baseline-path` (legacy scalar) > the committed default. This keeps maintainer-specific absolute paths out of version control. Resolvers: `scripts/lib/config/host-paths.mjs` (both keys) and `scripts/lib/named-baseline-resolver.mjs` (the `baselines:` match tier).
706
742
 
743
+ > **`SO_CONFIG_HOME` — the host-private config directory itself.** A sibling override, one layer below `owner.yaml`'s own contents rather than a key inside it: `scripts/lib/host-identity.mjs` `_privateDir()` resolves the directory holding `owner.yaml`, `host-private.json`, and the host-alias ledger (`SO_HOST_ALIASES_FILE`, see `host-identity.mjs`) with precedence env-var (`SO_CONFIG_HOME`, names the private dir ITSELF) > `XDG_CONFIG_HOME` (names its PARENT — `owner-config-loader.mjs` uses the same variable the same way) > the homedir default `~/.config/session-orchestrator`. Both env vars are read with `.trim() || fallback`, not a bare `||` (`.claude/rules/development.md` § Error Handling env-var-fallback-whitespace trap).
744
+
707
745
  > **Parser accepts three key-line renderings (#823).** The `vault-integration:` key line is recognized in plain form (`vault-integration:`), dash-bullet form (`- vault-integration:`), and bold-bullet form (`- **vault-integration:**`) — each paired with either the inline-object shape (`{ enabled: true, ... }` on the same line) or the indented block shape shown above. Parser: `scripts/lib/config/vault-integration.mjs` (`_parseVaultIntegration`).
708
746
 
709
747
  | Field | Type | Default | Description |
@@ -844,7 +882,7 @@ Memory proposals are one of five Epic #498 Phase 2 features that share the same
844
882
 
845
883
  Together: F2.1 captures fresh insight mid-flight, F2.2 consolidates old insight at scale, F2.3 surfaces it at the start, F2.4/F2.5 distill it into the durable peer-card profiles.
846
884
 
847
- **Used by:** `scripts/lib/memory-proposals/{schema,store,collector,sink}.mjs`, `scripts/memory-propose.mjs`, `agents/memory-proposal-collector.md`, `hooks/pre-bash-memory-propose-audit.mjs`, `skills/session-end/SKILL.md` Phase 3.6.3.
885
+ **Used by:** `scripts/lib/memory-proposals/{schema,store,collector,sink}.mjs`, `scripts/memory-propose.mjs`, `docs/memory-proposal-flow.md`, `hooks/pre-bash-memory-propose-audit.mjs`, `skills/session-end/SKILL.md` Phase 3.6.3.
848
886
 
849
887
  **Cross-reference:** issue #501, PRD F2.1 in the Learning-Memory Modernization PRD; issue #741.3 (`--dry-run` flag + `dry-run-ok` status). Sibling features: `memory.banner` (above, F2.3 / #505), `dialectic.cadence` (F2.5 / #506), Auto-Dream (F2.2 / #502, surfaced via `memory-cleanup-soft-limit`).
850
888
 
@@ -1240,7 +1278,7 @@ remote-hosts:
1240
1278
 
1241
1279
  **Two enums, never conflated.** `roles-allowed` holds `agent-mapping` roles (`test`, `ui`, `perf`) — NOT wave roles (`Impl-Core`, `Quality`, …). The wave→role translation is `OFFLOADABLE_WAVE_ROLES` in `scripts/lib/wave-resource-gate.mjs`; a wave role absent from that map is local-only by default.
1242
1280
 
1243
- **Placement contract.** The gate applies its offload arm only after the HR-004 heavy-repo cap, and only when the resource verdict was `reduce` or `coordinator-direct`. It does NOT probe the network: the coordinator supplies a readiness witness (`remoteReady: { m5: true }`, or an async `probeFn`). With no witness, no host counts as ready and the decision stays local — the gate fails toward local, never toward an unverified host. A role in `NEVER_FOREIGN_ROLES` (`scripts/lib/wave-executor/foreign-dispatch.mjs`) is never offloaded regardless.
1281
+ **Placement contract.** The gate applies its offload arm only after the HR-004 heavy-repo cap, and only when the resource verdict was `reduce` or `coordinator-direct`. It does NOT probe the network: the coordinator supplies a readiness witness (`remoteReady: { m5: true }`, or an async `probeFn`). With no witness, no host counts as ready and the decision stays local — the gate fails toward local, never toward an unverified host. A role in `NEVER_FOREIGN_ROLES` (`scripts/lib/wave-executor/dispatch-common.mjs`) is never offloaded regardless.
1244
1282
 
1245
1283
  **agent-mapping interaction.** A declared alias is what an `agent-mapping` value of the form `<role>: ssh:<alias>` validates against; naming an undeclared host throws at parse time, naming the `ssh` channel with no target throws as for any other channel.
1246
1284
 
@@ -1542,43 +1580,7 @@ SO_DISABLED_HOOKS=enforce-scope,enforce-commands claude ...
1542
1580
 
1543
1581
  Each hook handler imports `shouldRunHook` from `hooks/_lib/profile-gate.mjs` at the top level and calls `process.exit(0)` immediately when gated off. The exit is silent (no stdout, no stderr), so Claude Code sees an allow as if the hook had never run.
1544
1582
 
1545
- ## Webhooks (#228)
1546
-
1547
- Opt-in webhook notifications delivered by `scripts/lib/webhook-url.mjs`. The helper centralizes URL resolution so no personal-domain default ever silently fires — callers must supply a URL explicitly.
1548
-
1549
- ### Resolution order
1550
-
1551
- For every supported kind the resolver checks sources in this order; the first non-empty string wins:
1552
-
1553
- 1. **Environment variable** `SO_WEBHOOK_<KIND>_URL` — uppercase kind, hyphens → underscores
1554
- e.g. `SO_WEBHOOK_SLACK_URL`, `SO_WEBHOOK_GITLAB_PIPELINE_STATUS_URL`
1555
- 2. **Session Config** `webhooks.<kind>.url`
1556
- 3. **Error** — `WebhookConfigError` is thrown. No silent personal-domain fallback.
1557
-
1558
- ### Supported kinds
1559
-
1560
- | Kind | Env variable | Config key |
1561
- |------|-------------|------------|
1562
- | `slack` | `SO_WEBHOOK_SLACK_URL` | `webhooks.slack.url` |
1563
- | `discord` | `SO_WEBHOOK_DISCORD_URL` | `webhooks.discord.url` |
1564
- | `generic` | `SO_WEBHOOK_GENERIC_URL` | `webhooks.generic.url` |
1565
- | `gitlab-pipeline-status` | `SO_WEBHOOK_GITLAB_PIPELINE_STATUS_URL` | `webhooks.gitlab-pipeline-status.url` |
1566
-
1567
- ### Session Config example
1568
-
1569
- ```yaml
1570
- webhooks:
1571
- slack:
1572
- url: https://hooks.slack.com/services/REDACTED/REDACTED/REDACTED
1573
- discord:
1574
- url: https://discord.com/api/webhooks/REDACTED/REDACTED
1575
- generic:
1576
- url: https://example.com/hooks/session-events
1577
- gitlab-pipeline-status:
1578
- url: https://gitlab.example.com/hooks/pipeline
1579
- ```
1580
-
1581
- ### Clank Event Bus (events.mjs / on-stop.mjs)
1583
+ ## Clank Event Bus (events.mjs / on-stop.mjs)
1582
1584
 
1583
1585
  The internal Clank Event Bus webhook is controlled by two environment variables:
1584
1586
 
@@ -1647,24 +1649,23 @@ Set `express-path.enabled: false` when:
1647
1649
  - `skills/session-plan/SKILL.md` — Express Path Short-Circuit section (1-wave plan emission)
1648
1650
  - GitLab issue `#214` (foundation and codification)
1649
1651
 
1650
- ## Autopilot Multi-Story (#431)
1651
-
1652
- Opt-in configuration for `autopilot --multi-story` (`scripts/autopilot-multi.mjs`). Controls how parallel story pipelines are isolated when N stories run concurrently. Projects that do not use `--multi-story` leave this block unset and are unaffected.
1653
-
1654
- All fields live under a top-level `autopilot` object in your Session Config host file (`CLAUDE.md` or `AGENTS.md`), for example:
1655
-
1656
- ```yaml
1657
- autopilot:
1658
- bg-isolation: worktree # worktree | none (default: worktree)
1659
- ```
1652
+ ## Autopilot Multi-Story (#431) — removed
1660
1653
 
1661
- | Field | Type | Default | Description |
1662
- |-------|------|---------|-------------|
1663
- | `autopilot.bg-isolation` | `worktree` \| `none` | `worktree` | Isolation mode for concurrent story pipelines. `worktree` (default): each story creates its own git worktree — safe for parallel writes, costs disk space and EnterWorktree latency. `none`: no worktrees; sub-sessions spawn directly in the main working tree — faster for monorepos with heavy build state but requires explicit file-scope deconfliction (see below). |
1654
+ The `autopilot` block and its single field `autopilot.bg-isolation` are **gone**, not
1655
+ deprecated. Their only reader was `scripts/autopilot-multi.mjs`, retired together with <!-- path-check: historical -->
1656
+ `commands/autopilot-multi.md` by the 2026-09-06 360°-Audit 5A: 0 telemetry, 0 fleet
1657
+ invocations in 90 days, no runtime consumer).
1664
1658
 
1665
- **`bg-isolation: none` hard-error guard:** when `bg-isolation: none` AND `--max-stories > 1`, `autopilot-multi` requires `--deconflict-paths=<glob>` on the CLI to confirm that per-story file ownership is planned. Omitting the flag exits with code 1. This enforces the parallel-session discipline defined in `.claude/rules/parallel-sessions.md` PSA-001/002/003 — two agents editing the same file in the main tree simultaneously will corrupt each other's work.
1659
+ Verified 2026-09-06 at `e4674109`:
1660
+ `rg -n "bg-isolation|bgIsolation|deconflict-paths" scripts hooks tests` returns nothing;
1661
+ `scripts/parse-config.mjs` never parsed an `autopilot` key at all; `scripts/autopilot.mjs`
1662
+ has no `--multi-story` mode. Documenting the field as functional would therefore have been
1663
+ the exact failure the audit found elsewhere — a key an operator can set and no code can
1664
+ read. <!-- path-check: historical -->
1666
1665
 
1667
- **Feature introduced by:** GitLab issue #431 (CC 2.1.143 `worktree.bgIsolation` changelog adoption). Implementation: `scripts/autopilot-multi.mjs` reads `config?.autopilot?.['bg-isolation']` via `scripts/parse-config.mjs`. Documentation: `skills/autopilot/SKILL.md` § Configuration.
1666
+ **If your Session Config still carries an `autopilot:` block, delete it.** It is inert: no
1667
+ parser reads it, so removing it changes no behaviour. Single-story `/autopilot` is
1668
+ unaffected and takes no Session Config block.
1668
1669
 
1669
1670
  ## Wave Reviewers
1670
1671
 
@@ -57,6 +57,10 @@ special: "any repo-specific instructions" # freeform — orchestrator reads +
57
57
 
58
58
  Read by: `skills/session-start/SKILL.md` (Phase 4.5), `skills/session-plan/SKILL.md`, `skills/wave-executor/wave-loop.md`.
59
59
 
60
+ **The override key set is open.** `_coerceInteger` (`scripts/lib/config/coercers.mjs`) parses whatever keys stand inside the parentheses, so `agents-per-wave: 6 (deep: 18, ultradeep: 18)` is valid today with no code change — it yields `{"default": 6, "deep": 18, "ultradeep": 18}`.
61
+
62
+ **`session-profile` is NOT a Session Config key — do not add one here.** The wave-shape profile (`ultradeep`) lives in STATE.md frontmatter, written per session by the `/session ultradeep` argument alias, and is absent by default. `parseSessionConfig()` emits no such key, so writing one into a repo's `## Session Config` block is inert prose — the same trap as `session-type:`. Full contract: [`session-config-reference.md` § Session Profile](./session-config-reference.md). The PRD's `ultradeep.max-*` budget block is deliberately NOT implemented and no key of that name is read anywhere (deferred until measured, HR-105).
63
+
60
64
  ## VCS & Infrastructure
61
65
 
62
66
  ```yaml
@@ -274,7 +278,7 @@ memory:
274
278
 
275
279
  Agents invoke via `SO_WAVE_AGENT=1 node scripts/memory-propose.mjs …`. The `SO_WAVE_AGENT=1` env-var is set automatically by the wave-executor boilerplate; direct CLI calls without it exit `3` (`rejected-wrong-context`).
276
280
 
277
- Read by: `scripts/lib/memory-proposals/{schema,store,collector,sink}.mjs`, `scripts/memory-propose.mjs`, `agents/memory-proposal-collector.md`, `hooks/pre-bash-memory-propose-audit.mjs`, `skills/session-end/SKILL.md` Phase 3.6.3.
281
+ Read by: `scripts/lib/memory-proposals/{schema,store,collector,sink}.mjs`, `scripts/memory-propose.mjs`, `docs/memory-proposal-flow.md`, `hooks/pre-bash-memory-propose-audit.mjs`, `skills/session-end/SKILL.md` Phase 3.6.3.
278
282
 
279
283
  ## Auto-Dream Proposal Filter (#566)
280
284
 
@@ -612,26 +616,6 @@ express-path:
612
616
 
613
617
  Read by: `skills/session-start/phase-8-5-express-path.md`, `skills/session-plan/SKILL.md` (express-path short-circuit).
614
618
 
615
- ## Webhooks
616
-
617
- Opt-in webhook notifications. The `scripts/lib/webhook-url.mjs` resolver checks env first (`SO_WEBHOOK_<KIND>_URL`), then this Session Config block. **No personal-domain default** — callers must supply a URL or the resolver throws.
618
-
619
- ```yaml
620
- webhooks:
621
- slack:
622
- url: https://hooks.slack.com/services/REDACTED/REDACTED/REDACTED
623
- discord:
624
- url: https://discord.com/api/webhooks/REDACTED/REDACTED
625
- generic:
626
- url: https://example.com/hooks/session-events
627
- gitlab-pipeline-status:
628
- url: https://gitlab.example.com/hooks/pipeline
629
- ```
630
-
631
- Measured: `scripts/lib/webhook-url.mjs` (`resolveWebhookUrl`) is the only reader of this `webhooks:` block, and it currently has **zero callers repo-wide** (`grep -rn "webhook-url" scripts/ hooks/` outside itself and one exemption comment in `check-unwired-features.mjs`) — the block is unreachable at HEAD; follow-up issue pending.
632
-
633
- What actually fires a webhook today is a **separate** mechanism: `scripts/lib/events.mjs`'s `emitEvent()` reads `CLANK_EVENT_SECRET` + `CLANK_EVENT_URL` directly from the environment (never from this Session Config block) and, when both are set, fire-and-forget POSTs every emitted event to the internal Clank Event Bus. Every hook that calls `emitEvent()` — which is most of `hooks/` — participates in that path; none of them reads `webhooks:` here.
634
-
635
619
  ## Hook Runtime Profile (env-only, not config)
636
620
 
637
621
  `SO_HOOK_PROFILE` and `SO_DISABLED_HOOKS` are environment variables, **not Session Config fields**. They control hook execution at runtime without editing `hooks.json`.
@@ -682,7 +666,7 @@ That's enough for `/session feature` → `/go` → `/close` to work end-to-end.
682
666
 
683
667
  ## Full opt-in baseline (copy-paste)
684
668
 
685
- Everything turned on for a project that wants the full feature surface (vault, docs, drift checks, env-aware sizing, webhooks). Trim to taste:
669
+ Everything turned on for a project that wants the full feature surface (vault, docs, drift checks, env-aware sizing). Trim to taste:
686
670
 
687
671
  ```yaml
688
672
  ## Session Config
@@ -969,13 +953,6 @@ config-protection:
969
953
  mode: warn # warn | strict (strict blocks loosening, exit 2)
970
954
  allow-config-weakening: false # per-session bypass (mirrors allow-destructive-ops)
971
955
 
972
- # Webhooks (URLs are required when used — no defaults)
973
- # webhooks:
974
- # slack:
975
- # url: https://hooks.slack.com/services/...
976
- # gitlab-pipeline-status:
977
- # url: https://gitlab.example.com/hooks/pipeline
978
-
979
956
  # Agent mapping
980
957
  agent-mapping:
981
958
  impl: code-implementer
package/docs/telemetry.md CHANGED
@@ -39,8 +39,11 @@ projection unit test enforces the drop of any non-whitelisted input field.
39
39
  | `arch` | CPU architecture (e.g. `arm64`, `x64`). |
40
40
  | `node_major` | Major Node.js version in use. |
41
41
  | `ci` | Boolean — whether the run was detected as a CI environment. |
42
- | `fleet` | Boolean — whether this send came from an operator's own fleet-mode host (`owner.yaml` opt-in), as opposed to an external install. |
43
- | `session_type` | One of `housekeeping`, `feature`, `deep`, `other`. |
42
+ | `fleet` | Boolean — **DEPRECATED since 2026-09-06, removal 2027-03-06.** Identical in value to `fleet_self_declared` for the whole deprecation generation; kept so the server's existing `fleet` column stays comparable across the rename. |
43
+ | `fleet_self_declared` | Boolean, optional — the client's own claim that this send came from an operator host. **Self-declared, and the name says so on purpose:** the authoritative classification is server-side (see below). Derived from the *resolved consent state* (`enabled-fleet` from an `owner.yaml` opt-in, or `enabled-env` from `SO_TELEMETRY=1`), no longer from a raw `owner.yaml` read. |
44
+ | `session_profile` | Optional — the STATE.md frontmatter `session-profile`, and **whitelisted profile names only** (today exactly `ultradeep`). Anything else — a value your repo invented, a client name, a typo — is **omitted from the ping entirely**: never sent verbatim, and never flattened to `other` either. A SECOND axis beside `session_type`, never a substitute for it: an ultradeep session is `session_type: "deep"` PLUS `session_profile: "ultradeep"`. **Absent when no profile is set or the profile is not on the whitelist** (the key is omitted, never `null`), including on derived pings, which never invent one. The whitelist is enforced twice — client-side before the send, and again server-side, which rejects a record carrying an unlisted profile rather than storing it. |
45
+ | `session_record` | Optional — WHICH source the session facts in this ping came from: `ledger` (a matching `sessions.jsonl` record), `derived` (reconstructed from `events.jsonl`), `absent` (neither). When `absent`, `session_type` is `unknown` and `duration_bucket` is **not a measurement**. |
46
+ | `session_type` | One of `housekeeping`, `feature`, `deep`, `other`, `unknown`. `other` means MEASURED but not one of the three modes; `unknown` means NOT MEASURED. Before 2026-09-06 both collapsed to `other`. |
44
47
  | `duration_bucket` | One of `<15m`, `15-60m`, `1-3h`, `>3h` — a coarse bucket, never an exact duration. |
45
48
  | `skills[]` | Names of invoked skills, filtered against the shipped plugin roster — any name not in that roster becomes `"other"`. |
46
49
  | `commands[]` | Names of invoked slash-commands, same filtering rule as `skills[]`. |
@@ -79,7 +82,12 @@ towards anonymizing rather than towards attributing:
79
82
  This list is a hard invariant, not a deferral:
80
83
 
81
84
  - No repository names, no file paths, no git remotes.
82
- - No prompts, no session transcripts, no free-form text of any kind.
85
+ - No prompts, no session transcripts, no free-form text of any kind. Every
86
+ field on the wire is either a number, a boolean, or a value from a closed
87
+ set this repository ships. The last free-text field, `session_profile`, was
88
+ closed on 2026-09-06: it now carries whitelisted profile names only, and an
89
+ unlisted value is dropped before the payload is built (and refused again by
90
+ the server, so it cannot be stored even if some other client sent it).
83
91
  - No command arguments — only whitelisted command/skill *names*, and only
84
92
  from the shipped roster (anything else is reduced to `"other"`).
85
93
  - No hostnames.
@@ -180,6 +188,38 @@ the offline queue is non-empty or a session has completed since — the latter
180
188
  clause is what lets the fallback originate a ping instead of only retrying a
181
189
  failed one (#1138).
182
190
 
191
+ ## The other thing that leaves the host: the update check
192
+
193
+ Telemetry is not the only outbound request this plugin can make, so the second
194
+ one is documented here rather than somewhere an egress audit would miss it.
195
+
196
+ On **SessionStart**, `hooks/on-session-start.mjs` calls the plugin-update banner
197
+ (`scripts/lib/plugin-update-banner.mjs`), which asks npm whether a newer release
198
+ exists:
199
+
200
+ ```
201
+ GET https://registry.npmjs.org/session-orchestrator/latest
202
+ ```
203
+
204
+ What it is, precisely:
205
+
206
+ - **No payload and no identifier.** It is a plain `GET` of a constant, public
207
+ URL — no body, no query string, no `anon_id`, no headers this plugin adds.
208
+ npm sees a request for a public package's metadata, as `npm view` would.
209
+ - **At most once per 24 h.** The answer is cached
210
+ (`plugin-latest.json`, `CACHE_TTL_MS = 24 h`); within the TTL no request is
211
+ made at all. The request has a short timeout and every failure is silent.
212
+ - **Independent of telemetry consent.** It is not a ping and sends nothing about
213
+ you — but it is still traffic, so it honours the offline flags below.
214
+
215
+ **Kill switches** (any one of them, set to anything other than empty / `0` /
216
+ `false`, turns the whole check off — not merely the request; the SessionStart
217
+ record then also omits `plugin_version_latest`):
218
+
219
+ - `SO_DISABLE_UPDATE_CHECK` — this probe's own switch.
220
+ - `DO_NOT_TRACK` — the standard flag, also honoured by the telemetry path.
221
+ - `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC`.
222
+
183
223
  ## Retention
184
224
 
185
225
  - **Raw records:** kept 24 months, then pruned. The retention window exists
@@ -190,6 +230,99 @@ failed one (#1138).
190
230
  - **Anonymous ID rotation:** every 90 days, independent of retention — a
191
231
  rotated ID cannot be linked back to the one it replaced.
192
232
 
233
+ ## When a ping is sent
234
+
235
+ Two triggers, deliberately independent of each other:
236
+
237
+ 1. **SessionEnd** (`hooks/on-session-end.mjs`) — the mechanical close-time
238
+ flush.
239
+ 2. **SessionStart** (`backfillOnSessionStart` in
240
+ `scripts/backfill-abandoned-sessions.mjs`) — drains whatever the PREVIOUS
241
+ session left queued. Since 4.0.0 every queued record is re-projected and
242
+ `session_profile`-whitelisted at the transport boundary before it is sent,
243
+ and a batch the server rejects with HTTP 400/422 is EVICTED (breadcrumb
244
+ `reason: rejected-evicted`) instead of being re-queued forever — a poison
245
+ record can no longer block every later flush.
246
+
247
+ Trigger 2 exists because trigger 1 fires only on a REGULAR close, and most
248
+ sessions do not have one: measured 2026-09-06 over 90 fleet days, **429 clean
249
+ closes against 2.016 distinct `session.started` ids = 21,3 %**. Roughly four
250
+ sessions in five never reached the only code path that sends. SessionStart is
251
+ the trigger that survives whatever killed the previous session — the same
252
+ argument the abandoned-session backfill already makes for the ledger.
253
+
254
+ The start-time flush is bounded (1,5 s POST budget), lazily imported, gated by
255
+ the same consent check, and swallows every error: it can never delay or break a
256
+ session start. A timeout is lossless — the batch lands in the offline queue.
257
+
258
+ A ping no longer depends on `sessions.jsonl`. When the ledger has no matching
259
+ record, `session_type` and `duration_bucket` are reconstructed from
260
+ `events.jsonl` and the ping is stamped `session_record: "derived"`; when neither
261
+ source has a type, it is `session_type: "unknown"` with `session_record:
262
+ "absent"` — never a measured-looking `other`.
263
+
264
+ ### Sandbox guard
265
+
266
+ The sender refuses to send when it is not running in a real operator session.
267
+ This is not a nicety: on 2026-09-06 six agent sandboxes ran the SessionEnd hook
268
+ from a repo checkout and sent **six real pings to the production ingest server**,
269
+ minted against the operator's real `anon_id`, because `telemetry/paths.mjs`
270
+ resolves `~/.config/session-orchestrator/` from `homedir()` and does not honour
271
+ `SO_CONFIG_HOME` — faking the source never faked the destination.
272
+
273
+ A send is refused (no network, no queue write, no anon-ID mint) when **any** of:
274
+
275
+ - `SO_TELEMETRY_DISABLED=1` or `DO_NOT_TRACK` is set;
276
+ - `SO_CONFIG_HOME` / `XDG_CONFIG_HOME` points somewhere other than the directory
277
+ the telemetry state is actually read from (unless the caller redirected the
278
+ state path too — that redirect succeeded, which is the opposite of the leak);
279
+ - `CLAUDE_PROJECT_DIR`, or the cwd, sits under the OS temp directory or `/tmp`.
280
+
281
+ The guard also **fails closed**: if any of its own probes throws, the send is
282
+ refused with `sandbox:probe-failed` rather than permitted. An environment the
283
+ guard could not classify is treated as one it would have refused; nothing is
284
+ lost, because the next session re-probes from scratch.
285
+
286
+ If you invoke any telemetry writer by hand, export `SO_TELEMETRY_DISABLED=1`.
287
+
288
+ ## Server-side fleet attribution
289
+
290
+ The `fleet` flag on the wire is **self-declared and was measurably wrong**.
291
+ Until 2026-09-06 the client derived it as `ownerConfig?.telemetry?.enabled
292
+ === true` — a statement about a FILE, not about a person. The operator's
293
+ second Mac has consent granted but no `telemetry:` block in `owner.yaml`,
294
+ so it declared itself external: **394 of 490 server records (80,4 %)**
295
+ counted the operator as an external user, and every week's
296
+ `fleet_vs_external` was wrong by that margin.
297
+
298
+ Two independent repairs, because the client alone cannot close this:
299
+
300
+ 1. **Client-side** — `fleet_self_declared` is derived from the resolved
301
+ consent state (`enabled-fleet` / `enabled-env`), so a host opted in via
302
+ `SO_TELEMETRY=1` is no longer mistaken for an external install. The name
303
+ states the limit: a sandbox, or a host whose `owner.yaml` is unreachable,
304
+ still declares `false` however honest it is.
305
+ 2. **Server-side (authoritative)** — set `SO_INGEST_FLEET_ANON_IDS` on the
306
+ ingest server to a comma-separated list of the operator's own `anon_id`
307
+ values. A matching record is **stored** as fleet regardless of what it
308
+ claims. The allowlist can only PROMOTE, never demote: a host that honestly
309
+ declares itself fleet stays fleet even if the operator forgot to list it.
310
+
311
+ ```
312
+ SO_INGEST_FLEET_ANON_IDS=a3bb4907-…,c29cac99-…
313
+ ```
314
+
315
+ The record survives verbatim in `raw_json`, including its own `fleet` /
316
+ `fleet_self_declared` claim, so the client's declaration and the server's
317
+ verdict remain separable forever and the disagreement rate stays measurable.
318
+ `fleet_vs_external` in the weekly digest reads the stored column, i.e. the
319
+ server verdict. **The allowlist applies at INSERT time**, so it cannot repair
320
+ rows already written — a `fleet_vs_external` computed over a range that
321
+ predates the change is known-wrong and re-running the digest will not fix it.
322
+
323
+ Unset by default: with no `SO_INGEST_FLEET_ANON_IDS`, storage takes the
324
+ client's word exactly as it did before.
325
+
193
326
  ## Schema evolution
194
327
 
195
328
  The schema is **additive-only** within a given `schema_version`: new
@@ -198,6 +331,27 @@ without a version bump. The server accepts both the current and the
198
331
  immediately previous `schema_version`, so a slightly-outdated client is
199
332
  never hard-broken by a server-side schema update.
200
333
 
334
+ Unknown top-level fields are accepted by the server and preserved verbatim
335
+ inside `raw_json`, so an additive field round-trips through a server that
336
+ predates it — which is what makes a *rename* safe: emit both names for one
337
+ generation, then drop the old one.
338
+
339
+ **In flight now (added 2026-09-06, schema v1, additive):**
340
+
341
+ | Field | Status | Removal |
342
+ |---|---|---|
343
+ | `fleet_self_declared` | new name for `fleet` | — |
344
+ | `fleet` | deprecated alias, same value | **2027-03-06** |
345
+ | `session_record` | new (`ledger` \| `derived` \| `absent`) | — |
346
+ | `session_profile` | new (STATE.md `session-profile`, whitelisted names only; omitted when unset or unlisted) | — |
347
+
348
+ Client-side the frozen whitelist is split in two: `USAGE_PING_FIELDS` (the
349
+ REQUIRED v1 contract, which `tests/telemetry/parity.test.mjs` asserts the
350
+ server independently requires field by field) and
351
+ `USAGE_PING_OPTIONAL_FIELDS`. `projectUsagePing` projects the UNION, so the
352
+ data-minimization tripwire still holds — a field must be on a reviewed list
353
+ before it can reach the wire.
354
+
201
355
  ## Relationship to `telemetry-claims.md`
202
356
 
203
357
  This page describes the **opt-in, client-side usage-telemetry pipeline**