claude-code-session-manager 0.75.3 → 0.77.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 (164) hide show
  1. package/dist/assets/{AgentLibrary-CzQqcObq.js → AgentLibrary-B2ie8bbw.js} +2 -2
  2. package/dist/assets/{DataModel-Bj_WlLz8.js → DataModel-BIJPYw32.js} +1 -1
  3. package/dist/assets/{History-DnSi_OHm.js → History-CeY6dk9S.js} +2 -2
  4. package/dist/assets/{Hooks-0BB0dp3S.js → Hooks-BFH2ocKg.js} +2 -2
  5. package/dist/assets/{HostBilko-DHpwwsLQ.js → HostBilko-36gj9wLz.js} +1 -1
  6. package/dist/assets/{Library-CaJVqVvi.js → Library-C-hBct39.js} +1 -1
  7. package/dist/assets/{ListDetail-C1W2HmC2.js → ListDetail-CNq64VWV.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-5Ob9FW3z.js → MarkdownEditor-Bh3qt5-1.js} +1 -1
  9. package/dist/assets/{McpServers-JxCSfm1S.js → McpServers-DpGN0oyz.js} +1 -1
  10. package/dist/assets/{Memory-BDeqlqwH.js → Memory-D59hUjC4.js} +6 -6
  11. package/dist/assets/{Panel-Dh9ZHuEj.js → Panel-DCgbaoci.js} +1 -1
  12. package/dist/assets/{Permissions-DXy-CbEY.js → Permissions-DAmQ0DYV.js} +2 -2
  13. package/dist/assets/{Plugins-_n1Iuc8T.js → Plugins-Dyfgn6Is.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-BP_evfxE.js → ProvenanceBadge-BiYhPO1U.js} +1 -1
  15. package/dist/assets/SaveBar-RV7B6sOh.js +1 -0
  16. package/dist/assets/Scheduler-BPaNqx1b.js +14 -0
  17. package/dist/assets/{ScopeSwitcher-CAWzM6RI.js → ScopeSwitcher-P4mdLGNU.js} +1 -1
  18. package/dist/assets/{Settings-DRRozLyT.js → Settings-BL4vf5aX.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-DGHDWlz4.js → SkillReferenceGraph-BRBDyi1_.js} +1 -1
  20. package/dist/assets/{Skills-D8L66eiX.js → Skills-BV08gDUH.js} +2 -2
  21. package/dist/assets/{SystemPrompt-CYtUsonD.js → SystemPrompt-CLftSsDw.js} +1 -1
  22. package/dist/assets/TagLibrary-Bp8jGsd5.js +1 -0
  23. package/dist/assets/{TiptapBody-B2hRgbPE.js → TiptapBody-jCpuB6E5.js} +1 -1
  24. package/dist/assets/{Toggle-BTwsbxam.js → Toggle-D2paA1xf.js} +1 -1
  25. package/dist/assets/{index-DijufvkJ.js → index-BDRSqBl3.js} +704 -704
  26. package/dist/assets/{index-CMLnzdZC.css → index-CYhdtisq.css} +1 -1
  27. package/dist/assets/{settingsSchema-D6wzxAi6.js → settingsSchema-6IOLjZZN.js} +1 -1
  28. package/dist/index.html +2 -2
  29. package/package.json +8 -2
  30. package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
  31. package/scripts/lib/activeSessions.cjs +116 -6
  32. package/scripts/project-pages-logic/dist/logic.cjs +4709 -0
  33. package/scripts/render-project-pages/dist/renderer.cjs +18900 -0
  34. package/scripts/render-project-pages.cjs +70 -0
  35. package/scripts/scheduler-mcp-server.cjs +269 -96
  36. package/scripts/validate-project-pages-summary.cjs +62 -0
  37. package/src/main/__tests__/agentModelResolve.test.cjs +66 -0
  38. package/src/main/__tests__/epicStatusMirror.test.cjs +110 -0
  39. package/src/main/__tests__/health-delegation-chain.test.cjs +106 -0
  40. package/src/main/__tests__/prdAdminRoutes.test.cjs +295 -0
  41. package/src/main/__tests__/prdAgentType.test.cjs +103 -0
  42. package/src/main/__tests__/prdCreate.test.cjs +247 -0
  43. package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +117 -0
  44. package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +108 -0
  45. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +485 -0
  46. package/src/main/__tests__/projectPages.test.cjs +73 -1
  47. package/src/main/__tests__/rcaReport.test.cjs +54 -0
  48. package/src/main/__tests__/runVerify.test.cjs +94 -0
  49. package/src/main/__tests__/scheduler-autofix-select.test.cjs +58 -3
  50. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +103 -0
  51. package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +41 -0
  52. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +10 -0
  53. package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +78 -0
  54. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +242 -0
  55. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +31 -0
  56. package/src/main/__tests__/scheduler-launch-failure.test.cjs +201 -0
  57. package/src/main/__tests__/scheduler-leftover-fields.test.cjs +52 -0
  58. package/src/main/__tests__/scheduler-looks-done.test.cjs +241 -0
  59. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +135 -0
  60. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +222 -0
  61. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +207 -1
  62. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +212 -0
  63. package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +185 -0
  64. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +194 -0
  65. package/src/main/__tests__/seedAgentPersonas.test.cjs +75 -14
  66. package/src/main/__tests__/seedSchedulerMcp.test.cjs +66 -0
  67. package/src/main/__tests__/uniquePrdNumbers.test.cjs +14 -5
  68. package/src/main/bilkoHost.cjs +4 -3
  69. package/src/main/chatRunner.cjs +6 -1
  70. package/src/main/config.cjs +25 -33
  71. package/src/main/health.cjs +153 -2
  72. package/src/main/index.cjs +64 -5
  73. package/src/main/ipcSchemas.cjs +69 -1
  74. package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +179 -0
  75. package/src/main/lib/__tests__/childWithLog.test.cjs +141 -0
  76. package/src/main/lib/__tests__/delegationReadiness.test.cjs +391 -42
  77. package/src/main/lib/__tests__/ephemeralCwd.test.cjs +91 -0
  78. package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +5 -3
  79. package/src/main/lib/__tests__/fixChainDepth.test.cjs +40 -0
  80. package/src/main/lib/__tests__/gitWorktree.test.cjs +290 -5
  81. package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +107 -0
  82. package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +153 -0
  83. package/src/main/lib/__tests__/jobWorktree.test.cjs +6 -4
  84. package/src/main/lib/__tests__/landedSinceRun.test.cjs +73 -0
  85. package/src/main/lib/__tests__/launchFailure.test.cjs +220 -0
  86. package/src/main/lib/__tests__/loadGate.test.cjs +159 -0
  87. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +102 -0
  88. package/src/main/lib/__tests__/opsOwnership.test.cjs +7 -0
  89. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +151 -0
  90. package/src/main/lib/__tests__/opsRootResolve.test.cjs +149 -0
  91. package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +82 -0
  92. package/src/main/lib/__tests__/projectRootResolve.test.cjs +148 -0
  93. package/src/main/lib/__tests__/queueHealth.test.cjs +58 -0
  94. package/src/main/lib/__tests__/quietMachineLease.test.cjs +39 -0
  95. package/src/main/lib/__tests__/reaperHelpers.test.cjs +133 -0
  96. package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +19 -9
  97. package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +213 -0
  98. package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +125 -0
  99. package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +127 -0
  100. package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +109 -0
  101. package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +71 -0
  102. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +217 -0
  103. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +350 -0
  104. package/src/main/lib/activeIndexMerge.cjs +15 -0
  105. package/src/main/lib/activeIndexRebuild.cjs +133 -0
  106. package/src/main/lib/agentModelResolve.cjs +58 -0
  107. package/src/main/lib/buildTarget.cjs +3 -2
  108. package/src/main/lib/childWithLog.cjs +69 -2
  109. package/src/main/lib/claudeBin.cjs +54 -1
  110. package/src/main/lib/crossProjectFeedback.cjs +8 -1
  111. package/src/main/lib/definitionOfDone.cjs +3 -2
  112. package/src/main/lib/delegationReadiness.cjs +514 -26
  113. package/src/main/lib/ephemeralCwd.cjs +78 -0
  114. package/src/main/lib/epicDelegationStats.cjs +2 -1
  115. package/src/main/lib/epicMint.cjs +17 -1
  116. package/src/main/lib/epicStatusMirror.cjs +95 -0
  117. package/src/main/lib/epicValidationHook.cjs +2 -1
  118. package/src/main/lib/epicWorktreeMint.cjs +5 -2
  119. package/src/main/lib/fixChainDepth.cjs +45 -0
  120. package/src/main/lib/gitWorktree.cjs +520 -21
  121. package/src/main/lib/jobWorktree.cjs +2 -0
  122. package/src/main/lib/landedSinceRun.cjs +55 -0
  123. package/src/main/lib/launchFailure.cjs +357 -0
  124. package/src/main/lib/loadGate.cjs +134 -0
  125. package/src/main/lib/mcpToolCatalog.cjs +370 -0
  126. package/src/main/lib/opsErrorLog.cjs +12 -1
  127. package/src/main/lib/opsOwnership.cjs +106 -0
  128. package/src/main/lib/prdAdminRoutes.cjs +43 -3
  129. package/src/main/lib/prdAgentType.cjs +84 -0
  130. package/src/main/lib/prdCreate.cjs +103 -15
  131. package/src/main/lib/prdDeclaredPaths.cjs +70 -0
  132. package/src/main/lib/prdFrontmatter.cjs +17 -3
  133. package/src/main/lib/prdLocations.cjs +13 -6
  134. package/src/main/lib/projectHomeAdminRoutes.cjs +402 -0
  135. package/src/main/lib/projectPageSummarySchema.cjs +181 -0
  136. package/src/main/lib/projectRootResolve.cjs +134 -0
  137. package/src/main/lib/promptSessionSchema.cjs +7 -0
  138. package/src/main/lib/queueHealth.cjs +38 -0
  139. package/src/main/lib/queueStore.cjs +40 -7
  140. package/src/main/lib/quietMachineLease.cjs +48 -0
  141. package/src/main/lib/rcaReport.cjs +54 -4
  142. package/src/main/lib/reaperHelpers.cjs +64 -1
  143. package/src/main/lib/scheduleJobSchema.cjs +31 -0
  144. package/src/main/lib/scheduleJobTransitions.cjs +6 -2
  145. package/src/main/lib/schedulerBatch.cjs +301 -55
  146. package/src/main/lib/schedulerConfig.cjs +99 -0
  147. package/src/main/projectBrief.cjs +3 -2
  148. package/src/main/projectPages.cjs +162 -3
  149. package/src/main/promptSessionTranscript.cjs +0 -0
  150. package/src/main/pty.cjs +5 -0
  151. package/src/main/queueOps.cjs +15 -8
  152. package/src/main/runVerify.cjs +50 -9
  153. package/src/main/scheduler/prdParser.cjs +18 -1
  154. package/src/main/scheduler.cjs +1701 -130
  155. package/src/main/seedAgentPersonas.cjs +62 -21
  156. package/src/main/seedSchedulerMcp.cjs +58 -4
  157. package/src/main/templates/project-pages-catalog.json +741 -0
  158. package/src/main/templates/project-pages-pipeline.md +417 -0
  159. package/src/preload/api.d.ts +187 -3
  160. package/src/preload/index.cjs +9 -0
  161. package/src/seed/agents/project-home-builder.md +59 -0
  162. package/dist/assets/SaveBar-D-gCUx4n.js +0 -1
  163. package/dist/assets/Scheduler-Bpd4OGju.js +0 -14
  164. package/dist/assets/TagLibrary-E5CLeuVk.js +0 -1
