@try-works/dsh-recursive-mode 0.3.1 → 0.4.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.
- package/README.md +959 -0
- package/lib/client.js +9 -2
- package/lib/closeout-report.d.ts +113 -0
- package/lib/closeout-standards.d.ts +35 -0
- package/lib/closeout.d.ts +12 -0
- package/lib/commands.d.ts +1 -1
- package/lib/config.d.ts +202 -0
- package/lib/delegation.d.ts +123 -3
- package/lib/enforcement.d.ts +90 -1
- package/lib/errors.d.ts +168 -0
- package/lib/git-context.d.ts +17 -0
- package/lib/guard-log.d.ts +39 -0
- package/lib/handoff.d.ts +29 -0
- package/lib/hooks.d.ts +103 -0
- package/lib/identity.d.ts +61 -0
- package/lib/index.d.ts +32 -12
- package/lib/index.js +9819 -3858
- package/lib/job-log.d.ts +34 -0
- package/lib/jobs-runner.d.ts +105 -0
- package/lib/json-safe.d.ts +33 -0
- package/lib/lock.d.ts +42 -0
- package/lib/memory-feedback.d.ts +52 -0
- package/lib/memory-select.d.ts +78 -0
- package/lib/memory.d.ts +137 -0
- package/lib/model-inventory.d.ts +106 -0
- package/lib/phase-graph.d.ts +111 -0
- package/lib/phase-rules.d.ts +67 -8
- package/lib/plan-gate.d.ts +68 -0
- package/lib/policy-globs.d.ts +222 -0
- package/lib/policy-write.d.ts +42 -0
- package/lib/policy.d.ts +39 -0
- package/lib/recursive_ask.tool.d.ts +88 -0
- package/lib/recursive_closeout.tool.d.ts +1 -1
- package/lib/recursive_delegate.tool.d.ts +22 -0
- package/lib/recursive_preview.tool.d.ts +48 -0
- package/lib/recursive_review.tool.d.ts +28 -0
- package/lib/result-cap.d.ts +70 -0
- package/lib/review-round.d.ts +82 -0
- package/lib/review.d.ts +9 -0
- package/lib/role-route.d.ts +122 -0
- package/lib/router.d.ts +90 -5
- package/lib/runtime.d.ts +252 -12
- package/lib/settlement.d.ts +132 -0
- package/lib/skills-phase.d.ts +71 -0
- package/lib/status.d.ts +53 -1
- package/lib/teams-loop.d.ts +91 -2
- package/lib/training.d.ts +211 -0
- package/lib/ts-lint.d.ts +15 -0
- package/lib/types.d.ts +48 -0
- package/lib/workflow-audit.d.ts +207 -0
- package/package.json +29 -30
- package/preset/recursive.patch.yml +312 -0
- package/scripts/e2e-run.mjs +51 -0
- package/scripts/link-dsh.mjs +233 -0
- package/scripts/live/fake-llm.mjs +150 -0
- package/scripts/live-session-plugin.mjs +179 -0
- package/scripts/live-session-stock.mjs +106 -0
- package/scripts/live-session.mjs +139 -0
- package/src/client/derive.ts +18 -2
- package/src/closeout-report.ts +274 -0
- package/src/closeout-standards.ts +102 -0
- package/src/closeout.ts +39 -2
- package/src/commands.ts +116 -4
- package/src/config.ts +113 -0
- package/src/delegation.ts +336 -18
- package/src/enforcement.ts +262 -72
- package/src/errors.ts +197 -0
- package/src/git-context.ts +33 -2
- package/src/guard-log.ts +134 -0
- package/src/handoff.ts +62 -0
- package/src/hooks.ts +316 -0
- package/src/identity.ts +230 -0
- package/src/index.ts +385 -20
- package/src/job-log.ts +112 -0
- package/src/jobs-runner.ts +222 -0
- package/src/json-safe.ts +75 -0
- package/src/lock.ts +153 -16
- package/src/memory-feedback.ts +185 -0
- package/src/memory-select.ts +187 -0
- package/src/memory.ts +309 -0
- package/src/model-inventory.ts +196 -0
- package/src/phase-graph.ts +191 -0
- package/src/phase-rules.ts +236 -0
- package/src/plan-gate.ts +111 -0
- package/src/policy-globs.ts +636 -0
- package/src/policy-write.ts +210 -0
- package/src/policy.ts +70 -5
- package/src/recursive_ask.tool.ts +276 -0
- package/src/recursive_audit_team.tool.ts +7 -3
- package/src/recursive_closeout.tool.ts +36 -35
- package/src/recursive_delegate.tool.ts +194 -0
- package/src/recursive_init.tool.ts +4 -3
- package/src/recursive_lint.tool.ts +81 -6
- package/src/recursive_lock.tool.ts +21 -4
- package/src/recursive_phase.tool.ts +3 -2
- package/src/recursive_preview.tool.ts +142 -0
- package/src/recursive_review.tool.ts +190 -0
- package/src/recursive_scratch.tool.ts +5 -4
- package/src/recursive_status.tool.ts +3 -2
- package/src/recursive_worktree.tool.ts +6 -5
- package/src/result-cap.ts +130 -0
- package/src/review-round.ts +335 -0
- package/src/review.ts +17 -3
- package/src/role-route.ts +230 -0
- package/src/router.ts +128 -2
- package/src/runtime.ts +968 -39
- package/src/settlement.ts +355 -0
- package/src/skills-phase.ts +143 -0
- package/src/snapshot.ts +39 -8
- package/src/status.ts +209 -4
- package/src/teams-loop.ts +223 -9
- package/src/training.ts +565 -0
- package/src/ts-lint.ts +38 -4
- package/src/types.ts +51 -0
- package/src/workflow-audit.ts +288 -0
- package/scripts/install-preset.cmd +0 -7
- package/scripts/install-preset.js +0 -101
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T34 — re-link the `@deepseek-ai/*` devDependencies at whatever depth this repo
|
|
3
|
+
* happens to sit.
|
|
4
|
+
*
|
|
5
|
+
* THE DEFECT THIS FIXES. `package.json` declares each harness dependency as
|
|
6
|
+
* `link:D:/deepseek-harness/packages/...` — an ABSOLUTE path — but pnpm records the
|
|
7
|
+
* target RELATIVE in the lockfile (`version: link:../../deepseek-harness/...`). So
|
|
8
|
+
* `pnpm install` only produces working links when the repo sits at exactly the depth
|
|
9
|
+
* the lockfile assumed: two levels below the drive root, beside the checkout. Clone
|
|
10
|
+
* it anywhere else — deeper, or on another drive — and every `@deepseek-ai/*` module
|
|
11
|
+
* becomes unresolvable: `tsc` reports `TS2307: Cannot find module` for all of `src/`,
|
|
12
|
+
* and the specs that import them fail to load. A public repo that cannot be cloned
|
|
13
|
+
* anywhere is a defect, not a documentation gap.
|
|
14
|
+
*
|
|
15
|
+
* THE FIX, AND WHY IT IS THIS ONE. The absolute targets are already IN
|
|
16
|
+
* `package.json`, so the links can be rebuilt from it with no network, no registry
|
|
17
|
+
* and no re-resolution — which matters because the pinned revision is a local
|
|
18
|
+
* checkout that is not published. `DSH_HARNESS_ROOT` relocates the whole set when the
|
|
19
|
+
* checkout itself moved. An absent checkout DEGRADES with a clear message and exit 0
|
|
20
|
+
* rather than failing the install: a machine without the harness can still read the
|
|
21
|
+
* repo, and the failure it will see is the honest "this module is not linked".
|
|
22
|
+
*
|
|
23
|
+
* Windows note: directory symlinks need elevation, junctions do not — so this uses
|
|
24
|
+
* `junction` on win32, which is also what pnpm itself does.
|
|
25
|
+
*/
|
|
26
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, unlinkSync } from 'node:fs'
|
|
27
|
+
import { dirname, join, relative, resolve, sep } from 'node:path'
|
|
28
|
+
|
|
29
|
+
/** The package name prefix every harness dependency shares. */
|
|
30
|
+
export const LINKED_SCOPE = '@deepseek-ai/'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Layout directories that the inferred common prefix must not swallow: dropping one
|
|
34
|
+
* of these from a link target would point it at the wrong place.
|
|
35
|
+
*/
|
|
36
|
+
const LAYOUT_SEGMENTS = new Set(['packages', 'vendor'])
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The root the recorded absolute targets share, so a moved checkout can be
|
|
40
|
+
* relocated coherently. Returns null when the specs do not share one (which would
|
|
41
|
+
* mean the repo was hand-edited into an inconsistent state).
|
|
42
|
+
*
|
|
43
|
+
* The common prefix can OVERSHOOT into a layout directory: when every dependency
|
|
44
|
+
* happens to live under one subtree (all under `packages/`, say), the prefix stops
|
|
45
|
+
* at `packages` and re-rooting would silently drop that segment, pointing every link
|
|
46
|
+
* at `<root>/<pkg>` instead of `<root>/packages/<pkg>`. This repo's real dependency
|
|
47
|
+
* set spans `packages/` and `vendor/`, so the prefix already lands on the checkout
|
|
48
|
+
* root — but a set that does not must not silently produce wrong targets, so a
|
|
49
|
+
* trailing layout segment is stripped. `DSH_HARNESS_ROOT` therefore always means the
|
|
50
|
+
* CHECKOUT ROOT, which is what a user would expect to pass.
|
|
51
|
+
* @param {{ [name: string]: string }} devDependencies
|
|
52
|
+
* @returns {string | null}
|
|
53
|
+
*/
|
|
54
|
+
export function pinnedRootOf(devDependencies) {
|
|
55
|
+
const targets = linkTargetsOf(devDependencies).map((entry) => entry.target)
|
|
56
|
+
if (targets.length === 0) return null
|
|
57
|
+
const split = targets.map((target) => target.split(/[\\/]/))
|
|
58
|
+
const first = split[0]
|
|
59
|
+
const common = []
|
|
60
|
+
for (let i = 0; i < first.length; i += 1) {
|
|
61
|
+
const candidate = first[i]
|
|
62
|
+
if (!split.every((parts) => parts[i] === candidate)) break
|
|
63
|
+
common.push(candidate)
|
|
64
|
+
}
|
|
65
|
+
if (common.length > 1 && LAYOUT_SEGMENTS.has(common[common.length - 1])) common.pop()
|
|
66
|
+
return common.join('/') || null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Every scoped dependency declared as a `link:` spec, with its absolute target.
|
|
71
|
+
* @param {{ [name: string]: string }} devDependencies
|
|
72
|
+
* @returns {Array<{ name: string; target: string }>}
|
|
73
|
+
*/
|
|
74
|
+
export function linkTargetsOf(devDependencies) {
|
|
75
|
+
const out = []
|
|
76
|
+
for (const [name, spec] of Object.entries(devDependencies ?? {})) {
|
|
77
|
+
if (!name.startsWith(LINKED_SCOPE)) continue
|
|
78
|
+
if (typeof spec !== 'string' || !spec.startsWith('link:')) continue
|
|
79
|
+
out.push({ name, target: spec.slice('link:'.length).replace(/\\/g, '/') })
|
|
80
|
+
}
|
|
81
|
+
return out.sort((a, b) => a.name.localeCompare(b.name))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Where each dependency should point, given an optional relocated checkout root.
|
|
86
|
+
* @param {{ devDependencies?: object, harnessRoot?: string | null }} input
|
|
87
|
+
* @returns {Array<{ name: string; target: string; exists: boolean }>}
|
|
88
|
+
*/
|
|
89
|
+
export function planLinks(input) {
|
|
90
|
+
const targets = linkTargetsOf(input.devDependencies)
|
|
91
|
+
const pinned = pinnedRootOf(input.devDependencies)
|
|
92
|
+
const root = input.harnessRoot ?? pinned
|
|
93
|
+
return targets.map(({ name, target }) => {
|
|
94
|
+
let resolvedTarget = target
|
|
95
|
+
if (input.harnessRoot && pinned && target.startsWith(pinned)) {
|
|
96
|
+
const rest = relative(pinned, target).split(sep).join('/')
|
|
97
|
+
resolvedTarget = join(input.harnessRoot, rest).replace(/\\/g, '/')
|
|
98
|
+
}
|
|
99
|
+
return { name, target: resolvedTarget, exists: existsSync(resolvedTarget) }
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Where a scoped package's link lives inside the repo. */
|
|
104
|
+
export function linkPathOf(repoRoot, name) {
|
|
105
|
+
return join(repoRoot, 'node_modules', ...name.split('/'))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** True when the path is already a link (or junction) to the wanted target. */
|
|
109
|
+
function alreadyLinked(path, target) {
|
|
110
|
+
try {
|
|
111
|
+
const stat = lstatSync(path)
|
|
112
|
+
if (!stat.isSymbolicLink()) return false
|
|
113
|
+
const current = readlinkSync(path).replace(/\\/g, '/')
|
|
114
|
+
return resolve(current) === resolve(target)
|
|
115
|
+
} catch {
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Remove whatever currently occupies a link path.
|
|
122
|
+
*
|
|
123
|
+
* A LINK IS NOT A DIRECTORY. `rmSync(..., { recursive: true })` does not reliably
|
|
124
|
+
* remove a junction whose target is gone — which is exactly the state a clone at the
|
|
125
|
+
* wrong depth leaves behind, because pnpm created links to a relative path that does
|
|
126
|
+
* not exist. The removal then silently failed and every `symlinkSync` after it threw
|
|
127
|
+
* `EEXIST`. Links are therefore unlinked, not recursed into (which is also the SAFE
|
|
128
|
+
* choice: recursing through a live junction would delete the checkout's contents).
|
|
129
|
+
*/
|
|
130
|
+
function removeExisting(path) {
|
|
131
|
+
try {
|
|
132
|
+
const stat = lstatSync(path)
|
|
133
|
+
if (stat.isSymbolicLink()) {
|
|
134
|
+
unlinkSync(path)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
rmSync(path, { recursive: true, force: true })
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (err && err.code === 'ENOENT') return
|
|
140
|
+
throw err
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Create or repair every harness link under `<repoRoot>/node_modules`.
|
|
146
|
+
*
|
|
147
|
+
* Idempotent, and never throws for a missing checkout: the caller gets a report so
|
|
148
|
+
* it can print an honest message instead of a stack trace. Failures are RETURNED in
|
|
149
|
+
* `errors` as well as logged, because a logger that defaults to silence once hid
|
|
150
|
+
* fifteen of them.
|
|
151
|
+
* @param {{ repoRoot: string; devDependencies?: object; harnessRoot?: string | null; log?: (line: string) => void }} input
|
|
152
|
+
* @returns {{ linked: string[]; repaired: string[]; missing: string[]; errors: Array<{ name: string; message: string }> }}
|
|
153
|
+
*/
|
|
154
|
+
export function linkDsh(input) {
|
|
155
|
+
const log = input.log ?? (() => {})
|
|
156
|
+
const plan = planLinks({ devDependencies: input.devDependencies, harnessRoot: input.harnessRoot })
|
|
157
|
+
const linked = []
|
|
158
|
+
const repaired = []
|
|
159
|
+
const missing = []
|
|
160
|
+
const errors = []
|
|
161
|
+
for (const entry of plan) {
|
|
162
|
+
if (!entry.exists) {
|
|
163
|
+
missing.push(entry.name)
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
const path = linkPathOf(input.repoRoot, entry.name)
|
|
167
|
+
if (alreadyLinked(path, entry.target)) {
|
|
168
|
+
repaired.push(entry.name)
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
173
|
+
removeExisting(path)
|
|
174
|
+
symlinkSync(entry.target, path, process.platform === 'win32' ? 'junction' : 'dir')
|
|
175
|
+
linked.push(entry.name)
|
|
176
|
+
} catch (err) {
|
|
177
|
+
// One un-linkable package must not abort the rest: the remaining links are
|
|
178
|
+
// still worth having, and the caller reports what failed.
|
|
179
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
180
|
+
errors.push({ name: entry.name, message })
|
|
181
|
+
missing.push(entry.name)
|
|
182
|
+
log(' ! ' + entry.name + ': ' + message)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { linked, repaired, missing, errors }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Read this repo's devDependencies (the absolute targets live there, not in the lockfile). */
|
|
189
|
+
export function readDevDependencies(repoRoot) {
|
|
190
|
+
try {
|
|
191
|
+
const pkg = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8'))
|
|
192
|
+
return pkg.devDependencies ?? {}
|
|
193
|
+
} catch {
|
|
194
|
+
return {}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function main() {
|
|
199
|
+
const repoRoot = resolve(process.argv[2] ?? process.cwd())
|
|
200
|
+
const harnessRoot = process.env.DSH_HARNESS_ROOT ? resolve(process.env.DSH_HARNESS_ROOT) : null
|
|
201
|
+
const devDependencies = readDevDependencies(repoRoot)
|
|
202
|
+
const planned = planLinks({ devDependencies, harnessRoot })
|
|
203
|
+
|
|
204
|
+
if (planned.length === 0) {
|
|
205
|
+
console.log('[link-dsh] no ' + LINKED_SCOPE + '* link: dependencies to place')
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
const absentRoot = planned.every((entry) => !entry.exists)
|
|
209
|
+
if (absentRoot) {
|
|
210
|
+
console.log('[link-dsh] the DSH checkout was not found at ' + (harnessRoot ?? pinnedRootOf(devDependencies)))
|
|
211
|
+
console.log('[link-dsh] set DSH_HARNESS_ROOT to its location to link ' + planned.length + ' packages, e.g.')
|
|
212
|
+
console.log('[link-dsh] DSH_HARNESS_ROOT=/path/to/deepseek-harness pnpm run link:dsh')
|
|
213
|
+
console.log('[link-dsh] continuing WITHOUT links: typecheck and tests will report unresolved modules')
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const report = linkDsh({ repoRoot, devDependencies, harnessRoot, log: (line) => console.log(line) })
|
|
218
|
+
const placed = report.linked.length
|
|
219
|
+
const kept = report.repaired.length
|
|
220
|
+
console.log('[link-dsh] linked ' + placed + ', already correct ' + kept + ', unresolved ' + report.missing.length)
|
|
221
|
+
if (report.errors.length > 0) {
|
|
222
|
+
// Failures are printed unconditionally: a logger defaulting to silence once hid
|
|
223
|
+
// fifteen of them behind a cheerful "unresolved 15".
|
|
224
|
+
console.log('[link-dsh] FAILED to link ' + report.errors.length + ' package(s); first error: ' + report.errors[0].message)
|
|
225
|
+
}
|
|
226
|
+
if (report.missing.length > 0) {
|
|
227
|
+
console.log('[link-dsh] unresolved: ' + report.missing.join(', '))
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (process.argv[1] && import.meta.url === new URL('file://' + resolve(process.argv[1]).replace(/\\/g, '/')).href) {
|
|
232
|
+
main()
|
|
233
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FU-10 — a SCRIPTED LLM for the live-session harness.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. A real session needs a model, and a real model would make the verification
|
|
5
|
+
* non-deterministic and cost money. The harness's own e2e does exactly this: it registers a keyless
|
|
6
|
+
* `LlmAdapter` on the default provider route and scripts the tool calls it wants to see. The shape below
|
|
7
|
+
* is copied from `apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs` — a class extending
|
|
8
|
+
* `LlmAdapter` whose `stream()` yields chunk objects, registered through `ctx.llm.registerAdapter`.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IT SCRIPTS: the smallest sequence that reaches the delegated review path in a REAL session —
|
|
11
|
+
* `recursive_init`, then `recursive_review`, then a plain-text answer. Everything else about the session
|
|
12
|
+
* (the Agent, the tool runtime, the plugin, the enforcement) is the real thing; only the model is fake.
|
|
13
|
+
*/
|
|
14
|
+
import { ToolCallId } from '@deepseek-ai/dsh-llm'
|
|
15
|
+
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
|
16
|
+
import { writeFileSync } from 'node:fs'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* ⚠ FU-9 — THE ADAPTER PLAYS THE CHILD TOO.
|
|
21
|
+
*
|
|
22
|
+
* The parent leg alone could never exercise the repair: the review parks waiting for its child's reply, so
|
|
23
|
+
* a REVISE verdict is what makes the plugin send a `followup` to the SAME child. This adapter therefore
|
|
24
|
+
* also answers the child's brief, and it flips its verdict on the SECOND round so the loop closes:
|
|
25
|
+
*
|
|
26
|
+
* round 1 (the brief) → write a REVISE verdict to the reply file the brief names
|
|
27
|
+
* round 2 (after REPAIR) → write an APPROVE verdict to that SAME file
|
|
28
|
+
*
|
|
29
|
+
* The reply format is measured from `parseReplyVerdict` (`src/delegation.ts`): a JSON object with a
|
|
30
|
+
* `verdict` field, fenced or bare. Anything else and the runtime would fail closed — which is correct
|
|
31
|
+
* behaviour and would prove nothing about the repair.
|
|
32
|
+
*/
|
|
33
|
+
const repliesWritten = new Map()
|
|
34
|
+
|
|
35
|
+
const REVISE_REPLY = [
|
|
36
|
+
'# Review reply',
|
|
37
|
+
'',
|
|
38
|
+
'```json',
|
|
39
|
+
'{"verdict":"REVISE","findings":[{"title":"the lock chain permits an out-of-order transition"}]}',
|
|
40
|
+
'```',
|
|
41
|
+
'',
|
|
42
|
+
].join('\n')
|
|
43
|
+
|
|
44
|
+
const APPROVE_REPLY = [
|
|
45
|
+
'# Review reply (after repair)',
|
|
46
|
+
'',
|
|
47
|
+
'```json',
|
|
48
|
+
'{"verdict":"APPROVE","findings":[]}',
|
|
49
|
+
'```',
|
|
50
|
+
'',
|
|
51
|
+
].join('\n')
|
|
52
|
+
|
|
53
|
+
/** The text a turn was handed, from the user and system messages. */
|
|
54
|
+
function inputText(messages) {
|
|
55
|
+
return messages
|
|
56
|
+
.filter((message) => message.role === 'user' || message.role === 'system')
|
|
57
|
+
.flatMap((message) => (message.content ?? []).filter((block) => block.type === 'text').map((block) => block.text))
|
|
58
|
+
.join('\n')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Every tool call this session has already made, read back from the conversation. */
|
|
62
|
+
function calledTools(messages) {
|
|
63
|
+
return messages.flatMap((message) => message.role === 'assistant'
|
|
64
|
+
? message.content.filter((block) => block.type === 'tool-call').map((block) => block.name)
|
|
65
|
+
: [])
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toolCallChunks(specs, counter) {
|
|
69
|
+
const chunks = []
|
|
70
|
+
for (const [index, spec] of specs.entries()) {
|
|
71
|
+
const id = ToolCallId(`live-fixture-${counter.n++}`)
|
|
72
|
+
const args = JSON.stringify(spec.args)
|
|
73
|
+
chunks.push(
|
|
74
|
+
{ type: 'block-start', index, blockType: 'tool-call' },
|
|
75
|
+
{ type: 'tool-call-delta', index, id, name: spec.name, argumentsDelta: args },
|
|
76
|
+
{ type: 'block-end', index, block: { type: 'tool-call', id, name: spec.name, arguments: args } },
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
chunks.push(
|
|
80
|
+
{ type: 'usage', usage: { inputTokens: 10, outputTokens: 5 } },
|
|
81
|
+
{ type: 'finish', reason: { kind: 'tool-calls' } },
|
|
82
|
+
)
|
|
83
|
+
return chunks
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function textChunks(text) {
|
|
87
|
+
return [
|
|
88
|
+
{ type: 'block-start', index: 0, blockType: 'text' },
|
|
89
|
+
{ type: 'text-delta', index: 0, text },
|
|
90
|
+
{ type: 'block-end', index: 0, block: { type: 'text', text } },
|
|
91
|
+
{ type: 'usage', usage: { inputTokens: 10, outputTokens: 5 } },
|
|
92
|
+
{ type: 'finish', reason: { kind: 'stop' } },
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const RUN_ID = 'live-run'
|
|
97
|
+
|
|
98
|
+
class LiveFixtureAdapter extends LlmAdapter {
|
|
99
|
+
counter = { n: 0 }
|
|
100
|
+
|
|
101
|
+
async * stream(options) {
|
|
102
|
+
const messages = options.messages ?? []
|
|
103
|
+
const called = calledTools(messages)
|
|
104
|
+
|
|
105
|
+
// THE CHILD'S TURN — the brief names the reply file, so this turn belongs to the delegated reviewer.
|
|
106
|
+
// First time: REVISE (which is what makes the plugin send a repair to this SAME child). Second time:
|
|
107
|
+
// APPROVE, closing the loop.
|
|
108
|
+
const reply = inputText(messages).match(/Reply file:\s*(\S+)/i)
|
|
109
|
+
if (reply) {
|
|
110
|
+
const replyPath = reply[1]
|
|
111
|
+
const already = repliesWritten.get(replyPath) === true
|
|
112
|
+
writeFileSync(join(process.cwd(), replyPath), already ? APPROVE_REPLY : REVISE_REPLY, 'utf8')
|
|
113
|
+
repliesWritten.set(replyPath, true)
|
|
114
|
+
yield* textChunks(already
|
|
115
|
+
? 'APPROVE written to ' + replyPath + ' after the repair.'
|
|
116
|
+
: 'REVISE written to ' + replyPath + '.')
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 1. Scaffold the run.
|
|
121
|
+
if (!called.includes('recursive_init')) {
|
|
122
|
+
yield* toolCallChunks([{ name: 'recursive_init', args: { runId: RUN_ID } }], this.counter)
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
// 2. Ask for a review of phase 03 — the delegated path FU-9 is about.
|
|
126
|
+
//
|
|
127
|
+
// ⚠ ASKED REPEATEDLY, ON PURPOSE, because the review is TURN-SHAPED: the first call PARKS waiting for
|
|
128
|
+
// the child, and it is a LATER turn that resumes the SAME child (T36: "park, resume the SAME child").
|
|
129
|
+
// A script that asked once left the child with no turn at all — proven by a run whose reply.md was
|
|
130
|
+
// never written. Each further call drives one more round, which is what lets a REVISE verdict produce a
|
|
131
|
+
// repair and then an APPROVE.
|
|
132
|
+
const reviewCalls = called.filter((name) => name === 'recursive_review').length
|
|
133
|
+
if (reviewCalls < 4) {
|
|
134
|
+
yield* toolCallChunks([{ name: 'recursive_review', args: { runId: RUN_ID, phase: '03', role: 'code-reviewer' } }], this.counter)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
// 3. Report and stop. The marker is what the harness greps for in the session log.
|
|
138
|
+
yield* textChunks('LIVE_SESSION_REACHED_REVIEW')
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Cordis plugin name. */
|
|
143
|
+
export const name = 'live-fixture-llm'
|
|
144
|
+
/** The LLM registry this adapter registers into. */
|
|
145
|
+
export const inject = ['llm']
|
|
146
|
+
|
|
147
|
+
/** Register the keyless adapter on the shipped default provider route. */
|
|
148
|
+
export function apply(ctx) {
|
|
149
|
+
ctx.llm.registerAdapter(['deepseek-official'], new LiveFixtureAdapter())
|
|
150
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* FU-10 step 2 — THE LIVE SESSION WITH THIS PLUGIN MOUNTED.
|
|
4
|
+
*
|
|
5
|
+
* Derived from `live-session-stock.mjs`, which is PROVEN to boot (exit 0, agents ran, session logs
|
|
6
|
+
* written). Exactly two things change, and nothing else — that is the point of deriving rather than
|
|
7
|
+
* rewriting:
|
|
8
|
+
*
|
|
9
|
+
* 1. the scripted LLM is OURS (`scripts/live/fake-llm.mjs`), scripting `recursive_init` then
|
|
10
|
+
* `recursive_review`, so a session that gets that far has reached the delegated review path;
|
|
11
|
+
* 2. THIS PLUGIN is inserted into the profile **by file URL**, the pattern the harness uses for its own
|
|
12
|
+
* fixture — so no install and no dependency added to this repo.
|
|
13
|
+
*
|
|
14
|
+
* ⚠ EVERYTHING ELSE IS THE PROVEN RECIPE: the fixture's `dependencies` AND its three bundles (my
|
|
15
|
+
* hand-written profile was rejected as invalid without them), the same patch shape, the filtered env (a
|
|
16
|
+
* child must not inherit this session's `DSH_PROFILE=web`), and the absolute tsx loader file URL.
|
|
17
|
+
*/
|
|
18
|
+
import { spawnSync } from 'node:child_process'
|
|
19
|
+
import { existsSync, mkdirSync, openSync, closeSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
20
|
+
import { tmpdir } from 'node:os'
|
|
21
|
+
import { join } from 'node:path'
|
|
22
|
+
|
|
23
|
+
const HARNESS = 'D:\\deepseek-harness'
|
|
24
|
+
const PLUGIN = 'D:\\DEV\\dsh-recursive-mode'
|
|
25
|
+
const SRC_BIN = join(HARNESS, 'apps', 'cli', 'src', 'bin.ts')
|
|
26
|
+
const TSCONFIG = join(HARNESS, 'tsconfig.json')
|
|
27
|
+
const TSX_LOADER = 'file:///D:/deepseek-harness/node_modules/.pnpm/tsx@4.22.4/node_modules/tsx/dist/loader.mjs'
|
|
28
|
+
const PROMPT = 'Run recursive_init for run live-run, then ask recursive_review to review phase 03.'
|
|
29
|
+
|
|
30
|
+
const fileUrl = (path) => 'file:///' + path.replace(/\\/g, '/')
|
|
31
|
+
const base = join(existsSync('E:\\') ? 'E:\\dsh-rm-live' : tmpdir(), 'plugin-' + new Date().toISOString().replace(/[:.]/g, '-'))
|
|
32
|
+
const home = join(base, '.dsh')
|
|
33
|
+
const repo = join(base, 'repo')
|
|
34
|
+
const sessions = join(home, 'sessions')
|
|
35
|
+
const profileDir = join(home, 'profiles', 'headless')
|
|
36
|
+
|
|
37
|
+
mkdirSync(profileDir, { recursive: true })
|
|
38
|
+
mkdirSync(sessions, { recursive: true })
|
|
39
|
+
mkdirSync(join(repo, 'src'), { recursive: true })
|
|
40
|
+
writeFileSync(join(repo, 'README.md'), '# live session scratch repo\n', 'utf8')
|
|
41
|
+
spawnSync('git', ['init', '-q'], { cwd: repo, stdio: 'ignore' })
|
|
42
|
+
|
|
43
|
+
// The PROVEN profile (fixture deps + fixture bundles), verbatim.
|
|
44
|
+
writeFileSync(join(profileDir, 'package.json'), JSON.stringify({
|
|
45
|
+
name: 'dsh-profile-headless',
|
|
46
|
+
private: true,
|
|
47
|
+
dependencies: { '@deepseek-ai/dsh-experimental-agent-team-profile': 'workspace:^' },
|
|
48
|
+
dsh: {
|
|
49
|
+
profile: {
|
|
50
|
+
bundles: [
|
|
51
|
+
'@deepseek-ai/dsh-base',
|
|
52
|
+
'@deepseek-ai/dsh-headless',
|
|
53
|
+
'@deepseek-ai/dsh-experimental-agent-team-profile',
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}, undefined, 2) + '\n', 'utf8')
|
|
58
|
+
|
|
59
|
+
// The patch: real provider off, sessions into the temp home, OUR LLM and OUR PLUGIN inserted by file URL.
|
|
60
|
+
writeFileSync(join(profileDir, 'cordis.patch.yml'), [
|
|
61
|
+
'- id: llm-deepseek',
|
|
62
|
+
' disabled: true',
|
|
63
|
+
'- id: session-persistence-jsonl',
|
|
64
|
+
' config:',
|
|
65
|
+
" root: '" + sessions.replace(/\\/g, '/') + "'",
|
|
66
|
+
' compression: none',
|
|
67
|
+
'- insert:',
|
|
68
|
+
' - id: live-fixture-llm',
|
|
69
|
+
" name: '" + fileUrl(join(PLUGIN, 'scripts', 'live', 'fake-llm.mjs')) + "'",
|
|
70
|
+
' - id: recursive-mode',
|
|
71
|
+
" name: '" + fileUrl(join(PLUGIN, 'src', 'index.ts')) + "'",
|
|
72
|
+
'',
|
|
73
|
+
].join('\n'), 'utf8')
|
|
74
|
+
|
|
75
|
+
const outFile = join(base, 'stdout.txt')
|
|
76
|
+
const errFile = join(base, 'stderr.txt')
|
|
77
|
+
const outFd = openSync(outFile, 'w')
|
|
78
|
+
const errFd = openSync(errFile, 'w')
|
|
79
|
+
|
|
80
|
+
const env = { ...process.env }
|
|
81
|
+
for (const key of ['DSH_PROFILE', 'DSH_PROFILE_DIR', 'DSH_SESSION_ID', 'DSH_SHELL', 'DSH_WEB_URL']) delete env[key]
|
|
82
|
+
|
|
83
|
+
console.log('[plugin] base: ' + base)
|
|
84
|
+
console.log('[plugin] repo: ' + repo)
|
|
85
|
+
const result = spawnSync(process.execPath, [
|
|
86
|
+
'--import', TSX_LOADER,
|
|
87
|
+
SRC_BIN,
|
|
88
|
+
'--profile', 'headless',
|
|
89
|
+
PROMPT,
|
|
90
|
+
], {
|
|
91
|
+
cwd: repo,
|
|
92
|
+
stdio: ['ignore', outFd, errFd],
|
|
93
|
+
env: {
|
|
94
|
+
...env,
|
|
95
|
+
DSH_HOME: home,
|
|
96
|
+
DSH_AGENTS_HOME: join(base, '.agents'),
|
|
97
|
+
DSH_TELEMETRY_DISABLED: '1',
|
|
98
|
+
DEEPSEEK_API_KEY: '',
|
|
99
|
+
TSX_TSCONFIG_PATH: TSCONFIG,
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
closeSync(outFd)
|
|
103
|
+
closeSync(errFd)
|
|
104
|
+
|
|
105
|
+
const stdout = existsSync(outFile) ? readFileSync(outFile, 'utf8') : ''
|
|
106
|
+
const stderr = existsSync(errFile) ? readFileSync(errFile, 'utf8') : ''
|
|
107
|
+
console.log('[plugin] exit: ' + result.status + (result.error ? ' (spawn error: ' + result.error.message + ')' : ''))
|
|
108
|
+
console.log('[plugin] stdout: ' + stdout.length + ' bytes | stderr: ' + stderr.length + ' bytes')
|
|
109
|
+
if (stdout.trim() !== '') console.log('--- stdout tail ---\n' + stdout.trim().split('\n').slice(-14).join('\n'))
|
|
110
|
+
if (stderr.trim() !== '') console.log('--- stderr tail ---\n' + stderr.trim().split('\n').slice(-14).join('\n'))
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* FU-9 — THE SECOND, RESUMED INVOCATION.
|
|
114
|
+
*
|
|
115
|
+
* ⚠ WHY IT IS NEEDED, MEASURED: the review PARKS (T36's turn-shaped driver treats waiting as not-an-error)
|
|
116
|
+
* and a ONE-SHOT session exits before the child's first turn is ever scheduled — a run left no child
|
|
117
|
+
* session, no settlement and no reply. The headless app supports `--session-id`, documented as *"resumes a
|
|
118
|
+
* conversation that already exists"* (`packages/bundle/headless/src/index.ts`, via
|
|
119
|
+
* `agents.resume({ resumeSessionId })`), so the parked round gets a later turn in the SAME conversation —
|
|
120
|
+
* which is the shape this plugin was built for.
|
|
121
|
+
*/
|
|
122
|
+
function sessionIdUnder(root) {
|
|
123
|
+
if (!existsSync(root)) return null
|
|
124
|
+
for (const scoped of readdirSync(root, { withFileTypes: true })) {
|
|
125
|
+
if (!scoped.isDirectory()) continue
|
|
126
|
+
for (const inner of readdirSync(join(root, scoped.name), { withFileTypes: true })) {
|
|
127
|
+
// ⚠ THE PREFIX STAYS. Measured: passing the bare uuid gives
|
|
128
|
+
// `dsh: session "<uuid>" does not exist; omit --session-id to start a new Session` — the session id
|
|
129
|
+
// the CLI knows is the whole `session-<uuid>`, which is also what the log's first record carries.
|
|
130
|
+
if (inner.isDirectory() && inner.name.startsWith('session-')) return inner.name
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const sessionId = sessionIdUnder(sessions)
|
|
137
|
+
if (sessionId !== null) {
|
|
138
|
+
const outFile2 = join(base, 'stdout-resume.txt')
|
|
139
|
+
const errFile2 = join(base, 'stderr-resume.txt')
|
|
140
|
+
const outFd2 = openSync(outFile2, 'w')
|
|
141
|
+
const errFd2 = openSync(errFile2, 'w')
|
|
142
|
+
console.log('[plugin] resuming session: ' + sessionId)
|
|
143
|
+
const resumed = spawnSync(process.execPath, [
|
|
144
|
+
'--import', TSX_LOADER,
|
|
145
|
+
SRC_BIN,
|
|
146
|
+
'--profile', 'headless',
|
|
147
|
+
'--session-id', sessionId,
|
|
148
|
+
'The reviewer has replied. Continue the review round and report the verdict.',
|
|
149
|
+
], {
|
|
150
|
+
cwd: repo,
|
|
151
|
+
stdio: ['ignore', outFd2, errFd2],
|
|
152
|
+
env: {
|
|
153
|
+
...env,
|
|
154
|
+
DSH_HOME: home,
|
|
155
|
+
DSH_AGENTS_HOME: join(base, '.agents'),
|
|
156
|
+
DSH_TELEMETRY_DISABLED: '1',
|
|
157
|
+
DEEPSEEK_API_KEY: '',
|
|
158
|
+
TSX_TSCONFIG_PATH: TSCONFIG,
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
closeSync(outFd2)
|
|
162
|
+
closeSync(errFd2)
|
|
163
|
+
const out2 = existsSync(outFile2) ? readFileSync(outFile2, 'utf8') : ''
|
|
164
|
+
console.log('[plugin] resume exit: ' + resumed.status + ' | stdout: ' + out2.trim().split('\n').slice(-4).join(' / '))
|
|
165
|
+
} else {
|
|
166
|
+
console.log('[plugin] resume SKIPPED: no session id found to resume')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// The evidence a run happened: OUR plugin's run directory, written by its own tools in a real session.
|
|
170
|
+
const runDir = join(repo, '.recursive', 'run', 'live-run')
|
|
171
|
+
console.log('[plugin] run directory exists: ' + existsSync(runDir))
|
|
172
|
+
if (existsSync(runDir)) {
|
|
173
|
+
const files = readdirSync(runDir).slice(0, 12)
|
|
174
|
+
console.log('[plugin] run artifacts: ' + files.join(', '))
|
|
175
|
+
}
|
|
176
|
+
const reviews = existsSync(join(runDir, 'subagents')) ? readdirSync(join(runDir, 'subagents')).slice(0, 6) : []
|
|
177
|
+
console.log('[plugin] subagents dir: ' + (reviews.length > 0 ? reviews.join(', ') : '(none)'))
|
|
178
|
+
const logs = existsSync(sessions) ? readdirSync(sessions, { recursive: true }).map(String).filter((n) => n.endsWith('.jsonl')) : []
|
|
179
|
+
console.log('[plugin] session logs: ' + logs.length)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* FU-10 step 1 — THE STOCK SESSION, copied VERBATIM from the harness's own e2e.
|
|
4
|
+
*
|
|
5
|
+
* WHY A SEPARATE SCRIPT. My first attempt added this plugin on the very first run, which mixed two
|
|
6
|
+
* unknowns: whether the launch works at all, and whether my plugin mounts. This script removes the second
|
|
7
|
+
* unknown completely — it is the harness's own fixture (same profile package.json, same bundles, same
|
|
8
|
+
* patch, same scripted LLM, same prompt, same invocation) with nothing of mine added. If this boots, the
|
|
9
|
+
* remaining question is only how to mount a plugin; if it does not, the route is blocked here and no
|
|
10
|
+
* amount of plugin work would have fixed it.
|
|
11
|
+
*
|
|
12
|
+
* Source of every line below: apps/cli/tests/agent-team-headless.e2e.ts (L21-78) and the invocation
|
|
13
|
+
* printed from resolveExampleLaunch.
|
|
14
|
+
*/
|
|
15
|
+
import { spawnSync } from 'node:child_process'
|
|
16
|
+
import { existsSync, mkdirSync, mkdtempSync, openSync, closeSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
17
|
+
import { tmpdir } from 'node:os'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
|
|
20
|
+
const HARNESS = 'D:\\deepseek-harness'
|
|
21
|
+
const SRC_BIN = join(HARNESS, 'apps', 'cli', 'src', 'bin.ts')
|
|
22
|
+
const TSCONFIG = join(HARNESS, 'tsconfig.json')
|
|
23
|
+
// The absolute file URL to tsx's loader, exactly as resolveExampleLaunch emits it.
|
|
24
|
+
const TSX_LOADER = 'file:///D:/deepseek-harness/node_modules/.pnpm/tsx@4.22.4/node_modules/tsx/dist/loader.mjs'
|
|
25
|
+
const FIXTURE_LLM = join(HARNESS, 'apps', 'cli', 'tests', 'profiles', 'headless', 'tests', 'fixtures', 'team-llm.mjs')
|
|
26
|
+
// The prompt the e2e uses, which is what its scripted LLM is written against.
|
|
27
|
+
const PROMPT = '请先运行 workflow 检查,再使用 Agent Teams 把调研和实现拆给两个 teammate,等待完成后汇总。'
|
|
28
|
+
|
|
29
|
+
const fileUrl = (path) => 'file:///' + path.replace(/\\/g, '/')
|
|
30
|
+
const base = join(existsSync('E:\\') ? 'E:\\dsh-rm-live' : tmpdir(), 'stock-' + new Date().toISOString().replace(/[:.]/g, '-'))
|
|
31
|
+
const home = join(base, '.dsh')
|
|
32
|
+
const sessions = join(home, 'sessions')
|
|
33
|
+
const profileDir = join(home, 'profiles', 'headless')
|
|
34
|
+
|
|
35
|
+
mkdirSync(profileDir, { recursive: true })
|
|
36
|
+
mkdirSync(sessions, { recursive: true })
|
|
37
|
+
|
|
38
|
+
// 1:1 with the e2e's profile.
|
|
39
|
+
writeFileSync(join(profileDir, 'package.json'), JSON.stringify({
|
|
40
|
+
name: 'dsh-profile-headless',
|
|
41
|
+
private: true,
|
|
42
|
+
dependencies: { '@deepseek-ai/dsh-experimental-agent-team-profile': 'workspace:^' },
|
|
43
|
+
dsh: {
|
|
44
|
+
profile: {
|
|
45
|
+
bundles: [
|
|
46
|
+
'@deepseek-ai/dsh-base',
|
|
47
|
+
'@deepseek-ai/dsh-headless',
|
|
48
|
+
'@deepseek-ai/dsh-experimental-agent-team-profile',
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
}, undefined, 2) + '\n', 'utf8')
|
|
53
|
+
|
|
54
|
+
// 1:1 with the e2e's patch: real provider off, sessions into the temp home, fixture LLM inserted.
|
|
55
|
+
writeFileSync(join(profileDir, 'cordis.patch.yml'), [
|
|
56
|
+
'- id: llm-deepseek',
|
|
57
|
+
' disabled: true',
|
|
58
|
+
'- id: session-persistence-jsonl',
|
|
59
|
+
' config:',
|
|
60
|
+
" root: '" + sessions.replace(/\\/g, '/') + "'",
|
|
61
|
+
' compression: none',
|
|
62
|
+
'- insert:',
|
|
63
|
+
' - id: team-fixture-llm',
|
|
64
|
+
" name: '" + fileUrl(FIXTURE_LLM) + "'",
|
|
65
|
+
'',
|
|
66
|
+
].join('\n'), 'utf8')
|
|
67
|
+
|
|
68
|
+
const outFile = join(base, 'stdout.txt')
|
|
69
|
+
const errFile = join(base, 'stderr.txt')
|
|
70
|
+
const outFd = openSync(outFile, 'w')
|
|
71
|
+
const errFd = openSync(errFile, 'w')
|
|
72
|
+
|
|
73
|
+
// ⚠ The child must not inherit this session's identity: DSH_PROFILE=web made the CLI see two profiles.
|
|
74
|
+
const env = { ...process.env }
|
|
75
|
+
for (const key of ['DSH_PROFILE', 'DSH_PROFILE_DIR', 'DSH_SESSION_ID', 'DSH_SHELL', 'DSH_WEB_URL']) delete env[key]
|
|
76
|
+
|
|
77
|
+
console.log('[stock] base: ' + base)
|
|
78
|
+
const result = spawnSync(process.execPath, [
|
|
79
|
+
'--import', TSX_LOADER,
|
|
80
|
+
SRC_BIN,
|
|
81
|
+
'--profile', 'headless',
|
|
82
|
+
PROMPT,
|
|
83
|
+
], {
|
|
84
|
+
cwd: base,
|
|
85
|
+
stdio: ['ignore', outFd, errFd],
|
|
86
|
+
env: {
|
|
87
|
+
...env,
|
|
88
|
+
DSH_HOME: home,
|
|
89
|
+
DSH_AGENTS_HOME: join(base, '.agents'),
|
|
90
|
+
DSH_TELEMETRY_DISABLED: '1',
|
|
91
|
+
DEEPSEEK_API_KEY: '',
|
|
92
|
+
TSX_TSCONFIG_PATH: TSCONFIG,
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
closeSync(outFd)
|
|
96
|
+
closeSync(errFd)
|
|
97
|
+
|
|
98
|
+
const stdout = existsSync(outFile) ? readFileSync(outFile, 'utf8') : ''
|
|
99
|
+
const stderr = existsSync(errFile) ? readFileSync(errFile, 'utf8') : ''
|
|
100
|
+
console.log('[stock] exit: ' + result.status + (result.error ? ' (spawn error: ' + result.error.message + ')' : ''))
|
|
101
|
+
console.log('[stock] stdout: ' + stdout.length + ' bytes | stderr: ' + stderr.length + ' bytes')
|
|
102
|
+
if (stdout.trim() !== '') console.log('--- stdout tail ---\n' + stdout.trim().split('\n').slice(-10).join('\n'))
|
|
103
|
+
if (stderr.trim() !== '') console.log('--- stderr tail ---\n' + stderr.trim().split('\n').slice(-10).join('\n'))
|
|
104
|
+
const logs = existsSync(sessions) ? readdirSync(sessions, { recursive: true }).map(String).filter((n) => n.endsWith('.jsonl')) : []
|
|
105
|
+
console.log('[stock] session logs: ' + logs.length)
|
|
106
|
+
console.log('[stock] workflow marker (TEAM_WORKFLOW_OK): ' + stdout.includes('TEAM_WORKFLOW_OK'))
|