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
@@ -6,15 +6,30 @@
6
6
  * Run: timeout 300 npx vitest run src/main/lib/__tests__/delegationReadiness.test.cjs
7
7
  */
8
8
 
9
- import { test, expect, afterEach } from 'vitest';
9
+ import { test, expect, afterEach, beforeEach } from 'vitest';
10
10
  const fs = require('node:fs');
11
11
  const fsp = require('node:fs/promises');
12
12
  const os = require('node:os');
13
13
  const path = require('node:path');
14
- const { checkDelegationReadiness } = require('../delegationReadiness.cjs');
14
+ const {
15
+ checkDelegationReadiness,
16
+ installPrdWriteGuard,
17
+ installDestructiveGitGuard,
18
+ probeSchedulerMcpLive,
19
+ clearLiveProbeCache,
20
+ PRD_WRITE_GUARD_SCRIPT,
21
+ DESTRUCTIVE_GIT_GUARD_SCRIPT,
22
+ } = require('../delegationReadiness.cjs');
23
+
24
+ const REQUIRED_TOOLS = ['scheduler_create_prd', 'session_manager_help'];
25
+
26
+ beforeEach(() => {
27
+ clearLiveProbeCache();
28
+ });
15
29
 
16
30
  const tmpDirs = [];
17
31
  afterEach(async () => {
32
+ clearLiveProbeCache();
18
33
  while (tmpDirs.length) {
19
34
  const d = tmpDirs.pop();
20
35
  await fsp.rm(d, { recursive: true, force: true });
@@ -32,46 +47,86 @@ async function writeJson(absPath, value) {
32
47
  await fsp.writeFile(absPath, JSON.stringify(value), 'utf8');
33
48
  }
34
49
 
50
+ /** A stub stdio MCP server: answers initialize + tools/list over stdin/stdout. */
51
+ async function writeAnsweringStub(dir, { toolNames = REQUIRED_TOOLS, countFile = null } = {}) {
52
+ const scriptPath = path.join(dir, 'stub-mcp-server.cjs');
53
+ const body = `
54
+ 'use strict';
55
+ const readline = require('node:readline');
56
+ ${countFile ? `require('node:fs').appendFileSync(${JSON.stringify(countFile)}, 'x\\n');` : ''}
57
+ const rl = readline.createInterface({ input: process.stdin });
58
+ rl.on('line', (line) => {
59
+ let msg;
60
+ try { msg = JSON.parse(line); } catch { return; }
61
+ if (msg.id === 1) {
62
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: 1, result: { protocolVersion: '2024-11-05', capabilities: {}, serverInfo: { name: 'stub', version: '1' } } }) + '\\n');
63
+ } else if (msg.id === 2) {
64
+ const tools = ${JSON.stringify(toolNames)}.map((name) => ({ name }));
65
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: 2, result: { tools } }) + '\\n');
66
+ }
67
+ });
68
+ `;
69
+ await fsp.writeFile(scriptPath, body, 'utf8');
70
+ return scriptPath;
71
+ }
72
+
73
+ /** A stub that never responds to anything — exercises the timeout path. */
74
+ async function writeSilentStub(dir) {
75
+ const scriptPath = path.join(dir, 'silent-mcp-server.cjs');
76
+ await fsp.writeFile(scriptPath, 'process.stdin.resume();\n', 'utf8');
77
+ return scriptPath;
78
+ }
79
+
35
80
  async function makeGreenFixtures() {
36
81
  const homeDir = await mkTmp('sm-delegation-home-');
37
82
  const cwd = await mkTmp('sm-delegation-cwd-');
38
83
 
84
+ const scriptPath = await writeAnsweringStub(homeDir);
39
85
  await writeJson(path.join(homeDir, '.claude.json'), {
40
- mcpServers: { 'session-manager-scheduler': { type: 'stdio', command: 'node', args: [] } },
86
+ mcpServers: { 'session-manager-scheduler': { type: 'stdio', command: 'node', args: [scriptPath] } },
41
87
  });
42
88
  await writeJson(path.join(homeDir, '.claude', 'settings.json'), {
43
89
  enabledPlugins: { 'session-manager-dev@session-manager': true },
44
90
  });
45
91
  await fsp.mkdir(path.join(homeDir, '.claude', 'agents'), { recursive: true });
46
92
  await fsp.writeFile(path.join(homeDir, '.claude', 'agents', 'dev-lead.md'), '# dev-lead', 'utf8');
93
+ // The canonical, sanctioned form: an ABSOLUTE path to session-manager's own
94
+ // guard script (installPrdWriteGuard's "reference, never vendor" decision).
47
95
  await writeJson(path.join(cwd, '.claude', 'settings.json'), {
48
96
  hooks: {
49
97
  PreToolUse: [
50
98
  {
51
99
  matcher: 'Write|Edit|NotebookEdit',
52
- hooks: [{ type: 'command', command: 'node scripts/hooks/guard-prd-writes.cjs' }],
100
+ hooks: [{ type: 'command', command: `node ${PRD_WRITE_GUARD_SCRIPT}` }],
101
+ },
102
+ {
103
+ matcher: 'Bash',
104
+ hooks: [{ type: 'command', command: `node ${DESTRUCTIVE_GIT_GUARD_SCRIPT}` }],
53
105
  },
54
106
  ],
55
107
  },
56
108
  });
57
109
 
58
- return { homeDir, cwd };
110
+ return { homeDir, cwd, scriptPath };
59
111
  }
60
112
 
61
- test('all four checks pass on a fully-configured project', async () => {
113
+ test('all seven checks pass on a fully-configured project', async () => {
62
114
  const { homeDir, cwd } = await makeGreenFixtures();
63
- const result = checkDelegationReadiness({ cwd, homeDir });
115
+ const result = await checkDelegationReadiness({ cwd, homeDir });
64
116
 
65
117
  expect(result.ok).toBe(true);
66
- expect(result.checks).toHaveLength(4);
118
+ expect(result.checks).toHaveLength(7);
67
119
  expect(result.checks.every((c) => c.ok)).toBe(true);
68
120
  expect(result.checks.map((c) => c.id)).toEqual([
69
121
  'scheduler-mcp',
122
+ 'scheduler-mcp-live',
123
+ 'scheduler-mcp-project-duplicate',
70
124
  'dev-plugin',
71
125
  'agent-personas',
72
126
  'prd-write-guard',
127
+ 'destructive-git-guard',
73
128
  ]);
74
- });
129
+ }, 15_000);
75
130
 
76
131
  test('scheduler-mcp: passes via project-scope .mcp.json even without user scope', async () => {
77
132
  const { homeDir, cwd } = await makeGreenFixtures();
@@ -80,47 +135,72 @@ test('scheduler-mcp: passes via project-scope .mcp.json even without user scope'
80
135
  mcpServers: { 'session-manager-scheduler': { command: 'node', args: [] } },
81
136
  });
82
137
 
83
- const result = checkDelegationReadiness({ cwd, homeDir });
138
+ const result = await checkDelegationReadiness({ cwd, homeDir });
84
139
  const check = result.checks.find((c) => c.id === 'scheduler-mcp');
85
140
  expect(check.ok).toBe(true);
86
- expect(result.ok).toBe(true);
87
- });
141
+ }, 15_000);
88
142
 