@@ -83,7 +83,7 @@ Data-driven from 400+ scheduler runs: long hangs (not bad code) are the dominant
83
83
  - **Fail loud, fail fast.** On any step failure, print one diagnostic line and `exit 1`; don't swallow with `|| true` or spin in a silent retry. A `rateLimited` exit-1 is the scheduler's benign auto-pause (auto-resumes next window) — not a failure to engineer around.
84
84
  - **Stay in the AC.** Do not add work past the acceptance checklist ("while we're here" generators/fixtures are the post-AC-overrun incident). Body must be clean UTF-8 — no NUL/control bytes.
85
85
  - **You ARE the executor — never re-queue or self-schedule.** A headless PRD run must perform its own acceptance criteria directly. Do NOT invoke `/develop` or any queue-authoring skill from inside a run — those are interactive main-loop skills that author a *new* PRD and return, so the run exits 0 having done nothing (no commit, no sentinel → `needs_review` with `no_verdict_sentinel`). Do NOT call `ScheduleWakeup`/set a tracking loop either — the process exits when the run ends and nothing re-invokes it. This applies just as much to spawning your own review agents and waiting on them: do NOT invoke `/code-review`, `/security-review`, `requesting-code-review`, or any other skill/subagent as a background/async step and then end your turn with something like "I'll wait for the review agents to complete" — a headless run has no next turn, so that line is the run's last output, no verdict sentinel prints, and the job parks in `needs_review` even though the actual work already landed. If a PRD's acceptance criteria call for a second review pass, run it **synchronously, inline, before the finish protocol** — call the reviewer and read its result in the same turn, don't fire-and-wait. If the PRD's work looks large, decompose and execute it inline within this run; never delegate it back to the queue. (Incidents: PRD 460 invoked `/develop`, spawned a duplicate PRD 461, and exited 0 with no work. PRD 479 landed its commit correctly but then backgrounded `/code-review --fix` + `/security-review` and called `ScheduleWakeup` to "wait" for them — same class of failure, different entry point.)
