session-orchestrator 4.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/.agents/skills/session-plan/SKILL.md +1 -1
  2. package/.agents/skills/session-start/SKILL.md +1 -1
  3. package/.agents/skills/ux-grill/SKILL.md +22 -0
  4. package/.claude-plugin/marketplace.json +1 -1
  5. package/.claude-plugin/plugin.json +3 -2
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.codex-plugin/skills/session-plan/SKILL.md +1 -1
  8. package/.codex-plugin/skills/session-start/SKILL.md +1 -1
  9. package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
  10. package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
  11. package/.cursor/commands/ux-grill.md +14 -0
  12. package/.cursor/skills/session-plan/SKILL.md +1 -1
  13. package/.cursor/skills/session-start/SKILL.md +1 -1
  14. package/.cursor/skills/ux-grill/SKILL.md +13 -0
  15. package/.cursor-plugin/plugin.json +1 -1
  16. package/AGENTS.md +2 -1
  17. package/CHANGELOG.md +128 -1
  18. package/README.md +98 -86
  19. package/agents/dialectic-deriver.md +11 -0
  20. package/agents/ux-evaluator.md +1 -1
  21. package/commands/close.md +3 -3
  22. package/commands/go.md +2 -0
  23. package/commands/memory-cleanup.md +4 -3
  24. package/commands/persona-panel.md +1 -1
  25. package/commands/session.md +3 -2
  26. package/commands/ux-grill.md +51 -0
  27. package/docs/README.md +4 -4
  28. package/docs/USER-GUIDE.md +117 -50
  29. package/docs/agent-authoring.md +2 -2
  30. package/docs/baseline.md +55 -1
  31. package/docs/ci-setup.md +1 -1
  32. package/docs/codex-setup.md +9 -0
  33. package/docs/components.md +9 -9
  34. package/docs/cursor-setup.md +1 -0
  35. package/docs/events-schema.md +13 -6
  36. package/docs/github-mirror-protection.md +61 -20
  37. package/docs/instruction-delivery.md +1 -1
  38. package/docs/memory-proposal-flow.md +3 -3
  39. package/docs/migration-v4.md +2 -2
  40. package/docs/migration-v5.md +62 -0
  41. package/docs/owner-config-schema.md +74 -90
  42. package/docs/persona-panel.md +4 -4
  43. package/docs/pi-setup.md +1 -0
  44. package/docs/rule-authoring.md +13 -6
  45. package/docs/scope-collision-guard.md +16 -0
  46. package/docs/session-config-reference.md +55 -22
  47. package/docs/session-config-template.md +9 -5
  48. package/docs/vault-docs-architecture.md +4 -2
  49. package/hooks/_lib/hook-import-set.json +70 -3
  50. package/hooks/_lib/lock-bootstrap.mjs +84 -1
  51. package/hooks/_lib/vcs-create-matcher.mjs +401 -16
  52. package/hooks/enforce-scope.mjs +201 -0
  53. package/hooks/hooks-codex.json +1 -1
  54. package/hooks/hooks-cursor.json +5 -0
  55. package/hooks/hooks.json +7 -2
  56. package/hooks/on-session-start.mjs +171 -49
  57. package/hooks/post-bash-issue-budget-refund.mjs +375 -0
  58. package/hooks/pre-auq-clarity.mjs +70 -18
  59. package/hooks/pre-bash-issue-budget.mjs +170 -26
  60. package/hooks/subagent-telemetry.mjs +106 -20
  61. package/package.json +5 -4
  62. package/pi/prompts/ux-grill.md +12 -0
  63. package/scripts/baseline-archetypes.mjs +28 -0
  64. package/scripts/ci/assert-vitest-green.mjs +4 -2
  65. package/scripts/dialectic-deriver.mjs +32 -8
  66. package/scripts/emit-session.mjs +72 -1
  67. package/scripts/lib/agent-status.mjs +441 -9
  68. package/scripts/lib/auq/schema.mjs +10 -3
  69. package/scripts/lib/auto-dialectic.mjs +0 -68
  70. package/scripts/lib/baseline-archetypes.mjs +439 -0
  71. package/scripts/lib/build-live-signals.mjs +5 -6
  72. package/scripts/lib/ci-status-banner.mjs +29 -6
  73. package/scripts/lib/claude-md-budget-lint.mjs +52 -2
  74. package/scripts/lib/config/issue-budget.mjs +68 -8
  75. package/scripts/lib/config/private-config-dir.mjs +3 -2
  76. package/scripts/lib/config/remote-hosts.mjs +2 -2
  77. package/scripts/lib/config-schema.mjs +79 -0
  78. package/scripts/lib/config.mjs +12 -1
  79. package/scripts/lib/eval/engine.mjs +7 -1
  80. package/scripts/lib/file-lock.mjs +151 -8
  81. package/scripts/lib/git-porcelain.mjs +113 -0
  82. package/scripts/lib/instruction-budget-guard.mjs +415 -47
  83. package/scripts/lib/io.mjs +29 -4
  84. package/scripts/lib/issue-budget-reconcile.mjs +392 -0
  85. package/scripts/lib/issue-budget.mjs +412 -9
  86. package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
  87. package/scripts/lib/learnings/sizing-subject.mjs +44 -0
  88. package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
  89. package/scripts/lib/locks/state-md-lock.mjs +19 -41
  90. package/scripts/lib/maintenance-due-banner.mjs +450 -0
  91. package/scripts/lib/owner-config.example.yaml +29 -46
  92. package/scripts/lib/owner-yaml.mjs +14 -13
  93. package/scripts/lib/peer-cards/merger.mjs +143 -0
  94. package/scripts/lib/pre-dispatch-check.mjs +20 -14
  95. package/scripts/lib/project-hygiene.mjs +81 -30
  96. package/scripts/lib/quality-gate.mjs +27 -71
  97. package/scripts/lib/reconcile/engine.mjs +19 -1
  98. package/scripts/lib/reconcile/writer.mjs +278 -11
  99. package/scripts/lib/resource-probe/evaluate.mjs +19 -21
  100. package/scripts/lib/rules-sync.mjs +34 -4
  101. package/scripts/lib/scope-echo.mjs +346 -0
  102. package/scripts/lib/session-close-backfill.mjs +182 -40
  103. package/scripts/lib/session-end/phase-skip.mjs +85 -86
  104. package/scripts/lib/session-end/tail-runner.mjs +178 -0
  105. package/scripts/lib/session-lock.mjs +62 -2
  106. package/scripts/lib/session-record-repair.mjs +91 -0
  107. package/scripts/lib/session-schema/constants.mjs +6 -0
  108. package/scripts/lib/session-schema/filters.mjs +26 -1
  109. package/scripts/lib/session-schema/validator.mjs +20 -0
  110. package/scripts/lib/session-shape.mjs +558 -0
  111. package/scripts/lib/session-start-probes.mjs +429 -56
  112. package/scripts/lib/session-token-rollup.mjs +95 -10
  113. package/scripts/lib/state-md/frontmatter-mutators.mjs +22 -34
  114. package/scripts/lib/state-md.mjs +1 -0
  115. package/scripts/lib/subagents-schema.mjs +77 -9
  116. package/scripts/lib/telemetry/pricing.mjs +197 -0
  117. package/scripts/lib/telemetry/sync.mjs +50 -1
  118. package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
  119. package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
  120. package/scripts/lib/tmux-layout/layouts.mjs +62 -4
  121. package/scripts/lib/ux-grill/collect.mjs +1163 -0
  122. package/scripts/lib/ux-grill/compare.mjs +285 -0
  123. package/scripts/lib/ux-grill/manifest.mjs +618 -0
  124. package/scripts/lib/ux-grill/measures.mjs +431 -0
  125. package/scripts/lib/ux-grill/paths.mjs +224 -0
  126. package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
  127. package/scripts/lib/ux-grill/reconcile.mjs +344 -0
  128. package/scripts/lib/ux-grill/run-record.mjs +316 -0
  129. package/scripts/lib/ux-grill/schema.mjs +321 -0
  130. package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
  131. package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
  132. package/scripts/lib/validate/check-unwired-features.mjs +56 -27
  133. package/scripts/lib/vault-mirror/process.mjs +2 -1
  134. package/scripts/lib/vault-status/board-lock.mjs +18 -0
  135. package/scripts/lib/vault-status/board-writer.mjs +8 -0
  136. package/scripts/lib/vault-status/narrative-mirror.mjs +4 -4
  137. package/scripts/lib/wave-resource-gate.mjs +23 -27
  138. package/scripts/lib/wave-sizing.mjs +10 -3
  139. package/scripts/materialize-wave-scope.mjs +68 -14
  140. package/scripts/mcp-server.sh +16 -1
  141. package/scripts/print-applicable-rules.mjs +7 -6
  142. package/scripts/print-learnings-index.mjs +3 -2
  143. package/scripts/release.mjs +7 -2
  144. package/scripts/session-shape.mjs +266 -0
  145. package/skills/_shared/config-reading.md +15 -9
  146. package/skills/_shared/private-capability-context.md +89 -0
  147. package/skills/bootstrap/SKILL.md +60 -209
  148. package/skills/bootstrap/_shared-template.md +99 -14
  149. package/skills/bootstrap/deep-template.md +36 -26
  150. package/skills/bootstrap/fast-template.md +44 -8
  151. package/skills/bootstrap/intensity-heuristic.md +10 -4
  152. package/skills/bootstrap/private-contract.md +119 -0
  153. package/skills/bootstrap/public-fallback.md +30 -18
  154. package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
  155. package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
  156. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
  157. package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
  158. package/skills/bootstrap/standard-template.md +39 -24
  159. package/skills/claude-md-drift-check/SKILL.md +9 -2
  160. package/skills/claude-md-drift-check/checker.mjs +213 -21
  161. package/skills/discovery/SKILL.md +6 -173
  162. package/skills/discovery/probes/vault-staleness.mjs +35 -5
  163. package/skills/discovery/probes-docs.md +8 -4
  164. package/skills/discovery/probes-supply-chain.md +4 -2
  165. package/skills/discovery/probes-ui.md +8 -4
  166. package/skills/discovery/probes-vault.md +12 -4
  167. package/skills/discovery/references/discovery-interactive-triage.md +139 -0
  168. package/skills/discovery/references/discovery-triage-state.md +54 -0
  169. package/skills/docs-orchestrator/audience-mapping.md +1 -1
  170. package/skills/eval/rubric-v1.md +13 -0
  171. package/skills/evolve/SKILL.md +2 -458
  172. package/skills/evolve/references/evolve-analyze-mode.md +360 -0
  173. package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
  174. package/skills/gitlab-ops/SKILL.md +3 -3
  175. package/skills/grill/SKILL.md +1 -1
  176. package/skills/memory-cleanup/SKILL.md +2 -2
  177. package/skills/plan/mode-new.md +9 -0
  178. package/skills/plan/mode-retro.md +4 -3
  179. package/skills/reconcile/SKILL.md +11 -1
  180. package/skills/session-end/SKILL.md +3 -2
  181. package/skills/session-end/drift-operations.md +20 -5
  182. package/skills/session-end/metrics-collection.md +1 -0
  183. package/skills/session-end/phase-3-2-docs-verification.md +1 -1
  184. package/skills/session-end/phase-3-6-tail.md +27 -67
  185. package/skills/session-end/phase-3-7a-recommendations.md +2 -2
  186. package/skills/session-end/references/phase-2-quality-gate.md +3 -3
  187. package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
  188. package/skills/session-end/references/phase-5-issue-cleanup.md +32 -1
  189. package/skills/session-end/session-metrics-write.md +33 -12
  190. package/skills/session-plan/SKILL.md +46 -180
  191. package/skills/session-plan/references/session-plan-task-classification.md +152 -0
  192. package/skills/session-plan/wave-template.md +8 -15
  193. package/skills/session-start/SKILL.md +41 -7
  194. package/skills/session-start/phase-2-5-docs-planning.md +1 -1
  195. package/skills/session-start/phase-8-5-express-path.md +12 -9
  196. package/skills/session-start/references/operations-contract.md +114 -0
  197. package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
  198. package/skills/session-start/references/phase-4-ssot-environment-check.md +42 -24
  199. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
  200. package/skills/session-start/soul.md +2 -2
  201. package/skills/test-runner/SKILL.md +1 -1
  202. package/skills/test-runner/rubric-v1.md +2 -2
  203. package/skills/tmux-layout/SKILL.md +3 -1
  204. package/skills/ux-grill/SKILL.md +211 -0
  205. package/skills/ux-grill/rubric-v2.md +201 -0
  206. package/skills/ux-grill/soul.md +76 -0
  207. package/skills/wave-executor/SKILL.md +32 -127
  208. package/skills/wave-executor/circuit-breaker.md +3 -1
  209. package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
  210. package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
  211. package/skills/wave-executor/references/wave-loop-dispatch.md +12 -2
  212. package/skills/wave-executor/references/wave-loop-review.md +19 -6
  213. package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
  214. package/templates/_shared/ux-manifest.template.md +149 -0
  215. package/templates/nextjs-minimal/package.json +1 -1
  216. package/templates/node-minimal/package.json +1 -1
  217. package/scripts/lib/multi-provider-build/providers.mjs +0 -64
  218. package/scripts/lib/multi-provider-build/templating.mjs +0 -130
  219. package/scripts/lib/owner-config/coerce.mjs +0 -29
  220. package/scripts/lib/owner-config/constants.mjs +0 -21
  221. package/scripts/lib/owner-config/defaults.mjs +0 -50
  222. package/scripts/lib/owner-config/error.mjs +0 -19
  223. package/scripts/lib/owner-config/index.mjs +0 -13
  224. package/scripts/lib/owner-config/merge.mjs +0 -52
  225. package/scripts/lib/owner-config/validate.mjs +0 -259
  226. package/scripts/lib/owner-config-loader.mjs +0 -170
  227. package/scripts/lib/owner-config.mjs +0 -28
  228. package/scripts/lib/soul-resolve.mjs +0 -130
  229. package/scripts/lib/vault-mirror/render.mjs +0 -8
  230. package/templates/_shared/journey-manifest.md +0 -114
