session-orchestrator 3.22.0 → 3.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/commands/autopilot-multi.md +14 -0
  5. package/.cursor/commands/autopilot.md +14 -0
  6. package/.cursor/commands/bootstrap.md +14 -0
  7. package/.cursor/commands/brainstorm.md +14 -0
  8. package/.cursor/commands/close.md +13 -0
  9. package/.cursor/commands/contract-version-bump.md +14 -0
  10. package/.cursor/commands/debug.md +14 -0
  11. package/.cursor/commands/discovery.md +14 -0
  12. package/.cursor/commands/dispatcher.md +14 -0
  13. package/.cursor/commands/eli5.md +14 -0
  14. package/.cursor/commands/eval.md +14 -0
  15. package/.cursor/commands/evolve.md +14 -0
  16. package/.cursor/commands/go.md +14 -0
  17. package/.cursor/commands/grill.md +14 -0
  18. package/.cursor/commands/harness-audit.md +13 -0
  19. package/.cursor/commands/journey-audit.md +14 -0
  20. package/.cursor/commands/memory-cleanup.md +14 -0
  21. package/.cursor/commands/persona-panel.md +14 -0
  22. package/.cursor/commands/plan.md +14 -0
  23. package/.cursor/commands/portfolio.md +14 -0
  24. package/.cursor/commands/reconcile.md +14 -0
  25. package/.cursor/commands/release.md +14 -0
  26. package/.cursor/commands/repo-audit.md +13 -0
  27. package/.cursor/commands/session.md +14 -0
  28. package/.cursor/commands/spinout.md +14 -0
  29. package/.cursor/commands/sunset-review.md +14 -0
  30. package/.cursor/commands/templates-ack.md +14 -0
  31. package/.cursor/commands/test.md +14 -0
  32. package/.cursor/hooks.json +60 -0
  33. package/.cursor/rules/000-session-orchestrator.mdc +8 -0
  34. package/.cursor/rules/010-session-workflow.mdc +9 -1
  35. package/.cursor/rules/020-quality-gates.mdc +1 -1
  36. package/.cursor/rules/030-wave-execution.mdc +1 -1
  37. package/.cursor/rules/050-plan.mdc +2 -2
  38. package/.cursor/rules/070-gitlab-ops.mdc +73 -57
  39. package/.cursor/rules/080-ecosystem-health.mdc +7 -7
  40. package/.cursor/skills/architecture/SKILL.md +13 -0
  41. package/.cursor/skills/autopilot/SKILL.md +12 -0
  42. package/.cursor/skills/bootstrap/SKILL.md +12 -0
  43. package/.cursor/skills/brainstorm/SKILL.md +13 -0
  44. package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
  45. package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
  46. package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
  47. package/.cursor/skills/daily/SKILL.md +12 -0
  48. package/.cursor/skills/debug/SKILL.md +13 -0
  49. package/.cursor/skills/discovery/SKILL.md +13 -0
  50. package/.cursor/skills/dispatcher/SKILL.md +13 -0
  51. package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
  52. package/.cursor/skills/domain-model/SKILL.md +13 -0
  53. package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
  54. package/.cursor/skills/eli5/SKILL.md +13 -0
  55. package/.cursor/skills/eval/SKILL.md +12 -0
  56. package/.cursor/skills/evolve/SKILL.md +13 -0
  57. package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
  58. package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
  59. package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
  60. package/.cursor/skills/grill/SKILL.md +13 -0
  61. package/.cursor/skills/hook-development/SKILL.md +13 -0
  62. package/.cursor/skills/journey-audit/SKILL.md +13 -0
  63. package/.cursor/skills/mcp-builder/SKILL.md +13 -0
  64. package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
  65. package/.cursor/skills/mode-selector/SKILL.md +13 -0
  66. package/.cursor/skills/npm-publish/SKILL.md +12 -0
  67. package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
  68. package/.cursor/skills/persona-panel/SKILL.md +12 -0
  69. package/.cursor/skills/plan/SKILL.md +13 -0
  70. package/.cursor/skills/playwright-driver/SKILL.md +13 -0
  71. package/.cursor/skills/quality-gates/SKILL.md +13 -0
  72. package/.cursor/skills/reconcile/SKILL.md +12 -0
  73. package/.cursor/skills/remote-offload/SKILL.md +13 -0
  74. package/.cursor/skills/repo-audit/SKILL.md +13 -0
  75. package/.cursor/skills/session-end/SKILL.md +13 -0
  76. package/.cursor/skills/session-plan/SKILL.md +13 -0
  77. package/.cursor/skills/session-start/SKILL.md +13 -0
  78. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  79. package/.cursor/skills/spinout/SKILL.md +12 -0
  80. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  81. package/.cursor/skills/test-runner/SKILL.md +13 -0
  82. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  83. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  84. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  85. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  86. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  87. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  88. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  89. package/.mcp.json +4 -1
  90. package/CHANGELOG.md +446 -0
  91. package/README.md +22 -17
  92. package/agents/AGENTS.md +23 -4
  93. package/agents/code-implementer.md +2 -1
  94. package/agents/db-specialist.md +2 -2
  95. package/agents/docs-writer.md +3 -1
  96. package/agents/eval-judge.md +1 -1
  97. package/agents/session-reviewer.md +7 -1
  98. package/agents/test-writer.md +2 -1
  99. package/agents/ui-developer.md +2 -1
  100. package/commands/bootstrap.md +2 -2
  101. package/commands/close.md +3 -1
  102. package/commands/go.md +1 -1
  103. package/commands/journey-audit.md +43 -0
  104. package/docs/USER-GUIDE.md +2 -2
  105. package/docs/ci-setup.md +194 -25
  106. package/docs/codex-setup.md +64 -0
  107. package/docs/components.md +7 -7
  108. package/docs/cursor-setup.md +26 -47
  109. package/docs/events-schema.md +120 -10
  110. package/docs/github-mirror-protection.md +197 -0
  111. package/docs/pi-setup.md +2 -0
  112. package/docs/rule-authoring.md +3 -1
  113. package/docs/scope-collision-guard.md +49 -2
  114. package/docs/session-config-reference.md +89 -9
  115. package/docs/session-config-template.md +38 -7
  116. package/docs/telemetry/telemetry-claims.md +11 -10
  117. package/docs/telemetry.md +52 -1
  118. package/hooks/_lib/atomic-json.mjs +111 -0
  119. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  120. package/hooks/_lib/subagent-paths.mjs +143 -0
  121. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  122. package/hooks/cwd-change-restore.mjs +9 -29
  123. package/hooks/enforce-scope.mjs +93 -0
  124. package/hooks/hooks-codex.json +1 -1
  125. package/hooks/hooks-cursor.json +201 -20
  126. package/hooks/hooks-pi.json +1 -1
  127. package/hooks/hooks.json +2 -2
  128. package/hooks/on-session-end.mjs +486 -19
  129. package/hooks/on-session-start.mjs +263 -12
  130. package/hooks/on-stop.mjs +392 -24
  131. package/hooks/post-bash-write-verify.mjs +104 -4
  132. package/hooks/post-subagent-discovery-validator.mjs +182 -21
  133. package/hooks/post-tool-batch-wave-signal.mjs +165 -42
  134. package/hooks/post-tool-failure-corrective-context.mjs +9 -32
  135. package/hooks/pre-bash-issue-budget.mjs +117 -4
  136. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  138. package/hooks/pre-bash-staging-fence.mjs +4 -0
  139. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  140. package/hooks/skill-invocation-telemetry.mjs +21 -10
  141. package/hooks/subagent-telemetry.mjs +11 -26
  142. package/monitors/monitors.json +6 -0
  143. package/package.json +1 -1
  144. package/pi/prompts/journey-audit.md +12 -0
  145. package/rules/_index.md +9 -1
  146. package/rules/always-on/ask-via-tool.md +62 -0
  147. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  148. package/rules/always-on/build-value.md +47 -0
  149. package/rules/always-on/cross-session-messaging.md +59 -0
  150. package/rules/always-on/loop-and-monitor.md +221 -0
  151. package/rules/always-on/parallel-sessions.md +142 -12
  152. package/rules/always-on/receiving-review.md +108 -0
  153. package/rules/always-on/test-value.md +40 -0
  154. package/rules/always-on/verification-before-completion.md +77 -0
  155. package/scripts/archive-closed-prds.mjs +258 -18
  156. package/scripts/autopilot.mjs +31 -12
  157. package/scripts/backfill-abandoned-sessions.mjs +80 -11
  158. package/scripts/backfill-evidence-digest.mjs +376 -0
  159. package/scripts/cursor-install.mjs +89 -48
  160. package/scripts/emit-event.mjs +10 -2
  161. package/scripts/export-hw-learnings.mjs +143 -2
  162. package/scripts/express-path.mjs +299 -0
  163. package/scripts/generate-cursor-adapter.mjs +253 -0
  164. package/scripts/github-protection-audit.mjs +358 -0
  165. package/scripts/lib/auq/parse.mjs +5 -29
  166. package/scripts/lib/auto-dialectic.mjs +68 -0
  167. package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
  168. package/scripts/lib/build-live-signals.mjs +49 -27
  169. package/scripts/lib/ci-status-banner.mjs +158 -11
  170. package/scripts/lib/cold-start-detector.mjs +23 -14
  171. package/scripts/lib/command-blocker.mjs +70 -0
  172. package/scripts/lib/config/block-header.mjs +55 -0
  173. package/scripts/lib/config/discovery-validator.mjs +7 -2
  174. package/scripts/lib/config/health-endpoints.mjs +383 -0
  175. package/scripts/lib/config/reconcile.mjs +79 -4
  176. package/scripts/lib/config/remote-hosts.mjs +233 -0
  177. package/scripts/lib/config/section-extractor.mjs +235 -36
  178. package/scripts/lib/config-schema.mjs +9 -1
  179. package/scripts/lib/config.mjs +87 -8
  180. package/scripts/lib/convergence-monitor.mjs +13 -2
  181. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  182. package/scripts/lib/dispatcher/cli.mjs +2 -2
  183. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  184. package/scripts/lib/events-schema.mjs +48 -0
  185. package/scripts/lib/events.mjs +238 -5
  186. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  187. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  188. package/scripts/lib/express-path.mjs +327 -0
  189. package/scripts/lib/file-lock.mjs +22 -4
  190. package/scripts/lib/gates/gate-full.mjs +81 -8
  191. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  192. package/scripts/lib/git-config-drift.mjs +134 -5
  193. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  194. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  195. package/scripts/lib/host-identity.mjs +247 -2
  196. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  197. package/scripts/lib/issue-budget.mjs +229 -30
  198. package/scripts/lib/learnings/io.mjs +55 -10
  199. package/scripts/lib/learnings/schema.mjs +95 -28
  200. package/scripts/lib/lock-reaper.mjs +7 -1
  201. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  202. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  203. package/scripts/lib/memory-banner.mjs +25 -10
  204. package/scripts/lib/memory-paths.mjs +15 -6
  205. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  206. package/scripts/lib/peer-discovery.mjs +20 -2
  207. package/scripts/lib/platform.mjs +72 -9
  208. package/scripts/lib/plugin-root.mjs +143 -19
  209. package/scripts/lib/project-hygiene.mjs +43 -3
  210. package/scripts/lib/quality-gate.mjs +271 -13
  211. package/scripts/lib/reconcile/emitter.mjs +87 -19
  212. package/scripts/lib/reconcile/engine.mjs +517 -18
  213. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  214. package/scripts/lib/reconcile/renderer.mjs +148 -3
  215. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  216. package/scripts/lib/reconcile/writer.mjs +415 -84
  217. package/scripts/lib/rule-loader.mjs +37 -2
  218. package/scripts/lib/rules-sync.mjs +51 -8
  219. package/scripts/lib/scope-gate.mjs +126 -0
  220. package/scripts/lib/session-close-backfill.mjs +427 -37
  221. package/scripts/lib/session-discovery.mjs +69 -5
  222. package/scripts/lib/session-end/phase-skip.mjs +38 -5
  223. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  224. package/scripts/lib/session-id.mjs +30 -14
  225. package/scripts/lib/session-identity/own-session.mjs +220 -0
  226. package/scripts/lib/session-lock.mjs +85 -30
  227. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  228. package/scripts/lib/session-schema/validator.mjs +40 -0
  229. package/scripts/lib/session-start-probes.mjs +608 -0
  230. package/scripts/lib/session-transition.mjs +277 -0
  231. package/scripts/lib/sessions-canonical.mjs +446 -0
  232. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  233. package/scripts/lib/spiral-carryover.mjs +90 -9
  234. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  235. package/scripts/lib/state-md/mission-status.mjs +350 -52
  236. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  237. package/scripts/lib/state-md.mjs +12 -2
  238. package/scripts/lib/telemetry/schema.mjs +74 -8
  239. package/scripts/lib/telemetry/sync.mjs +91 -16
  240. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  241. package/scripts/lib/validate/check-agents.mjs +66 -0
  242. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  243. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  244. package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
  245. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  246. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  247. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  248. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  249. package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
  250. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  251. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  252. package/scripts/lib/validate/check-unwired-features.mjs +333 -32
  253. package/scripts/lib/validate/check-validator-registration.mjs +248 -0
  254. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  255. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  256. package/scripts/lib/validate/repo-files.mjs +275 -0
  257. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  258. package/scripts/lib/vault-mirror/process.mjs +99 -43
  259. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  260. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  261. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  262. package/scripts/lib/vault-status/board-writer.mjs +381 -141
  263. package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
  264. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  265. package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
  266. package/scripts/lib/wave-resource-gate.mjs +127 -7
  267. package/scripts/lib/wave-transcript-tail.mjs +889 -0
  268. package/scripts/materialize-wave-scope.mjs +228 -15
  269. package/scripts/mcp-server.sh +11 -2
  270. package/scripts/memory-propose.mjs +132 -8
  271. package/scripts/parse-config.mjs +65 -0
  272. package/scripts/promote-vault-strict.mjs +4 -15
  273. package/scripts/site-numbers.mjs +36 -4
  274. package/scripts/token-audit.sh +9 -2
  275. package/scripts/validate-plugin.mjs +29 -0
  276. package/scripts/validate-wave-scope.mjs +67 -0
  277. package/scripts/vault-consolidate.mjs +3 -11
  278. package/scripts/vault-integration-watcher.mjs +2 -4
  279. package/scripts/vault-mirror.mjs +305 -51
  280. package/skills/_shared/monitor-patterns.md +31 -5
  281. package/skills/_shared/parallel-aware-auq.md +31 -2
  282. package/skills/_shared/parallel-aware-preamble.md +19 -4
  283. package/skills/_shared/platform-tools.md +11 -5
  284. package/skills/_shared/state-ownership.md +29 -2
  285. package/skills/autopilot/SKILL.md +5 -1
  286. package/skills/bootstrap/SKILL.md +3 -3
  287. package/skills/bootstrap/_shared-template.md +18 -10
  288. package/skills/bootstrap/deep-template.md +10 -6
  289. package/skills/bootstrap/fast-template.md +15 -8
  290. package/skills/bootstrap/standard-template.md +10 -6
  291. package/skills/claude-md-drift-check/checker.mjs +39 -11
  292. package/skills/contract-version-bump/SKILL.md +1 -1
  293. package/skills/dispatcher/SKILL.md +1 -1
  294. package/skills/ecosystem-health/SKILL.md +4 -1
  295. package/skills/ecosystem-health/wizard.md +5 -0
  296. package/skills/evolve/SKILL.md +38 -1
  297. package/skills/journey-audit/SKILL.md +270 -0
  298. package/skills/peekaboo-driver/SKILL.md +15 -3
  299. package/skills/persona-panel/SKILL.md +1 -1
  300. package/skills/reconcile/SKILL.md +46 -3
  301. package/skills/remote-offload/SKILL.md +89 -0
  302. package/skills/session-end/SKILL.md +17 -4
  303. package/skills/session-end/metrics-collection.md +7 -4
  304. package/skills/session-end/phase-3-6-tail.md +20 -9
  305. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  306. package/skills/session-plan/SKILL.md +6 -1
  307. package/skills/session-plan/wave-template.md +1 -0
  308. package/skills/session-start/SKILL.md +54 -17
  309. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  310. package/skills/session-start/phase-8-5-express-path.md +77 -12
  311. package/skills/vault-sync/validator.mjs +31 -0
  312. package/skills/wave-executor/SKILL.md +5 -3
  313. package/skills/wave-executor/circuit-breaker.md +34 -9
  314. package/skills/wave-executor/wave-loop.md +143 -22
  315. package/templates/_shared/journey-manifest.md +110 -0
  316. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -52,6 +52,8 @@ import {
52
52
  } from '../scripts/lib/session-registry.mjs';