86
- - **A shared-repo `cwd` can be occupied by a concurrent job — check before you touch shared state.** When a PRD's `cwd` is a repo other headless runs may also target (a shared team repo like sigma, not a private single-purpose project), a `git checkout`/`gh pr checkout` can land you in another job's live worktree with its own uncommitted WIP. Before running `git stash`, `git reset`, or any command that discards or hides working-tree state, check `git stash list` and `git status` first, and if you must set aside pre-existing uncommitted changes that aren't yours, **stash with a descriptive message** (`git stash push -m "pre-existing WIP found by PRD <NN>, not mine"`) and **restore it before your run ends** (or, if you can't safely restore because your own commit depends on that worktree state, leave it stashed with the message and say so explicitly in your finish output — never let the run end silently dropping someone else's stash). Never `git stash drop`/`git clean -fd` on state you didn't create. (Incident: PRD 477 stashed a concurrent job's rAF-throttle-revert WIP to get its own checkout, finished, and exited without restoring it — orphaning the other job's uncommitted work in `stash@{0}` with no record of whose it was.)
86
+ - **Never `git stash`, `git reset`, `git checkout -- <path>`, or `git clean` against working-tree state you did not create — there is no polite version of this.** You are normally already isolated in your own `git worktree` on branch `sm-job/<slug>` (`src/main/lib/gitWorktree.cjs` — dirty base trees no longer disable this isolation, and the scheduler salvages any uncommitted diff on teardown, so you don't need to touch shared state to preserve your own work). Confirm you're isolated with `git rev-parse --git-common-dir` (a worktree's common-dir differs from its own `.git`) or by checking your cwd, before assuming otherwise. If you find yourself in a SHARED tree with pre-existing dirty tracked files anyway, work *around* them — scope `git add`/commits to only the paths you touched — or stop and report the conflict; never discard, stash, or revert someone else's uncommitted changes to get a clean checkout. A shared tree may be feeding a **running service** reading its config straight from the working tree (the trading repo's `data/spread_config.json` is live input, not a local scratch file) — reverting it is a production change, not a local inconvenience, and read-only inspection (`git diff`, `git status`) is always fine. The scheduler also runs a shared-tree stash guard as a backstop (`scheduler.cjs` `checkSharedTreeGuard`/`evaluateSharedTreeGuard`) that auto-restores a stash your run created and flags any baseline-dirty path that went clean with no commit — but that guard exists to catch a mistake, not to license one. (Incidents: PRD 477 stashed a concurrent job's rAF-throttle-revert WIP to get its own checkout and exited without restoring it, orphaning it in `stash@{0}` with no record of whose it was. 2026-09-01: a headless executor in the trader repo ran a blanket `git stash` against an operator's uncommitted LIVE TRADING CONFIG edit and exited without restoring it — `stash@{0}` "WIP on fix/trade-log-reconciliation-out-of-band-closes", `data/spread_config.json` — the third incident of this class.)
87
87
  - **`gh pr edit --body` can fail on repos with legacy GitHub Projects (classic) boards** — the underlying GraphQL query fetches `repository.pullRequest.projectCards`, a field GitHub is sunsetting, and errors with `GraphQL: Projects (classic) is being deprecated ... (repository.pullRequest.projectCards)` even though the edit itself would otherwise succeed. This is a known `gh` CLI quirk, not a defect in your work. Prefer `gh api -X PATCH repos/<owner>/<repo>/pulls/<n> -f body="$(cat body.md)"` for updating a PR description headlessly — it doesn't touch the deprecated field. If you do use `gh pr edit` and it fails this way, don't leave the bare GraphQL error as the last thing in that step (it reads as an unrecovered error in the final-20%-of-transcript verifier heuristic): immediately retry with the `gh api` form and print one line noting the known-bug fallback, so the recovery is adjacent to the error.
88
88
  - **`gh pr checks`/`gh run watch` exit non-zero while CI is merely *pending*, not failed — don't let that surface as a bare error.** Polling `gh pr checks <n>` before checks finish returns a non-zero exit (e.g. 8) with output like `check pending 0 <url>` — this is normal, documented `gh` CLI behavior, not a failure. If you retry with a *differently-worded* command (e.g. dropping a `sleep N &&` prefix, or switching to `gh run watch <id> --exit-status`), the verifier's self-recovery detector pairs retries by exact command-description match and may not recognize the differently-worded retry as the same recovery, leaving the original pending-state error looking unrecovered in the transcript (incident: `745-pr188-ci-lint-docs-integrity`, a fully green, committed, pushed run flagged `needs_review` over exactly this). Prefer polling with the *same* command/description each time (e.g. loop `gh pr checks <n>` unchanged, or use `gh run watch <id> --exit-status` from the start rather than switching mid-poll) so a later success is recognized as recovering the earlier pending-state failure.
89
89
  - **Negative-assertion checks must exit 0 when clean.** A check that verifies the *absence* of something (a `grep` that should find nothing, "no leftover X", `diff` expecting no change) must return exit 0 on the clean case. A bare `grep` exits **1 on no-match** — so the *success* path surfaces as `is_error=true` and the verifier downgrades a perfect run to `needs_review`. Always invert: `if <detector>; then echo "HALT: <what was found>"; exit 1; fi; echo clean`. Never let the no-match/empty path carry the non-zero exit.
@@ -11,8 +11,28 @@
11
11
  const fs = require('node:fs');
12
12
  const os = require('node:os');
13
13
  const path = require('node:path');
14
+ const { KIND_CONFIG: WORKTREE_KIND_CONFIG } = require('../../src/main/lib/gitWorktree.cjs');
14
15
 
15
16
  const HOME = os.homedir();
17
+ const TMPDIR = os.tmpdir();
18
+
19
+ // The last-resort drop targets for addCwd's tmp guard.
20
+ // - exactDropCwds: dropped only on an EXACT match — os.tmpdir() itself
21
+ // (e.g. `/tmp`). NOT a prefix match: this codebase's own test suite
22
+ // routinely stubs HOME to a tmp dir and nests a fake project cwd
23
+ // underneath it, a legitimate, unrelated use of os.tmpdir() that a
24
+ // blanket prefix match would wrongly drop.
25
+ // - prefixDropRoots: dropped on an exact match OR any nested path — the two
26
+ // managed worktree roots (both live under os.tmpdir() per gitWorktree
27
+ // .cjs's KIND_CONFIG). Nothing but managed worktree checkouts is ever
28
+ // created under these roots, so a prefix match here is safe. A resolvable
29
+ // worktree (managed or user-made, anywhere on disk) is already handled by
30
+ // worktreeMainRootOf above; this guard only catches what that resolution
31
+ // could not — e.g. a bare, .git-less worktree-root directory.
32
+ const TMP_DROP_ROOTS = {
33
+ exactDropCwds: [TMPDIR],
34
+ prefixDropRoots: [WORKTREE_KIND_CONFIG.job.root, WORKTREE_KIND_CONFIG.epic.root],
35
+ };
16
36
 
17
37
  // Transcript reads only need the last line with a `cwd` field — a few dozen
18
38
  // lines is always enough. 64 KB keeps peak RSS proportionate.
@@ -37,16 +57,93 @@ const MAX_CWDS = 50;
37
57
  // signal survives instead of being silently lost.
38
58
  const OPS_DIRNAME = 'session-manager-operations';
39
59
 
60
+ // Bound on the ancestor walk in worktreeMainRootOf — well past any real
61
+ // filesystem depth, purely to guarantee termination without relying on
62
+ // reaching '/' (e.g. a symlink loop or an unusually deep path).
63
+ const MAX_WORKTREE_WALK = 40;
64
+
65
+ const WORKTREES_MARKER = `${path.sep}.git${path.sep}worktrees${path.sep}`;
66
+
67
+ /**
68
+ * worktreeMainRootOf(cwd) → the main tree's root when cwd sits inside a
69
+ * linked `git worktree` (job/epic worktrees under os.tmpdir(), or a
70
+ * user-made one anywhere). Walks UP from cwd looking for a `.git` entry:
71
+ * - `.git` is a FILE (linked worktree) whose first line is
72
+ * `gitdir: <main>/.git/worktrees/<name>` → returns <main>.
73
+ * - `.git` is a DIRECTORY (already the main tree) → returns that ancestor.
74
+ * - nothing found within MAX_WORKTREE_WALK levels → returns null.
75
+ * Pure fs, synchronous, never throws — any unexpected shape (garbled file,
76
+ * unreadable entry, gitdir path without the worktrees marker) yields null so
77
+ * the caller leaves the cwd unchanged rather than guessing.
78
+ */
79
+ function worktreeMainRootOf(cwd) {
80
+ let dir = cwd;
81
+ for (let i = 0; i < MAX_WORKTREE_WALK; i++) {
82
+ const gitPath = path.join(dir, '.git');
83
+ let stat;
84
+ try {
85
+ stat = fs.statSync(gitPath);
86
+ } catch {
87
+ stat = null;
88
+ }
89
+ if (stat) {
90
+ if (stat.isDirectory()) return dir;
91
+ if (stat.isFile()) {
92
+ let body;
93
+ try {
94
+ body = fs.readFileSync(gitPath, 'utf8');
95
+ } catch {
96
+ return null;
97
+ }
98
+ const firstLine = body.split('\n', 1)[0].trim();
99
+ const prefix = 'gitdir:';
100
+ if (!firstLine.startsWith(prefix)) return null;
101
+ const gitdir = firstLine.slice(prefix.length).trim();
102
+ const markerIdx = gitdir.indexOf(WORKTREES_MARKER);
103
+ if (markerIdx <= 0) return null;
104
+ const candidateMain = gitdir.slice(0, markerIdx);
105
+ const worktreeName = gitdir.slice(markerIdx + WORKTREES_MARKER.length).split(path.sep)[0];
106
+ // Round-trip verification: gitdir's content is a plain file that
107
+ // anything on disk could have written (e.g. a nested `.git` file
108
+ // tracked inside an untrusted repo), so it must not be trusted to
109
+ // redirect callers to an arbitrary attacker-chosen absolute path.
110
+ // A genuine linked worktree's admin dir back-references the exact
111
+ // `.git` FILE we are resolving via its own `gitdir` pointer file —
112
+ // require that round trip before accepting candidateMain.
113
+ if (!worktreeName) return null;
114
+ const adminGitdirFile = path.join(candidateMain, '.git', 'worktrees', worktreeName, 'gitdir');
115
+ let backRef;
116
+ try {
117
+ backRef = fs.readFileSync(adminGitdirFile, 'utf8').trim();
118
+ } catch {
119
+ return null;
120
+ }
121
+ if (path.resolve(backRef) !== path.resolve(gitPath)) return null;
122
+ return candidateMain;
123
+ }
124
+ return null;
125
+ }
126
+ const parent = path.dirname(dir);
127
+ if (parent === dir) return null;
128
+ dir = parent;
129
+ }
130
+ return null;
131
+ }
132
+
40
133
  /**
41
134
  * projectRootOf(cwd) → the project root for a cwd that may sit inside an ops
42
- * tree. Returns cwd unchanged when it does not. Only the FIRST occurrence
43
- * matters — a nested stray ops root is itself the bug, never a project.
135
+ * tree and/or a linked git worktree. Returns cwd unchanged when neither
136
+ * applies. Only the FIRST ops-dir occurrence matters — a nested stray ops
137
+ * root is itself the bug, never a project. The ops-dir truncation runs
138
+ * first, then the worktree resolution, so a cwd deep inside a worktree's OWN
139
+ * ops tree still lands on the real project's main root.
44
140
  */
45
141
  function projectRootOf(cwd) {
46
142
  const parts = cwd.split(path.sep);
47
143
  const i = parts.indexOf(OPS_DIRNAME);
48
- if (i <= 0) return cwd;
49
- return parts.slice(0, i).join(path.sep) || path.sep;
144
+ const truncated = i > 0 ? (parts.slice(0, i).join(path.sep) || path.sep) : cwd;
145
+ const mainRoot = worktreeMainRootOf(truncated);
146
+ return mainRoot || truncated;
50
147
  }
51
148
 
52
149
  /**
@@ -85,11 +182,13 @@ function readTailLines(filePath, maxBytes) {
85
182
  * Complexity: O(P × L) over P project dirs, each bounded-tail read (64 KB).
86
183
  *
87
184
  * opts (for testing):
88
- * projectsDir — override the default ~/.claude/projects path
185
+ * projectsDir — override the default ~/.claude/projects path
186
+ * tmpDropRoots — override the tmp guard's drop roots (default TMP_DROP_ROOTS)
89
187
  */
90
188
  function activeProjectCwds(maxAgeMin = 90, {
91
189
  projectsDir = path.join(HOME, '.claude', 'projects'),
92
190
  maxCwds = MAX_CWDS,
191
+ tmpDropRoots = TMP_DROP_ROOTS,
93
192
  } = {}) {
94
193
  // maxAgeMin === Infinity means "every project ever seen, no recency filter"
95
194
  // (allProjectCwds below). Date.now() - Infinity is -Infinity, which every
@@ -111,6 +210,17 @@ function activeProjectCwds(maxAgeMin = 90, {
111
210
  // whole per-project state off these strings — a project is a cwd, and a
112
211
  // cwd is an absolute path.
113
212
  if (!path.isAbsolute(cwd)) return;
213
+ // Drop-guard of last resort: a cwd that is (or sits inside) a known
214
+ // worktree-scratch root after projectRootOf's normalization is one
215
+ // worktreeMainRootOf could not resolve to a main tree — never a real
216
+ // project.
217
+ if (tmpDropRoots.exactDropCwds.includes(cwd)) return;
218
+ const isUnderPrefixDropRoot = tmpDropRoots.prefixDropRoots.some((root) => {
219
+ if (cwd === root) return true;
220
+ const rel = path.relative(root, cwd);
221
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
222
+ });
223
+ if (isUnderPrefixDropRoot) return;
114
224
  if (seen.has(cwd) || result.length >= maxCwds) return;
115
225
  // Must exist AND be a directory — a transcript naming a since-deleted
116
226
  // path, or a file, is not a project.
@@ -178,4 +288,4 @@ function allProjectCwds(opts = {}) {
178
288
  return activeProjectCwds(Infinity, { maxCwds: 500, ...opts });
179
289
  }
180
290
 
181
- module.exports = { activeProjectCwds, allProjectCwds, projectRootOf };
291
+ module.exports = { activeProjectCwds, allProjectCwds, projectRootOf, worktreeMainRootOf };