89
143
  test('scheduler-mcp: fails with a runnable fix when absent from both scopes', async () => {
90
144
  const { homeDir, cwd } = await makeGreenFixtures();
91
145
  await writeJson(path.join(homeDir, '.claude.json'), { mcpServers: {} });
92
146
 
93
- const result = checkDelegationReadiness({ cwd, homeDir });
147
+ const result = await checkDelegationReadiness({ cwd, homeDir });
94
148
  const check = result.checks.find((c) => c.id === 'scheduler-mcp');
95
149
  expect(check.ok).toBe(false);
96
150
  expect(result.ok).toBe(false);
97
151
  expect(check.fix).toMatch(/^claude mcp add session-manager-scheduler --scope user/);
98
152
  expect(check.fix).toContain('scheduler-mcp-server.cjs');
99
- });
153
+ }, 15_000);
154
+
155
+ test('scheduler-mcp: fails when the user-scope script path is RELATIVE', async () => {
156
+ const { homeDir, cwd } = await makeGreenFixtures();
157
+ await writeJson(path.join(homeDir, '.claude.json'), {
158
+ mcpServers: { 'session-manager-scheduler': { command: 'node', args: ['scripts/scheduler-mcp-server.cjs'] } },
159
+ });
160
+
161
+ const result = await checkDelegationReadiness({ cwd, homeDir });
162
+ const check = result.checks.find((c) => c.id === 'scheduler-mcp');
163
+ expect(check.ok).toBe(false);
164
+ expect(check.detail).toMatch(/non-absolute/);
165
+ expect(check.fix).toMatch(/^claude mcp add session-manager-scheduler --scope user/);
166
+ // Live probe has nothing runnable to trust, so it must not report ready either.
167
+ expect(result.checks.find((c) => c.id === 'scheduler-mcp-live').skipped).toBe(true);
168
+ }, 15_000);
169
+
170
+ test('scheduler-mcp: fails when the user-scope script path is absolute but MISSING', async () => {
171
+ const { homeDir, cwd } = await makeGreenFixtures();
172
+ await writeJson(path.join(homeDir, '.claude.json'), {
173
+ mcpServers: { 'session-manager-scheduler': { command: 'node', args: [path.join(homeDir, 'does-not-exist.cjs')] } },
174
+ });
175
+
176
+ const result = await checkDelegationReadiness({ cwd, homeDir });
177
+ const check = result.checks.find((c) => c.id === 'scheduler-mcp');
178
+ expect(check.ok).toBe(false);
179
+ expect(check.detail).toMatch(/no longer exists/);
180
+ }, 15_000);
100
181
 
