@ljwei-stak/dsh-model-router 0.19.0 → 0.20.1
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/.dsh-plugin/client.js +82 -20
- package/.dsh-plugin/index.mjs +220 -74
- package/.dsh-plugin/shared/official-team-runtime.mjs +189 -49
- package/.dsh-plugin/shared/official-tool-executor.mjs +342 -69
- package/.dsh-plugin/shared/official-tools-remote.mjs +12 -0
- package/.dsh-plugin/shared/official-tools-runtime.mjs +22 -1
- package/.dsh-plugin/shared/run-ledger.mjs +51 -2
- package/.dsh-plugin/shared/task-executors.mjs +62 -18
- package/.dsh-plugin/shared/tool-health.mjs +24 -6
- package/CHANGELOG.md +26 -0
- package/README.i18n.yaml +3 -2
- package/README.md +247 -50
- package/README.zh.md +357 -160
- package/docs/WORKBENCH_USER_GUIDE.zh.md +32 -8
- package/docs/releases/0.20.0-validation.md +26 -0
- package/package.json +5 -3
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* worktree for review. No package supplies an executable or shell fragment.
|
|
6
6
|
*/
|
|
7
7
|
import { spawn } from 'node:child_process'
|
|
8
|
-
import { appendFile, mkdir, realpath, stat } from 'node:fs/promises'
|
|
9
|
-
import { randomUUID } from 'node:crypto'
|
|
8
|
+
import { appendFile, lstat, mkdir, realpath, stat } from 'node:fs/promises'
|
|
9
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
10
|
+
import { tmpdir } from 'node:os'
|
|
10
11
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
11
12
|
import { runOfficialTool, officialToolExecutionCapabilities, officialToolReadiness } from './official-tool-executor.mjs'
|
|
12
13
|
import { toolForProvider } from './official-tool-registry.mjs'
|
|
14
|
+
import { redactDiagnostic, verifiedDiagnostic } from './task-executors.mjs'
|
|
13
15
|
|
|
14
16
|
const MAX_GIT_OUTPUT = 64_000_000
|
|
15
17
|
const GIT_TIMEOUT_MS = 120_000
|
|
@@ -26,6 +28,61 @@ async function canonicalDirectory(path) {
|
|
|
26
28
|
return canonical
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
function pathIdentity(path) {
|
|
32
|
+
return process.platform === 'win32' ? resolve(path).toLowerCase() : resolve(path)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function approvedSource(workspace, allowedRoot) {
|
|
36
|
+
const source = await canonicalDirectory(workspace)
|
|
37
|
+
const root = await canonicalDirectory(allowedRoot ?? source)
|
|
38
|
+
if (!within(root, source)) throw new Error('原工作区不在当前 Harness 会话批准的工作区范围内。')
|
|
39
|
+
return source
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sourceStorageKey(source) {
|
|
43
|
+
return createHash('sha256').update(pathIdentity(source)).digest('hex')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requireDisjointStorage(source, storage) {
|
|
47
|
+
if (within(source, storage) || within(storage, source)) {
|
|
48
|
+
throw new Error('Host 独立工作区存储必须与原工作区互不包含。')
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Reject junctions before creating deeper directories in Host-managed storage. */
|
|
53
|
+
async function storageDirectory(parent, name) {
|
|
54
|
+
const target = join(parent, name)
|
|
55
|
+
try { await mkdir(target) }
|
|
56
|
+
catch (error) { if (error?.code !== 'EEXIST') throw error }
|
|
57
|
+
const entry = await lstat(target)
|
|
58
|
+
const canonical = await canonicalDirectory(target)
|
|
59
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || pathIdentity(canonical) !== pathIdentity(target)) {
|
|
60
|
+
throw new Error('Host 独立工作区存储不能经过符号链接或目录联接。')
|
|
61
|
+
}
|
|
62
|
+
return canonical
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Windows ACL grants need WRITE_OWNER as well as ordinary file modification.
|
|
67
|
+
* Use the user's existing private temp storage rather than changing a source
|
|
68
|
+
* drive's ACL. Only the individual run receives the sandbox write capability;
|
|
69
|
+
* the SDK's separate random private temp remains disjoint from that run.
|
|
70
|
+
*/
|
|
71
|
+
async function windowsStorageParent(source, create = false) {
|
|
72
|
+
const temporary = await canonicalDirectory(tmpdir())
|
|
73
|
+
const rootPath = join(temporary, '.model-router-workspaces')
|
|
74
|
+
requireDisjointStorage(source, rootPath)
|
|
75
|
+
const root = create ? await storageDirectory(temporary, '.model-router-workspaces')
|
|
76
|
+
: await canonicalDirectory(rootPath)
|
|
77
|
+
if (pathIdentity(root) !== pathIdentity(rootPath)) throw new Error('Host 独立工作区存储路径校验失败。')
|
|
78
|
+
const key = sourceStorageKey(source)
|
|
79
|
+
const parentPath = join(root, key)
|
|
80
|
+
const parent = create ? await storageDirectory(root, key) : await canonicalDirectory(parentPath)
|
|
81
|
+
if (pathIdentity(parent) !== pathIdentity(parentPath)) throw new Error('Host 独立工作区存储路径校验失败。')
|
|
82
|
+
requireDisjointStorage(source, parent)
|
|
83
|
+
return parent
|
|
84
|
+
}
|
|
85
|
+
|
|
29
86
|
/** The session owner, never model arguments, selects the filesystem scope. */
|
|
30
87
|
export async function sessionWorkspace(ctx, exec) {
|
|
31
88
|
const session = exec?.agent?.session
|
|
@@ -97,6 +154,13 @@ async function cleanRepoRoot(workspace, signal) {
|
|
|
97
154
|
return repo
|
|
98
155
|
}
|
|
99
156
|
|
|
157
|
+
/** Preview-only validation; never creates a worktree or changes ignore rules. */
|
|
158
|
+
export async function preflightOfficialWorkspace(workspace, mode = 'read-only', signal) {
|
|
159
|
+
const source = await canonicalDirectory(workspace)
|
|
160
|
+
if (mode === 'workspace-write') await cleanRepoRoot(source, signal)
|
|
161
|
+
return source
|
|
162
|
+
}
|
|
163
|
+
|
|
100
164
|
/** Keep nested worktrees out of ordinary git status without changing tracked files. */
|
|
101
165
|
async function excludeNestedWorktrees(workspace, signal) {
|
|
102
166
|
const ignored = await git(['check-ignore', '-q', '--', '.model-router-workspaces/'], { cwd: workspace, signal })
|
|
@@ -114,25 +178,31 @@ async function excludeNestedWorktrees(workspace, signal) {
|
|
|
114
178
|
if (!confirmed.ok) throw gitError('确认独立工作区忽略规则', confirmed)
|
|
115
179
|
}
|
|
116
180
|
|
|
117
|
-
/**
|
|
181
|
+
/** Source authorization is independent from the Host's isolated output storage. */
|
|
118
182
|
async function isolatedWorktree(workspace, signal, allowedRoot) {
|
|
183
|
+
workspace = await approvedSource(workspace, allowedRoot)
|
|
119
184
|
await cleanRepoRoot(workspace, signal)
|
|
120
185
|
const base = await git(['rev-parse', 'HEAD'], { cwd: workspace, signal })
|
|
121
186
|
if (!base.ok || !/^[0-9a-f]{40,64}$/i.test(base.output.toString('utf8').trim())) {
|
|
122
187
|
throw gitError('记录原仓库基线', base)
|
|
123
188
|
}
|
|
124
189
|
const baseCommit = base.output.toString('utf8').trim()
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
190
|
+
let isolatedRoot
|
|
191
|
+
if (process.platform === 'win32') isolatedRoot = await windowsStorageParent(workspace, true)
|
|
192
|
+
else {
|
|
193
|
+
const policyRoot = await canonicalDirectory(allowedRoot ?? workspace)
|
|
194
|
+
const sibling = join(dirname(workspace), '.model-router-workspaces', basename(workspace))
|
|
195
|
+
const parent = within(policyRoot, sibling) ? sibling : join(workspace, '.model-router-workspaces')
|
|
196
|
+
if (parent === join(workspace, '.model-router-workspaces')) await excludeNestedWorktrees(workspace, signal)
|
|
197
|
+
await mkdir(parent, { recursive: true })
|
|
198
|
+
isolatedRoot = await canonicalDirectory(parent)
|
|
199
|
+
}
|
|
131
200
|
const target = join(isolatedRoot, `run-${randomUUID()}`)
|
|
132
201
|
const added = await git(['worktree', 'add', '--detach', target, 'HEAD'], { cwd: workspace, signal })
|
|
133
202
|
if (!added.ok) throw gitError('创建独立 Git 工作区', added)
|
|
134
203
|
const runPath = await canonicalDirectory(target)
|
|
135
|
-
if (
|
|
204
|
+
if (pathIdentity(dirname(runPath)) !== pathIdentity(isolatedRoot)
|
|
205
|
+
|| pathIdentity(runPath) !== pathIdentity(target)) throw new Error('独立工作区路径校验失败')
|
|
136
206
|
return { workspace: runPath, isolatedRoot, baseCommit, source: workspace }
|
|
137
207
|
}
|
|
138
208
|
|
|
@@ -198,13 +268,11 @@ function checkedTeamCliModels(cliModels, assigned) {
|
|
|
198
268
|
|
|
199
269
|
function teamCliModel(item, cliModels) {
|
|
200
270
|
// Harness directory IDs and vendor CLI aliases are independent namespaces.
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
// user supplies a verified CLI mapping through that vendor's own settings.
|
|
271
|
+
// Every vendor uses its own configured default unless a package or tool
|
|
272
|
+
// explicitly binds the model name accepted by that CLI.
|
|
204
273
|
if (cliModels[item.id]) return cliModels[item.id]
|
|
205
274
|
if (cliModels[item.toolId]) return cliModels[item.toolId]
|
|
206
|
-
return
|
|
207
|
-
? item.recommendedModel : null
|
|
275
|
+
return null
|
|
208
276
|
}
|
|
209
277
|
|
|
210
278
|
function reportedCliModel(toolId, value) {
|
|
@@ -241,8 +309,8 @@ function packagePrompt(task, item, completed) {
|
|
|
241
309
|
}
|
|
242
310
|
|
|
243
311
|
/** Preflight every assignment before any model request or workspace write. */
|
|
244
|
-
export function executableTeamPackages(plan, installedIds = [], mode = 'read-only') {
|
|
245
|
-
const capabilities = new Map(officialToolExecutionCapabilities().map(item => [item.id, item]))
|
|
312
|
+
export function executableTeamPackages(plan, installedIds = [], mode = 'read-only', executionEnvironment = 'harness') {
|
|
313
|
+
const capabilities = new Map(officialToolExecutionCapabilities({ executionEnvironment }).map(item => [item.id, item]))
|
|
246
314
|
const packages = Array.isArray(plan?.team?.workPackages) ? plan.team.workPackages : []
|
|
247
315
|
const blocking = []
|
|
248
316
|
const assigned = packages.map(item => {
|
|
@@ -266,6 +334,34 @@ async function removeWorktree(source, target) {
|
|
|
266
334
|
try { await git(['worktree', 'remove', '--force', target], { cwd: source }) } catch { /* left for manual cleanup */ }
|
|
267
335
|
}
|
|
268
336
|
|
|
337
|
+
function executionDiagnostic(value) {
|
|
338
|
+
let detail = String(value ?? '')
|
|
339
|
+
for (const key of ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN',
|
|
340
|
+
'OPENAI_API_KEY', 'CODEX_API_KEY', 'KIMI_API_KEY', 'MOONSHOT_API_KEY', 'MINIMAX_API_KEY',
|
|
341
|
+
'MIMO_API_KEY', 'XAI_API_KEY', 'ZAI_API_KEY', 'GEMINI_API_KEY', 'GOOGLE_API_KEY']) {
|
|
342
|
+
const secret = process.env[key]
|
|
343
|
+
if (typeof secret === 'string' && secret) detail = detail.split(secret).join('[redacted]')
|
|
344
|
+
}
|
|
345
|
+
return redactDiagnostic(detail, '', 2_000)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function executionException(error, signal) {
|
|
349
|
+
const detail = executionDiagnostic(error?.message ?? error)
|
|
350
|
+
const cancelled = signal?.aborted === true || error?.name === 'AbortError'
|
|
351
|
+
return { status: cancelled ? 'cancelled' : 'failed', exitCode: null, finalText: '',
|
|
352
|
+
error: `${cancelled ? '官方 CLI 执行已取消' : '官方 CLI 执行器异常'}${detail ? `:${detail}` : '。'}`,
|
|
353
|
+
detail }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** The ledger accepts this bounded, scrubbed field instead of raw CLI streams. */
|
|
357
|
+
function withFailureDiagnostic(result) {
|
|
358
|
+
if (result.status === 'succeeded') return result
|
|
359
|
+
const diagnostic = executionDiagnostic(result.diagnostic ?? verifiedDiagnostic(result))
|
|
360
|
+
return { ...result, ...(result.error ? { error: executionDiagnostic(result.error) } : {}),
|
|
361
|
+
...(result.reason ? { reason: executionDiagnostic(result.reason) } : {}),
|
|
362
|
+
...(diagnostic ? { diagnostic } : {}) }
|
|
363
|
+
}
|
|
364
|
+
|
|
269
365
|
/**
|
|
270
366
|
* Copy the changes of an earlier, never integrated worktree into a fresh one.
|
|
271
367
|
* Refuses when the original checkout moved since that run (the patch would be
|
|
@@ -279,11 +375,25 @@ async function seedWorktree(isolated, seedFrom, signal) {
|
|
|
279
375
|
let prior
|
|
280
376
|
try { prior = await canonicalDirectory(seedFrom.workspace) }
|
|
281
377
|
catch { throw new Error('原运行的独立工作区已不存在,无法套用之前的改动;请重新执行整个团队任务。') }
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
const
|
|
378
|
+
// A basename is not ownership evidence: require this repository's membership
|
|
379
|
+
// and an exact canonical parent derived from this source, including old runs.
|
|
380
|
+
const listed = await git(['worktree', 'list', '--porcelain', '-z'], { cwd: isolated.source, signal })
|
|
381
|
+
const worktrees = listed.ok ? listed.output.toString('utf8').split('\0').filter(line => line.startsWith('worktree ')).map(line => line.slice(9)) : []
|
|
285
382
|
const known = await Promise.all(worktrees.map(path => canonicalDirectory(path).catch(() => null)))
|
|
286
|
-
|
|
383
|
+
const expectedParents = [join(isolated.source, '.model-router-workspaces'),
|
|
384
|
+
join(dirname(isolated.source), '.model-router-workspaces', basename(isolated.source))]
|
|
385
|
+
if (process.platform === 'win32') {
|
|
386
|
+
try { expectedParents.push(await windowsStorageParent(isolated.source)) } catch { /* a removed/redirected Host parent is not trusted */ }
|
|
387
|
+
}
|
|
388
|
+
const parents = await Promise.all(expectedParents.map(async path => {
|
|
389
|
+
try {
|
|
390
|
+
const canonical = await canonicalDirectory(path)
|
|
391
|
+
return pathIdentity(canonical) === pathIdentity(path) ? canonical : null
|
|
392
|
+
} catch { return null }
|
|
393
|
+
}))
|
|
394
|
+
if (!known.some(path => path && pathIdentity(path) === pathIdentity(prior))
|
|
395
|
+
|| !/^run-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(basename(prior))
|
|
396
|
+
|| !parents.some(path => path && pathIdentity(path) === pathIdentity(dirname(prior)))) {
|
|
287
397
|
throw new Error('原独立工作区不是本仓库由插件创建的 Git 工作树,拒绝读取。')
|
|
288
398
|
}
|
|
289
399
|
const staged = await git(['add', '-A'], { cwd: prior, signal })
|
|
@@ -301,7 +411,9 @@ async function seedWorktree(isolated, seedFrom, signal) {
|
|
|
301
411
|
}
|
|
302
412
|
|
|
303
413
|
/** Sequential execution preserves DAG dependencies and avoids edit conflicts. */
|
|
304
|
-
export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode = 'read-only', installedIds = [], cliModels, signal, sandbox, runtime, previous = [], onlyIds = null, seedFrom = null }) {
|
|
414
|
+
export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode = 'read-only', executionEnvironment = 'harness', installedIds = [], cliModels, signal, sandbox, runtime, previous = [], onlyIds = null, seedFrom = null }) {
|
|
415
|
+
if (!['harness', 'cli-native'].includes(executionEnvironment)
|
|
416
|
+
|| (executionEnvironment === 'cli-native' && mode !== 'workspace-write')) throw new TypeError('invalid team CLI execution environment')
|
|
305
417
|
// A retry runs only `onlyIds`; earlier results feed dependency context. An
|
|
306
418
|
// editable retry needs `seedFrom` (the earlier, never integrated worktree and
|
|
307
419
|
// its base commit): a fresh worktree gets that worktree's changes first, so
|
|
@@ -310,22 +422,23 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
|
|
|
310
422
|
if (retry && mode !== 'read-only' && !(seedFrom && typeof seedFrom.workspace === 'string' && typeof seedFrom.baseCommit === 'string')) {
|
|
311
423
|
throw new TypeError('an editable team retry needs the earlier isolated worktree (seedFrom)')
|
|
312
424
|
}
|
|
313
|
-
const selected = executableTeamPackages(plan, installedIds, mode)
|
|
425
|
+
const selected = executableTeamPackages(plan, installedIds, mode, executionEnvironment)
|
|
314
426
|
const assigned = retry ? selected.assigned.filter(item => onlyIds.includes(item.id)) : selected.assigned
|
|
315
427
|
const blocking = retry ? selected.blocking.filter(item => onlyIds.includes(item.id)) : selected.blocking
|
|
316
428
|
if (assigned.length === 0) return { status: 'blocked', blocking: [{ reason: '计划没有工作包。' }], results: [] }
|
|
317
429
|
if (blocking.length > 0) return { status: 'blocked', blocking, results: [] }
|
|
318
430
|
if (mode !== 'read-only' && mode !== 'workspace-write') throw new TypeError('team mode must be read-only or workspace-write')
|
|
319
431
|
const configuredCliModels = checkedTeamCliModels(cliModels, assigned)
|
|
320
|
-
const source = await
|
|
321
|
-
if (typeof sandbox?.confine !== 'function') return { status: 'blocked', blocking: [{ reason: '官方 Harness 进程沙箱不可用。' }], results: [] }
|
|
432
|
+
const source = await approvedSource(workspace, allowedRoot)
|
|
433
|
+
if (executionEnvironment === 'harness' && typeof sandbox?.confine !== 'function') return { status: 'blocked', blocking: [{ reason: '官方 Harness 进程沙箱不可用。' }], results: [] }
|
|
322
434
|
// The Host passes no runtime override. Tests can inject inert CLI adapters to
|
|
323
435
|
// exercise assignment, stopping, and Git integration without provider accounts.
|
|
324
436
|
const readinessCheck = runtime?.readiness ?? officialToolReadiness
|
|
325
437
|
const runTool = runtime?.runTool ?? runOfficialTool
|
|
326
|
-
const readiness = await Promise.all([...new Set(assigned.map(item => item.toolId))].map(id => readinessCheck(id, source)))
|
|
327
|
-
const unready = readiness.filter(item => !item.ready)
|
|
328
|
-
if (unready.length > 0) return { status: 'blocked', blocking: unready.map(item => ({ toolId: item.id,
|
|
438
|
+
const readiness = await Promise.all([...new Set(assigned.map(item => item.toolId))].map(id => readinessCheck(id, source, { executionEnvironment })))
|
|
439
|
+
const unready = readiness.filter(item => !item.ready || !(item.readyModes ?? item.modes ?? [mode]).includes(mode))
|
|
440
|
+
if (unready.length > 0) return { status: 'blocked', blocking: unready.map(item => ({ toolId: item.id,
|
|
441
|
+
reason: item.reason ?? `官方 CLI 尚未就绪,或不支持 ${mode} 模式。` })), results: [] }
|
|
329
442
|
const isolated = mode === 'workspace-write' ? await isolatedWorktree(source, signal, allowedRoot) : null
|
|
330
443
|
const runWorkspace = isolated?.workspace ?? source
|
|
331
444
|
let seeded = null
|
|
@@ -339,18 +452,28 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
|
|
|
339
452
|
const completed = []
|
|
340
453
|
const context = retry ? (Array.isArray(previous) ? previous : []).filter(item => item?.id && typeof item.finalText === 'string') : []
|
|
341
454
|
for (const item of assigned) {
|
|
342
|
-
if (signal?.aborted) return { status: 'cancelled', workspace: runWorkspace, results: completed
|
|
455
|
+
if (signal?.aborted) return { status: 'cancelled', workspace: runWorkspace, results: completed,
|
|
456
|
+
...(isolated ? { baseCommit: isolated.baseCommit } : {}), ...(seeded ? { seeded } : {}) }
|
|
343
457
|
const cliModel = teamCliModel(item, configuredCliModels)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
458
|
+
let result
|
|
459
|
+
try {
|
|
460
|
+
result = await runTool({
|
|
461
|
+
toolId: item.toolId,
|
|
462
|
+
modelId: cliModel,
|
|
463
|
+
task: packagePrompt(task, item, [...context, ...completed]),
|
|
464
|
+
workspace: runWorkspace,
|
|
465
|
+
mode,
|
|
466
|
+
isolatedRoot: isolated?.isolatedRoot,
|
|
467
|
+
sandbox,
|
|
468
|
+
signal,
|
|
469
|
+
executionEnvironment,
|
|
470
|
+
})
|
|
471
|
+
if (!result || typeof result.status !== 'string') throw new TypeError('官方 CLI 执行器没有返回有效状态。')
|
|
472
|
+
} catch (error) {
|
|
473
|
+
// Keep earlier paid results and the isolated worktree available for the
|
|
474
|
+
// ledger and retry even when a later adapter rejects before returning.
|
|
475
|
+
result = executionException(error, signal)
|
|
476
|
+
}
|
|
354
477
|
const reportedModel = reportedCliModel(item.toolId, result.reportedModel)
|
|
355
478
|
const modelMismatch = result.status === 'succeeded' && cliModel && reportedModel
|
|
356
479
|
&& cliModel !== reportedModel
|
|
@@ -358,19 +481,26 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
|
|
|
358
481
|
completed.push({ id: item.id, name: item.name,
|
|
359
482
|
...(item.objective ? { objective: item.objective } : {}), provider: item.recommendedProvider,
|
|
360
483
|
recommendedModel: item.recommendedModel, toolId: item.toolId,
|
|
361
|
-
requestedCliModel: result.requestedModel
|
|
484
|
+
requestedCliModel: Object.hasOwn(result, 'requestedModel') ? result.requestedModel : cliModel,
|
|
362
485
|
actualModel: reportedModel,
|
|
486
|
+
...(result.reportedModel !== undefined ? { reportedModel: result.reportedModel } : {}),
|
|
487
|
+
...(result.usageIncomplete === true ? { usageIncomplete: true } : {}),
|
|
488
|
+
...(typeof result.source === 'string' ? { source: result.source } : {}),
|
|
489
|
+
...(typeof result.sandboxEnforcement === 'string' ? { sandboxEnforcement: result.sandboxEnforcement } : {}),
|
|
490
|
+
executionEnvironment,
|
|
363
491
|
modelNotice: modelMismatch
|
|
364
492
|
? `官方 CLI 回报模型 ${reportedModel},与本包指定的 ${cliModel} 不一致;后续工作包已停止。`
|
|
365
493
|
: result.modelNotice ?? (!cliModel
|
|
366
494
|
? 'Harness 推荐模型 ID 未经此厂商 CLI 验证;本包使用厂商 CLI 已配置的默认模型。'
|
|
367
495
|
: reportedModel ? null : '该 CLI 未返回可核验的实际模型 ID;请以厂商运行记录核对。'),
|
|
368
496
|
status: packageStatus, finalText: result.finalText ?? '',
|
|
497
|
+
exitCode: result.exitCode ?? null,
|
|
369
498
|
...(result.usage ? { usage: result.usage } : {}),
|
|
370
499
|
...(Number.isFinite(result.reportedCostUsd) ? { reportedCostUsd: result.reportedCostUsd } : {}),
|
|
371
|
-
error: modelMismatch ? '官方 CLI 实际模型与指定模型不一致。'
|
|
372
|
-
|
|
373
|
-
|
|
500
|
+
error: modelMismatch ? '官方 CLI 实际模型与指定模型不一致。'
|
|
501
|
+
: (result.error || result.reason) ? executionDiagnostic(result.error || result.reason) : null,
|
|
502
|
+
outputTail: result.status === 'succeeded' ? null : executionDiagnostic(verifiedDiagnostic(result)) || null })
|
|
503
|
+
if (packageStatus !== 'succeeded') return { status: packageStatus === 'cancelled' ? 'cancelled' : 'incomplete', workspace: runWorkspace, results: completed,
|
|
374
504
|
...(isolated ? { baseCommit: isolated.baseCommit } : {}), ...(seeded ? { seeded } : {}) }
|
|
375
505
|
}
|
|
376
506
|
if (!isolated) return { status: 'cli-completed', workspace: runWorkspace, results: completed,
|
|
@@ -390,14 +520,24 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
|
|
|
390
520
|
}
|
|
391
521
|
|
|
392
522
|
/** One editable CLI task also uses the same isolated workspace and merge gate. */
|
|
393
|
-
export async function runOfficialTask({ toolId, task, workspace, allowedRoot, modelId, mode = 'read-only', signal, sandbox }) {
|
|
394
|
-
const source = await
|
|
523
|
+
export async function runOfficialTask({ toolId, task, workspace, allowedRoot, modelId, mode = 'read-only', signal, sandbox, runtime }) {
|
|
524
|
+
const source = await approvedSource(workspace, allowedRoot)
|
|
395
525
|
if (typeof sandbox?.confine !== 'function') return { toolId, status: 'unsupported', reason: '官方 Harness 进程沙箱不可用。' }
|
|
396
|
-
|
|
397
|
-
|
|
526
|
+
// The Host supplies no runtime override; offline tests use inert adapters.
|
|
527
|
+
const readiness = await (runtime?.readiness ?? officialToolReadiness)(toolId, source)
|
|
528
|
+
const readyModes = readiness.readyModes ?? readiness.modes ?? []
|
|
529
|
+
if (!readiness.ready || !readyModes.includes(mode)) return { toolId, status: 'unsupported',
|
|
530
|
+
reason: readiness.reason ?? readiness.modeReasons?.[mode] ?? `官方 CLI 尚未就绪,或不支持 ${mode} 模式。` }
|
|
398
531
|
const isolated = mode === 'workspace-write' ? await isolatedWorktree(source, signal, allowedRoot) : null
|
|
399
|
-
|
|
400
|
-
|
|
532
|
+
let result
|
|
533
|
+
try {
|
|
534
|
+
result = await (runtime?.runTool ?? runOfficialTool)({ toolId, task, modelId, workspace: isolated?.workspace ?? source,
|
|
535
|
+
mode, isolatedRoot: isolated?.isolatedRoot, signal, sandbox })
|
|
536
|
+
if (!result || typeof result.status !== 'string') throw new TypeError('官方 CLI 执行器没有返回有效状态。')
|
|
537
|
+
} catch (error) {
|
|
538
|
+
result = executionException(error, signal)
|
|
539
|
+
}
|
|
540
|
+
result = withFailureDiagnostic(result)
|
|
401
541
|
if (!isolated || result.status !== 'succeeded') return { ...result, ...(isolated ? { isolatedWorkspace: isolated.workspace, baseCommit: isolated.baseCommit } : {}) }
|
|
402
542
|
try {
|
|
403
543
|
const integration = await integrateWorktree(source, isolated.workspace, isolated.baseCommit, signal)
|