53
53
  import { detectColdStart, consumeMarker } from '../scripts/lib/cold-start-detector.mjs';
54
54
  import { parseSessionId } from '../scripts/lib/session-id.mjs';
55
+ import { readTelemetryState, resolveConsent, isCiEnv } from '../scripts/lib/telemetry/consent.mjs';
56
+ import { loadOwnerConfig } from '../scripts/lib/owner-yaml.mjs';
55
57
 
56
58
  const execFileAsync = promisify(execFile);
57
59
 
@@ -79,6 +81,19 @@ const execFileAsync = promisify(execFile);
79
81
  // re-introduce the bug.
80
82
  const bannerLines = [];
81
83
 
84
+ /**
85
+ * Coordinator-directed context for the single end-of-hook flush (#1138).
86
+ *
87
+ * The banner above is for the OPERATOR (`systemMessage`); this is for the
88
+ * MODEL (`hookSpecificOutput.additionalContext`). Both leave in the SAME JSON
89
+ * object for the same reason the banner was consolidated: Claude Code surfaces
90
+ * only the FIRST JSON object a SessionStart hook writes to stdout, so a second
91
+ * `writeSync` would be silently discarded.
92
+ *
93
+ * @type {string|null}
94
+ */
95
+ let pendingAdditionalContext = null;
96
+
82
97
  /**
83
98
  * Queue one or more banner lines for the single end-of-hook flush.
84
99
  * @param {string|null|undefined} line — multi-line strings are pushed verbatim
@@ -89,7 +104,70 @@ function pushBanner(line) {
89
104
  }
90
105
 
91
106
  /**
92
- * Emit every queued banner line as ONE systemMessage envelope.
107
+ * Render a session id compactly for a banner line.
108
+ *
109
+ * Semantic ids (`main-2026-08-23-deep-1`) are already short and are printed
110
+ * verbatim — they carry the branch and the mode, which is exactly what an
111
+ * operator needs to recognise a peer. A UUID (Claude Code's `session_id` on
112
+ * hosts where no semantic id was resolved) is truncated to its first 8 chars:
113
+ * long enough to disambiguate the handful of peers a repo can have, short
114
+ * enough that three of them fit on one banner line.
115
+ *
116
+ * Ceiling: 8 chars is ~4 billion values against at most a few dozen live
117
+ * sessions per host — revisit only if two peers ever collide in a banner.
118
+ *
119
+ * @param {string} id
120
+ * @returns {string}
121
+ */
122
+ function shortSessionId(id) {
123
+ if (typeof id !== 'string' || id.length === 0) return '?';
124
+ return id.length > 24 ? id.slice(0, 8) : id;
125
+ }
126
+
127
+ /**
128
+ * GH#67 — render the provenance marker for ONE mechanically-detected peer.
129
+ *
130
+ * `discoverActiveSessions()` annotates registry-sourced peers additively
131
+ * (`registryOnly` / `lockSuperseded` / `lockOwnerId`); lock-sourced peers carry
132
+ * none of the three, so they render byte-identically to the pre-GH#67 banner.
133
+ *
134
+ * `lockSuperseded: true` means a LIVE lock at THIS root is held by a different
135
+ * raw session_id than the registry entry — the common cause is a finished task
136
+ * whose SessionEnd never ran, leaving a fresh-but-orphaned registry record.
137
+ * It is a HINT, never a verdict: the session lock is advisory, so the entry may
138
+ * equally be a live peer that lost the acquire race (#1085). The marker is
139
+ * therefore purely additive — it never removes the peer, and never changes the
140
+ * count or the WARN decision (HR-106).
141
+ *
142
+ * @param {{registryOnly?:boolean, lockSuperseded?:boolean, lockOwnerId?:string|null}} peer
143
+ * @returns {string} '' for a lock-sourced peer, else a bracketed marker.
144
+ */
145
+ function supersessionMarker(peer) {
146
+ if (!peer || peer.registryOnly !== true) return '';
147
+ if (peer.lockSuperseded === true) return ' [registry-only, superseded]';
148
+ return ' [registry-only]';
149
+ }
150
+
151
+ /**
152
+ * Queue the ONE `additionalContext` string for the single end-of-hook flush.
153
+ * Last writer wins; there is deliberately no accumulation, because every
154
+ * character here costs context on every session start.
155
+ * @param {string|null|undefined} text
156
+ */
157
+ function setAdditionalContext(text) {
158
+ if (typeof text !== 'string' || text.length === 0) return;
159
+ pendingAdditionalContext = text;
160
+ }
161
+
162
+ /**
163
+ * Emit the queued banner lines AND any queued additionalContext as ONE JSON
164
+ * envelope.
165
+ *
166
+ * Both keys are valid siblings of one hook-output object: the shipped Claude
167
+ * Code binary (v2.1.241) documents `systemMessage` ("Display a message to the
168
+ * user (all hooks)") and `hookSpecificOutput.additionalContext` ("Text injected
169
+ * into model context") as fields of the same "Hook JSON Output" object, and its
170
+ * output validator recognises both keys on that one object.
93
171
  *
94
172
  * Uses `fs.writeSync(1, …)` rather than `console.log`: the top-level guard
95
173
  * calls `process.exit(0)`, which discards anything still sitting in libuv's
@@ -101,11 +179,22 @@ function pushBanner(line) {
101
179
  * Idempotent: a second call after a flush is a no-op.
102
180
  */