101
182
  test('dev-plugin: fails independently when enabledPlugins is missing the key', async () => {
102
183
  const { homeDir, cwd } = await makeGreenFixtures();
103
184
  await writeJson(path.join(homeDir, '.claude', 'settings.json'), { enabledPlugins: {} });
104
185
 
105
- const result = checkDelegationReadiness({ cwd, homeDir });
186
+ const result = await checkDelegationReadiness({ cwd, homeDir });
106
187
  const check = result.checks.find((c) => c.id === 'dev-plugin');
107
188
  expect(check.ok).toBe(false);
108
189
  expect(result.ok).toBe(false);
109
190
  expect(check.fix).toBeTruthy();
110
- // the other three checks still pass independently
191
+ // the other checks still pass independently
111
192
  expect(result.checks.filter((c) => c.id !== 'dev-plugin').every((c) => c.ok)).toBe(true);
112
- });
193
+ }, 15_000);
113
194
 
114
195
  test('agent-personas: fails independently when ~/.claude/agents has no personas', async () => {
115
196
  const { homeDir, cwd } = await makeGreenFixtures();
116
197
  await fsp.rm(path.join(homeDir, '.claude', 'agents', 'dev-lead.md'));
117
198
 
118
- const result = checkDelegationReadiness({ cwd, homeDir });
199
+ const result = await checkDelegationReadiness({ cwd, homeDir });
119
200
  const check = result.checks.find((c) => c.id === 'agent-personas');
120
201
  expect(check.ok).toBe(false);
121
202
  expect(result.ok).toBe(false);
122
- expect(result.checks.filter((c) => c.id !== 'agent-personas').every((c) => c.ok)).toBe(true);
123
- });
203
+ }, 15_000);
124
204
 
125
205
  test('agent-personas: passes via project-scope .claude/agents/ even without global personas', async () => {
126
206
  const { homeDir, cwd } = await makeGreenFixtures();
@@ -128,31 +208,20 @@ test('agent-personas: passes via project-scope .claude/agents/ even without glob
128
208
  await fsp.mkdir(path.join(cwd, '.claude', 'agents'), { recursive: true });
129
209
  await fsp.writeFile(path.join(cwd, '.claude', 'agents', 'builder.md'), '# builder', 'utf8');
130
210
 
131
- const result = checkDelegationReadiness({ cwd, homeDir });
211
+ const result = await checkDelegationReadiness({ cwd, homeDir });
132
212
  const check = result.checks.find((c) => c.id === 'agent-personas');
133
213
  expect(check.ok).toBe(true);
134
- expect(result.ok).toBe(true);
135
- });
136
-
137
- test('agent-personas: fails when the directory does not exist at all', async () => {
138
- const { homeDir, cwd } = await makeGreenFixtures();
139
- await fsp.rm(path.join(homeDir, '.claude', 'agents'), { recursive: true, force: true });
140
-
141
- const result = checkDelegationReadiness({ cwd, homeDir });
142
- const check = result.checks.find((c) => c.id === 'agent-personas');
143
- expect(check.ok).toBe(false);
144
- });
214
+ }, 15_000);
145
215
 
146
216
  test('prd-write-guard: fails independently when the hook is missing', async () => {
147
217
  const { homeDir, cwd } = await makeGreenFixtures();
148
218
  await writeJson(path.join(cwd, '.claude', 'settings.json'), { hooks: { PreToolUse: [] } });
149
219
 
150
- const result = checkDelegationReadiness({ cwd, homeDir });
220
+ const result = await checkDelegationReadiness({ cwd, homeDir });
151
221
  const check = result.checks.find((c) => c.id === 'prd-write-guard');
152
222
  expect(check.ok).toBe(false);
153
223
  expect(result.ok).toBe(false);
154
- expect(result.checks.filter((c) => c.id !== 'prd-write-guard').every((c) => c.ok)).toBe(true);
155
- });
224
+ }, 15_000);
156
225
 