@@ -7,7 +7,7 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "echo '🎯 Session Orchestrator v4.1.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
+ "command": "echo '🎯 Session Orchestrator v5.0.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
11
11
  "async": false
12
12
  },
13
13
  {
@@ -161,6 +161,11 @@
161
161
  "type": "command",
162
162
  "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-tool-failure-corrective-context.mjs\"",
163
163
  "timeout": 5
164
+ },
165
+ {
166
+ "type": "command",
167
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-bash-issue-budget-refund.mjs\"",
168
+ "timeout": 5
164
169
  }
165
170
  ]
166
171
  }
package/hooks/hooks.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "hooks": {
3
3
  "SessionStart": [
4
4
  {
5
- "matcher": "startup|clear|compact",
5
+ "matcher": "startup|resume|clear|compact",
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "echo '🎯 Session Orchestrator v4.1.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
9
+ "command": "echo '🎯 Session Orchestrator v5.0.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
10
  "async": false
11
11
  },
12
12
  {
@@ -206,6 +206,11 @@
206
206
  "type": "command",
207
207
  "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/run-node.sh\" \"$CLAUDE_PLUGIN_ROOT/hooks/post-tool-failure-corrective-context.mjs\"",
208
208
  "timeout": 5
209
+ },
210
+ {
211
+ "type": "command",
212
+ "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/run-node.sh\" \"$CLAUDE_PLUGIN_ROOT/hooks/post-bash-issue-budget-refund.mjs\"",
213
+ "timeout": 5
209
214
  }
210
215
  ]
211
216
  }
