session-orchestrator 5.4.0 → 5.5.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 (150) 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/rules/060-evolve.mdc +5 -5
  5. package/.cursor-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +95 -3
  7. package/README.md +11 -11
  8. package/docs/ci-setup.md +120 -23
  9. package/docs/components.md +7 -7
  10. package/docs/events-schema.md +17 -14
  11. package/docs/install.md +14 -2
  12. package/docs/migration-v4.md +24 -17
  13. package/docs/plugin-architecture-v3.md +1 -1
  14. package/docs/scope-collision-guard.md +15 -13
  15. package/docs/session-config-reference.md +7 -7
  16. package/docs/vault-docs-architecture.md +8 -4
  17. package/hooks/_lib/hook-import-set.json +17 -8
  18. package/hooks/_lib/lock-bootstrap.mjs +118 -26
  19. package/hooks/config-protection.mjs +1 -1
  20. package/hooks/enforce-commands.mjs +28 -13
  21. package/hooks/enforce-scope.mjs +201 -23
  22. package/hooks/hooks-codex.json +1 -1
  23. package/hooks/hooks.json +1 -1
  24. package/hooks/on-session-start.mjs +4 -2
  25. package/hooks/on-stop.mjs +70 -170
  26. package/hooks/post-bash-write-verify.mjs +98 -17
  27. package/hooks/post-edit-validate.mjs +15 -5
  28. package/hooks/post-tool-batch-wave-signal.mjs +32 -154
  29. package/hooks/pre-bash-memory-propose-audit.mjs +19 -7
  30. package/hooks/pre-bash-staging-fence.mjs +29 -8
  31. package/hooks/pre-task-scope-disjoint.mjs +584 -207
  32. package/hooks/wave-scope-commit-guard.mjs +168 -72
  33. package/package.json +2 -6
  34. package/scripts/apply-session-learnings.mjs +35 -14
  35. package/scripts/archive-closed-prds.mjs +38 -11
  36. package/scripts/autopilot.mjs +8 -1
  37. package/scripts/backfill-abandoned-sessions.mjs +80 -5
  38. package/scripts/backfill-learnings-from-vault.mjs +49 -22
  39. package/scripts/codex-install.mjs +3 -0
  40. package/scripts/cursor-install.mjs +46 -2
  41. package/scripts/emit-session.mjs +5 -3
  42. package/scripts/export-hw-learnings.mjs +18 -7
  43. package/scripts/fleet-checkin.mjs +26 -3
  44. package/scripts/lib/autopilot/loop.mjs +25 -10
  45. package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
  46. package/scripts/lib/blocked-commands-policy.mjs +40 -0
  47. package/scripts/lib/common.mjs +41 -0
  48. package/scripts/lib/config/vault-integration.mjs +151 -16
  49. package/scripts/lib/config.mjs +5 -1
  50. package/scripts/lib/eval/engine.mjs +13 -0
  51. package/scripts/lib/events-retention-banner.mjs +393 -0
  52. package/scripts/lib/events-rotation.mjs +21 -6
  53. package/scripts/lib/events.mjs +51 -3
  54. package/scripts/lib/fleet-protocol.mjs +20 -2
  55. package/scripts/lib/hardening.mjs +1 -0
  56. package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
  57. package/scripts/lib/instruction-budget-guard.mjs +8 -0
  58. package/scripts/lib/learnings/expiry-sweep.mjs +387 -20
  59. package/scripts/lib/learnings/io.mjs +125 -16
  60. package/scripts/lib/lock-reaper.mjs +38 -14
  61. package/scripts/lib/maintenance-due-banner.mjs +23 -8
  62. package/scripts/lib/named-vault-resolver.mjs +48 -5
  63. package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
  64. package/scripts/lib/orphan-reaper/reaper-audit.mjs +101 -69
  65. package/scripts/lib/orphan-reaper/scan-throttle.mjs +79 -27
  66. package/scripts/lib/orphan-reaper/scan.mjs +4 -0
  67. package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
  68. package/scripts/lib/orphan-reaper.mjs +5 -1
  69. package/scripts/lib/platform.mjs +206 -2
  70. package/scripts/lib/plugin-update-banner.mjs +3 -1
  71. package/scripts/lib/process-group.mjs +83 -22
  72. package/scripts/lib/scope-baseline.mjs +116 -25
  73. package/scripts/lib/scope-echo.mjs +7 -0
  74. package/scripts/lib/scope-gate.mjs +194 -27
  75. package/scripts/lib/session-close-backfill.mjs +129 -4
  76. package/scripts/lib/session-end/tail-runner.mjs +10 -5
  77. package/scripts/lib/session-lock-shape.mjs +6 -3
  78. package/scripts/lib/session-lock.mjs +320 -67
  79. package/scripts/lib/session-registry.mjs +41 -16
  80. package/scripts/lib/session-start-probes.mjs +242 -68
  81. package/scripts/lib/session-token-rollup.mjs +39 -34
  82. package/scripts/lib/sessions-staleness-banner.mjs +3 -1
  83. package/scripts/lib/tail-window.mjs +27 -3
  84. package/scripts/lib/telemetry/pricing.mjs +5 -2
  85. package/scripts/lib/telemetry/sync.mjs +17 -0
  86. package/scripts/lib/tests-src-ratio.mjs +77 -2
  87. package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
  88. package/scripts/lib/validate/check-test-value-bans.mjs +320 -32
  89. package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
  90. package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
  91. package/scripts/lib/vault-mirror/namespace.mjs +47 -26
  92. package/scripts/lib/vault-mirror/process.mjs +350 -62
  93. package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
  94. package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
  95. package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
  96. package/scripts/lib/vault-mirror/telemetry.mjs +18 -11
  97. package/scripts/lib/vault-status/board-writer.mjs +5 -1
  98. package/scripts/lib/vcs-repo-spec.mjs +8 -0
  99. package/scripts/materialize-wave-scope.mjs +7 -4
  100. package/scripts/release.mjs +1 -0
  101. package/scripts/run-migrate-v2-cross-repo.mjs +14 -2
  102. package/scripts/run-quality-gate.mjs +8 -5
  103. package/scripts/self-update.mjs +229 -0
  104. package/scripts/sweep-expired-learnings.mjs +560 -28
  105. package/scripts/validate-wave-scope.mjs +21 -2
  106. package/scripts/vault-backfill.mjs +50 -37
  107. package/scripts/vault-mirror.mjs +170 -33
  108. package/skills/_shared/fleet-protocol.md +74 -71
  109. package/skills/_shared/state-ownership.md +38 -14
  110. package/skills/autopilot/SKILL.md +1 -1
  111. package/skills/bootstrap/SKILL.md +5 -3
  112. package/skills/bootstrap/_shared-template.md +96 -1
  113. package/skills/bootstrap/fast-template.md +3 -0
  114. package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
  115. package/skills/bootstrap/standard-template.md +2 -0
  116. package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
  117. package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
  118. package/skills/discovery/probes/vault-staleness.mjs +9 -3
  119. package/skills/docs-orchestrator/SKILL.md +12 -5
  120. package/skills/docs-orchestrator/audience-mapping.md +5 -3
  121. package/skills/evolve/SKILL.md +11 -2
  122. package/skills/evolve/references/evolve-analyze-mode.md +37 -17
  123. package/skills/session-end/learning-patterns.md +3 -3
  124. package/skills/session-end/metrics-collection.md +1 -1
  125. package/skills/session-end/phase-3-2-docs-verification.md +7 -6
  126. package/skills/session-end/plan-verification.md +25 -9
  127. package/skills/session-end/references/phase-3-documentation-updates.md +11 -1
  128. package/skills/session-end/session-metrics-write.md +45 -10
  129. package/skills/session-start/SKILL.md +3 -3
  130. package/skills/session-start/references/operations-contract.md +5 -3
  131. package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
  132. package/skills/session-start/references/phase-4-ssot-environment-check.md +8 -1
  133. package/skills/vault-mirror/SKILL.md +21 -11
  134. package/skills/wave-executor/SKILL.md +1 -1
  135. package/skills/wave-executor/circuit-breaker.md +1 -1
  136. package/skills/wave-executor/references/wave-executor-state-init.md +1 -1
  137. package/skills/wave-executor/references/wave-loop-dispatch.md +8 -0
  138. package/skills/wave-executor/references/wave-loop-review.md +2 -2
  139. package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
  140. package/.agents/skills/navigator/SKILL.md +0 -22
  141. package/.agents/skills/navigator/agents/openai.yaml +0 -5
  142. package/.codex-plugin/skills/navigator/SKILL.md +0 -22
  143. package/.codex-plugin/skills/navigator/agents/openai.yaml +0 -5
  144. package/.cursor/commands/navigator.md +0 -15
  145. package/.cursor/skills/navigator/SKILL.md +0 -13
  146. package/pi/prompts/navigator.md +0 -12
  147. package/skills/navigator/SKILL.md +0 -193
  148. package/skills/navigator/references/account-check.sh +0 -87
  149. package/skills/navigator/references/fleet-decisions.mjs +0 -231
  150. package/skills/navigator/references/slot-check.sh +0 -114
