@swarmclawai/swarmclaw 0.7.7 → 0.8.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 (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -0,0 +1,513 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { after, before, describe, it } from 'node:test'
6
+
7
+ const originalEnv = {
8
+ DATA_DIR: process.env.DATA_DIR,
9
+ WORKSPACE_DIR: process.env.WORKSPACE_DIR,
10
+ SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
11
+ }
12
+
13
+ let tempDir = ''
14
+ let delegationJobs: typeof import('./delegation-jobs')
15
+
16
+ before(async () => {
17
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-delegation-adv-'))
18
+ process.env.DATA_DIR = path.join(tempDir, 'data')
19
+ process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
20
+ process.env.SWARMCLAW_BUILD_MODE = '1'
21
+ delegationJobs = await import('./delegation-jobs')
22
+ })
23
+
24
+ after(() => {
25
+ if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
26
+ else process.env.DATA_DIR = originalEnv.DATA_DIR
27
+ if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
28
+ else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
29
+ if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
30
+ else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
31
+ fs.rmSync(tempDir, { recursive: true, force: true })
32
+ })
33
+
34
+ describe('delegation-jobs-advanced', () => {
35
+ it('multi-agent delegation chain — parent→child→grandchild with ordered completions', () => {
36
+ const parent = delegationJobs.createDelegationJob({
37
+ kind: 'delegate',
38
+ task: 'Orchestrate full pipeline',
39
+ backend: 'claude',
40
+ parentSessionId: 'chain-root',
41
+ agentId: 'agent-orchestrator',
42
+ agentName: 'Orchestrator',
43
+ cwd: '/workspace',
44
+ })
45
+ const child = delegationJobs.createDelegationJob({
46
+ kind: 'subagent',
47
+ task: 'Implement feature X',
48
+ backend: 'codex',
49
+ parentSessionId: parent.id,
50
+ agentId: 'agent-developer',
51
+ agentName: 'Developer',
52
+ cwd: '/workspace/src',
53
+ })
54
+ const grandchild = delegationJobs.createDelegationJob({
55
+ kind: 'subagent',
56
+ task: 'Write tests for feature X',
57
+ parentSessionId: child.id,
58
+ agentId: 'agent-tester',
59
+ agentName: 'Tester',
60
+ cwd: '/workspace/tests',
61
+ })
62
+
63
+ // Verify initial state
64
+ assert.equal(parent.status, 'queued')
65
+ assert.equal(child.status, 'queued')
66
+ assert.equal(grandchild.status, 'queued')
67
+ assert.ok(parent.createdAt > 0)
68
+ assert.ok(child.createdAt >= parent.createdAt)
69
+ assert.ok(grandchild.createdAt >= child.createdAt)
70
+
71
+ // Start all three
72
+ const parentStarted = delegationJobs.startDelegationJob(parent.id)
73
+ const childStarted = delegationJobs.startDelegationJob(child.id)
74
+ const grandchildStarted = delegationJobs.startDelegationJob(grandchild.id)
75
+
76
+ assert.equal(parentStarted?.status, 'running')
77
+ assert.equal(childStarted?.status, 'running')
78
+ assert.equal(grandchildStarted?.status, 'running')
79
+ assert.ok(parentStarted?.startedAt)
80
+ assert.ok(childStarted?.startedAt)
81
+ assert.ok(grandchildStarted?.startedAt)
82
+
83
+ // Complete grandchild first, then child, then parent
84
+ const grandchildDone = delegationJobs.completeDelegationJob(grandchild.id, 'Tests passing: 42/42')
85
+ assert.equal(grandchildDone?.status, 'completed')
86
+ assert.equal(grandchildDone?.result, 'Tests passing: 42/42')
87
+ assert.ok(grandchildDone?.completedAt)
88
+
89
+ const childDone = delegationJobs.completeDelegationJob(child.id, 'Feature X implemented')
90
+ assert.equal(childDone?.status, 'completed')
91
+ assert.ok(childDone?.completedAt)
92
+ assert.ok(childDone.completedAt! >= grandchildDone!.completedAt!)
93
+
94
+ const parentDone = delegationJobs.completeDelegationJob(parent.id, 'Pipeline complete')
95
+ assert.equal(parentDone?.status, 'completed')
96
+ assert.ok(parentDone?.completedAt)
97
+ assert.ok(parentDone.completedAt! >= childDone!.completedAt!)
98
+
99
+ // Verify all three are retrievable
100
+ assert.equal(delegationJobs.getDelegationJob(parent.id)?.status, 'completed')
101
+ assert.equal(delegationJobs.getDelegationJob(child.id)?.status, 'completed')
102
+ assert.equal(delegationJobs.getDelegationJob(grandchild.id)?.status, 'completed')
103
+
104
+ // Verify timestamps are monotonically increasing
105
+ assert.ok(parentDone!.updatedAt >= childDone!.updatedAt)
106
+ assert.ok(childDone!.updatedAt >= grandchildDone!.updatedAt)
107
+ })
108
+
109
+ it('concurrent delegation fan-out — 5 subagent jobs with mixed outcomes', () => {
110
+ const parentId = 'fanout-parent'
111
+ const jobs = Array.from({ length: 5 }, (_, i) =>
112
+ delegationJobs.createDelegationJob({
113
+ kind: 'subagent',
114
+ task: `Subtask ${i}`,
115
+ parentSessionId: parentId,
116
+ agentId: `agent-worker-${i}`,
117
+ agentName: `Worker ${i}`,
118
+ }),
119
+ )
120
+
121
+ // Start all
122
+ for (const job of jobs) {
123
+ delegationJobs.startDelegationJob(job.id)
124
+ }
125
+
126
+ // Complete jobs 0 and 1
127
+ delegationJobs.completeDelegationJob(jobs[0].id, 'Result 0')
128
+ delegationJobs.completeDelegationJob(jobs[1].id, 'Result 1')
129
+
130
+ // Fail job 2
131
+ delegationJobs.failDelegationJob(jobs[2].id, 'Out of memory')
132
+
133
+ // Cancel job 3
134
+ delegationJobs.cancelDelegationJob(jobs[3].id)
135
+
136
+ // Leave job 4 running
137
+
138
+ // Verify individual statuses
139
+ assert.equal(delegationJobs.getDelegationJob(jobs[0].id)?.status, 'completed')
140
+ assert.equal(delegationJobs.getDelegationJob(jobs[1].id)?.status, 'completed')
141
+ assert.equal(delegationJobs.getDelegationJob(jobs[2].id)?.status, 'failed')
142
+ assert.equal(delegationJobs.getDelegationJob(jobs[3].id)?.status, 'cancelled')
143
+ assert.equal(delegationJobs.getDelegationJob(jobs[4].id)?.status, 'running')
144
+
145
+ // Verify filter by status
146
+ const completedJobs = delegationJobs.listDelegationJobs({ parentSessionId: parentId, status: 'completed' })
147
+ assert.equal(completedJobs.length, 2)
148
+
149
+ const failedJobs = delegationJobs.listDelegationJobs({ parentSessionId: parentId, status: 'failed' })
150
+ assert.equal(failedJobs.length, 1)
151
+ assert.equal(failedJobs[0].error, 'Out of memory')
152
+
153
+ const cancelledJobs = delegationJobs.listDelegationJobs({ parentSessionId: parentId, status: 'cancelled' })
154
+ assert.equal(cancelledJobs.length, 1)
155
+
156
+ const runningJobs = delegationJobs.listDelegationJobs({ parentSessionId: parentId, status: 'running' })
157
+ assert.equal(runningJobs.length, 1)
158
+ assert.equal(runningJobs[0].id, jobs[4].id)
159
+
160
+ // Verify filter by parentSessionId only returns all 5
161
+ const allForParent = delegationJobs.listDelegationJobs({ parentSessionId: parentId })
162
+ assert.equal(allForParent.length, 5)
163
+ })
164
+
165
+ it('checkpoint accumulation caps at 24 most recent entries', () => {
166
+ const job = delegationJobs.createDelegationJob({
167
+ kind: 'delegate',
168
+ task: 'Long running checkpoint test',
169
+ parentSessionId: 'checkpoint-parent',
170
+ backend: 'claude',
171
+ })
172
+ delegationJobs.startDelegationJob(job.id)
173
+
174
+ // Append 30 checkpoints (job already has 1 from creation = 31 total before capping)
175
+ for (let i = 0; i < 30; i++) {
176
+ delegationJobs.appendDelegationCheckpoint(job.id, `Checkpoint ${i}`)
177
+ }
178
+
179
+ const final = delegationJobs.getDelegationJob(job.id)
180
+ assert.ok(final)
181
+ assert.ok(final.checkpoints)
182
+ assert.equal(final.checkpoints.length, 24)
183
+
184
+ // The most recent checkpoint should be the last one we appended
185
+ const lastCheckpoint = final.checkpoints[final.checkpoints.length - 1]
186
+ assert.equal(lastCheckpoint.note, 'Checkpoint 29')
187
+
188
+ // The first checkpoint should NOT be the original "Job queued" since it was pushed off
189
+ // With 31 total entries capped to 24, the first 7 are dropped
190
+ // Entry 0: "Job queued", entries 1-30: "Checkpoint 0" through "Checkpoint 29"
191
+ // Kept: entries 7-30, i.e. "Checkpoint 6" through "Checkpoint 29"
192
+ const firstCheckpoint = final.checkpoints[0]
193
+ assert.equal(firstCheckpoint.note, 'Checkpoint 6')
194
+ })
195
+
196
+ it('terminal status immutability — completed job resists state changes', () => {
197
+ const job = delegationJobs.createDelegationJob({
198
+ kind: 'delegate',
199
+ task: 'Immutability test',
200
+ parentSessionId: 'immutable-parent',
201
+ })
202
+ delegationJobs.startDelegationJob(job.id)
203
+ const completed = delegationJobs.completeDelegationJob(job.id, 'Final result')
204
+ assert.equal(completed?.status, 'completed')
205
+
206
+ // Try to start a completed job
207
+ const afterStart = delegationJobs.startDelegationJob(job.id)
208
+ assert.equal(afterStart?.status, 'completed')
209
+
210
+ // Try to fail a completed job
211
+ const afterFail = delegationJobs.failDelegationJob(job.id, 'Should not work')
212
+ assert.equal(afterFail?.status, 'completed')
213
+ assert.equal(afterFail?.error, null) // error should remain null from completion
214
+
215
+ // Try to cancel a completed job
216
+ const afterCancel = delegationJobs.cancelDelegationJob(job.id)
217
+ assert.equal(afterCancel?.status, 'completed')
218
+
219
+ // Try to append checkpoint with a different status
220
+ const afterCheckpoint = delegationJobs.appendDelegationCheckpoint(job.id, 'Sneaky', 'failed')
221
+ assert.equal(afterCheckpoint?.status, 'completed')
222
+
223
+ // Verify the result is still intact
224
+ const latest = delegationJobs.getDelegationJob(job.id)
225
+ assert.equal(latest?.status, 'completed')
226
+ assert.equal(latest?.result, 'Final result')
227
+ })
228
+
229
+ it('artifact accumulation with 24-cap across multiple batches', () => {
230
+ const job = delegationJobs.createDelegationJob({
231
+ kind: 'subagent',
232
+ task: 'Artifact accumulation test',
233
+ parentSessionId: 'artifact-parent',
234
+ })
235
+ delegationJobs.startDelegationJob(job.id)
236
+
237
+ // Batch 1: 10 artifacts
238
+ const batch1 = Array.from({ length: 10 }, (_, i) => ({
239
+ type: 'file' as const,
240
+ value: `/output/file-${i}.ts`,
241
+ label: `File ${i}`,
242
+ }))
243
+ delegationJobs.appendDelegationArtifacts(job.id, batch1)
244
+
245
+ const afterBatch1 = delegationJobs.getDelegationJob(job.id)
246
+ assert.ok(afterBatch1?.artifacts)
247
+ assert.equal(afterBatch1.artifacts.length, 10)
248
+
249
+ // Batch 2: 10 more artifacts (total 20, still under cap)
250
+ const batch2 = Array.from({ length: 10 }, (_, i) => ({
251
+ type: 'text' as const,
252
+ value: `Log output ${i}`,
253
+ label: `Log ${i}`,
254
+ }))
255
+ delegationJobs.appendDelegationArtifacts(job.id, batch2)
256
+
257
+ const afterBatch2 = delegationJobs.getDelegationJob(job.id)
258
+ assert.ok(afterBatch2?.artifacts)
259
+ assert.equal(afterBatch2.artifacts.length, 20)
260
+
261
+ // Batch 3: 10 more artifacts (total 30, should cap at 24)
262
+ const batch3 = Array.from({ length: 10 }, (_, i) => ({
263
+ type: 'image' as const,
264
+ value: `/screenshots/screenshot-${i}.png`,
265
+ label: `Screenshot ${i}`,
266
+ }))
267
+ delegationJobs.appendDelegationArtifacts(job.id, batch3)
268
+
269
+ const afterBatch3 = delegationJobs.getDelegationJob(job.id)
270
+ assert.ok(afterBatch3?.artifacts)
271
+ assert.equal(afterBatch3.artifacts.length, 24)
272
+
273
+ // Verify the 24 kept are the most recent (last 24 of 30)
274
+ // Dropped: first 6 from batch1 (file-0 through file-5)
275
+ // Kept: file-6..file-9 (4) + all batch2 (10) + all batch3 (10) = 24
276
+ const first = afterBatch3.artifacts[0]
277
+ assert.equal(first.type, 'file')
278
+ assert.equal(first.value, '/output/file-6.ts')
279
+
280
+ const last = afterBatch3.artifacts[23]
281
+ assert.equal(last.type, 'image')
282
+ assert.equal(last.value, '/screenshots/screenshot-9.png')
283
+ })
284
+
285
+ it('stale job recovery skips jobs with registered runtime handles', () => {
286
+ const staleSessions = ['stale-a', 'stale-b', 'stale-c']
287
+ const staleJobs = staleSessions.map((sid) => {
288
+ const job = delegationJobs.createDelegationJob({
289
+ kind: 'delegate',
290
+ task: `Stale task for ${sid}`,
291
+ parentSessionId: sid,
292
+ backend: 'claude',
293
+ })
294
+ delegationJobs.startDelegationJob(job.id)
295
+ return job
296
+ })
297
+
298
+ // Register a runtime handle only for the first job
299
+ let handleCancelCalled = false
300
+ delegationJobs.registerDelegationRuntime(staleJobs[0].id, {
301
+ cancel: () => { handleCancelCalled = true },
302
+ })
303
+
304
+ // Use maxAgeMs=-1 to make ALL jobs appear stale (threshold = now+1)
305
+ // Only jobs without runtime handles should be recovered.
306
+ // Note: other running jobs from previous tests may also be recovered,
307
+ // so we check >= 2 rather than exactly 2.
308
+ const recovered = delegationJobs.recoverStaleDelegationJobs(-1)
309
+
310
+ // At least the 2 stale jobs without handles should be failed
311
+ assert.ok(recovered >= 2, `Expected at least 2 recovered, got ${recovered}`)
312
+
313
+ assert.equal(delegationJobs.getDelegationJob(staleJobs[0].id)?.status, 'running')
314
+ assert.equal(delegationJobs.getDelegationJob(staleJobs[1].id)?.status, 'failed')
315
+ assert.equal(delegationJobs.getDelegationJob(staleJobs[2].id)?.status, 'failed')
316
+
317
+ // The handle's cancel should NOT have been called
318
+ assert.equal(handleCancelCalled, false)
319
+
320
+ // Verify error message on recovered jobs
321
+ assert.match(
322
+ delegationJobs.getDelegationJob(staleJobs[1].id)?.error ?? '',
323
+ /interrupted/i,
324
+ )
325
+ assert.match(
326
+ delegationJobs.getDelegationJob(staleJobs[2].id)?.error ?? '',
327
+ /interrupted/i,
328
+ )
329
+ })
330
+
331
+ it('parent session cancellation cascade — preserves completed jobs', () => {
332
+ const parentId = 'cascade-parent'
333
+
334
+ // Create 4 jobs under the same parent
335
+ const runningA = delegationJobs.createDelegationJob({
336
+ kind: 'delegate',
337
+ task: 'Running task A',
338
+ parentSessionId: parentId,
339
+ backend: 'codex',
340
+ })
341
+ const runningB = delegationJobs.createDelegationJob({
342
+ kind: 'subagent',
343
+ task: 'Running task B',
344
+ parentSessionId: parentId,
345
+ agentId: 'agent-b',
346
+ })
347
+ const queued = delegationJobs.createDelegationJob({
348
+ kind: 'subagent',
349
+ task: 'Queued task',
350
+ parentSessionId: parentId,
351
+ agentId: 'agent-q',
352
+ })
353
+ const alreadyCompleted = delegationJobs.createDelegationJob({
354
+ kind: 'delegate',
355
+ task: 'Already completed task',
356
+ parentSessionId: parentId,
357
+ backend: 'claude',
358
+ })
359
+
360
+ // Set up states: 2 running, 1 queued, 1 completed
361
+ delegationJobs.startDelegationJob(runningA.id)
362
+ delegationJobs.startDelegationJob(runningB.id)
363
+ // queued stays queued
364
+ delegationJobs.startDelegationJob(alreadyCompleted.id)
365
+ delegationJobs.completeDelegationJob(alreadyCompleted.id, 'Previously completed')
366
+
367
+ // Verify pre-conditions
368
+ assert.equal(delegationJobs.getDelegationJob(runningA.id)?.status, 'running')
369
+ assert.equal(delegationJobs.getDelegationJob(runningB.id)?.status, 'running')
370
+ assert.equal(delegationJobs.getDelegationJob(queued.id)?.status, 'queued')
371
+ assert.equal(delegationJobs.getDelegationJob(alreadyCompleted.id)?.status, 'completed')
372
+
373
+ // Cancel all for parent session
374
+ const cancelledCount = delegationJobs.cancelDelegationJobsForParentSession(parentId, 'User aborted')
375
+
376
+ // Should cancel the 2 running + 1 queued = 3
377
+ assert.equal(cancelledCount, 3)
378
+
379
+ assert.equal(delegationJobs.getDelegationJob(runningA.id)?.status, 'cancelled')
380
+ assert.equal(delegationJobs.getDelegationJob(runningB.id)?.status, 'cancelled')
381
+ assert.equal(delegationJobs.getDelegationJob(queued.id)?.status, 'cancelled')
382
+
383
+ // Completed job must remain completed
384
+ assert.equal(delegationJobs.getDelegationJob(alreadyCompleted.id)?.status, 'completed')
385
+ assert.equal(delegationJobs.getDelegationJob(alreadyCompleted.id)?.result, 'Previously completed')
386
+
387
+ // Verify the cancellation note appears in checkpoints
388
+ const runningAFinal = delegationJobs.getDelegationJob(runningA.id)
389
+ assert.ok(
390
+ runningAFinal?.checkpoints?.some((cp) => cp.note === 'User aborted'),
391
+ 'Expected cancellation note in checkpoints',
392
+ )
393
+ })
394
+
395
+ it('result preview truncation at 1000 characters', () => {
396
+ const job = delegationJobs.createDelegationJob({
397
+ kind: 'delegate',
398
+ task: 'Truncation test',
399
+ parentSessionId: 'truncation-parent',
400
+ })
401
+ delegationJobs.startDelegationJob(job.id)
402
+
403
+ // Create a 2000-character result
404
+ const longResult = 'A'.repeat(2000)
405
+ const completed = delegationJobs.completeDelegationJob(job.id, longResult)
406
+
407
+ assert.ok(completed)
408
+ assert.equal(completed.result?.length, 2000)
409
+ assert.equal(completed.resultPreview?.length, 1000)
410
+ assert.equal(completed.resultPreview, 'A'.repeat(1000))
411
+
412
+ // Verify via getDelegationJob too
413
+ const fetched = delegationJobs.getDelegationJob(job.id)
414
+ assert.equal(fetched?.resultPreview?.length, 1000)
415
+ assert.equal(fetched?.result?.length, 2000)
416
+ })
417
+
418
+ it('rapid status transitions — create→start→fail cannot be restarted', () => {
419
+ const job = delegationJobs.createDelegationJob({
420
+ kind: 'subagent',
421
+ task: 'Rapid transitions',
422
+ parentSessionId: 'rapid-parent',
423
+ agentId: 'agent-rapid',
424
+ })
425
+
426
+ assert.equal(job.status, 'queued')
427
+ assert.equal(job.startedAt, null)
428
+
429
+ const started = delegationJobs.startDelegationJob(job.id)
430
+ assert.equal(started?.status, 'running')
431
+ assert.ok(started?.startedAt)
432
+
433
+ const failed = delegationJobs.failDelegationJob(job.id, 'Connection lost')
434
+ assert.equal(failed?.status, 'failed')
435
+ assert.equal(failed?.error, 'Connection lost')
436
+ assert.ok(failed?.completedAt)
437
+
438
+ // Try to start again — should be immutable since 'failed' is terminal
439
+ const restartAttempt = delegationJobs.startDelegationJob(job.id)
440
+ assert.equal(restartAttempt?.status, 'failed')
441
+ assert.equal(restartAttempt?.error, 'Connection lost')
442
+
443
+ // Try to complete — should also be immutable
444
+ const completeAttempt = delegationJobs.completeDelegationJob(job.id, 'Late success')
445
+ assert.equal(completeAttempt?.status, 'failed')
446
+
447
+ // Try to cancel — should also be immutable
448
+ const cancelAttempt = delegationJobs.cancelDelegationJob(job.id)
449
+ assert.equal(cancelAttempt?.status, 'failed')
450
+
451
+ // Verify final state
452
+ const finalState = delegationJobs.getDelegationJob(job.id)
453
+ assert.equal(finalState?.status, 'failed')
454
+ assert.equal(finalState?.error, 'Connection lost')
455
+ })
456
+
457
+ it('mixed kind filtering — delegate and subagent jobs', () => {
458
+ const mixedParent = 'mixed-kind-parent'
459
+
460
+ const delegateJobs = Array.from({ length: 3 }, (_, i) =>
461
+ delegationJobs.createDelegationJob({
462
+ kind: 'delegate',
463
+ task: `Delegate task ${i}`,
464
+ parentSessionId: mixedParent,
465
+ backend: 'codex',
466
+ }),
467
+ )
468
+ const subagentJobs = Array.from({ length: 4 }, (_, i) =>
469
+ delegationJobs.createDelegationJob({
470
+ kind: 'subagent',
471
+ task: `Subagent task ${i}`,
472
+ parentSessionId: mixedParent,
473
+ agentId: `agent-mixed-${i}`,
474
+ agentName: `Mixed Agent ${i}`,
475
+ }),
476
+ )
477
+
478
+ // Verify all 7 are listed under the parent
479
+ const allJobs = delegationJobs.listDelegationJobs({ parentSessionId: mixedParent })
480
+ assert.equal(allJobs.length, 7)
481
+
482
+ // Verify kinds are correct
483
+ const delegates = allJobs.filter((j) => j.kind === 'delegate')
484
+ const subagents = allJobs.filter((j) => j.kind === 'subagent')
485
+ assert.equal(delegates.length, 3)
486
+ assert.equal(subagents.length, 4)
487
+
488
+ // Start and complete one delegate, start and fail one subagent
489
+ delegationJobs.startDelegationJob(delegateJobs[0].id)
490
+ delegationJobs.completeDelegationJob(delegateJobs[0].id, 'Delegate 0 done')
491
+
492
+ delegationJobs.startDelegationJob(subagentJobs[0].id)
493
+ delegationJobs.failDelegationJob(subagentJobs[0].id, 'Subagent 0 crashed')
494
+
495
+ // Verify status filtering works across kinds
496
+ const completedMixed = delegationJobs.listDelegationJobs({ parentSessionId: mixedParent, status: 'completed' })
497
+ assert.equal(completedMixed.length, 1)
498
+ assert.equal(completedMixed[0].kind, 'delegate')
499
+
500
+ const failedMixed = delegationJobs.listDelegationJobs({ parentSessionId: mixedParent, status: 'failed' })
501
+ assert.equal(failedMixed.length, 1)
502
+ assert.equal(failedMixed[0].kind, 'subagent')
503
+
504
+ const queuedMixed = delegationJobs.listDelegationJobs({ parentSessionId: mixedParent, status: 'queued' })
505
+ assert.equal(queuedMixed.length, 5)
506
+
507
+ // Verify delegate vs subagent fields
508
+ assert.ok(delegateJobs[0].backend)
509
+ assert.equal(delegateJobs[0].agentId, null)
510
+ assert.ok(subagentJobs[0].agentId)
511
+ assert.ok(subagentJobs[0].agentName)
512
+ })
513
+ })
@@ -0,0 +1,60 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'fs'
3
+ import os from 'os'
4
+ import path from 'path'
5
+ import { afterEach, describe, it } from 'node:test'
6
+ import { resolveDevServerLaunchDir } from './devserver-launch'
7
+
8
+ const tempDirs: string[] = []
9
+
10
+ afterEach(() => {
11
+ while (tempDirs.length) {
12
+ const dir = tempDirs.pop()
13
+ if (!dir) continue
14
+ fs.rmSync(dir, { recursive: true, force: true })
15
+ }
16
+ })
17
+
18
+ function makeTempDir(): string {
19
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-devserver-launch-'))
20
+ tempDirs.push(dir)
21
+ return dir
22
+ }
23
+
24
+ describe('resolveDevServerLaunchDir', () => {
25
+ it('resolves the repo root when launched from src/app', () => {
26
+ const repoRoot = path.resolve(process.cwd())
27
+ const nested = path.join(repoRoot, 'src', 'app')
28
+ const result = resolveDevServerLaunchDir(nested)
29
+ assert.equal(result.launchDir, repoRoot)
30
+ assert.equal(result.packageRoot, repoRoot)
31
+ assert.equal(result.framework, 'next')
32
+ })
33
+
34
+ it('returns the nearest npm package root for nested package folders', () => {
35
+ const root = makeTempDir()
36
+ const nested = path.join(root, 'src', 'feature')
37
+ fs.mkdirSync(nested, { recursive: true })
38
+ fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({
39
+ name: 'fixture',
40
+ scripts: { dev: 'vite' },
41
+ devDependencies: { vite: '^6.0.0' },
42
+ }))
43
+
44
+ const result = resolveDevServerLaunchDir(nested)
45
+ assert.equal(result.launchDir, root)
46
+ assert.equal(result.packageRoot, root)
47
+ assert.equal(result.framework, 'npm')
48
+ })
49
+
50
+ it('falls back to the input directory when no package root exists', () => {
51
+ const root = makeTempDir()
52
+ const nested = path.join(root, 'plain', 'folder')
53
+ fs.mkdirSync(nested, { recursive: true })
54
+
55
+ const result = resolveDevServerLaunchDir(nested)
56
+ assert.equal(result.launchDir, nested)
57
+ assert.equal(result.packageRoot, null)
58
+ assert.equal(result.framework, 'unknown')
59
+ })
60
+ })
@@ -0,0 +1,85 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ type FrameworkKind = 'next' | 'npm' | 'unknown'
5
+
6
+ interface PackageJsonLike {
7
+ scripts?: Record<string, unknown>
8
+ dependencies?: Record<string, unknown>
9
+ devDependencies?: Record<string, unknown>
10
+ }
11
+
12
+ export interface DevServerLaunchResolution {
13
+ inputDir: string
14
+ launchDir: string
15
+ packageRoot: string | null
16
+ framework: FrameworkKind
17
+ }
18
+
19
+ const NEXT_CONFIG_FILES = [
20
+ 'next.config.js',
21
+ 'next.config.mjs',
22
+ 'next.config.ts',
23
+ ]
24
+
25
+ function readPackageJson(dir: string): PackageJsonLike | null {
26
+ const pkgPath = path.join(dir, 'package.json')
27
+ if (!fs.existsSync(pkgPath)) return null
28
+ try {
29
+ const parsed: unknown = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
30
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
31
+ ? parsed as PackageJsonLike
32
+ : null
33
+ } catch {
34
+ return null
35
+ }
36
+ }
37
+
38
+ function hasNextDependency(pkg: PackageJsonLike): boolean {
39
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
40
+ return typeof deps.next === 'string' && deps.next.trim().length > 0
41
+ }
42
+
43
+ function hasNextScript(pkg: PackageJsonLike): boolean {
44
+ const scripts = Object.values(pkg.scripts || {})
45
+ return scripts.some((value) => typeof value === 'string' && /\bnext\b/.test(value))
46
+ }
47
+
48
+ function hasNextConfig(dir: string): boolean {
49
+ return NEXT_CONFIG_FILES.some((file) => fs.existsSync(path.join(dir, file)))
50
+ }
51
+
52
+ function classifyPackageRoot(dir: string, pkg: PackageJsonLike): FrameworkKind {
53
+ return hasNextDependency(pkg) || hasNextScript(pkg) || hasNextConfig(dir)
54
+ ? 'next'
55
+ : 'npm'
56
+ }
57
+
58
+ export function resolveDevServerLaunchDir(startDir: string): DevServerLaunchResolution {
59
+ const inputDir = path.resolve(startDir)
60
+ let current = inputDir
61
+
62
+ while (true) {
63
+ const pkg = readPackageJson(current)
64
+ if (pkg) {
65
+ const framework = classifyPackageRoot(current, pkg)
66
+ return {
67
+ inputDir,
68
+ launchDir: current,
69
+ packageRoot: current,
70
+ framework,
71
+ }
72
+ }
73
+
74
+ const parent = path.dirname(current)
75
+ if (parent === current) {
76
+ return {
77
+ inputDir,
78
+ launchDir: inputDir,
79
+ packageRoot: null,
80
+ framework: 'unknown',
81
+ }
82
+ }
83
+ current = parent
84
+ }
85
+ }