@@ -54,6 +54,11 @@ import { detectColdStart, consumeMarker } from '../scripts/lib/cold-start-detect
54
54
  import { parseSessionId } from '../scripts/lib/session-id.mjs';
55
55
  import { readTelemetryState, resolveConsent, isCiEnv } from '../scripts/lib/telemetry/consent.mjs';
56
56
  import { loadOwnerConfig } from '../scripts/lib/owner-yaml.mjs';
57
+ // SSOT for the "is this a re-entry into the same logical session?" question
58
+ // (#1091). Defined in the lock-bootstrap leaf module, which this hook already
59
+ // loads, so the preservation branch below and the lock force-refresh gate can
60
+ // never drift apart.
61
+ import { SAME_LOGICAL_SESSION_SOURCES } from './_lib/lock-bootstrap.mjs';
57
62
 
58
63
  const execFileAsync = promisify(execFile);
59
64
 
@@ -336,6 +341,9 @@ async function readStdinJson(timeoutMs = 500) {
336
341
  * Best-effort: any persistence failure is swallowed (hook must remain non-blocking).
337
342
  */
338
343
  async function resolveSessionId(input, projectRoot) {
344
+ const nativeSource = typeof input?.source === 'string' && input.source.length > 0
345
+ ? input.source
346
+ : null;
339
347
  const fromStdin = (input && (input.session_id || input.sessionId)) ?? null;
340
348
  const parsedStdinId = parseSessionId(fromStdin);
341
349
  const rawStdinSessionId = parsedStdinId?.format === 'uuid' ? fromStdin : null;
@@ -364,61 +372,136 @@ async function resolveSessionId(input, projectRoot) {
364
372
  source = 'generated-uuid';
365
373
  }
366
374
 
375
+ const sessionFilePath = path.join(projectRoot, '.orchestrator', 'current-session.json');
376
+
377
+ // Read the PREVIOUS current-session.json BEFORE minting a semantic id: on a
378
+ // same-logical-session re-entry it decides whether a new id is minted at all
379
+ // (#1091 F1 below). Best-effort — absent/unparseable leaves prev = null.
380
+ let prev = null;
381
+ try {
382
+ prev = JSON.parse(await readFile(sessionFilePath, 'utf8'));
383
+ if (typeof prev !== 'object' || prev === null) prev = null;
384
+ } catch { /* absent / unparseable → no linkage, no preservation */ }
385
+
386
+ // #1091 F1 — raw-id linkage on a same-logical-session re-entry.
387
+ //
388
+ // resolveSemanticSessionId() has NO self-exclusion: it projects every active
389
+ // session (including OUR OWN lock and registry entry) to n and returns
390
+ // maxN + 1. So a second SessionStart of the SAME session mints
391
+ // `…-session-31` where the first minted `…-session-30`, the
392
+ // `prev.semantic_session_id === semanticSessionId` preservation branch below
393
+ // is FALSE, and last_wave / last_batch / last_wave_completed /
394
+ // wave_start_sha are dropped — re-opening #612, #980 and #1193. Before
395
+ // #1091 that path was unreachable on Claude Code because `resume` was absent
396
+ // from the SessionStart matcher; adding it made it live.
397
+ //
398
+ // The trustworthy linkage is the RAW id: Claude Code preserves `session_id`
399
+ // across a native resume, and current-session.json records the raw id the
400
+ // previous invocation of this hook resolved. When it matches, this is
401
+ // provably the same logical session and its semantic label is REUSED rather
402
+ // than re-minted. When it does NOT match, today's behaviour stands (fresh
403
+ // id, no preservation) — that is the "unverified restart" #1091 defines, and
404
+ // guessing continuity there would adopt a foreign session's high-water marks
405
+ // (`.claude/rules/identity-and-locks.md` — a working-copy artefact is not an
406
+ // identity witness).
407
+ //
408
+ // Sources: `clear` and `compact` are re-entries into the same logical
409
+ // session for exactly the reason the comment block below states (the UUID
410
+ // changes, the semantic label does not); `resume` is the third per #1091.
411
+ // `startup` is deliberately absent — a fresh process start must never
412
+ // inherit a predecessor's markers.
413
+ let resumeLinkage = 'none';
414
+ if (
415
+ nativeSource !== null
416
+ && SAME_LOGICAL_SESSION_SOURCES.has(nativeSource)
417
+ && prev
418
+ && prev.session_id === sessionId
419
+ && typeof prev.semantic_session_id === 'string'
420
+ && prev.semantic_session_id.length > 0
421
+ ) {
422
+ semanticSessionId = prev.semantic_session_id;
423
+ resumeLinkage = 'raw-id';
424
+ }
425
+
367
426
  // Derive a descriptive semantic label for either raw-id source. Best-effort:
368
427
  // a failure leaves it null without changing the physical raw session_id.
369
- try {
370
- const semCandidate = await deriveSemanticCandidate({
371
- projectRoot,
372
- mode: normalizedMode,
373
- });
374
- if (semCandidate) semanticSessionId = semCandidate;
375
- } catch { /* best effort — leave semanticSessionId = null */ }
428
+ if (semanticSessionId === null) {
429
+ try {
430
+ const semCandidate = await deriveSemanticCandidate({
431
+ projectRoot,
432
+ mode: normalizedMode,
433
+ });
434
+ if (semCandidate) semanticSessionId = semCandidate;
435
+ } catch { /* best effort — leave semanticSessionId = null */ }
436
+ }
376
437
 
377
438
  try {
378
- const dir = path.join(projectRoot, '.orchestrator');
379
- await mkdir(dir, { recursive: true });
380
- const sessionFilePath = path.join(dir, 'current-session.json');
439
+ await mkdir(path.dirname(sessionFilePath), { recursive: true });
381
440
 
382
441
  // High-water-mark preservation (#612 root-cause fix).
383
- // SessionStart fires on startup|clear|compact|resume of the SAME logical
384
- // session. On clear/compact/resume the UUID `session_id` changes but the
385
- // `semantic_session_id` (branch+date+mode+n) stays stable. A naive full
386
- // overwrite of current-session.json drops the `last_wave` / `last_batch`
387
- // markers written mid-session by post-tool-batch-wave-signal.mjs, which
388
- // makes the next PostToolBatch re-read last_wave as absent→0 and re-emit a
389
- // duplicate orchestrator.wave.started{N} with no intervening
390
- // wave.completed. To prevent that, PRESERVE last_wave/last_batch across a
391
- // SessionStart of the SAME logical session (matching semantic id), while
392
- // still RESETTING them for a genuinely new session (different/absent
393
- // semantic id, or an unparseable prior file). Best-effort: a read failure
394
- // must never throw we simply fall through to the reset path.
442
+ // SessionStart fires once per source in the matcher of the platform's
443
+ // hooks file — `startup|resume|clear|compact` on Claude Code and Codex
444
+ // (`resume` was ABSENT from hooks.json until #1091, so on Claude Code this
445
+ // hook never ran on a native resume), `startup|reload|new|resume|fork` on
446
+ // pi, and every source on Cursor (empty matcher). Several of those sources
447
+ // are re-entries into the SAME logical session. On clear/compact the UUID
448
+ // `session_id` changes while the `semantic_session_id`
449
+ // (branch+date+mode+n) stays stable; whether Claude Code PRESERVES the raw
450
+ // `session_id` across a native resume is what #1091 measures via
451
+ // `native_source` + `resume_linkage` on `orchestrator.session.started`
452
+ // nothing here ASSUMES either answer, it only reacts to the raw id it can
453
+ // observe. A naive full overwrite of current-session.json drops the
454
+ // `last_wave` / `last_batch` markers written mid-session by
455
+ // post-tool-batch-wave-signal.mjs, which makes the next PostToolBatch
456
+ // re-read last_wave as absent→0 and re-emit a duplicate
457
+ // orchestrator.wave.started{N} with no intervening wave.completed.
458
+ //
459
+ // Three cases, exactly one of which preserves nothing:
460
+ // 'raw-id' — same-logical-session source AND the recorded raw
461
+ // session_id equals ours → the semantic label was reused
462
+ // above, markers PRESERVED. (#1091 F1)
463
+ // 'semantic' — the freshly minted label equals the recorded one →
464
+ // markers PRESERVED. The pre-#1091 behaviour, unchanged.
465
+ // 'none' — a genuinely new session, or an unverified restart
466
+ // (raw id changed, label re-minted) → markers RESET.
467
+ // Best-effort throughout: `prev` is null on a read/parse failure, which
468
+ // lands in 'none' and therefore on the reset path.
395
469
  const preserved = {};
396
470
  if (typeof semanticSessionId === 'string' && semanticSessionId.length > 0) {
397
- try {
398
- const prevRaw = await readFile(sessionFilePath, 'utf8');
399
- const prev = JSON.parse(prevRaw);
400
- if (prev && prev.semantic_session_id === semanticSessionId) {
401
- if (Object.prototype.hasOwnProperty.call(prev, 'last_wave')) {
402
- preserved.last_wave = prev.last_wave;
403
- }
404
- if (Object.prototype.hasOwnProperty.call(prev, 'last_batch')) {
405
- preserved.last_batch = prev.last_batch;
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
- }
413
- // #980 — the OPEN half of the wave-diff pair, written by
414
- // post-tool-batch-wave-signal.mjs. Dropping it across a /clear leaves
415
- // the running wave with no start point, so the next
416
- // `orchestrator.wave.completed` silently omits `files_changed`.
417
- if (Object.prototype.hasOwnProperty.call(prev, 'wave_start_sha')) {
418
- preserved.wave_start_sha = prev.wave_start_sha;
419
- }
471
+ // Two ways into the SAME logical session, both preserving:
472
+ // 1. `resumeLinkage === 'raw-id'` the raw session_id matched on a
473
+ // resume/clear/compact re-entry, so the label above was REUSED
474
+ // (#1091 F1). Reachable since `resume` entered the matcher.
475
+ // 2. semantic match — the freshly minted label equals the recorded
476
+ // one. This is the case the branch already covered (a clear or
477
+ // compact whose discovery view happened to re-mint the same n),
478
+ // and it stays untouched.
479
+ // Anything else (different or absent semantic id, unparseable prior
480
+ // file, `startup`) is a genuinely new session and RESETS the markers.
481
+ if (resumeLinkage === 'none' && prev && prev.semantic_session_id === semanticSessionId) {
482
+ resumeLinkage = 'semantic';
483
+ }
484
+ if (prev && (resumeLinkage === 'raw-id' || prev.semantic_session_id === semanticSessionId)) {
485
+ if (Object.prototype.hasOwnProperty.call(prev, 'last_wave')) {
486
+ preserved.last_wave = prev.last_wave;
487
+ }
488
+ if (Object.prototype.hasOwnProperty.call(prev, 'last_batch')) {
489
+ preserved.last_batch = prev.last_batch;
420
490
  }
421
- } catch { /* absent / unparseable no preservation (reset) */ }
491
+ // #1193 the final-wave completion marker must survive a
492
+ // clear/compact too: dropping it re-arms a duplicate SessionEnd
493
+ // `orchestrator.wave.completed` for a wave already closed.
494
+ if (Object.prototype.hasOwnProperty.call(prev, 'last_wave_completed')) {
495
+ preserved.last_wave_completed = prev.last_wave_completed;
496
+ }
497
+ // #980 — the OPEN half of the wave-diff pair, written by
498
+ // post-tool-batch-wave-signal.mjs. Dropping it across a /clear leaves
499
+ // the running wave with no start point, so the next
500
+ // `orchestrator.wave.completed` silently omits `files_changed`.
501
+ if (Object.prototype.hasOwnProperty.call(prev, 'wave_start_sha')) {
502
+ preserved.wave_start_sha = prev.wave_start_sha;
503
+ }
504
+ }
422
505
  }
423
506
 
424
507
  // Epic #583 W5-F1c — surface semantic_session_id (Q5 H1 / Issue #587 completion).
@@ -441,7 +524,19 @@ async function resolveSessionId(input, projectRoot) {
441
524
  );
442
525
  } catch { /* best effort */ }
443
526
 
444
- return { sessionId, semanticSessionId, mode: normalizedMode };
527
+ return {
528
+ sessionId,
529
+ semanticSessionId,
530
+ mode: normalizedMode,
531
+ nativeSource,
532
+ resumeLinkage,
533
+ // The raw session_id our OWN previous run of this hook recorded, or null.
534
+ // Surfaced (rather than kept local) because bootstrapLock() needs it as the
535
+ // third conjunct of the #1091 F2 force-refresh — see the call site in main().
536
+ predecessorSessionId: typeof prev?.session_id === 'string' && prev.session_id.length > 0
537
+ ? prev.session_id
538
+ : null,
539
+ };
445
540
  }
446
541
 
447
542
  /**
@@ -711,7 +806,12 @@ async function main() {
711
806
  // v3.1.0 multi-session registry (#168). All steps best-effort — failures
712
807
  // must never break the hook, which is informational-only.
713
808
  const input = await stdinPromise;
714
- const { sessionId, semanticSessionId, mode } = await resolveSessionId(input, projectRoot);
809
+ // #1091 the native SessionStart `source` (Claude Code: startup|resume|
810
+ // clear|compact), plus the linkage decision resolveSessionId() derived from
811
+ // it (see SAME_LOGICAL_SESSION_SOURCES). Both are surfaced on the event so
812
+ // #1091 gets its measurement instead of an inference.
813
+ const { sessionId, semanticSessionId, mode, nativeSource, resumeLinkage, predecessorSessionId } =
814
+ await resolveSessionId(input, projectRoot);
715
815
  // getPlatform() ALREADY implements the SO_PLATFORM override as step 1 of its
716
816
  // precedence — and, unlike a bare `??`, it validates the value against the
717
817
  // four-platform allowlist and trims it. The former `process.env.SO_PLATFORM ??
@@ -737,6 +837,13 @@ async function main() {
737
837
  semanticSessionId,
738
838
  mode,
739
839
  ttlHours: 4,
840
+ nativeSource,
841
+ // The raw session_id our OWN previous run of this hook recorded. It is the
842
+ // third conjunct of the same-logical-session force-refresh (#1091 F2): a
843
+ // semantic-label match alone is collidable on one host (#1066), so the
844
+ // live lock must ALSO carry our predecessor's raw id before we take it
845
+ // over. `prev` is read above, before the semantic id is minted.
846
+ predecessorSessionId,
740
847
  });
741
848
  } catch { /* hook must remain non-blocking */ }
742
849
 
@@ -927,7 +1034,7 @@ async function main() {
927
1034
 
928
1035
  // Phase 4 measurement probes — the mechanical caller (#1128).
929
1036
  //
930
- // `skills/session-start/SKILL.md` § Phase 4 names 18 probes with module paths
1037
+ // `skills/session-start/SKILL.md` § Phase 4 names 19 probes with module paths
931
1038
  // and entry functions. Measured 2026-08-23 at `4f6404e`, not one of them had
932
1039
  // a caller anywhere in hooks/, npm scripts, .gitlab-ci.yml or .husky/ — the
933
1040
  // only caller was the prose itself, and across 336 recorded session starts
@@ -1008,6 +1115,21 @@ async function main() {
1008
1115
  peers_superseded: mechanicalPeersSuperseded,
1009
1116
  };
1010
1117
 
1118
+ // #1091 — record the native SessionStart source so "does the same raw
1119
+ // session_id repeat under source=resume?" becomes answerable from
1120
+ // events.jsonl instead of guessed. OMITTED when the harness sends no source
1121
+ // (docs/events-schema.md optional-field convention: absent ≠ measured-empty).
1122
+ if (nativeSource) payload.native_source = nativeSource;
1123
+
1124
+ // #1091 F1 — WHICH linkage the source produced: 'raw-id' (prior raw
1125
+ // session_id matched → semantic label reused, high-water marks preserved),
1126
+ // 'semantic' (the freshly minted label happened to equal the recorded one),
1127
+ // or 'none' (a new session, or an unverified restart whose continuity is
1128
+ // deliberately not guessed). Same optional-field convention as
1129
+ // `native_source`: OMITTED when the harness sends no source at all, so a
1130
+ // Codex/Cursor row is never read as a measured 'none'.
1131
+ if (nativeSource) payload.resume_linkage = resumeLinkage;
1132
+
1011
1133
  // #nnn — installed/latest plugin version on the session record.
1012
1134
  //
1013
1135
  // HR-105: a verdict nothing records is unfalsifiable. `installed` is stamped