@@ -107,7 +107,12 @@ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
107
107
  let isPathInside;
108
108
  let relativeFromRoot;
109
109
  let resolveProjectDir;
110
- let findScopeFile;
110
+ let resolveSessionRoot;
111
+ // The launch dir the harness states — platform.mjs's one definition of the env
112
+ // chain, so the relocation trace below cannot drift from the clamp (#1504).
113
+ let launchDirFromEnv;
114
+ // #1504 point 6 — the manifest that governs THIS session; bound from scope-gate below.
115
+ let findOwnScopeFile;
111
116
  let pathMatchesPattern;
112
117
  let suggestForScopeViolation;
113
118
  let readJson;
@@ -197,8 +202,8 @@ async function bootstrap() {
197
202
 
198
203
  ({ readStdin, emitAllow, emitDeny, emitWarn } = modules.io);
199
204
  ({ isPathInside, relativeFromRoot } = modules.pathUtils);
200
- ({ resolveProjectDir } = modules.platform);
201
- ({ findScopeFile, pathMatchesPattern, suggestForScopeViolation } = modules.hardening);
205
+ ({ resolveProjectDir, resolveSessionRoot, launchDirFromEnv } = modules.platform);
206
+ ({ pathMatchesPattern, suggestForScopeViolation } = modules.hardening);
202
207
  ({ readJson } = modules.common);
203
208
  ({
204
209
  classifyEmptyScope,
@@ -206,6 +211,7 @@ async function bootstrap() {
206
211
  sessionStartedAtMs,
207
212
  gradeScopeEntry,
208
213
  canonicalizeGrantPrefix,
214
+ findOwnScopeFile,
209
215
  } = modules.scopeGate);
210
216
  ({ readProcessLocalSessionIds, classifyManifestSession } = modules.sessionIdentity);
211
217
  }
@@ -224,7 +230,13 @@ async function main() {
224
230
  // Gate 2: file_path must be a non-empty string
225
231
  if (!filePath || typeof filePath !== 'string') return emitAllow();
226
232
 
227
- const projectRootRaw = resolveProjectDir();
233
+ // #1492 — the SESSION's working copy, not the launch dir: after
234
+ // `EnterWorktree`, `$CLAUDE_PROJECT_DIR` still names the dir the session was
235
+ // launched in, while the manifest lives in the worktree. Never above the launch
236
+ // dir; a harness subagent worktree (`isolation: "worktree"`) is lifted to the
237
+ // launch checkout it sits under — still unenforced in an entered-worktree
238
+ // session (named ceiling in `resolveSessionRoot`, scripts/lib/platform.mjs).
239
+ const projectRootRaw = resolveSessionRoot(input.cwd);
228
240
 
229
241
  // Resolve symlinks in the project root itself so that realpath(file) comparisons
230
242
  // are consistent. On macOS /tmp → /private/tmp; mismatches would cause false denials.
@@ -235,9 +247,26 @@ async function main() {
235
247
  projectRoot = projectRootRaw;
236
248
  }
237
249
 
238
- // Gate 3: no wave-scope.json → nothing to enforce
239
- const scopePath = findScopeFile(projectRoot);
240
- if (!scopePath) return emitAllow();
250
+ // `new Set(...)` is load-bearing: `readProcessLocalSessionIds` returns a
251
+ // string[], and `classifyManifestSession` does `ownIds instanceof Set ?
252
+ // ownIds : new Set()` — a bare array would silently become EMPTY, making
253
+ // every manifest read `unknown`. Process-local only (#1194, see Gate 3b).
254
+ const ownIds = new Set(readProcessLocalSessionIds({ hookInput: input }));
255
+
256
+ // Gate 3: no wave-scope.json → nothing to enforce.
257
+ //
258
+ // #1504 point 6 — the first manifest that is not provably a peer's, in the
259
+ // `.pi` > `.cursor` > `.codex` > `.claude` order. `findScopeFile` stopped at
260
+ // the first file that exists, so a peer's `.pi`/`.cursor` manifest made Gate
261
+ // 3b stand down and this session's own `.claude/wave-scope.json` was never
262
+ // read. Only when EVERY candidate is foreign does the first of them come back
263
+ // (`foreignPath`), so Gate 3b's stand-down below stays unchanged.
264
+ const located = findOwnScopeFile(projectRoot, ownIds, classifyManifestSession);
265
+ const scopePath = located.path ?? located.foreignPath;
266
+ if (!scopePath) {
267
+ await recordHiddenManifestOncePerWave({ projectRootRaw, projectRoot, ownIds, filePath });
268
+ return emitAllow();
269
+ }
241
270
 
242
271
  // SECURITY-REQ-08: read scope file once; pass parsed object to all subsequent checks
243
272
  //
@@ -305,11 +334,7 @@ async function main() {
305
334
  // `enforcement: "off"` already grants in the same file — this gate adds no new
306
335
  // authority, and the manifest is the coordinator's own artefact either way.
307
336
  {
308
- // `new Set(...)` is load-bearing: `readProcessLocalSessionIds` returns a
309
- // string[], and `classifyManifestSession` does `ownIds instanceof Set ?
310
- // ownIds : new Set()` — a bare array would silently become EMPTY, making
311
- // every manifest read `unknown`.
312
- const ownIds = new Set(readProcessLocalSessionIds({ hookInput: input }));
337
+ // `ownIds` is the process-local set built above Gate 3.
313
338
  const { verdict, manifestIds } = classifyManifestSession(scope, ownIds);
314
339
  if (verdict === 'foreign') {
315
340
  // Observability only, and deliberately NOT emitWarn: this branch is hit on
@@ -489,6 +514,7 @@ async function main() {
489
514
  if (matchedGrant !== null) {
490
515
  const grade = gradeScopeEntry(matchedGrant, { resolve: canonicalizeGrantPrefix });
491
516
  if (grade?.verdict === 'error') {
517
+ await recordRefusedGrantOncePerWave({ scope, scopePath, projectRoot, input, grant: matchedGrant, grade, filePath: resolvedPath });
492
518
  return emitWarn(
493
519
  `Gate 5b honoured an out-of-repo grant that scripts/validate-wave-scope.mjs would REFUSE ` +
494
520
  `before dispatch — allowedPaths ${grade.message}. The write to '${resolvedPath}' is ` +
@@ -538,7 +564,9 @@ async function main() {
538
564
  // dispatched wave agent's. See {@link classifyCaller} for the measurement.
539
565
  // A subagent falls through to the gates below — under a manifest that does
540
566
  // not grant the path (Discovery's `allowedPaths: []`) that is a DENY.
541
- const memoryDirs = await ownMemoryDirs(projectRootRaw, projectRoot);
567
+ // #1492: the LAUNCH dir too — the harness keys auto-memory on the path it
568
+ // was launched with, which after `EnterWorktree` is no longer the root.
569
+ const memoryDirs = await ownMemoryDirs([resolveProjectDir(), projectRootRaw, projectRoot]);
542
570
  const caller = classifyCaller(input);
543
571
  if (caller !== 'subagent' && memoryDirs.some((dir) => isInsideDir(resolvedPath, dir))) {
544
572
  // One event per decision point, awaited before emitAllow() —
@@ -652,6 +680,155 @@ async function main() {
652
680
  return emitAllow();
653
681
  }
654
682
 
683
+ /**
684
+ * #1485 point 1 — the catch event for Gate 5b's WARN branch, ONCE per wave.
685
+ *
686
+ * `scripts/validate-wave-scope.mjs` has no mechanical caller; the Gate 5b WARN
687
+ * is the only place a refused grant is seen at all, and a `systemMessage` is not
688
+ * countable. Owner decision 2026-10-02, option (b): the FIRST WARN of a wave
689
+ * writes `orchestrator.scope.refused_grant_allowed`, so "how many waves ran on
690
+ * a manifest the validator would refuse" becomes a ledger query (HR-105). The
691
+ * WARN itself is unchanged and still fires on every write.
692
+ *
693
+ * DEDUPE: a marker `.orchestrator/tmp/gate5b-refused-grant/<session>-w<wave>`
694
+ * created with `flag: 'wx'` (O_CREAT|O_EXCL). Exclusive create is atomic, so of
695
+ * two hook processes racing on the first WARN exactly one wins and emits — no
696
+ * lock, no read-modify-write. The session joins the key because wave numbers
697
+ * restart in every session sharing this working copy. `.orchestrator/tmp/` is
698
+ * gitignored; one marker per (session, wave) that ever hit a refused grant.
699
+ *
700
+ * Any marker failure OTHER than EEXIST (unwritable dir, ENOTDIR) still emits:
701
+ * over-reporting a rare event beats re-creating the silent gap this closes.
702
+ *
703
+ * COST: runs only on the Gate 5b `error` branch — an ordinary in-repo write
704
+ * never reaches it, so the hot path pays nothing. NAMED CEILING (BV-004): a
705
+ * legacy manifest without a session id and a payload without one share the key
706
+ * `no-session-w<N>` across sessions; revisit if such manifests are still seen.
707
+ *
708
+ * Never throws — observability must not change the WARN.
709
+ *
710
+ * @param {{scope: Record<string, unknown>, scopePath: string, projectRoot: string,
711
+ * input: Record<string, unknown>, grant: string, grade: {code?: string},
712
+ * filePath: string}} facts
713
+ * @returns {Promise<void>}
714
+ */
715
+ async function recordRefusedGrantOncePerWave({ scope, scopePath, projectRoot, input, grant, grade, filePath }) {
716
+ try {
717
+ const session = String(
718
+ scope.session_id ?? scope.session ?? input?.session_id ?? process.env.CLAUDE_CODE_SESSION_ID ?? 'no-session',
719
+ );
720
+ const key = `${session}-w${scope.wave ?? 'unknown'}`.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 160);
721
+ const markerDir = path.join(projectRoot, '.orchestrator', 'tmp', 'gate5b-refused-grant');
722
+ try { await fs.mkdir(markerDir, { recursive: true }); } catch { /* the create below decides */ }
723
+ try {
724
+ await fs.writeFile(path.join(markerDir, key), `${new Date().toISOString()}\n`, { flag: 'wx' });
725
+ } catch (e) {
726
+ if (e?.code === 'EEXIST') return; // this wave already reported
727
+ }
728
+ const { emitEvent } = await import('../scripts/lib/events.mjs');
729
+ await emitEvent(
730
+ 'orchestrator.scope.refused_grant_allowed',
731
+ {
732
+ hook: HOOK_NAME,
733
+ manifest: scopePath,
734
+ wave: scope.wave,
735
+ grant,
736
+ code: grade?.code,
737
+ file_path: filePath,
738
+ },
739
+ { repoRoot: projectRoot },
740
+ );
741
+ } catch { /* observability is best-effort — never changes the WARN */ }
742
+ }
743
+
744
+ /**
745
+ * #1504 point 3 — the catch event for a session root that HID an active wave,
746
+ * ONCE per (session, wave, session root).
747
+ *
748
+ * `resolveSessionRoot()` answers with the repo the payload `cwd` sits in. A wave
749
+ * agent that `cd`s into a nested or sibling repo (or a session that entered a
750
+ * worktree) therefore resolves a root without a manifest, and Gate 3 allows
751
+ * every write there while the wave's manifest sits at the launch dir, unread.
752
+ * The revisit trigger `resolveSessionRoot` names for exactly that relocation
753
+ * ("an `orchestrator.scope.*` record") could never fire: this allow left no
754
+ * trace. Behaviour stays ALLOW — this is the trace, not a new deny.
755
+ *
756
+ * Fires only when BOTH hold, so the ordinary no-manifest allow — every write in
757
+ * every repo without a wave — stays silent (HR-101):
758
+ * 1. the harness states a launch dir and it is not the resolved session root;
759
+ * 2. the launch root carries a manifest that is not provably a peer's
760
+ * ({@link findOwnScopeFile}: own, or unbound/legacy — one that WOULD have
761
+ * been enforced). A peer's manifest at the launch root hides nothing of ours.
762
+ *
763
+ * COST on the hot path: rung 1 is an env read plus a string compare — no
764
+ * filesystem call while the session root IS the launch dir, i.e. for every
765
+ * session that never left its checkout. One `realpath` when only the spelling
766
+ * differs (macOS `/var` → `/private/var`); the ≤4 `existsSync` of rung 2 only
767
+ * for a genuinely relocated root.
768
+ *
769
+ * DEDUPE: the {@link recordRefusedGrantOncePerWave} pattern — an exclusive
770
+ * create (`flag: 'wx'`) of `<launch>/.orchestrator/tmp/scope-root-relocated/<key>`.
771
+ * The event goes to the LAUNCH repo's ledger, where the wave's other records
772
+ * live. The session root's basename joins the key so a second relocated repo in
773
+ * one wave is reported too. NAMED CEILING (BV-004): two relocated roots sharing a
774
+ * basename in one wave report once; revisit if a record ever names the wrong repo.
775
+ *
776
+ * Never throws — observability must not change the allow.
777
+ *
778
+ * @param {{projectRootRaw: string, projectRoot: string, ownIds: Set<string>,
779
+ * filePath: string}} facts
780
+ * @returns {Promise<void>}
781
+ */
782
+ async function recordHiddenManifestOncePerWave({ projectRootRaw, projectRoot, ownIds, filePath }) {
783
+ try {
784
+ const launchRaw = launchDirFromEnv();
785
+ if (launchRaw === '') return;
786
+ const launchResolved = path.resolve(launchRaw);
787
+ if (launchResolved === projectRootRaw || launchResolved === projectRoot) return;
788
+ let launch;
789
+ try {
790
+ launch = await fs.realpath(launchResolved);
791
+ } catch {
792
+ return; // a launch dir that does not exist holds no manifest
793
+ }
794
+ if (launch === projectRoot) return;
795
+
796
+ const { path: manifest } = findOwnScopeFile(launch, ownIds, classifyManifestSession);
797
+ if (!manifest) return;
798
+
799
+ let scope = {};
800
+ try {
801
+ const parsed = JSON.parse(await fs.readFile(manifest, 'utf8'));
802
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) scope = parsed;
803
+ } catch { /* unreadable → keyed and reported as wave `unknown` */ }
804
+
805
+ const session = String(scope.session_id ?? scope.session ?? [...ownIds][0] ?? 'no-session');
806
+ const key = `${session}-w${scope.wave ?? 'unknown'}-${path.basename(projectRoot)}`
807
+ .replace(/[^A-Za-z0-9._-]/g, '_')
808
+ .slice(0, 160);
809
+ const markerDir = path.join(launch, '.orchestrator', 'tmp', 'scope-root-relocated');
810
+ try { await fs.mkdir(markerDir, { recursive: true }); } catch { /* the create below decides */ }
811
+ try {
812
+ await fs.writeFile(path.join(markerDir, key), `${new Date().toISOString()}\n`, { flag: 'wx' });
813
+ } catch (e) {
814
+ if (e?.code === 'EEXIST') return; // this root already reported for this wave
815
+ }
816
+ const { emitEvent } = await import('../scripts/lib/events.mjs');
817
+ await emitEvent(
818
+ 'orchestrator.scope.manifest_hidden_by_session_root',
819
+ {
820
+ hook: HOOK_NAME,
821
+ manifest,
822
+ launch_root: launch,
823
+ session_root: projectRoot,
824
+ wave: scope.wave,
825
+ file_path: path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath),
826
+ },
827
+ { repoRoot: launch },
828
+ );
829
+ } catch { /* observability is best-effort — never changes the allow */ }
830
+ }
831
+
655
832
  /**
656
833
  * `mtimeMs` of a file, or `null` when it cannot be stat'ed.
657
834
  *
@@ -679,21 +856,22 @@ async function mtimeMsOf(file) {
679
856
  /**
680
857
  * #1295 — the Claude Code auto-memory directories that belong to THIS repo.
681
858
  *
682
- * Returns at most two paths, both naming the SAME repo: the encoding of the
683
- * project root as the harness saw it (`CLAUDE_PROJECT_DIR` / cwd) and — when it
684
- * differs — the encoding of its realpath. Both are needed because Claude Code
685
- * encodes the path it was LAUNCHED with, while this hook compares against the
686
- * canonical root (on macOS `/tmp` → `/private/tmp`). Two encodings of one repo
687
- * is not a wider grant: a sibling repo's root encodes to neither.
859
+ * One path per distinct root (per home spelling): the encoding of the launch
860
+ * dir the harness saw (`CLAUDE_PROJECT_DIR` / cwd), of the session root
861
+ * (#1492 — a worktree the session entered) and of its realpath. All are needed
862
+ * because Claude Code encodes the path it was LAUNCHED with, while this hook
863
+ * compares against the canonical session root (on macOS `/tmp` →
864
+ * `/private/tmp`). Keeping the launch dir keeps the pre-#1492 grant exactly
865
+ * (the launch dir WAS the root then); the session-root entries name the
866
+ * worktree this session works in. A sibling repo's root encodes to none of them.
688
867
  *
689
868
  * Returns `[]` when the encoder cannot be loaded, so the caller falls through to
690
869
  * its deny (fail-closed).
691
870
  *
692
- * @param {string} projectRootRaw — project root as resolved from env/cwd
693
- * @param {string} projectRoot — its realpath
871
+ * @param {string[]} candidateRoots — launch dir, session root, its realpath
694
872
  * @returns {Promise<string[]>}
695
873
  */
696
- async function ownMemoryDirs(projectRootRaw, projectRoot) {
874
+ async function ownMemoryDirs(candidateRoots) {
697
875
  try {
698
876
  const [{ encodeProjectDir }, { homedir }] = await Promise.all([
699
877
  import('../scripts/lib/wave-transcript-tail.mjs'),
@@ -710,7 +888,7 @@ async function ownMemoryDirs(projectRootRaw, projectRoot) {
710
888
  } catch { /* non-existent home — the raw form is all there is */ }
711
889
  const homes = new Set([home, homeReal]);
712
890
  const roots = new Set(
713
- [projectRootRaw, projectRoot].filter((p) => typeof p === 'string' && p.length > 0),
891
+ candidateRoots.filter((p) => typeof p === 'string' && p.length > 0),
714
892
  );
715
893
  const dirs = [];
716
894
  for (const h of homes) {
@@ -7,7 +7,7 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "echo '🎯 Session Orchestrator v5.4.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
+ "command": "echo '🎯 Session Orchestrator v5.5.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
11
11
  "async": false
12
12
  },
13
13
  {
package/hooks/hooks.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "echo '🎯 Session Orchestrator v5.4.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
9
+ "command": "echo '🎯 Session Orchestrator v5.5.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
10
  "async": false
11
11
  },
12
12
  {
@@ -1098,8 +1098,10 @@ async function main() {
1098
1098
 
1099
1099
  // Phase 4 measurement probes — the mechanical caller (#1128).
1100
1100
  //
1101
- // `skills/session-start/SKILL.md` § Phase 4 names 19 probes with module paths
1102
- // and entry functions. Measured 2026-08-23 at `4f6404e`, not one of them had
1101
+ // `skills/session-start/SKILL.md` § Phase 4 names 20 probes with module paths
1102
+ // and entry functions (2026-10-02; the `PROBES` registry in
1103
+ // `scripts/lib/session-start-probes.mjs` is the count's source of truth).
1104
+ // Measured 2026-08-23 at `4f6404e` (a smaller family then), not one of them had
1103
1105
  // a caller anywhere in hooks/, npm scripts, .gitlab-ci.yml or .husky/ — the
1104
1106
  // only caller was the prose itself, and across 336 recorded session starts
1105
1107
  // there was no event proving any of them had ever run. Built, documented,