Merge branch 'master' into refactor/agent-leaf-dependencies
This commit is contained in:
@@ -1194,6 +1194,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
|
||||
- `@deepseek-ai/dsh-brand` ([`packages/util/brand/src/index.ts`](../packages/util/brand/src/index.ts))
|
||||
- `@deepseek-ai/dsh-hook-protocol` ([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts))
|
||||
- `@deepseek-ai/dsh-jsonrpc-agent` ([`packages/ui/jsonrpc-agent/src/index.ts`](../packages/ui/jsonrpc-agent/src/index.ts))
|
||||
- `@deepseek-ai/dsh-loader-smoke` ([`packages/support/loader-smoke/src/index.ts`](../packages/support/loader-smoke/src/index.ts))
|
||||
- `@deepseek-ai/dsh-scope` ([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts))
|
||||
- `@deepseek-ai/dsh-subagent-inprocess` ([`packages/subagent/subagent-inprocess/src/index.ts`](../packages/subagent/subagent-inprocess/src/index.ts))
|
||||
- `@deepseek-ai/dsh-subagent-subprocess` ([`packages/subagent/subagent-subprocess/src/index.ts`](../packages/subagent/subagent-subprocess/src/index.ts))
|
||||
|
||||
@@ -89,6 +89,7 @@ flowchart TD
|
||||
pkg_acp_snapshot["acp-snapshot"]
|
||||
pkg_invariants["invariants"]
|
||||
pkg_llm_replay["llm-replay"]
|
||||
pkg_loader_smoke["loader-smoke"]
|
||||
pkg_subagent_mock["subagent-mock"]
|
||||
end
|
||||
subgraph group_ui["packages/ui"]
|
||||
@@ -352,6 +353,7 @@ flowchart TD
|
||||
| [`skill`](../packages/skill/skill) | `skill` | — |
|
||||
| [`subagent-subprocess`](../packages/subagent/subagent-subprocess) | `subagent` | — |
|
||||
| [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | — |
|
||||
| [`loader-smoke`](../packages/support/loader-smoke) | `support` | — |
|
||||
| [`app-boot`](../packages/ui/app-boot) | `ui` | — |
|
||||
| [`jsonrpc-agent`](../packages/ui/jsonrpc-agent) | `ui` | — |
|
||||
| [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | — |
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ Each example has both:
|
||||
|
||||
Mock-only examples require only the keyless tier; state that exception in the test.
|
||||
|
||||
Temp-cwd keyless smokes set `TSX_TSCONFIG_PATH` to the root tsconfig and pass `--expose-internals` when loading HMR.
|
||||
Keyless stdio smokes use `@deepseek-ai/dsh-loader-smoke` for isolation, root-tsconfig loading, subprocess lifecycle, diagnostics, EOF, and cleanup; tests supply paths, environment, input, and assertions.
|
||||
|
||||
Do not inventory example tests here; the `tests/` trees and root scripts are authoritative.
|
||||
|
||||
|
||||
@@ -1,92 +1,27 @@
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
|
||||
|
||||
/**
|
||||
* Keyless Loader-path smoke for the Code Mode overlay: boot the real example through the
|
||||
* `@deepseek-ai/dsh-stdio-agent` bin against `code-mode.cordis.yml` (the cordis Loader,
|
||||
* `unwrapExports`, the include patches over ./cordis.yml, the worker-thread code runtime, and
|
||||
* the registry in `mode: code`), then close stdin with no prompt and assert the Code Mode
|
||||
* banner + a clean exit. A dummy key satisfies adapter boot, but no prompt means
|
||||
* no model call; the with-key proof lives in `code-mode.e2e.ts`.
|
||||
* Keyless Loader-path smoke for the Code Mode overlay: boot the real include
|
||||
* tree through stdio-agent and `code-mode.cordis.yml`, then close stdin without
|
||||
* a prompt and assert the banner. No model or `run_code` turn runs.
|
||||
*/
|
||||
|
||||
const binScript = fileURLToPath(new URL('../../../packages/ui/stdio-agent/src/bin.ts', import.meta.url))
|
||||
const configPath = fileURLToPath(new URL('../code-mode.cordis.yml', import.meta.url))
|
||||
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
||||
// Dev/test run UNBUILT: resolve `@deepseek-ai/dsh-*` through the root tsconfig
|
||||
// `paths` map; tsx searches UP from cwd, and we spawn from a temp dir outside
|
||||
// the repo, so point it at the repo tsconfig.
|
||||
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
// Under parallel e2e load, cold tsx/Loader startup can exceed a tight deadline;
|
||||
// 30s still detects a wedged child.
|
||||
const PROCESS_TIMEOUT_MS = 30_000
|
||||
// Leave enough room for the process-owned timeout to report captured output
|
||||
// before Vitest aborts the test itself.
|
||||
const TEST_TIMEOUT_MS = PROCESS_TIMEOUT_MS + 15_000
|
||||
|
||||
let child: ChildProcessWithoutNullStreams | undefined
|
||||
let workdir: string | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
if (child !== undefined && child.exitCode === null) child.kill('SIGKILL')
|
||||
child = undefined
|
||||
if (workdir !== undefined) await rm(workdir, { recursive: true, force: true })
|
||||
workdir = undefined
|
||||
})
|
||||
|
||||
async function bootAndEof(): Promise<{ stdout: string; code: number }> {
|
||||
workdir = await mkdtemp(join(tmpdir(), 'code-mode-smoke-'))
|
||||
const cwd = workdir
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(
|
||||
process.execPath,
|
||||
// --expose-internals: the included cordis.yml loads the HMR plugin (mirrors demo:code-mode).
|
||||
['--expose-internals', '--import', tsxLoader, binScript, configPath],
|
||||
{
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
TSX_TSCONFIG_PATH: repoTsconfig,
|
||||
// A dummy key so llm-deepseek's apply() (key-PRESENT check only) boots.
|
||||
// No prompt is sent, so the adapter never streams — no network call.
|
||||
DEEPSEEK_API_KEY: 'keyless-smoke-no-call',
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
},
|
||||
)
|
||||
child = proc
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
proc.stdout.setEncoding('utf8')
|
||||
proc.stdout.on('data', (chunk: string) => { stdout += chunk })
|
||||
proc.stderr.setEncoding('utf8')
|
||||
proc.stderr.on('data', (chunk: string) => { stderr += chunk })
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
proc.kill('SIGKILL')
|
||||
reject(new Error(`code-mode overlay did not exit within ${PROCESS_TIMEOUT_MS / 1_000}s. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, PROCESS_TIMEOUT_MS)
|
||||
|
||||
proc.on('exit', (code) => {
|
||||
clearTimeout(timer)
|
||||
if (code === 0) resolve({ stdout, code })
|
||||
else reject(new Error(`code-mode overlay exited ${code}. stderr:\n${stderr}`))
|
||||
})
|
||||
proc.on('error', (err) => { clearTimeout(timer); reject(err) })
|
||||
|
||||
// No prompt — just EOF, so the stdio UI exits without ever running a turn.
|
||||
proc.stdin.end()
|
||||
})
|
||||
}
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
|
||||
describe('code-mode overlay keyless smoke (real code-mode.cordis.yml via the Loader)', () => {
|
||||
it('boots the Code Mode plugin tree, prints its banner, and exits cleanly on EOF', async () => {
|
||||
const { stdout, code } = await bootAndEof()
|
||||
expect(code).toBe(0)
|
||||
const { stdout } = await runLoaderSmoke({
|
||||
label: 'code-mode overlay',
|
||||
tempDirPrefix: 'code-mode-smoke-',
|
||||
binScript,
|
||||
configPath,
|
||||
tsconfigPath,
|
||||
env: { DEEPSEEK_API_KEY: 'keyless-smoke-no-call' },
|
||||
})
|
||||
expect(stdout).toContain('code-mode agent ready.')
|
||||
}, TEST_TIMEOUT_MS)
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
})
|
||||
@@ -1,90 +1,28 @@
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
|
||||
|
||||
/**
|
||||
* Boots the real example through the stdio bin and `cordis.yml`, covering Loader,
|
||||
* `unwrapExports`, the full plugin tree, the agent-core bundle, and the readline module.
|
||||
* A dummy key permits startup; closing stdin before a prompt prevents network calls,
|
||||
* while with-key suites cover product behavior.
|
||||
* Keyless Loader-path smoke for examples/coding-agent: boot the real example
|
||||
* through the stdio-agent bin and its `cordis.yml`, then close stdin without a
|
||||
* prompt and assert the banner. The dummy key satisfies adapter construction;
|
||||
* immediate EOF guarantees there is no model call.
|
||||
*/
|
||||
|
||||
// TODO(loader-smoke-harness): share spawn/tempdir/timeout/EOF setup with the other keyless smoke tests.
|
||||
// The temp-cwd child needs absolute bin and config paths.
|
||||
const binScript = fileURLToPath(new URL('../../../packages/ui/stdio-agent/src/bin.ts', import.meta.url))
|
||||
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
|
||||
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
||||
// The temp cwd cannot discover the root tsconfig used for unbuilt package aliases.
|
||||
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
// Allow cold Loader startup under parallel load while still detecting hangs.
|
||||
const PROCESS_TIMEOUT_MS = 30_000
|
||||
// Let the child timeout report captured output before Vitest aborts.
|
||||
const TEST_TIMEOUT_MS = PROCESS_TIMEOUT_MS + 15_000
|
||||
|
||||
let child: ChildProcessWithoutNullStreams | undefined
|
||||
let workdir: string | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
if (child !== undefined && child.exitCode === null) child.kill('SIGKILL')
|
||||
child = undefined
|
||||
if (workdir !== undefined) await rm(workdir, { recursive: true, force: true })
|
||||
workdir = undefined
|
||||
})
|
||||
|
||||
async function bootAndEof(): Promise<{ stdout: string; code: number }> {
|
||||
workdir = await mkdtemp(join(tmpdir(), 'coding-smoke-'))
|
||||
const cwd = workdir
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(
|
||||
process.execPath,
|
||||
// --expose-internals: cordis.yml loads the HMR plugin (mirrors demo:repl).
|
||||
['--expose-internals', '--import', tsxLoader, binScript, configPath],
|
||||
{
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
TSX_TSCONFIG_PATH: repoTsconfig,
|
||||
// A dummy key so llm-deepseek's apply() (key-PRESENT check only) boots.
|
||||
// No prompt is sent, so the adapter never streams — no network call.
|
||||
DEEPSEEK_API_KEY: 'keyless-smoke-no-call',
|
||||
DSH_HOME: join(cwd, '.dsh'),
|
||||
DSH_AGENTS_HOME: join(cwd, '.agents'),
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
},
|
||||
)
|
||||
child = proc
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
proc.stdout.setEncoding('utf8')
|
||||
proc.stdout.on('data', (chunk: string) => { stdout += chunk })
|
||||
proc.stderr.setEncoding('utf8')
|
||||
proc.stderr.on('data', (chunk: string) => { stderr += chunk })
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
proc.kill('SIGKILL')
|
||||
reject(new Error(`coding-agent did not exit within ${PROCESS_TIMEOUT_MS / 1_000}s. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, PROCESS_TIMEOUT_MS)
|
||||
|
||||
proc.on('exit', (code) => {
|
||||
clearTimeout(timer)
|
||||
if (code === 0) resolve({ stdout, code })
|
||||
else reject(new Error(`coding-agent exited ${code}. stderr:\n${stderr}`))
|
||||
})
|
||||
proc.on('error', (err) => { clearTimeout(timer); reject(err) })
|
||||
|
||||
// No prompt — just EOF, so the stdio UI exits without ever running a turn.
|
||||
proc.stdin.end()
|
||||
})
|
||||
}
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
|
||||
describe('coding-agent keyless smoke (real cordis.yml via the Loader)', () => {
|
||||
it('boots the full plugin tree, prints its banner, and exits cleanly on EOF', async () => {
|
||||
const { stdout, code } = await bootAndEof()
|
||||
expect(code).toBe(0)
|
||||
const { stdout } = await runLoaderSmoke({
|
||||
label: 'coding-agent',
|
||||
tempDirPrefix: 'coding-smoke-',
|
||||
binScript,
|
||||
configPath,
|
||||
tsconfigPath,
|
||||
env: { DEEPSEEK_API_KEY: 'keyless-smoke-no-call' },
|
||||
})
|
||||
expect(stdout).toContain('agent REPL ready.')
|
||||
}, TEST_TIMEOUT_MS)
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
})
|
||||
@@ -1,94 +1,27 @@
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
|
||||
|
||||
/**
|
||||
* Keyless Loader-path smoke for examples/cordis-agent: boot the real example through the
|
||||
* `@deepseek-ai/dsh-stdio-agent` bin against its `cordis.yml` — the cordis Loader,
|
||||
* `unwrapExports`, the full plugin tree INCLUDING the `@deepseek-ai/dsh-tool-cordis` package
|
||||
* resolved by name (whose `inject` would crash a collapsed export shape at load, see
|
||||
* docs/postmortem/0001) — then close stdin with no prompt and assert the ready banner + a
|
||||
* clean exit. A dummy key satisfies adapter boot, but no prompt means no network
|
||||
* call; `cordis-tools.e2e.ts` owns the with-key product proof.
|
||||
* Keyless Loader-path smoke for examples/cordis-agent: boot the real tree,
|
||||
* including tool-cordis resolved by package name, then close stdin without a
|
||||
* prompt and assert the banner. The dummy key never reaches a model call.
|
||||
*/
|
||||
|
||||
// The temp-cwd child needs absolute bin and config paths.
|
||||
const binScript = fileURLToPath(new URL('../../../packages/ui/stdio-agent/src/bin.ts', import.meta.url))
|
||||
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
|
||||
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
||||
// Dev/test run UNBUILT: resolve `@deepseek-ai/dsh-*` through the root tsconfig
|
||||
// `paths` map; tsx searches UP from cwd, and we spawn from a temp dir outside
|
||||
// the repo, so point it at the repo tsconfig (root is three levels up).
|
||||
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
// Under parallel e2e load, cold tsx/Loader startup can exceed a tight deadline;
|
||||
// 30s still detects a wedged child.
|
||||
const PROCESS_TIMEOUT_MS = 30_000
|
||||
// Leave enough room for the process-owned timeout to report captured output
|
||||
// before Vitest aborts the test itself.
|
||||
const TEST_TIMEOUT_MS = PROCESS_TIMEOUT_MS + 15_000
|
||||
|
||||
let child: ChildProcessWithoutNullStreams | undefined
|
||||
let workdir: string | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
if (child !== undefined && child.exitCode === null) child.kill('SIGKILL')
|
||||
child = undefined
|
||||
if (workdir !== undefined) await rm(workdir, { recursive: true, force: true })
|
||||
workdir = undefined
|
||||
})
|
||||
|
||||
async function bootAndEof(): Promise<{ stdout: string; code: number }> {
|
||||
workdir = await mkdtemp(join(tmpdir(), 'cordis-smoke-'))
|
||||
const cwd = workdir
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(
|
||||
process.execPath,
|
||||
// --expose-internals: cordis.yml loads the HMR plugin (mirrors demo:cordis).
|
||||
['--expose-internals', '--import', tsxLoader, binScript, configPath],
|
||||
{
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
TSX_TSCONFIG_PATH: repoTsconfig,
|
||||
// A dummy key so llm-deepseek's apply() (key-PRESENT check only) boots.
|
||||
// No prompt is sent, so the adapter never streams — no network call.
|
||||
DEEPSEEK_API_KEY: 'keyless-smoke-no-call',
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
},
|
||||
)
|
||||
child = proc
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
proc.stdout.setEncoding('utf8')
|
||||
proc.stdout.on('data', (chunk: string) => { stdout += chunk })
|
||||
proc.stderr.setEncoding('utf8')
|
||||
proc.stderr.on('data', (chunk: string) => { stderr += chunk })
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
proc.kill('SIGKILL')
|
||||
reject(new Error(`cordis-agent did not exit within ${PROCESS_TIMEOUT_MS / 1_000}s. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, PROCESS_TIMEOUT_MS)
|
||||
|
||||
proc.on('exit', (code) => {
|
||||
clearTimeout(timer)
|
||||
if (code === 0) resolve({ stdout, code })
|
||||
else reject(new Error(`cordis-agent exited ${code}. stderr:\n${stderr}`))
|
||||
})
|
||||
proc.on('error', (err) => { clearTimeout(timer); reject(err) })
|
||||
|
||||
// No prompt — just EOF, so the stdio UI exits without ever running a turn.
|
||||
proc.stdin.end()
|
||||
})
|
||||
}
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
|
||||
describe('cordis-agent keyless smoke (real cordis.yml via the Loader)', () => {
|
||||
it('boots the full plugin tree incl. tool-cordis, prints its banner, and exits cleanly on EOF', async () => {
|
||||
const { stdout, code } = await bootAndEof()
|
||||
expect(code).toBe(0)
|
||||
const { stdout } = await runLoaderSmoke({
|
||||
label: 'cordis-agent',
|
||||
tempDirPrefix: 'cordis-smoke-',
|
||||
binScript,
|
||||
configPath,
|
||||
tsconfigPath,
|
||||
env: { DEEPSEEK_API_KEY: 'keyless-smoke-no-call' },
|
||||
})
|
||||
expect(stdout).toContain('cordis-agent ready.')
|
||||
}, TEST_TIMEOUT_MS)
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
})
|
||||
@@ -1,111 +1,43 @@
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
|
||||
|
||||
/**
|
||||
* Keyless Loader-path smoke for examples/echo-agent: boot the real example through the
|
||||
* `@deepseek-ai/dsh-stdio-agent` bin against this example's `cordis.yml` (the cordis Loader,
|
||||
* `unwrapExports`, the whole plugin tree), pipe a script of stdin lines, and assert the
|
||||
* rendered stdout. The mock adapter is network-free, making this the complete
|
||||
* smoke; inputs cover both the echo-tool round trip and direct-reply branch.
|
||||
* Keyless-by-nature Loader-path coverage for examples/echo-agent. The real
|
||||
* tree uses its deterministic mock model, so this suite is both the boot smoke
|
||||
* and the complete behavior proof for the example.
|
||||
*/
|
||||
|
||||
// The temp-cwd child needs absolute bin and config paths.
|
||||
const binScript = fileURLToPath(new URL('../../../packages/ui/stdio-agent/src/bin.ts', import.meta.url))
|
||||
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
|
||||
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
||||
// The temp cwd is outside the repo, so point tsx at the root config that resolves
|
||||
// unbuilt workspace packages through `paths`.
|
||||
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
// Under parallel e2e load, cold tsx/Loader startup can exceed a tight deadline;
|
||||
// 30s still detects a wedged child.
|
||||
const PROCESS_TIMEOUT_MS = 30_000
|
||||
// Leave enough room for the process-owned timeout to report captured output
|
||||
// before Vitest aborts the test itself.
|
||||
const TEST_TIMEOUT_MS = PROCESS_TIMEOUT_MS + 15_000
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
|
||||
let child: ChildProcessWithoutNullStreams | undefined
|
||||
let workdir: string | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
if (child !== undefined && child.exitCode === null) child.kill('SIGKILL')
|
||||
child = undefined
|
||||
if (workdir !== undefined) await rm(workdir, { recursive: true, force: true })
|
||||
workdir = undefined
|
||||
})
|
||||
|
||||
/**
|
||||
* Boot echo-agent, write `lines` to its stdin, close stdin, and resolve with
|
||||
* the full stdout once the process exits (the stdio UI exits on EOF after the
|
||||
* agent settles). Rejects on a non-zero exit or the process deadline.
|
||||
*/
|
||||
async function runEcho(lines: string[]): Promise<{ stdout: string; code: number }> {
|
||||
workdir = await mkdtemp(join(tmpdir(), 'echo-smoke-'))
|
||||
const cwd = workdir
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(
|
||||
process.execPath,
|
||||
// --expose-internals: the example's cordis.yml loads the HMR plugin, which requires it
|
||||
// (mirrors the `demo:echo` script).
|
||||
['--expose-internals', '--import', tsxLoader, binScript, configPath],
|
||||
{
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
TSX_TSCONFIG_PATH: repoTsconfig,
|
||||
DSH_HOME: join(cwd, '.dsh'),
|
||||
DSH_AGENTS_HOME: join(cwd, '.agents'),
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
},
|
||||
)
|
||||
child = proc
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
proc.stdout.setEncoding('utf8')
|
||||
proc.stdout.on('data', (chunk: string) => { stdout += chunk })
|
||||
proc.stderr.setEncoding('utf8')
|
||||
proc.stderr.on('data', (chunk: string) => { stderr += chunk })
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
proc.kill('SIGKILL')
|
||||
reject(new Error(`echo-agent did not exit within ${PROCESS_TIMEOUT_MS / 1_000}s. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, PROCESS_TIMEOUT_MS)
|
||||
|
||||
proc.on('exit', (code) => {
|
||||
clearTimeout(timer)
|
||||
if (code === 0) resolve({ stdout, code })
|
||||
else reject(new Error(`echo-agent exited ${code}. stderr:\n${stderr}`))
|
||||
})
|
||||
proc.on('error', (err) => { clearTimeout(timer); reject(err) })
|
||||
|
||||
// Feed the script, then EOF so the stdio UI exits after the agent settles.
|
||||
for (const line of lines) proc.stdin.write(`${line}\n`)
|
||||
proc.stdin.end()
|
||||
async function runEcho(stdinLines: readonly string[]): Promise<string> {
|
||||
const { stdout } = await runLoaderSmoke({
|
||||
label: 'echo-agent',
|
||||
tempDirPrefix: 'echo-smoke-',
|
||||
binScript,
|
||||
configPath,
|
||||
tsconfigPath,
|
||||
stdinLines,
|
||||
})
|
||||
return stdout
|
||||
}
|
||||
|
||||
describe('echo-agent keyless smoke (real cordis.yml via the Loader)', () => {
|
||||
it('boots, prints its welcome banner, and exits cleanly on stdin EOF', async () => {
|
||||
const { stdout, code } = await runEcho([])
|
||||
expect(code).toBe(0)
|
||||
expect(stdout).toContain('echo-agent ready.')
|
||||
}, TEST_TIMEOUT_MS)
|
||||
expect(await runEcho([])).toContain('echo-agent ready.')
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('runs the echo tool round-trip for an "echo …" line', async () => {
|
||||
const { stdout } = await runEcho(['echo hello world'])
|
||||
// mock-llm.ts emits a tool-call for the echo tool; echo-tool.ts uppercases.
|
||||
const stdout = await runEcho(['echo hello world'])
|
||||
expect(stdout).toContain('[tool call] echo')
|
||||
expect(stdout).toContain('[tool result] ECHO: HELLO WORLD')
|
||||
}, TEST_TIMEOUT_MS)
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('streams a direct canned reply for a non-echo line', async () => {
|
||||
const { stdout } = await runEcho(['just chatting'])
|
||||
// The direct-response branch of mock-llm.ts quotes the input back.
|
||||
const stdout = await runEcho(['just chatting'])
|
||||
expect(stdout).toContain('just chatting')
|
||||
expect(stdout).not.toContain('[tool call]')
|
||||
}, TEST_TIMEOUT_MS)
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
})
|
||||
@@ -45,6 +45,11 @@
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts"],
|
||||
"ignoreDependencies": ["cordis"]
|
||||
},
|
||||
"packages/support/loader-smoke": {
|
||||
"entry": ["tests/**/*.spec.ts", "tests/fixtures/*.ts"],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts"],
|
||||
"ignoreDependencies": ["cordis"]
|
||||
},
|
||||
"packages/core/agent-loop": {
|
||||
"entry": ["tests/**/*.spec.ts", "tests/**/*.e2e.ts"],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts"]
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ Packages are grouped by modular role at `packages/<group>/<pkg>/`. The group dir
|
||||
| [`session-persistence/`](session-persistence/README.md) | Persistence capability family: the seam + JSONL/SQLite backends | Product — stable surface |
|
||||
| [`session-query/`](session-query/README.md) | Session retrieval family: logical corpus, surface records, and bounded exact reads | Product — stable surface |
|
||||
| [`ui/`](ui/README.md) | Editor/client integration surfaces: ACP bridge, JSON-RPC SDK server, app packages, user-approval and user-interaction seams, ask-user tool | Product — stable surface |
|
||||
| [`support/`](support/README.md) | Dev/test/example infrastructure (invariants, replay adapter, subagent mock) | Support — lower compatibility expectations |
|
||||
| [`support/`](support/README.md) | Support infrastructure (invariants, replay, Loader smokes) | Support — lower compatibility expectations |
|
||||
| [`util/`](util/README.md) | Low-level zero-dependency utilities shared across groups (the `Branded<B>` primitive) | Support — small, stable, harness-dep-free |
|
||||
|
||||
The split is the point: a package's group says whether it is part of the product API or support/test/example infrastructure, so release and removal decisions do not treat every package as an equal public contract. New packages join an existing group; adding a new top-level group is a deliberate act (extend the group READMEs and this table).
|
||||
|
||||
@@ -6,7 +6,8 @@ Packages that exist to serve development, testing, and the examples rather than
|
||||
|---|---|---|
|
||||
| `acp-snapshot/` | ACP snapshot suite kit: subprocess scenario harness + golden normalizers + the `defineAcpSnapshotSuite` factory | (library — imported by example `*.snapshot.ts` suites) |
|
||||
| `invariants/` | Runtime event-contract assertions for development diagnostics | (listens on `session/*`, `agent/*`) |
|
||||
| `loader-smoke/` | Shared real-Loader subprocess harness for keyless example smokes | (library — imported by example e2e suites) |
|
||||
| `llm-replay/` | Record/replay adapter: short-circuits `llm/stream` from a recorded session JSONL (keyless snapshot tests) | (listens on `llm/stream`) |
|
||||
| `subagent-mock/` | Scripted `SubagentProvider` for deterministic seam/tool tests | (registers on `ctx.subagents`) |
|
||||
|
||||
`invariants` is development support but has no environment guard: it runs wherever registered, and the default `dsh-agent-core` bundle mounts it unconditionally. `llm-replay` backs the demos and the snapshot test tier under the per-file coverage gate. `acp-snapshot` carries the snapshot tier's harness/normalizer/suite machinery so every example's suite is a scenario table over one shared, gate-covered implementation. `subagent-mock` exercises the real `ctx.subagents` load path without a model or child agent. A package graduates OUT of `support/` into a product group only when it gains documented product consumers.
|
||||
`invariants` is development support but has no environment guard: it runs wherever registered, and the default `dsh-agent-core` bundle mounts it unconditionally. `llm-replay` backs the demos and the snapshot test tier under the per-file coverage gate. `acp-snapshot` carries the snapshot tier's harness/normalizer/suite machinery, while `loader-smoke` owns the parallel stdio/Loader process boundary used by keyless example e2e suites. `subagent-mock` exercises the real `ctx.subagents` load path without a model or child agent. A package graduates OUT of `support/` into a product group only when it gains documented product consumers.
|
||||
@@ -0,0 +1,17 @@
|
||||
# `@deepseek-ai/dsh-loader-smoke`
|
||||
|
||||
Shared subprocess harness for keyless example smokes that boot the real stdio-agent bin and a real `cordis.yml` through the Cordis Loader. A test supplies absolute bin/config/tsconfig paths, optional environment overrides, and stdin lines; `runLoaderSmoke` owns the isolated cwd, DSH homes, tsx path resolution, 30-second process deadline, captured diagnostics, forced kill, EOF, and cleanup.
|
||||
|
||||
Successful runs return stdout and stderr only after a zero exit. Non-zero exits and deadlines reject with both captured streams. `LOADER_SMOKE_TEST_TIMEOUT_MS` leaves Vitest enough room for the process-owned diagnostic timeout to fire first.
|
||||
|
||||
This is support-tier test infrastructure, not product API. The consumers are the Loader-path smokes under `examples/{echo-agent,coding-agent,cordis-agent}`.
|
||||
|
||||
## Model Experience
|
||||
|
||||
None, as this test-only harness boots example processes and inspects their streams without changing an assembled model request.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **Only the unbuilt tsx/Loader path is exercised** — built-bin artifacts remain the responsibility of their separate e2e smokes.
|
||||
- **Captured stdout and stderr are unbounded** — a runaway child can consume memory until the deadline kills it.
|
||||
- **Timeout kills only the direct child** — a process tree spawned by a faulty fixture can outlive the smoke and needs external cleanup.
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-loader-smoke",
|
||||
"description": "Shared subprocess harness for keyless real-Loader example smoke tests",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tsx": "^4.22.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Shared subprocess harness for keyless example smokes that boot a real
|
||||
* `cordis.yml` through the stdio-agent bin and Cordis Loader.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-loader-smoke
|
||||
*/
|
||||
|
||||
import { spawn } from 'node:child_process'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const DEFAULT_PROCESS_TIMEOUT_MS = 30_000
|
||||
const TSX_LOADER = fileURLToPath(import.meta.resolve('tsx'))
|
||||
|
||||
/** Vitest deadline that leaves room for the subprocess-owned 30-second diagnostic timeout. */
|
||||
export const LOADER_SMOKE_TEST_TIMEOUT_MS = DEFAULT_PROCESS_TIMEOUT_MS + 15_000
|
||||
|
||||
/** Inputs that vary between real-Loader example smokes. */
|
||||
export interface LoaderSmokeOptions {
|
||||
/** Human-readable example name used in failure diagnostics. */
|
||||
readonly label: string
|
||||
/** Prefix for the isolated temporary process cwd. */
|
||||
readonly tempDirPrefix: string
|
||||
/** Absolute stdio-agent bin path. */
|
||||
readonly binScript: string
|
||||
/** Absolute real Loader config path. */
|
||||
readonly configPath: string
|
||||
/** Absolute repo tsconfig path used for unbuilt workspace-package resolution. */
|
||||
readonly tsconfigPath: string
|
||||
/** Environment overrides layered over the parent and isolated DSH homes. */
|
||||
readonly env?: Readonly<NodeJS.ProcessEnv>
|
||||
/** Lines written to stdin before EOF; omitted means immediate EOF. */
|
||||
readonly stdinLines?: readonly string[]
|
||||
/** Process deadline override for harness tests. */
|
||||
readonly processTimeoutMs?: number
|
||||
}
|
||||
|
||||
/** Captured output from a Loader smoke that exited successfully. */
|
||||
export interface LoaderSmokeResult {
|
||||
/** Complete stdout after clean exit. */
|
||||
readonly stdout: string
|
||||
/** Complete stderr after clean exit. */
|
||||
readonly stderr: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot one real Loader tree from an isolated cwd, write the requested stdin
|
||||
* script, close stdin, and await a clean exit. The helper owns process kill and
|
||||
* temp-directory cleanup on every outcome.
|
||||
* @param options - example paths, environment, stdin, and diagnostic identity.
|
||||
* @returns captured stdout and stderr after a zero exit.
|
||||
*/
|
||||
export async function runLoaderSmoke(options: LoaderSmokeOptions): Promise<LoaderSmokeResult> {
|
||||
const cwd = await mkdtemp(join(tmpdir(), options.tempDirPrefix))
|
||||
const processTimeoutMs = options.processTimeoutMs ?? DEFAULT_PROCESS_TIMEOUT_MS
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
['--expose-internals', '--import', TSX_LOADER, options.binScript, options.configPath],
|
||||
{
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
DSH_HOME: join(cwd, '.dsh'),
|
||||
DSH_AGENTS_HOME: join(cwd, '.agents'),
|
||||
...options.env,
|
||||
TSX_TSCONFIG_PATH: options.tsconfigPath,
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
},
|
||||
)
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
let deferredFailure: Error | undefined
|
||||
child.stdout.setEncoding('utf8')
|
||||
child.stdout.on('data', (chunk: string) => { stdout += chunk })
|
||||
child.stderr.setEncoding('utf8')
|
||||
child.stderr.on('data', (chunk: string) => { stderr += chunk })
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
deferredFailure = new Error(`${options.label} did not exit within ${processTimeoutMs / 1_000}s. stdout:\n${stdout}\nstderr:\n${stderr}`)
|
||||
child.kill('SIGKILL')
|
||||
}, processTimeoutMs)
|
||||
|
||||
child.once('exit', (code) => {
|
||||
clearTimeout(timer)
|
||||
if (deferredFailure !== undefined) {
|
||||
reject(deferredFailure)
|
||||
} else if (code === 0) {
|
||||
resolve({ stdout, stderr })
|
||||
} else {
|
||||
reject(new Error(`${options.label} exited ${String(code)}. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}
|
||||
})
|
||||
|
||||
// process.execPath and a just-created pipe make these OS-error paths
|
||||
// impractical to induce without replacing the boundary under test.
|
||||
/* v8 ignore start */
|
||||
child.once('error', (error) => {
|
||||
clearTimeout(timer)
|
||||
reject(new Error(`${options.label} failed to start: ${error.message}`))
|
||||
})
|
||||
child.stdin.once('error', (error) => {
|
||||
deferredFailure ??= new Error(`${options.label} stdin failed: ${error.message}`)
|
||||
child.kill('SIGKILL')
|
||||
})
|
||||
/* v8 ignore stop */
|
||||
|
||||
child.stdin.end((options.stdinLines ?? []).map(line => `${line}\n`).join(''))
|
||||
})
|
||||
} finally {
|
||||
await rm(cwd, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/** Non-zero subprocess fixture for the Loader-smoke harness. */
|
||||
|
||||
console.error('fixture failed')
|
||||
process.exitCode = 7
|
||||
@@ -0,0 +1,4 @@
|
||||
/** Deadline subprocess fixture for the Loader-smoke harness. */
|
||||
|
||||
console.log('fixture hanging')
|
||||
setInterval(() => {}, 1_000)
|
||||
@@ -0,0 +1,16 @@
|
||||
/** Successful subprocess fixture for the Loader-smoke harness. */
|
||||
|
||||
let input = ''
|
||||
process.stdin.setEncoding('utf8')
|
||||
process.stdin.on('data', (chunk: string) => { input += chunk })
|
||||
process.stdin.on('end', () => {
|
||||
console.log(JSON.stringify({
|
||||
configPath: process.argv[2],
|
||||
cwd: process.cwd(),
|
||||
dshHome: process.env.DSH_HOME,
|
||||
agentsHome: process.env.DSH_AGENTS_HOME,
|
||||
marker: process.env.LOADER_SMOKE_MARKER,
|
||||
input,
|
||||
}))
|
||||
console.error('fixture stderr')
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
|
||||
|
||||
const configPath = '/tmp/fixture.cordis.yml'
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url))
|
||||
const fixture = (name: string): string => fileURLToPath(new URL(`./fixtures/${name}.ts`, import.meta.url))
|
||||
const canonicalTempPath = (path: string): string => path.replace(/^\/private(?=\/var\/)/, '')
|
||||
|
||||
describe('runLoaderSmoke', () => {
|
||||
it('isolates the process, writes stdin, captures output, and removes the cwd', async () => {
|
||||
const result = await runLoaderSmoke({
|
||||
label: 'success fixture',
|
||||
tempDirPrefix: 'loader-smoke-success-',
|
||||
binScript: fixture('success'),
|
||||
configPath,
|
||||
tsconfigPath,
|
||||
env: { LOADER_SMOKE_MARKER: 'present' },
|
||||
stdinLines: ['one', 'two'],
|
||||
})
|
||||
const output = JSON.parse(result.stdout) as {
|
||||
configPath: string
|
||||
cwd: string
|
||||
dshHome: string
|
||||
agentsHome: string
|
||||
marker: string
|
||||
input: string
|
||||
}
|
||||
expect(output).toMatchObject({
|
||||
configPath,
|
||||
marker: 'present',
|
||||
input: 'one\ntwo\n',
|
||||
})
|
||||
expect(canonicalTempPath(output.dshHome)).toBe(`${canonicalTempPath(output.cwd)}/.dsh`)
|
||||
expect(canonicalTempPath(output.agentsHome)).toBe(`${canonicalTempPath(output.cwd)}/.agents`)
|
||||
expect(result.stderr).toContain('fixture stderr')
|
||||
expect(existsSync(output.cwd)).toBe(false)
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('rejects a non-zero exit with captured diagnostics', async () => {
|
||||
await expect(runLoaderSmoke({
|
||||
label: 'failure fixture',
|
||||
tempDirPrefix: 'loader-smoke-fail-',
|
||||
binScript: fixture('fail'),
|
||||
configPath,
|
||||
tsconfigPath,
|
||||
})).rejects.toThrow('failure fixture exited 7. stdout:\n\nstderr:\nfixture failed')
|
||||
})
|
||||
|
||||
it('kills a process at its deadline and reports captured output', async () => {
|
||||
await expect(runLoaderSmoke({
|
||||
label: 'hanging fixture',
|
||||
tempDirPrefix: 'loader-smoke-hang-',
|
||||
binScript: fixture('hang'),
|
||||
configPath,
|
||||
tsconfigPath,
|
||||
processTimeoutMs: 100,
|
||||
})).rejects.toThrow('hanging fixture did not exit within 0.1s.')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": []
|
||||
}
|
||||
Generated
+10
@@ -1135,6 +1135,16 @@ importers:
|
||||
specifier: ^4.0.0-rc.7
|
||||
version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)
|
||||
|
||||
packages/support/loader-smoke:
|
||||
dependencies:
|
||||
tsx:
|
||||
specifier: ^4.22.4
|
||||
version: 4.22.4
|
||||
devDependencies:
|
||||
cordis:
|
||||
specifier: ^4.0.0-rc.6
|
||||
version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)
|
||||
|
||||
packages/support/subagent-mock:
|
||||
dependencies:
|
||||
schemastery:
|
||||
|
||||
@@ -55,6 +55,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/subagent/subagent-subprocess': { kind: 'indirect', reason: 'Only process-based subagent backends compose a child model request.' },
|
||||
'packages/support/acp-snapshot': { kind: 'none', reason: 'The test harness observes and normalizes transcripts without changing live requests.' },
|
||||
'packages/support/invariants': { kind: 'none', reason: 'The observer validates requests but never rewrites their context.' },
|
||||
'packages/support/loader-smoke': { kind: 'none', reason: 'The test harness observes child-process streams without changing live requests.' },
|
||||
'packages/support/llm-replay': { kind: 'none', reason: 'The keyless adapter invokes no provider model.' },
|
||||
'packages/support/subagent-mock': { kind: 'indirect', reason: 'Only dsh-tool-subagent renders its configured test outcome.' },
|
||||
'packages/ui/acp-agent': { kind: 'indirect', reason: 'The app bundle delegates request composition to dsh-agent-core and dsh-acp.' },
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
{ "path": "./packages/ui/stdio-agent" },
|
||||
{ "path": "./packages/support/llm-replay" },
|
||||
{ "path": "./packages/support/acp-snapshot" },
|
||||
{ "path": "./packages/support/loader-smoke" },
|
||||
{ "path": "./packages/subagent/subagent" },
|
||||
{ "path": "./packages/support/subagent-mock" },
|
||||
{ "path": "./packages/subagent/tool-subagent" },
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
{ "path": "./packages/ui/stdio-agent" },
|
||||
{ "path": "./packages/support/llm-replay" },
|
||||
{ "path": "./packages/support/acp-snapshot" },
|
||||
{ "path": "./packages/support/loader-smoke" },
|
||||
{ "path": "./packages/subagent/subagent" },
|
||||
{ "path": "./packages/support/subagent-mock" },
|
||||
{ "path": "./packages/subagent/tool-subagent" },
|
||||
|
||||
Reference in New Issue
Block a user