deplyze-code 0.1.1 → 0.1.3
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.
- package/README.en.md +161 -163
- package/README.md +161 -163
- package/package.json +3 -1
- package/preload.ts +2 -2
- package/scripts/smoke.ts +491 -0
- package/src/QueryEngine.ts +2 -3
- package/src/commands/peers/index.ts +50 -0
- package/src/components/PromptInput/Notifications.tsx +9 -4
- package/src/components/PromptInput/PromptInputFooterLeftSide.tsx +3 -4
- package/src/components/PromptInput/footerAuthStatus.ts +41 -0
- package/src/components/memory/MemoryFileSelector.tsx +9 -7
- package/src/coordinator/coordinatorMode.ts +1 -5
- package/src/coordinator/workerAgent.ts +30 -0
- package/src/hooks/useApiKeyVerification.ts +71 -39
- package/src/main.tsx +22 -25
- package/src/screens/REPL.tsx +35 -40
- package/src/screens/ResumeConversation.tsx +30 -34
- package/src/services/autoDream/autoDream.ts +21 -3
- package/src/services/autoDream/config.ts +35 -3
- package/src/tools/AgentTool/AgentTool.tsx +3 -3
- package/src/tools/AgentTool/builtInAgents.ts +6 -8
- package/src/tools/ListPeersTool/ListPeersTool.ts +79 -0
- package/src/tools.ts +4 -11
- package/src/utils/concurrentSessions.ts +182 -32
- package/src/utils/sessionRestore.ts +5 -9
- package/src/utils/systemPrompt.ts +0 -1
- package/src/utils/toolPool.ts +4 -8
- package/src/utils/udsClient.ts +40 -0
- package/src/utils/udsMessaging.ts +98 -0
- package/tests/autoDream.test.ts +140 -0
- package/tests/coordinatorMode.test.ts +80 -0
- package/tests/footerAuthStatus.test.ts +54 -0
- package/tests/udsInbox.test.ts +124 -0
package/scripts/smoke.ts
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto'
|
|
2
|
+
import { spawn } from 'child_process'
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, unlink, writeFile } from 'fs/promises'
|
|
4
|
+
import { tmpdir } from 'os'
|
|
5
|
+
import { dirname, join } from 'path'
|
|
6
|
+
import { fileURLToPath } from 'url'
|
|
7
|
+
import { getSessionId, setOriginalCwd } from '../src/bootstrap/state.js'
|
|
8
|
+
import peersCommand from '../src/commands/peers/index.ts'
|
|
9
|
+
import { getBuiltInAgents } from '../src/tools/AgentTool/builtInAgents.ts'
|
|
10
|
+
import { ListPeersTool } from '../src/tools/ListPeersTool/ListPeersTool.ts'
|
|
11
|
+
import { executeAutoDream, initAutoDream } from '../src/services/autoDream/autoDream.ts'
|
|
12
|
+
import {
|
|
13
|
+
AUTO_DREAM_SMOKE_ENV,
|
|
14
|
+
isAutoDreamSmokeEnabled,
|
|
15
|
+
isAutoDreamSupported,
|
|
16
|
+
} from '../src/services/autoDream/config.ts'
|
|
17
|
+
import { getDefaultAppState, type AppState } from '../src/state/AppStateStore.ts'
|
|
18
|
+
import { ASYNC_AGENT_ALLOWED_TOOLS } from '../src/constants/tools.ts'
|
|
19
|
+
import { enableConfigs } from '../src/utils/config.ts'
|
|
20
|
+
import { getModelOptions } from '../src/utils/model/modelOptions.ts'
|
|
21
|
+
import { getPlatform } from '../src/utils/platform.ts'
|
|
22
|
+
import { getProjectDir } from '../src/utils/sessionStorage.js'
|
|
23
|
+
import { buildEffectiveSystemPrompt } from '../src/utils/systemPrompt.ts'
|
|
24
|
+
import { listLocalPeers, sendToUdsSocket } from '../src/utils/udsClient.ts'
|
|
25
|
+
import { getUdsMessagingSocketPath, startUdsMessaging } from '../src/utils/udsMessaging.ts'
|
|
26
|
+
|
|
27
|
+
type SmokePhase = 'common' | 'phase1' | 'phase2' | 'phase3'
|
|
28
|
+
|
|
29
|
+
const GEMINI_API_KEY = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY
|
|
30
|
+
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
31
|
+
const MEMORY_SELECTOR_PATH = join(
|
|
32
|
+
ROOT,
|
|
33
|
+
'src',
|
|
34
|
+
'components',
|
|
35
|
+
'memory',
|
|
36
|
+
'MemoryFileSelector.tsx',
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
function stripAnsi(value: string): string {
|
|
40
|
+
return value.replace(/\u001B\[[0-9;]*[A-Za-z]/g, '')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizePathForComparison(value: string): string {
|
|
44
|
+
const withForwardSlashes = value.replaceAll('\\', '/').toLowerCase()
|
|
45
|
+
return withForwardSlashes.replace(/^([a-z]):/, (_match, drive: string) => {
|
|
46
|
+
return `/${drive}`
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function assert(condition: unknown, message: string): asserts condition {
|
|
51
|
+
if (!condition) {
|
|
52
|
+
throw new Error(message)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function runProcess(
|
|
57
|
+
args: string[],
|
|
58
|
+
options?: {
|
|
59
|
+
env?: NodeJS.ProcessEnv
|
|
60
|
+
stdin?: string
|
|
61
|
+
timeoutMs?: number
|
|
62
|
+
cwd?: string
|
|
63
|
+
expectExitCode?: number
|
|
64
|
+
},
|
|
65
|
+
): Promise<{ stdout: string; stderr: string; code: number | null }> {
|
|
66
|
+
const child = spawn(process.execPath, args, {
|
|
67
|
+
cwd: options?.cwd ?? ROOT,
|
|
68
|
+
env: options?.env ?? process.env,
|
|
69
|
+
stdio: 'pipe',
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
let stdout = ''
|
|
73
|
+
let stderr = ''
|
|
74
|
+
child.stdout.on('data', chunk => {
|
|
75
|
+
stdout += chunk.toString()
|
|
76
|
+
})
|
|
77
|
+
child.stderr.on('data', chunk => {
|
|
78
|
+
stderr += chunk.toString()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
if (options?.stdin) {
|
|
82
|
+
child.stdin.write(options.stdin)
|
|
83
|
+
}
|
|
84
|
+
child.stdin.end()
|
|
85
|
+
|
|
86
|
+
const result = await new Promise<{
|
|
87
|
+
stdout: string
|
|
88
|
+
stderr: string
|
|
89
|
+
code: number | null
|
|
90
|
+
}>((resolve, reject) => {
|
|
91
|
+
const timeout = setTimeout(() => {
|
|
92
|
+
child.kill()
|
|
93
|
+
reject(
|
|
94
|
+
new Error(
|
|
95
|
+
`Timed out after ${options?.timeoutMs ?? 60_000}ms: bun ${args.join(' ')}`,
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
}, options?.timeoutMs ?? 60_000)
|
|
99
|
+
|
|
100
|
+
child.on('error', error => {
|
|
101
|
+
clearTimeout(timeout)
|
|
102
|
+
reject(error)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
child.on('exit', code => {
|
|
106
|
+
clearTimeout(timeout)
|
|
107
|
+
resolve({
|
|
108
|
+
stdout: stripAnsi(stdout),
|
|
109
|
+
stderr: stripAnsi(stderr),
|
|
110
|
+
code,
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
options?.expectExitCode !== undefined &&
|
|
117
|
+
result.code !== options.expectExitCode
|
|
118
|
+
) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
[
|
|
121
|
+
`Unexpected exit code ${result.code} for bun ${args.join(' ')}`,
|
|
122
|
+
result.stdout && `stdout:\n${result.stdout}`,
|
|
123
|
+
result.stderr && `stderr:\n${result.stderr}`,
|
|
124
|
+
]
|
|
125
|
+
.filter(Boolean)
|
|
126
|
+
.join('\n\n'),
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function launchStabilityCheck(env: NodeJS.ProcessEnv): Promise<void> {
|
|
134
|
+
const child = spawn(process.execPath, ['run', './bin/deplyze'], {
|
|
135
|
+
cwd: ROOT,
|
|
136
|
+
env,
|
|
137
|
+
stdio: 'pipe',
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
let stderr = ''
|
|
141
|
+
child.stderr.on('data', chunk => {
|
|
142
|
+
stderr += chunk.toString()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const exitEarly = await new Promise<{ code: number | null; signal: NodeJS.Signals | null } | null>(
|
|
146
|
+
resolve => {
|
|
147
|
+
const timer = setTimeout(() => resolve(null), 1_500)
|
|
148
|
+
child.on('exit', (code, signal) => {
|
|
149
|
+
clearTimeout(timer)
|
|
150
|
+
resolve({ code, signal })
|
|
151
|
+
})
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if (exitEarly) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Interactive launch exited early (code=${exitEarly.code}, signal=${exitEarly.signal ?? 'none'})\n${stripAnsi(stderr)}`,
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
child.kill()
|
|
162
|
+
await new Promise(resolve => child.on('exit', resolve))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function runCommonSmoke(phase: string): Promise<void> {
|
|
166
|
+
assert(
|
|
167
|
+
GEMINI_API_KEY,
|
|
168
|
+
'GEMINI_API_KEY or GOOGLE_API_KEY must be set for live smoke tests.',
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
const env = {
|
|
172
|
+
...process.env,
|
|
173
|
+
GEMINI_API_KEY,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await launchStabilityCheck(env)
|
|
177
|
+
|
|
178
|
+
const plain = await runProcess(
|
|
179
|
+
['run', './bin/deplyze', '-p', 'Reply exactly OK.'],
|
|
180
|
+
{ env, expectExitCode: 0, timeoutMs: 120_000 },
|
|
181
|
+
)
|
|
182
|
+
assert(plain.stdout.includes('OK'), 'Plain prompt smoke did not return OK.')
|
|
183
|
+
|
|
184
|
+
const bash = await runProcess(
|
|
185
|
+
[
|
|
186
|
+
'run',
|
|
187
|
+
'./bin/deplyze',
|
|
188
|
+
'-p',
|
|
189
|
+
'Use Bash to print the current working directory and answer with only that path.',
|
|
190
|
+
'--allowedTools',
|
|
191
|
+
'Bash',
|
|
192
|
+
'--dangerously-skip-permissions',
|
|
193
|
+
],
|
|
194
|
+
{ env, expectExitCode: 0, timeoutMs: 120_000 },
|
|
195
|
+
)
|
|
196
|
+
const bashOut = normalizePathForComparison(bash.stdout.trim())
|
|
197
|
+
const rootPath = normalizePathForComparison(ROOT)
|
|
198
|
+
assert(
|
|
199
|
+
bashOut.includes(rootPath),
|
|
200
|
+
`Bash smoke did not include workspace path.\n${bash.stdout}`,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
const smokeDir = join(ROOT, '.tmp-smoke')
|
|
204
|
+
const smokeFile = join(smokeDir, `${phase}.txt`)
|
|
205
|
+
await mkdir(smokeDir, { recursive: true })
|
|
206
|
+
await unlink(smokeFile).catch(() => {})
|
|
207
|
+
const fileWrite = await runProcess(
|
|
208
|
+
[
|
|
209
|
+
'run',
|
|
210
|
+
'./bin/deplyze',
|
|
211
|
+
'-p',
|
|
212
|
+
`Use Write to create ${smokeFile} containing PHASE_OK and answer only DONE.`,
|
|
213
|
+
'--allowedTools',
|
|
214
|
+
'Write',
|
|
215
|
+
'--dangerously-skip-permissions',
|
|
216
|
+
],
|
|
217
|
+
{ env, expectExitCode: 0, timeoutMs: 120_000 },
|
|
218
|
+
)
|
|
219
|
+
assert(
|
|
220
|
+
fileWrite.stdout.includes('DONE'),
|
|
221
|
+
`File write smoke did not report DONE.\n${fileWrite.stdout}`,
|
|
222
|
+
)
|
|
223
|
+
const written = await readFile(smokeFile, 'utf8')
|
|
224
|
+
assert(written.includes('PHASE_OK'), 'File write smoke did not write PHASE_OK.')
|
|
225
|
+
await unlink(smokeFile)
|
|
226
|
+
|
|
227
|
+
enableConfigs()
|
|
228
|
+
const modelLabels = getModelOptions()
|
|
229
|
+
.map(option => option.label.toLowerCase())
|
|
230
|
+
.join('\n')
|
|
231
|
+
assert(
|
|
232
|
+
modelLabels.includes('gemini 3.1 pro') &&
|
|
233
|
+
modelLabels.includes('gemini 3 flash') &&
|
|
234
|
+
modelLabels.includes('gemini 3.1 flash-lite'),
|
|
235
|
+
`Model smoke did not surface Gemini options.\n${modelLabels}`,
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function runPhase1FeatureSmoke(): Promise<void> {
|
|
240
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'deplyze-smoke-phase1-'))
|
|
241
|
+
const previousConfigDir = process.env.CLAUDE_CONFIG_DIR
|
|
242
|
+
const previousSmokeEnv = process.env[AUTO_DREAM_SMOKE_ENV]
|
|
243
|
+
const previousRemote = process.env.CLAUDE_CODE_REMOTE
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
process.env.CLAUDE_CONFIG_DIR = join(tempDir, '.claude')
|
|
247
|
+
process.env[AUTO_DREAM_SMOKE_ENV] = '1'
|
|
248
|
+
delete process.env.CLAUDE_CODE_REMOTE
|
|
249
|
+
|
|
250
|
+
const projectDir = join(tempDir, 'project')
|
|
251
|
+
setOriginalCwd(projectDir)
|
|
252
|
+
const transcriptDir = getProjectDir(projectDir)
|
|
253
|
+
await mkdir(transcriptDir, { recursive: true })
|
|
254
|
+
await writeFile(join(transcriptDir, `${randomUUID()}.jsonl`), '')
|
|
255
|
+
|
|
256
|
+
let appState: AppState = getDefaultAppState()
|
|
257
|
+
const setAppState = (updater: (prev: AppState) => AppState) => {
|
|
258
|
+
appState = updater(appState)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
initAutoDream()
|
|
262
|
+
await executeAutoDream({
|
|
263
|
+
toolUseContext: {
|
|
264
|
+
getAppState: () => appState,
|
|
265
|
+
setAppState,
|
|
266
|
+
},
|
|
267
|
+
} as never)
|
|
268
|
+
|
|
269
|
+
const [task] = Object.values(appState.tasks)
|
|
270
|
+
assert(task, 'Auto-dream smoke did not register a task.')
|
|
271
|
+
assert(task.type === 'dream', 'Auto-dream smoke registered the wrong task type.')
|
|
272
|
+
assert(task.status === 'completed', 'Auto-dream smoke task did not complete.')
|
|
273
|
+
assert(
|
|
274
|
+
task.type === 'dream' && task.turns[0]?.text.includes('smoke trigger'),
|
|
275
|
+
'Auto-dream smoke task did not record the smoke progress turn.',
|
|
276
|
+
)
|
|
277
|
+
assert(isAutoDreamSmokeEnabled(), 'Auto-dream smoke env was not recognized.')
|
|
278
|
+
assert(isAutoDreamSupported(), 'Auto-dream should be supported in the local Gemini runtime.')
|
|
279
|
+
|
|
280
|
+
const selectorSource = await readFile(MEMORY_SELECTOR_PATH, 'utf8')
|
|
281
|
+
assert(
|
|
282
|
+
!selectorSource.includes('/dream to run'),
|
|
283
|
+
'Memory selector still advertises the broken /dream path.',
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
await writeFile(join(transcriptDir, `${getSessionId()}.jsonl`), '')
|
|
287
|
+
} finally {
|
|
288
|
+
if (previousConfigDir === undefined) {
|
|
289
|
+
delete process.env.CLAUDE_CONFIG_DIR
|
|
290
|
+
} else {
|
|
291
|
+
process.env.CLAUDE_CONFIG_DIR = previousConfigDir
|
|
292
|
+
}
|
|
293
|
+
if (previousSmokeEnv === undefined) {
|
|
294
|
+
delete process.env[AUTO_DREAM_SMOKE_ENV]
|
|
295
|
+
} else {
|
|
296
|
+
process.env[AUTO_DREAM_SMOKE_ENV] = previousSmokeEnv
|
|
297
|
+
}
|
|
298
|
+
if (previousRemote === undefined) {
|
|
299
|
+
delete process.env.CLAUDE_CODE_REMOTE
|
|
300
|
+
} else {
|
|
301
|
+
process.env.CLAUDE_CODE_REMOTE = previousRemote
|
|
302
|
+
}
|
|
303
|
+
await rm(tempDir, { recursive: true, force: true })
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function runPhase2FeatureSmoke(): Promise<void> {
|
|
308
|
+
const previousCoordinatorMode = process.env.CLAUDE_CODE_COORDINATOR_MODE
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
process.env.CLAUDE_CODE_COORDINATOR_MODE = '1'
|
|
312
|
+
|
|
313
|
+
const coordinatorAgents = getBuiltInAgents()
|
|
314
|
+
assert(
|
|
315
|
+
coordinatorAgents.length === 1,
|
|
316
|
+
`Coordinator mode should expose exactly one worker agent, got ${coordinatorAgents.length}.`,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
const [worker] = coordinatorAgents
|
|
320
|
+
assert(worker?.agentType === 'worker', 'Coordinator worker agent is missing.')
|
|
321
|
+
|
|
322
|
+
const workerPrompt = worker.getSystemPrompt({
|
|
323
|
+
toolUseContext: { options: {} } as never,
|
|
324
|
+
})
|
|
325
|
+
assert(
|
|
326
|
+
workerPrompt.includes('worker agent operating under a coordinator'),
|
|
327
|
+
'Coordinator worker prompt did not load.',
|
|
328
|
+
)
|
|
329
|
+
assert(
|
|
330
|
+
!workerPrompt.toLowerCase().includes('claude.ai'),
|
|
331
|
+
'Coordinator worker prompt should stay provider-agnostic.',
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
assert(
|
|
335
|
+
worker.tools?.length === 1 && worker.tools[0] === '*',
|
|
336
|
+
'Coordinator worker should rely on the async worker tool pool.',
|
|
337
|
+
)
|
|
338
|
+
assert(
|
|
339
|
+
ASYNC_AGENT_ALLOWED_TOOLS.has('Bash') &&
|
|
340
|
+
ASYNC_AGENT_ALLOWED_TOOLS.has('Read') &&
|
|
341
|
+
ASYNC_AGENT_ALLOWED_TOOLS.has('Edit') &&
|
|
342
|
+
ASYNC_AGENT_ALLOWED_TOOLS.has('Write') &&
|
|
343
|
+
!ASYNC_AGENT_ALLOWED_TOOLS.has('Agent') &&
|
|
344
|
+
!ASYNC_AGENT_ALLOWED_TOOLS.has('SendMessage'),
|
|
345
|
+
'Coordinator worker async tool pool no longer matches the expected provider-agnostic contract.',
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
const prompt = buildEffectiveSystemPrompt({
|
|
349
|
+
mainThreadAgentDefinition: undefined,
|
|
350
|
+
toolUseContext: { options: {} } as never,
|
|
351
|
+
customSystemPrompt: undefined,
|
|
352
|
+
defaultSystemPrompt: ['default prompt'],
|
|
353
|
+
appendSystemPrompt: undefined,
|
|
354
|
+
overrideSystemPrompt: undefined,
|
|
355
|
+
})
|
|
356
|
+
const promptText = prompt.join('\n')
|
|
357
|
+
assert(
|
|
358
|
+
promptText.includes('You are a **coordinator**'),
|
|
359
|
+
'Coordinator system prompt did not activate under the env gate.',
|
|
360
|
+
)
|
|
361
|
+
assert(
|
|
362
|
+
promptText.includes('subagent_type `worker`'),
|
|
363
|
+
'Coordinator system prompt does not describe the worker handoff path.',
|
|
364
|
+
)
|
|
365
|
+
} finally {
|
|
366
|
+
if (previousCoordinatorMode === undefined) {
|
|
367
|
+
delete process.env.CLAUDE_CODE_COORDINATOR_MODE
|
|
368
|
+
} else {
|
|
369
|
+
process.env.CLAUDE_CODE_COORDINATOR_MODE = previousCoordinatorMode
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function runPhase3FeatureSmoke(): Promise<void> {
|
|
375
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'deplyze-smoke-phase3-'))
|
|
376
|
+
const previousConfigDir = process.env.CLAUDE_CONFIG_DIR
|
|
377
|
+
const platform = getPlatform()
|
|
378
|
+
const holder = spawn(process.execPath, ['-e', 'setTimeout(() => {}, 30000)'], {
|
|
379
|
+
stdio: 'ignore',
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const holderPid = holder.pid
|
|
384
|
+
assert(holderPid, 'Phase 3 smoke could not create a holder process.')
|
|
385
|
+
|
|
386
|
+
process.env.CLAUDE_CONFIG_DIR = join(tempDir, '.claude')
|
|
387
|
+
const sessionsDir = join(process.env.CLAUDE_CONFIG_DIR, 'sessions')
|
|
388
|
+
await mkdir(sessionsDir, { recursive: true })
|
|
389
|
+
const sessionId = randomUUID()
|
|
390
|
+
await writeFile(
|
|
391
|
+
join(sessionsDir, `${holderPid}.json`),
|
|
392
|
+
JSON.stringify(
|
|
393
|
+
{
|
|
394
|
+
pid: holderPid,
|
|
395
|
+
sessionId,
|
|
396
|
+
cwd: ROOT,
|
|
397
|
+
startedAt: Date.now(),
|
|
398
|
+
kind: 'interactive',
|
|
399
|
+
name: 'phase3-peer',
|
|
400
|
+
status: 'idle',
|
|
401
|
+
messagingSocketPath: '/tmp/phase3-peer.sock',
|
|
402
|
+
},
|
|
403
|
+
null,
|
|
404
|
+
2,
|
|
405
|
+
),
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
const peers = await listLocalPeers()
|
|
409
|
+
assert(peers.length === 1, 'Phase 3 smoke did not discover the registry peer.')
|
|
410
|
+
assert(peers[0]?.name === 'phase3-peer', 'Phase 3 smoke peer name mismatch.')
|
|
411
|
+
|
|
412
|
+
const toolResult = await ListPeersTool.call(
|
|
413
|
+
{},
|
|
414
|
+
{} as never,
|
|
415
|
+
(() => {
|
|
416
|
+
throw new Error('not used')
|
|
417
|
+
}) as never,
|
|
418
|
+
{} as never,
|
|
419
|
+
)
|
|
420
|
+
assert(
|
|
421
|
+
toolResult.data.peers[0]?.sessionId === sessionId,
|
|
422
|
+
'ListPeersTool did not return the registry peer.',
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
const commandModule = await peersCommand.load()
|
|
426
|
+
const commandResult = await commandModule.call('', {} as never)
|
|
427
|
+
assert(
|
|
428
|
+
commandResult.type === 'text' &&
|
|
429
|
+
commandResult.value.includes('phase3-peer') &&
|
|
430
|
+
commandResult.value.includes('transport:'),
|
|
431
|
+
'/peers did not return the expected local peer summary.',
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
await startUdsMessaging(join(tempDir, 'sockets', 'phase3.sock'))
|
|
435
|
+
if (platform === 'windows') {
|
|
436
|
+
assert(
|
|
437
|
+
getUdsMessagingSocketPath() === undefined,
|
|
438
|
+
'Native Windows should not advertise a live UDS socket in Phase 3.',
|
|
439
|
+
)
|
|
440
|
+
await sendToUdsSocket('/tmp/phase3-peer.sock', 'hello').then(
|
|
441
|
+
() => {
|
|
442
|
+
throw new Error('Native Windows transport should remain deferred.')
|
|
443
|
+
},
|
|
444
|
+
error => {
|
|
445
|
+
assert(
|
|
446
|
+
String(error).includes('native windows'),
|
|
447
|
+
`Unexpected Windows UDS error: ${String(error)}`,
|
|
448
|
+
)
|
|
449
|
+
},
|
|
450
|
+
)
|
|
451
|
+
assert(
|
|
452
|
+
peers[0]?.canMessage === false,
|
|
453
|
+
'Native Windows peers must remain non-messageable in Phase 3.',
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
} finally {
|
|
457
|
+
holder.kill()
|
|
458
|
+
if (previousConfigDir === undefined) {
|
|
459
|
+
delete process.env.CLAUDE_CONFIG_DIR
|
|
460
|
+
} else {
|
|
461
|
+
process.env.CLAUDE_CONFIG_DIR = previousConfigDir
|
|
462
|
+
}
|
|
463
|
+
await rm(tempDir, { recursive: true, force: true })
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function main(): Promise<void> {
|
|
468
|
+
const phase = (process.argv[2] ?? 'common') as SmokePhase
|
|
469
|
+
|
|
470
|
+
switch (phase) {
|
|
471
|
+
case 'common':
|
|
472
|
+
await runCommonSmoke('common')
|
|
473
|
+
break
|
|
474
|
+
case 'phase1':
|
|
475
|
+
await runCommonSmoke('phase1')
|
|
476
|
+
await runPhase1FeatureSmoke()
|
|
477
|
+
break
|
|
478
|
+
case 'phase2':
|
|
479
|
+
await runCommonSmoke('phase2')
|
|
480
|
+
await runPhase2FeatureSmoke()
|
|
481
|
+
break
|
|
482
|
+
case 'phase3':
|
|
483
|
+
await runCommonSmoke('phase3')
|
|
484
|
+
await runPhase3FeatureSmoke()
|
|
485
|
+
break
|
|
486
|
+
default:
|
|
487
|
+
throw new Error(`Unsupported smoke phase: ${phase}`)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
await main()
|
package/src/QueryEngine.ts
CHANGED
|
@@ -112,9 +112,8 @@ import {
|
|
|
112
112
|
const getCoordinatorUserContext: (
|
|
113
113
|
mcpClients: ReadonlyArray<{ name: string }>,
|
|
114
114
|
scratchpadDir?: string,
|
|
115
|
-
) => { [k: string]: string } =
|
|
116
|
-
|
|
117
|
-
: () => ({})
|
|
115
|
+
) => { [k: string]: string } =
|
|
116
|
+
require('./coordinator/coordinatorMode.js').getCoordinatorUserContext
|
|
118
117
|
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
119
118
|
|
|
120
119
|
// Dead code elimination: conditional import for snip compaction
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Command, LocalCommandCall } from '../../types/command.js'
|
|
2
|
+
import { listLocalPeers } from '../../utils/concurrentSessions.js'
|
|
3
|
+
import { getPlatform } from '../../utils/platform.js'
|
|
4
|
+
|
|
5
|
+
const call: LocalCommandCall = async () => {
|
|
6
|
+
const peers = await listLocalPeers()
|
|
7
|
+
if (peers.length === 0) {
|
|
8
|
+
const platform = getPlatform()
|
|
9
|
+
const suffix =
|
|
10
|
+
platform === 'windows'
|
|
11
|
+
? ' Native Windows live UDS transport remains deferred in this phase.'
|
|
12
|
+
: ''
|
|
13
|
+
return {
|
|
14
|
+
type: 'text',
|
|
15
|
+
value: `No local peers found.${suffix}`,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const value = peers
|
|
20
|
+
.map(peer => {
|
|
21
|
+
const header = `${peer.name ?? peer.sessionId} [${peer.kind}]`
|
|
22
|
+
const details = [
|
|
23
|
+
`session: ${peer.sessionId}`,
|
|
24
|
+
`pid: ${peer.pid}`,
|
|
25
|
+
`cwd: ${peer.cwd}`,
|
|
26
|
+
`transport: ${peer.transport}${peer.address ? ` (${peer.address})` : ''}`,
|
|
27
|
+
`messageable: ${peer.canMessage ? 'yes' : `no${peer.reason ? ` - ${peer.reason}` : ''}`}`,
|
|
28
|
+
]
|
|
29
|
+
if (peer.status) {
|
|
30
|
+
details.splice(3, 0, `status: ${peer.status}`)
|
|
31
|
+
}
|
|
32
|
+
return [header, ...details].join('\n')
|
|
33
|
+
})
|
|
34
|
+
.join('\n\n')
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
type: 'text',
|
|
38
|
+
value,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const peers = {
|
|
43
|
+
type: 'local',
|
|
44
|
+
name: 'peers',
|
|
45
|
+
description: 'List discoverable local peer sessions for inbox coordination',
|
|
46
|
+
supportsNonInteractive: true,
|
|
47
|
+
load: () => Promise.resolve({ call }),
|
|
48
|
+
} satisfies Command
|
|
49
|
+
|
|
50
|
+
export default peers
|