103
181
  function flushBanner() {
104
- if (bannerLines.length === 0) return;
105
- const payload = JSON.stringify({ systemMessage: bannerLines.join('\n') });
182
+ const hasBanner = bannerLines.length > 0;
183
+ const hasContext = typeof pendingAdditionalContext === 'string' && pendingAdditionalContext.length > 0;
184
+ if (!hasBanner && !hasContext) return;
185
+
186
+ const envelope = {};
187
+ if (hasBanner) envelope.systemMessage = bannerLines.join('\n');
188
+ if (hasContext) {
189
+ envelope.hookSpecificOutput = {
190
+ hookEventName: 'SessionStart',
191
+ additionalContext: pendingAdditionalContext,
192
+ };
193
+ }
106
194
  bannerLines.length = 0;
195
+ pendingAdditionalContext = null;
107
196
  try {
108
- writeSync(1, `${payload}\n`);
197
+ writeSync(1, `${JSON.stringify(envelope)}\n`);
109
198
  } catch { /* stdout closed — the hook is informational and never blocks */ }
110
199
  }
111
200
 
@@ -315,6 +404,12 @@ async function resolveSessionId(input, projectRoot) {
315
404
  if (Object.prototype.hasOwnProperty.call(prev, 'last_batch')) {
316
405
  preserved.last_batch = prev.last_batch;
317
406
  }
407
+ // #1193 — the final-wave completion marker must survive a
408
+ // clear/compact too: dropping it re-arms a duplicate SessionEnd
409
+ // `orchestrator.wave.completed` for a wave already closed.
410
+ if (Object.prototype.hasOwnProperty.call(prev, 'last_wave_completed')) {
411
+ preserved.last_wave_completed = prev.last_wave_completed;
412
+ }
318
413
  }