157
226
  test('unparseable JSON files yield ok:false with a detail, never a throw', async () => {
158
227
  const { homeDir, cwd } = await makeGreenFixtures();
@@ -160,8 +229,8 @@ test('unparseable JSON files yield ok:false with a detail, never a throw', async
160
229
  await fs.promises.writeFile(path.join(homeDir, '.claude', 'settings.json'), '{ not valid json', 'utf8');
161
230
  await fs.promises.writeFile(path.join(cwd, '.claude', 'settings.json'), '{ not valid json', 'utf8');
162
231
 
163
- expect(() => checkDelegationReadiness({ cwd, homeDir })).not.toThrow();
164
- const result = checkDelegationReadiness({ cwd, homeDir });
232
+ await expect(checkDelegationReadiness({ cwd, homeDir })).resolves.toBeTruthy();
233
+ const result = await checkDelegationReadiness({ cwd, homeDir });
165
234
 
166
235
  const scheduler = result.checks.find((c) => c.id === 'scheduler-mcp');
167
236
  const devPlugin = result.checks.find((c) => c.id === 'dev-plugin');
@@ -173,13 +242,293 @@ test('unparseable JSON files yield ok:false with a detail, never a throw', async
173
242
  expect(guard.ok).toBe(false);
174
243
  expect(guard.detail).toBeTruthy();
175
244
  expect(result.ok).toBe(false);
176
- });
245
+ }, 15_000);
246
+
247
+ // ─────────────────────────────── scheduler-mcp-project-duplicate
248
+
249
+ test('scheduler-mcp-project-duplicate: warns when both scopes register the server', async () => {
250
+ const { homeDir, cwd } = await makeGreenFixtures();
251
+ await writeJson(path.join(cwd, '.mcp.json'), {
252
+ mcpServers: { 'session-manager-scheduler': { command: 'node', args: ['scripts/scheduler-mcp-server.cjs'] } },
253
+ });
177
254
 
178
- test('missing cwd/.claude/settings.json entirely is treated as guard-absent, not a throw', async () => {
255
+ const result = await checkDelegationReadiness({ cwd, homeDir });
256
+ const check = result.checks.find((c) => c.id === 'scheduler-mcp-project-duplicate');
257
+ expect(check.ok).toBe(true);
258
+ expect(check.warn).toBe(true);
259
+ expect(check.detail).toMatch(/user scope is canonical/);
260
+ // A warning never fails the overall gate.
261
+ expect(result.ok).toBe(true);
262
+ }, 15_000);
263
+
264
+ test('scheduler-mcp-project-duplicate: no warning when only user scope is registered', async () => {
265
+ const { homeDir, cwd } = await makeGreenFixtures();
266
+
267
+ const result = await checkDelegationReadiness({ cwd, homeDir });
268
+ const check = result.checks.find((c) => c.id === 'scheduler-mcp-project-duplicate');
269
+ expect(check.ok).toBe(true);
270
+ expect(check.warn).toBe(false);
271
+ }, 15_000);
272
+
273
+ // ─────────────────────────────── scheduler-mcp-live
274
+
275
+ test('scheduler-mcp-live: ok when the server answers tools/list with both required tools', async () => {
276
+ const dir = await mkTmp('sm-live-probe-');
277
+ const scriptPath = await writeAnsweringStub(dir);
278
+
279
+ const result = await probeSchedulerMcpLive({ command: 'node', args: [scriptPath], env: {} });
280
+ expect(result.ok).toBe(true);
281
+ expect(result.detail).toContain('scheduler_create_prd');
282
+ }, 15_000);
283
+
284
+ test('scheduler-mcp-live: fails naming the missing tool when the server answers WITHOUT it', async () => {
285
+ const dir = await mkTmp('sm-live-probe-');
286
+ const scriptPath = await writeAnsweringStub(dir, { toolNames: ['scheduler_create_prd'] });
287
+
288
+ const result = await probeSchedulerMcpLive({ command: 'node', args: [scriptPath], env: {} });
289
+ expect(result.ok).toBe(false);
290
+ expect(result.detail).toContain('session_manager_help');
291
+ }, 15_000);
292
+
293
+ test('scheduler-mcp-live: times out (bounded) against a server that never answers, and kills the child', async () => {
294
+ const dir = await mkTmp('sm-live-probe-');
295
+ const scriptPath = await writeSilentStub(dir);
296
+
297
+ const start = Date.now();
298
+ const result = await probeSchedulerMcpLive({ command: 'node', args: [scriptPath], env: {} });
299
+ const elapsed = Date.now() - start;
300
+
301
+ expect(result.ok).toBe(false);
302
+ expect(result.detail).toBe('timeout');
303
+ expect(elapsed).toBeLessThan(15_000);
304
+ }, 20_000);
305
+
306
+ test('scheduler-mcp-live: is skipped, not failed, when scheduler-mcp already failed', async () => {
307
+ const { homeDir, cwd } = await makeGreenFixtures();
308
+ await writeJson(path.join(homeDir, '.claude.json'), { mcpServers: {} });
309
+
310
+ const result = await checkDelegationReadiness({ cwd, homeDir });
311
+ const live = result.checks.find((c) => c.id === 'scheduler-mcp-live');
312
+ expect(live.ok).toBe(true);
313
+ expect(live.skipped).toBe(true);
314
+ // One red row for the registration failure, not two.
315
+ expect(result.checks.filter((c) => !c.ok)).toHaveLength(1);
316
+ }, 15_000);
317
+
318
+ test('scheduler-mcp-live: memoizes the probe per registration signature within the TTL', async () => {
319
+ const dir = await mkTmp('sm-live-probe-');
320
+ const countFile = path.join(dir, 'spawn-count.txt');
321
+ const scriptPath = await writeAnsweringStub(dir, { countFile });
322
+
323
+ const { homeDir, cwd } = await makeGreenFixtures();
324
+ await writeJson(path.join(homeDir, '.claude.json'), {
325
+ mcpServers: { 'session-manager-scheduler': { type: 'stdio', command: 'node', args: [scriptPath] } },
326
+ });
327
+
328
+ await checkDelegationReadiness({ cwd, homeDir });
329
+ await checkDelegationReadiness({ cwd, homeDir });
330
+
331
+ const spawnCount = (await fsp.readFile(countFile, 'utf8')).trim().split('\n').filter(Boolean).length;
332
+ expect(spawnCount).toBe(1);
333
+ }, 15_000);
334
+
335
+ // ─────────────────────────────── installPrdWriteGuard (unchanged behavior)
336
+
337
+ test('installPrdWriteGuard: writes the canonical ABSOLUTE-path entry and turns the check green', async () => {
179
338
  const { homeDir, cwd } = await makeGreenFixtures();
180
339
  await fsp.rm(path.join(cwd, '.claude', 'settings.json'));
181
340
 
182
- expect(() => checkDelegationReadiness({ cwd, homeDir })).not.toThrow();
183
- const result = checkDelegationReadiness({ cwd, homeDir });
184
- expect(result.checks.find((c) => c.id === 'prd-write-guard').ok).toBe(false);
341
+ const r = await installPrdWriteGuard({ cwd });
342
+ expect(r.ok).toBe(true);
343
+ expect(r.action).toBe('installed');
344
+ expect(r.command).toBe(`node ${PRD_WRITE_GUARD_SCRIPT}`);
345
+
346
+ const written = JSON.parse(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8'));
347
+ expect(written.hooks.PreToolUse).toEqual([
348
+ { matcher: 'Write|Edit|NotebookEdit', hooks: [{ type: 'command', command: `node ${PRD_WRITE_GUARD_SCRIPT}` }] },
349
+ ]);
350
+ const result = await checkDelegationReadiness({ cwd, homeDir });
351
+ expect(result.checks.find((c) => c.id === 'prd-write-guard').ok).toBe(true);
352
+ }, 15_000);
353
+
354
+ test('installPrdWriteGuard: is idempotent — a healthy guard is a no-op', async () => {
355
+ const { cwd } = await makeGreenFixtures();
356
+ await installPrdWriteGuard({ cwd });
357
+ const before = fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8');
358
+
359
+ const again = await installPrdWriteGuard({ cwd });
360
+ expect(again.action).toBe('already-installed');
361
+ expect(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8')).toBe(before);
362
+ });
363
+
364
+ test('installPrdWriteGuard: refuses on unparseable settings rather than discarding them', async () => {
365
+ const { cwd } = await makeGreenFixtures();
366
+ await fsp.writeFile(path.join(cwd, '.claude', 'settings.json'), '{ not valid json', 'utf8');
367
+
368
+ const r = await installPrdWriteGuard({ cwd });
369
+ expect(r.ok).toBe(false);
370
+ expect(r.action).toBe('error');
371
+ expect(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8')).toBe('{ not valid json');
372
+ });
373
+
374
+ // The guard script is not just referenced — it is EXERCISED here, so the
375
+ // "certifies a config that has never been proven to run" gap is closed by a
376
+ // real end-to-end run of the exact command installPrdWriteGuard writes.
377
+ test('the installed command actually DENIES a scheduler PRD write and allows a normal one', async () => {
378
+ const { cwd } = await makeGreenFixtures();
379
+ const { execFileSync } = require('node:child_process');
380
+
381
+ const run = (toolInput) => JSON.parse(execFileSync('node', [PRD_WRITE_GUARD_SCRIPT], {
382
+ input: JSON.stringify({ tool_name: 'Write', tool_input: toolInput, cwd }),
383
+ encoding: 'utf8',
384
+ }));
385
+
386
+ const denied = run({ file_path: path.join(cwd, 'session-manager-operations', 'scheduler', 'prds', '1234-x.md'), content: '# x' });
387
+ expect(denied.hookSpecificOutput?.permissionDecision).toBe('deny');
388
+
389
+ const allowed = run({ file_path: path.join(cwd, 'src', 'x.ts'), content: 'x' });
390
+ expect(allowed.hookSpecificOutput?.permissionDecision).not.toBe('deny');
391
+ });
392
+
393
+ // ─────────────────────────────── destructive-git-guard / installDestructiveGitGuard
394
+
395
+ test('destructive-git-guard: fails independently when the hook is missing', async () => {
396
+ const { homeDir, cwd } = await makeGreenFixtures();
397
+ await writeJson(path.join(cwd, '.claude', 'settings.json'), {
398
+ hooks: {
399
+ PreToolUse: [
400
+ {
401
+ matcher: 'Write|Edit|NotebookEdit',
402
+ hooks: [{ type: 'command', command: `node ${PRD_WRITE_GUARD_SCRIPT}` }],
403
+ },
404
+ ],
405
+ },
406
+ });
407
+
408
+ const result = await checkDelegationReadiness({ cwd, homeDir });
409
+ const check = result.checks.find((c) => c.id === 'destructive-git-guard');
410
+ expect(check.ok).toBe(false);
411
+ expect(result.ok).toBe(false);
412
+ // the other checks still pass independently
413
+ expect(result.checks.filter((c) => c.id !== 'destructive-git-guard').every((c) => c.ok)).toBe(true);
414
+ }, 15_000);
415
+
416
+ test('destructive-git-guard: fails when the hook names a script that does not exist', async () => {
417
+ const { homeDir, cwd } = await makeGreenFixtures();
418
+ await writeJson(path.join(cwd, '.claude', 'settings.json'), {
419
+ hooks: {
420
+ PreToolUse: [
421
+ {
422
+ matcher: 'Write|Edit|NotebookEdit',
423
+ hooks: [{ type: 'command', command: `node ${PRD_WRITE_GUARD_SCRIPT}` }],
424
+ },
425
+ {
426
+ matcher: 'Bash',
427
+ hooks: [{ type: 'command', command: 'node /does/not/exist/guard-destructive-git.cjs' }],
428
+ },
429
+ ],
430
+ },
431
+ });
432
+
433
+ const result = await checkDelegationReadiness({ cwd, homeDir });
434
+ const check = result.checks.find((c) => c.id === 'destructive-git-guard');
435
+ expect(check.ok).toBe(false);
436
+ expect(check.detail).toMatch(/does not exist/);
437
+ }, 15_000);
438
+
439
+ test('installDestructiveGitGuard: writes the canonical ABSOLUTE-path entry and turns the check green', async () => {
440
+ const { homeDir, cwd } = await makeGreenFixtures();
441
+ await writeJson(path.join(cwd, '.claude', 'settings.json'), {
442
+ hooks: {
443
+ PreToolUse: [
444
+ {
445
+ matcher: 'Write|Edit|NotebookEdit',
446
+ hooks: [{ type: 'command', command: `node ${PRD_WRITE_GUARD_SCRIPT}` }],
447
+ },
448
+ ],
449
+ },
450
+ });
451
+
452
+ const r = await installDestructiveGitGuard({ cwd });
453
+ expect(r.ok).toBe(true);
454
+ expect(r.action).toBe('installed');
455
+ expect(r.command).toBe(`node ${DESTRUCTIVE_GIT_GUARD_SCRIPT}`);
456
+
457
+ const written = JSON.parse(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8'));
458
+ // The pre-existing Write|Edit|NotebookEdit matcher (and its hook) survives untouched.
459
+ expect(written.hooks.PreToolUse).toContainEqual(
460
+ { matcher: 'Write|Edit|NotebookEdit', hooks: [{ type: 'command', command: `node ${PRD_WRITE_GUARD_SCRIPT}` }] },
461
+ );
462
+ expect(written.hooks.PreToolUse).toContainEqual(
463
+ { matcher: 'Bash', hooks: [{ type: 'command', command: `node ${DESTRUCTIVE_GIT_GUARD_SCRIPT}` }] },
464
+ );
465
+
466
+ const result = await checkDelegationReadiness({ cwd, homeDir });
467
+ expect(result.checks.find((c) => c.id === 'destructive-git-guard').ok).toBe(true);
468
+ expect(result.checks.find((c) => c.id === 'prd-write-guard').ok).toBe(true);
469
+ }, 15_000);
470
+
471
+ test('installDestructiveGitGuard: is idempotent — a healthy guard is a no-op', async () => {
472
+ const { cwd } = await makeGreenFixtures();
473
+ const before = fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8');
474
+
475
+ const again = await installDestructiveGitGuard({ cwd });
476
+ expect(again.action).toBe('already-installed');
477
+ expect(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8')).toBe(before);
478
+ });
479
+
480
+ test('installDestructiveGitGuard: repairs a broken entry in place rather than duplicating it', async () => {
481
+ const { cwd } = await makeGreenFixtures();
482
+ await writeJson(path.join(cwd, '.claude', 'settings.json'), {
483
+ hooks: {
484
+ PreToolUse: [
485
+ {
486
+ matcher: 'Write|Edit|NotebookEdit',
487
+ hooks: [{ type: 'command', command: `node ${PRD_WRITE_GUARD_SCRIPT}` }],
488
+ },
489
+ {
490
+ matcher: 'Bash',
491
+ hooks: [{ type: 'command', command: 'node /does/not/exist/guard-destructive-git.cjs' }],
492
+ },
493
+ ],
494
+ },
495
+ });
496
+
497
+ const r = await installDestructiveGitGuard({ cwd });
498
+ expect(r.action).toBe('repaired');
499
+
500
+ const written = JSON.parse(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8'));
501
+ const bashMatchers = written.hooks.PreToolUse.filter((m) => m.matcher === 'Bash');
502
+ expect(bashMatchers).toHaveLength(1);
503
+ expect(bashMatchers[0].hooks).toEqual([{ type: 'command', command: `node ${DESTRUCTIVE_GIT_GUARD_SCRIPT}` }]);
504
+ });
505
+
506
+ test('installDestructiveGitGuard: refuses on unparseable settings rather than discarding them', async () => {
507
+ const { cwd } = await makeGreenFixtures();
508
+ await fsp.writeFile(path.join(cwd, '.claude', 'settings.json'), '{ not valid json', 'utf8');
509
+
510
+ const r = await installDestructiveGitGuard({ cwd });
511
+ expect(r.ok).toBe(false);
512
+ expect(r.action).toBe('error');
513
+ expect(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8')).toBe('{ not valid json');
514
+ });
515
+
516
+ // The guard script is EXERCISED here too, same rationale as the
517
+ // installPrdWriteGuard end-to-end test above.
518
+ test('the installed destructive-git command actually DENIES a shared-tree stash and allows it inside an sm-job/ worktree', async () => {
519
+ const { cwd } = await makeGreenFixtures();
520
+ const { execFileSync } = require('node:child_process');
521
+ const os2 = require('node:os');
522
+
523
+ const run = (command, runCwd) => JSON.parse(execFileSync('node', [DESTRUCTIVE_GIT_GUARD_SCRIPT], {
524
+ input: JSON.stringify({ tool_name: 'Bash', tool_input: { command }, cwd: runCwd }),
525
+ encoding: 'utf8',
526
+ }));
527
+
528
+ const denied = run('git stash', cwd);
529
+ expect(denied.hookSpecificOutput?.permissionDecision).toBe('deny');
530
+
531
+ const worktreeCwd = path.join(os2.tmpdir(), 'session-manager-job-worktrees', 'somehash', 'some-slug');
532
+ const allowed = run('git stash', worktreeCwd);
533
+ expect(allowed.hookSpecificOutput?.permissionDecision).not.toBe('deny');
185
534
  });
@@ -0,0 +1,91 @@
1
+ /**
2
+ * ephemeralCwd.test.cjs — isEphemeralCwd(cwd) must refuse a cwd that
3
+ * resolves to os.tmpdir() itself, into a managed worktree root, or onto a
4
+ * linked git worktree's own root — and must allow an ordinary project cwd.
5
+ *
6
+ * See ephemeralCwd.cjs's header for the live incident (2026-09-01) this
7
+ * predicate closes: queueStore.writeSplit materializing scheduler state
8
+ * inside an ephemeral epic worktree, torn down with the Epic and silently
9
+ * losing the write.
10
+ *
11
+ * Run: timeout 120 npx vitest run src/main/lib/__tests__/ephemeralCwd.test.cjs
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ import { test, expect, afterEach } from 'vitest';
17
+ const fs = require('node:fs');
18
+ const fsp = require('node:fs/promises');
19
+ const os = require('node:os');
20
+ const path = require('node:path');
21
+ const { isEphemeralCwd } = require('../ephemeralCwd.cjs');
22
+ const { KIND_CONFIG } = require('../gitWorktree.cjs');
23
+
24
+ const tmpDirs = [];
25
+
26
+ afterEach(async () => {
27
+ while (tmpDirs.length) {
28
+ await fsp.rm(tmpDirs.pop(), { recursive: true, force: true }).catch(() => {});
29
+ }
30
+ });
31
+
32
+ // A real project must never itself live under /tmp — only ephemeral fixtures
33
+ // (worktree checkouts) do. `test-results/` is already gitignored.
34
+ const NON_TMP_SCRATCH_ROOT = path.join(process.cwd(), 'test-results', 'ephemeral-cwd-fixtures');
35
+
36
+ async function mkNonTmpDir(prefix) {
37
+ fs.mkdirSync(NON_TMP_SCRATCH_ROOT, { recursive: true });
38
+ return fsp.mkdtemp(path.join(NON_TMP_SCRATCH_ROOT, prefix));
39
+ }
40
+
41
+ async function makeWorktreeFixture() {
42
+ const main = await mkNonTmpDir('sm-ephemeral-main-');
43
+ tmpDirs.push(main);
44
+ fs.mkdirSync(path.join(main, '.git'), { recursive: true });
45
+ const worktree = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-ephemeral-worktree-'));
46
+ tmpDirs.push(worktree);
47
+ const worktreeName = 'sm-epic-abc123';
48
+ const worktreeGitFile = path.join(worktree, '.git');
49
+ const adminDir = path.join(main, '.git', 'worktrees', worktreeName);
50
+ fs.mkdirSync(adminDir, { recursive: true });
51
+ fs.writeFileSync(path.join(adminDir, 'gitdir'), `${worktreeGitFile}\n`, 'utf8');
52
+ fs.writeFileSync(worktreeGitFile, `gitdir: ${adminDir}\n`, 'utf8');
53
+ return { main, worktree };
54
+ }
55
+
56
+ test('isEphemeralCwd refuses os.tmpdir() itself', () => {
57
+ expect(isEphemeralCwd(os.tmpdir())).toBe(true);
58
+ });
59
+
60
+ test('isEphemeralCwd refuses a cwd under the managed epic worktree root', () => {
61
+ const cwd = path.join(KIND_CONFIG.epic.root, 'some-hash', 'some-epic-slug');
62
+ expect(isEphemeralCwd(cwd)).toBe(true);
63
+ });
64
+
65
+ test('isEphemeralCwd refuses a cwd under the managed job worktree root', () => {
66
+ const cwd = path.join(KIND_CONFIG.job.root, 'some-job-id');
67
+ expect(isEphemeralCwd(cwd)).toBe(true);
68
+ });
69
+
70
+ test('isEphemeralCwd refuses a linked git worktree root anywhere on disk', async () => {
71
+ const { worktree } = await makeWorktreeFixture();
72
+ expect(isEphemeralCwd(worktree)).toBe(true);
73
+ });
74
+
75
+ test('isEphemeralCwd allows an ordinary project cwd (the worktree fixture\'s own main tree)', async () => {
76
+ const { main } = await makeWorktreeFixture();
77
+ expect(isEphemeralCwd(main)).toBe(false);
78
+ });
79
+
80
+ test('isEphemeralCwd allows an ordinary tmpdir-adjacent fixture used by other test suites (not os.tmpdir() itself, not a managed worktree root, not a linked worktree)', async () => {
81
+ const cwd = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-some-other-test-fixture-'));
82
+ tmpDirs.push(cwd);
83
+ expect(isEphemeralCwd(cwd)).toBe(false);
84
+ });
85
+
86
+ test('isEphemeralCwd is false for non-absolute or missing input, never throws', () => {
87
+ expect(isEphemeralCwd('relative/path')).toBe(false);
88
+ expect(isEphemeralCwd('')).toBe(false);
89
+ expect(isEphemeralCwd(null)).toBe(false);
90
+ expect(isEphemeralCwd(undefined)).toBe(false);
91
+ });
@@ -80,12 +80,14 @@ test('a non-git cwd resolves to null rather than throwing (Epic proceeds unisola
80
80
  expect(result).toBeNull();
81
81
  });
82
82
 
83
- test('a dirty base tree resolves to null rather than throwing', async () => {
83
+ test('a dirty base tree still creates the worktree, carrying the uncommitted edit in rather than falling back unisolated', async () => {
84
84
  const cwd = await mkRepoCwd();
85
- fs.writeFileSync(path.join(cwd, 'dirty.txt'), 'uncommitted\n', 'utf8');
85
+ fs.writeFileSync(path.join(cwd, 'README.md'), 'uncommitted tracked edit\n', 'utf8');
86
86
 
87
87
  const result = await handler(null, { cwd, epicId: 'epic-mint-3' });
88
- expect(result).toBeNull();
88
+ expect(result).not.toBeNull();
89
+ expect(fs.readFileSync(path.join(result.dir, 'README.md'), 'utf8')).toBe('uncommitted tracked edit\n');
90
+ expect(fs.readFileSync(path.join(cwd, 'README.md'), 'utf8')).toBe('uncommitted tracked edit\n');
89
91
  });
90
92
 
91
93
  test('SM_EPIC_WORKTREE_DISABLE=1 resolves to null rather than throwing', async () => {