319
414
  } catch { /* absent / unparseable → no preservation (reset) */ }
320
415
  }
@@ -349,6 +444,10 @@ async function resolveSessionId(input, projectRoot) {
349
444
  *
350
445
  * Returns the candidate string on success, null on any failure. Never throws.
351
446
  *
447
+ * Contract (#1066, "Minimal"): the semantic id is a best-effort, host-wide
448
+ * monotonic LABEL, never an ownership key — ownership stays `(raw session_id,
449
+ * owner proof)`, and the two identity forms are never interchangeable.
450
+ *
352
451
  * Used by both branches of resolveSessionId() so the semantic_session_id
353
452
  * field on the session.lock is consistently populated regardless of whether
354
453
  * the SessionStart stdin payload provided a UUID (Claude Code) or nothing
@@ -359,7 +458,7 @@ async function resolveSessionId(input, projectRoot) {
359
458
  */
360
459
  async function deriveSemanticCandidate({ projectRoot, mode }) {
361
460
  try {
362
- const { resolveSemanticSessionId } = await import('../scripts/lib/session-id.mjs');
461
+ const { resolveSemanticSessionId, parseSessionId } = await import('../scripts/lib/session-id.mjs');
363
462
  const { discoverActiveSessions } = await import('../scripts/lib/session-discovery.mjs');
364
463
  const { execSync } = await import('node:child_process');
365
464
 
@@ -387,10 +486,28 @@ async function deriveSemanticCandidate({ projectRoot, mode }) {
387
486
  discoverActiveSessions(projectRoot).catch(() => []),
388
487
  readRegistry().catch(() => []),
389
488
  ]);
390
- const registrySessions = registryEntries.map((r) => ({
391
- sessionId: r.session_id,
392
- mode: r.mode ?? 'session',
393
- }));
489
+
490
+ // Registry census (#1066 AC1): the registry keeps the two identity forms in
491
+ // SEPARATE fields — `session_id` is the RAW id (a UUID on Claude Code) and
492
+ // `semantic_session_id` is the label. resolveSemanticSessionId counts only
493
+ // semantic candidates, so projecting `session_id` alone made the host-wide
494
+ // registry contribute NOTHING to the n-increment, and two sessions on one
495
+ // host could mint the same label. Preference order per entry:
496
+ // 1. `semantic_session_id`, when it parses as semantic;
497
+ // 2. `session_id`, ONLY when it itself parses as semantic (Codex/Cursor
498
+ // may write a semantic raw id).
499
+ // A legacy v1 entry (no `semantic_session_id`, UUID `session_id`) matches
500
+ // neither and is dropped — a UUID is never reintroduced as a candidate.
501
+ const semanticLabelOf = (entry) => {
502
+ for (const candidate of [entry?.semantic_session_id, entry?.session_id]) {
503
+ if (parseSessionId(candidate)?.format === 'semantic') return candidate;
504
+ }
505
+ return null;
506
+ };
507
+ const registrySessions = registryEntries.flatMap((r) => {
508
+ const label = semanticLabelOf(r);
509
+ return label === null ? [] : [{ sessionId: label, mode: r.mode ?? 'session' }];
510
+ });
394
511
  const activeSessions = [...localSessions, ...registrySessions];
395
512
 
396
513
  return await resolveSemanticSessionId({
@@ -467,6 +584,60 @@ async function emitHostBanner(projectRoot) {
467
584
  }
468
585
  }
469
586
 
587
+ /**
588
+ * The one-time telemetry-consent instruction handed to the coordinator (#1138).
589
+ *
590
+ * Deliberately terse: it is injected into the model context on EVERY session
591
+ * start until the operator decides, so every character is a recurring cost.
592
+ * It names the phase rather than restating it, so the AUQ wording stays
593
+ * single-sourced in `skills/session-start/SKILL.md` § Phase 6.8.
594
+ */
595
+ const CONSENT_NUDGE =
596
+ 'Telemetrie-Consent ist auf diesem Host unentschieden. Stelle JETZT, vor jeder anderen Arbeit, '
597
+ + 'genau einmal die consent-neutrale AskUserQuestion aus skills/session-start/SKILL.md § Phase 6.8 '
598
+ + '(zwei Optionen, keine "(Recommended)"-Markierung; Codex/Cursor: nummerierte Liste) und rufe danach '
599
+ + 'grantConsent() bzw. denyConsent() aus scripts/lib/telemetry/consent.mjs. Danach nie wieder fragen.';
600
+
601
+ /**
602
+ * #1138 — queue the consent nudge when, and only when, this host has no
603
+ * telemetry decision on record.
604
+ *
605
+ * WHY A HOOK AT ALL. Phase 6.8 was prose only: it fired when the coordinator
606
+ * happened to reach line ~1060 of a 1230-line skill, after 24 other phases.
607
+ * Measured 2026-08-23: 0 records from any host other than the author's.
608
+ *
609
+ * WHY `isCiEnv` AND NOT `isHeadless`. `isHeadless()` answers "can THIS process
610
+ * prompt on a TTY?" — and this process is a hook whose stdout is a pipe, so it
611
+ * would answer `true` unconditionally and the nudge could never fire. Measured
612
+ * in a plain non-TTY subprocess on this host, 2026-08-23:
613
+ * `isHeadless()=true isCiEnv()=false stdout.isTTY=undefined`
614
+ * The question that actually matters here is a different one: "is there an
615
+ * operator who can answer an AskUserQuestion?" The prompting happens in the
616
+ * live session, not in this process, so TTY-ness of the hook is irrelevant and
617
+ * `isCiEnv()` — the fail-closed "this is unattended automation" probe — is the
618
+ * correct gate. (The same trap sits in the Phase 6.8 prose, which passes
619
+ * `interactive: !isHeadless()` from a `node -e` subprocess whose stdout is also
620
+ * a pipe: it would have resolved `prompt:false` every time even if executed.)
621
+ *
622
+ * `resolveConsent` remains the single decision point: `prompt` is true only for
623
+ * a host with no stored decision, no env override, and no fleet flag.
624
+ *
625
+ * Never throws — a failure here must not affect session start.
626
+ */
627
+ function maybeQueueConsentNudge() {
628
+ try {
629
+ if (isCiEnv(process.env)) return;
630
+ const consent = resolveConsent({
631
+ env: process.env,
632
+ ownerConfig: loadOwnerConfig().config,
633
+ state: readTelemetryState().record,
634
+ interactive: true,
635
+ });
636
+ if (consent.prompt !== true) return;
637
+ setAdditionalContext(CONSENT_NUDGE);
638
+ } catch { /* best effort — the nudge is informational, never blocks */ }
639
+ }
640
+
470
641
  // ---------------------------------------------------------------------------
471
642
  // Main
472
643
  // ---------------------------------------------------------------------------
@@ -597,6 +768,11 @@ async function main() {
597
768
  } catch { /* hook must remain non-blocking */ }
598
769
 
599
770
  let peers = [];
771
+ // GH#67 consumer half — how many mechanically-detected peers were registry-
772
+ // sourced entries that a LIVE lock at this root supersedes. Counted for the
773
+ // session.started payload below; NEVER subtracted from any peer count
774
+ // (HR-106: the banner reports the number the verdict judged).
775
+ let mechanicalPeersSuperseded = 0;
600
776
  try {
601
777
  await sweepZombies().catch(() => ({ removed: [], logged: 0 }));
602
778
  try {
@@ -698,23 +874,89 @@ async function main() {
698
874
  const { discoverActiveSessions } = await import('../scripts/lib/session-discovery.mjs');
699
875
  const allActive = await discoverActiveSessions(projectRoot);
700
876
  const mechanicalPeers = allActive.filter((s) => s.sessionId !== sessionId);
877
+ mechanicalPeersSuperseded = mechanicalPeers.filter((p) => p.lockSuperseded === true).length;
701
878
  if (mechanicalPeers.length > 0) {
879
+ // #1137 part 1 — say WHERE, not just how many. discoverActiveSessions()
880
+ // walks EVERY path `git worktree list` reports, which includes worktrees
881
+ // another coordinator parked outside this working copy (e.g. under
882
+ // /private/tmp/…). Measured 2026-08-23: all 4 reported "peers" were the
883
+ // 4 registered worktrees, none of them in this checkout — and the old
884
+ // wording ("active in same repo") read as 4 sessions competing for THIS
885
+ // directory. The surface is deliberately unchanged (a foreign-worktree
886
+ // session IS a peer of this repo); only the label and the per-peer
887
+ // detail are, so the operator can tell the two apart at a glance.
702
888
  const summary = mechanicalPeers
703
889
  .slice(0, 3)
704
- .map((p) => `${p.sessionId}:${p.mode ?? 'session'}`)
890
+ .map((p) => {
891
+ const where = p.worktreePath ? path.basename(p.worktreePath) : '?';
892
+ // #1151 — carry the peer's MODE when the source recorded one.
893
+ // `worktree:id` says where a peer sits but not what it is doing,
894
+ // and the operator's next decision (wait / promote / proceed)
895
+ // turns on exactly that: a `deep` peer holding this worktree set
896
+ // is a different situation from a `discovery` one. Guarded rather
897
+ // than assumed — sessionFromLock() passes `lock.mode` verbatim, so
898
+ // a lock written without the field yields undefined here, while
899
+ // sessionFromRegistryEntry() always defaults it to 'session'.
900
+ const mode = typeof p.mode === 'string' && p.mode.length > 0 ? `:${p.mode}` : '';
901
+ return `${where}:${shortSessionId(p.sessionId)}${mode}${supersessionMarker(p)}`;
902
+ })
705
903
  .join(', ');
706
904
  const overflow = mechanicalPeers.length > 3 ? ` +${mechanicalPeers.length - 3} more` : '';
707
- pushBanner(`🔍 Mechanical peer-detection: ${mechanicalPeers.length} active in same repo (${summary}${overflow})`);
905
+ pushBanner(`🔍 Mechanical peer-detection: ${mechanicalPeers.length} active in this repo's worktree set (${summary}${overflow})`);
906
+ if (mechanicalPeersSuperseded > 0) {
907
+ pushBanner(` ${mechanicalPeersSuperseded} of them registry-only and superseded by this root's live lock — likely a finished task without SessionEnd (GH#67). Still counted above: the session lock is advisory.`);
908
+ }
708
909
  }
709
910
  } catch { /* best effort — banner is informational, never blocks */ }
710
911
  }
711
912
 
913
+ // Phase 4 measurement probes — the mechanical caller (#1128).
914
+ //
915
+ // `skills/session-start/SKILL.md` § Phase 4 names 18 probes with module paths
916
+ // and entry functions. Measured 2026-08-23 at `4f6404e`, not one of them had
917
+ // a caller anywhere in hooks/, npm scripts, .gitlab-ci.yml or .husky/ — the
918
+ // only caller was the prose itself, and across 336 recorded session starts
919
+ // there was no event proving any of them had ever run. Built, documented,
920
+ // never wired.
921
+ //
922
+ // Placement is deliberate: AFTER the backfill (so `sessions-staleness` and
923
+ // `sessions-integrity` measure the reconstructed ledger, not the stale one)
924
+ // and BEFORE flushBanner() (so probe findings ride the single systemMessage
925
+ // envelope instead of adding a second stdout object Claude Code would
926
+ // discard — see the bannerLines docstring and HR-106).
927
+ //
928
+ // Best-effort exactly like `backfillOnSessionStart` above: the runner has no
929
+ // rejecting path of its own, and this try/catch is defence-in-depth so a
930
+ // future regression inside it still cannot block a session start.
931
+ // Escape hatch: SO_DISABLE_STARTUP_PROBES=1.
932
+ //
933
+ // `enable-host-banner: false` silences the DISPLAY, never the MEASUREMENT.
934
+ // The documented opt-out ("a user who silenced session-start banners has
935
+ // opted out of ALL session-start banners") governs `pushBanner` and nothing
936
+ // else: the probes still run and `orchestrator.probes.completed` is still
937
+ // written, because the defect this wiring repairs is precisely that nobody
938
+ // could tell whether the probes had ever run (HR-105). Gating the run on a
939
+ // display preference would rebuild that blind spot behind a config key.
940
+ if (process.env.SO_DISABLE_STARTUP_PROBES !== '1') {
941
+ try {
942
+ const { runSessionStartProbes } = await import('../scripts/lib/session-start-probes.mjs');
943
+ const probeRun = await runSessionStartProbes({ repoRoot: projectRoot });
944
+ if (bannerData) {
945
+ for (const line of probeRun.bannerLines) pushBanner(line);
946
+ }
947
+ } catch { /* hook must remain non-blocking */ }
948
+ }
949
+
712
950
  const payload = {
713
951
  platform,
714
952
  project: projectName,
715
953
  branch,
716
954
  session_id: sessionId,
717
955
  peer_count: peers.length,
956
+ // GH#67 — additive: how many mechanically-detected peers a live lock at
957
+ // this root supersedes. `peer_count` is deliberately unchanged; this field
958
+ // makes the supersession rate measurable instead of inferred (HR-105).
959
+ peers_superseded: mechanicalPeersSuperseded,
718
960
  };
719
961
  if (bannerData) {
720
962
  payload.host_class = bannerData.host.host_class;
@@ -736,7 +978,16 @@ async function main() {
736
978
  payload.memory_pressure_pct_free = bannerData.resources.memory_pressure_pct_free ?? null;
737
979
  payload.peer_sessions_count = bannerData.resources.peer_sessions_count ?? null;
738
980
  }
739
- await emitEvent('orchestrator.session.started', payload);
981
+ // #1183 — a malformed record throws EventValidationError BEFORE any side
982
+ // effect (scripts/lib/events.mjs); this hook must never abort on that, so
983
+ // the emit is wrapped rather than left to propagate.
984
+ try {
985
+ await emitEvent('orchestrator.session.started', payload);
986
+ } catch { /* telemetry never blocks the hook (#1183) */ }
987
+
988
+ // #1138 — one-time telemetry-consent nudge. Queued (never written) here so it
989
+ // rides the single stdout envelope below.
990
+ maybeQueueConsentNudge();
740
991
 
741
992
  // Single flush — see the bannerLines docstring for why this must stay the
742
993
  // only stdout write in the hook.