Merge remote-tracking branch 'origin/master' into worktree/provider-routed-llm-adapters

# Conflicts:
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	packages/context/time-context/tests/time-context.spec.ts
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/core/session/README.md
#	packages/core/session/src/types.ts
#	packages/core/session/tests/surface.spec.ts
#	packages/examples/acp-demo/tests/acp-agent.spec.ts
#	packages/examples/stdio-demo/tests/stdio-agent.spec.ts
#	packages/hooks/hooks-claude/tests/coverage.spec.ts
#	packages/hooks/hooks-codex/tests/coverage.spec.ts
#	packages/session-query/session-query/tests/session-query.spec.ts
#	packages/support/invariants/tests/invariants.spec.ts
#	packages/ui/acp/tests/harness.ts
This commit is contained in:
Tianyi Cui
2026-07-17 21:56:10 +08:00
358 changed files with 18578 additions and 2877 deletions

View File

@@ -1,5 +1,5 @@
{
"AGENTS.md": 1370,
"AGENTS.md": 1500,
"docs/AGENTS.md": 1100,
"docs/architecture.md": 1790,
"docs/cordis-primer.md": 600,

View File

@@ -1,7 +1,7 @@
/**
* Typecheck Markdown `ts` fences against workspace sources. `ignore-check`
* fences are reported as opt-outs; generated catalog fragments and
* `type-equiv` blocks are skipped here because their owning gates verify them.
* Typecheck Markdown `ts` fences against the workspace API. `ignore-check` fences are reported as
* opt-outs; generated catalog fragments and `type-equiv` blocks are skipped here because their
* owning gates verify them. A build-coordinated mode consumes existing declarations without emit.
*/
import { execFileSync } from 'node:child_process'
@@ -62,25 +62,106 @@ function extractBlocks(absPath: string): Block[] {
return blocks
}
const configHost: ts.ParseConfigFileHost = {
...ts.sys,
getCurrentDirectory: () => root,
onUnRecoverableConfigFileDiagnostic(diagnostic) {
throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'))
},
}
/** Load root settings and redirect workspace aliases to declarations from the coordinated build. */
function builtTypeCompilerOptions(): ts.CompilerOptions {
const configPath = join(root, 'tsconfig.json')
const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, configHost)
if (!parsed) throw new Error(`doc-typecheck: cannot parse ${configPath}`)
if (parsed.errors.length > 0) {
throw new Error(parsed.errors.map(error => ts.flattenDiagnosticMessageText(error.messageText, '\n')).join('\n'))
}
if (parsed.options.paths === undefined) throw new Error('doc-typecheck: root tsconfig has no workspace paths')
const paths = Object.fromEntries(Object.entries(parsed.options.paths).map(([specifier, candidates]) => [
specifier,
candidates.map((candidate) => {
if (!candidate.endsWith('/src')) {
throw new Error(`doc-typecheck: cannot map workspace source path to built declarations: ${candidate}`)
}
return `${candidate.slice(0, -'/src'.length)}/lib/types`
}),
]))
const options: ts.CompilerOptions = {
...parsed.options,
paths,
noEmit: true,
composite: false,
incremental: false,
declaration: false,
declarationMap: false,
sourceMap: false,
noUnusedLocals: false,
noUnusedParameters: false,
}
delete options.tsBuildInfoFile
return options
}
/** Compile Markdown blocks as virtual files against declarations from the coordinated build. */
function compileBlocksAgainstBuiltTypes(blocks: Block[]): readonly ts.Diagnostic[] {
const options = builtTypeCompilerOptions()
const sources = new Map<string, string>()
for (const [index, block] of blocks.entries()) {
const fileName = resolve(root, '.doc-typecheck', `block-${index}.ts`)
sources.set(fileName, block.code.endsWith('\n') ? block.code : `${block.code}\n`)
}
const baseHost = ts.createCompilerHost(options, true)
const host: ts.CompilerHost = {
...baseHost,
fileExists(fileName) {
return sources.has(resolve(fileName)) || baseHost.fileExists(fileName)
},
readFile(fileName) {
return sources.get(resolve(fileName)) ?? baseHost.readFile(fileName)
},
getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile) {
const source = sources.get(resolve(fileName))
if (source !== undefined) return ts.createSourceFile(fileName, source, languageVersion, true)
return baseHost.getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile)
},
writeFile() {
throw new Error('doc-typecheck: noEmit compilation attempted to write output')
},
}
const program = ts.createProgram([...sources.keys()], options, host)
return ts.getPreEmitDiagnostics(program)
}
/** Render compiler diagnostics with virtual block paths mapped back to Markdown. */
function formatDiagnostics(diagnostics: readonly ts.Diagnostic[], blocks: Block[]): string {
const formatted = ts.formatDiagnostics(diagnostics, {
getCanonicalFileName: fileName => fileName,
getCurrentDirectory: () => root,
getNewLine: () => ts.sys.newLine,
})
return remapBlockPaths(formatted, blocks)
}
/** Reuse the repo typecheck graph references from a temp project one directory below root. */
function workspaceReferences(): { path: string }[] {
const file = join(root, 'tsconfig.json')
// Parse with TypeScript's own JSONC reader, not a hand-rolled comment strip:
// a regex strip mistakes the `/*/` in a wildcard path candidate
// (`./packages/core/*/src`) for a block comment and corrupts the map.
const result = ts.readConfigFile(file, p => readFileSync(p, 'utf8'))
// Parse with TypeScript's own JSONC reader: a regex comment stripper corrupts the `/*/` path
// candidate in the workspace wildcard.
const result = ts.readConfigFile(file, path => readFileSync(path, 'utf8'))
if (result.error) {
throw new Error(`doc-typecheck: cannot read ${file}: ${ts.flattenDiagnosticMessageText(result.error.messageText, '\n')}`)
}
// `config` is typed `any` by the TS API; narrow it to the one field we read.
const { references } = result.config as { compilerOptions: { paths: Record<string, string[]> }; references: { path: string }[] }
return references.map(({ path }) => {
const relativeToTemp = path.startsWith('./') ? `../${path.slice(2)}` : `../${path}`
return { path: relativeToTemp }
})
// `config` is typed `any` by the TS API; narrow it to the one field read here.
const { references } = result.config as { references: { path: string }[] }
return references.map(({ path }) => ({
path: path.startsWith('./') ? `../${path.slice(2)}` : `../${path}`,
}))
}
/** The standalone tsconfig for the temp typecheck project. */
/** The standalone temp project used when no coordinated build owns declaration freshness. */
function tempTsconfig(): string {
return JSON.stringify({
extends: '../tsconfig.json',
@@ -94,6 +175,39 @@ function tempTsconfig(): string {
})
}
/** Compile blocks through project references for the standalone command. */
function compileBlocksStandalone(blocks: Block[]): string | undefined {
const tmp = mkdtempSync(join(root, '.doc-typecheck-'))
try {
writeFileSync(join(tmp, 'tsconfig.json'), tempTsconfig())
for (const [index, block] of blocks.entries()) {
writeFileSync(join(tmp, `block-${index}.ts`), block.code.endsWith('\n') ? block.code : `${block.code}\n`)
}
try {
// Invoke tsc's JS entry through Node instead of a platform-specific shell shim.
execFileSync(process.execPath, ['node_modules/typescript/bin/tsc', '-b', join(tmp, 'tsconfig.json')], {
cwd: root,
stdio: 'pipe',
})
return undefined
} catch (error: unknown) {
const failed = error as { stdout?: Buffer; stderr?: Buffer }
return remapBlockPaths(`${failed.stdout?.toString() ?? ''}${failed.stderr?.toString() ?? ''}`, blocks)
}
} finally {
rmSync(tmp, { recursive: true, force: true })
}
}
/** Map virtual or temporary block paths back to their owning Markdown fences. */
function remapBlockPaths(output: string, blocks: Block[]): string {
return output.replace(/(?:[^\s:()]*[/\\])?block-(\d+)\.ts\((\d+),(\d+)\)/g, (_match, index: string, line: string, column: string) => {
const block = blocks[Number(index)]
if (!block) return `block-${index}.ts(${line},${column})`
return `${block.file} (block at line ${block.line}, +${line}:${column})`
})
}
const markdownGlobs = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md']
const files: string[] = []
@@ -114,45 +228,24 @@ if (checked.length === 0) {
process.exit(0)
}
const tmp = mkdtempSync(join(root, '.doc-typecheck-'))
try {
writeFileSync(join(tmp, 'tsconfig.json'), tempTsconfig())
const fileForBlock = new Map<string, Block>()
checked.forEach((block, i) => {
const name = `block-${i}.ts`
writeFileSync(join(tmp, name), block.code.endsWith('\n') ? block.code : `${block.code}\n`)
fileForBlock.set(name, block)
})
try {
// tsc's JS entry via the current node, not the .bin shim: the extensionless
// shim is not spawnable on Windows (the CVE-2024-27980 class the sibling
// scripts hit), and the .cmd variant would need shell:true, which
// concatenates args UNESCAPED — a hazard for the temp project path. The JS
// entry behaves identically on every platform.
execFileSync(process.execPath, ['node_modules/typescript/bin/tsc', '-b', join(tmp, 'tsconfig.json')], { cwd: root, stdio: 'pipe' })
} catch (error: unknown) {
const failed = error as { stdout?: Buffer; stderr?: Buffer }
const out = `${failed.stdout?.toString() ?? ''}${failed.stderr?.toString() ?? ''}`
// Rewrite "block-N.ts(line,col)" to the real "file:fenceLine" for triage.
const remapped = out.replace(/(?:[^\s:()]*[/\\])?block-(\d+)\.ts\((\d+),(\d+)\)/g, (_m, idx: string, ln: string, col: string) => {
const block = fileForBlock.get(`block-${idx}.ts`)
if (!block) return `block-${idx}.ts(${ln},${col})`
return `${block.file} (block at line ${block.line}, +${ln}:${col})`
})
console.error('doc-typecheck: documentation code blocks failed to compile.\n')
console.error(remapped)
process.exit(1)
}
const ratio = ignored.length / ratioDenominator
const skipped = all.length - ratioDenominator
console.log(`doc-typecheck: ${checked.length} block(s) compiled, ${ignored.length} ignored (${(ratio * 100).toFixed(0)}% opt-out), ${skipped} type-equiv/catalog (checked elsewhere).`)
// Guard against the escape hatch becoming the norm.
if (ratioDenominator >= 4 && ratio > 0.5) {
console.error(`doc-typecheck: too many blocks opt out of checking (${ignored.length}/${ratioDenominator}). Make them compile or delete them.`)
process.exit(1)
}
} finally {
rmSync(tmp, { recursive: true, force: true })
const useBuiltTypes = process.env.DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT === '1'
const compilationError = useBuiltTypes
? (() => {
const diagnostics = compileBlocksAgainstBuiltTypes(checked)
return diagnostics.length === 0 ? undefined : formatDiagnostics(diagnostics, checked)
})()
: compileBlocksStandalone(checked)
if (compilationError !== undefined) {
console.error('doc-typecheck: documentation code blocks failed to compile.\n')
console.error(compilationError)
process.exit(1)
}
const ratio = ignored.length / ratioDenominator
const skipped = all.length - ratioDenominator
console.log(`doc-typecheck: ${checked.length} block(s) compiled, ${ignored.length} ignored (${(ratio * 100).toFixed(0)}% opt-out), ${skipped} type-equiv/catalog (checked elsewhere).`)
// Guard against the escape hatch becoming the norm.
if (ratioDenominator >= 4 && ratio > 0.5) {
console.error(`doc-typecheck: too many blocks opt out of checking (${ignored.length}/${ratioDenominator}). Make them compile or delete them.`)
process.exit(1)
}

View File

@@ -67,6 +67,7 @@ const GROUP_ORDER = [
'tasks',
'workflow',
'web',
'spill',
'todo',
'cordis',
'hooks',
@@ -100,15 +101,15 @@ const SERVICE_ROLES: ServiceRole[] = [
title: 'Durable session persistence seam',
mode: 'seam',
implementations: ['session-persistence-jsonl', 'session-persistence-sqlite'],
consumers: ['agent-loop', 'acp', 'session-query'],
consumers: ['agent-loop', 'tool-bash', 'hooks-claude', 'hooks-codex', 'acp', 'session-query'],
note: 'Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time.',
},
{
key: 'sessionQuery',
pkg: 'session-query',
title: 'Exact session-history reads',
title: 'Exact session-history reads and traces',
mode: 'seam',
note: 'Resolves live and optional persisted logs into one logical corpus for exact reads.',
note: 'Resolves live and optional persisted logs into one logical corpus for exact reads and relationship traces.',
},
{
key: 'systemPrompt',
@@ -169,6 +170,13 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['tool-bash', 'hooks-claude', 'hooks-codex'],
note: 'The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors replace bash-local without touching them.',
},
{
key: 'bashEnv',
pkg: 'tool-bash',
title: 'Managed bash environment registry',
mode: 'core',
note: 'Plugins declare effect-scoped DSH_* facts; tool-bash collects one trusted snapshot per execution and the executor rebuilds the namespace.',
},
{
key: 'sandbox',
pkg: 'sandbox',
@@ -250,6 +258,15 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['tool-web'],
note: 'Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names.',
},
{
key: 'spillStore',
pkg: 'spill',
title: 'Spill storage seam',
mode: 'seam',
implementations: ['spill-local'],
consumers: ['spill-policy'],
note: 'The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill.',
},
{
key: 'workflows',
pkg: 'workflow',
@@ -860,7 +877,7 @@ function renderToolPipeline(): string {
` owned["Tool-owned session events<br/>${mermaidCode('todo/write')}, ${mermaidCode('fs/observed')}, ${mermaidCode('hook/invoked')}, ${mermaidCode('hook/result')}, ${mermaidCode('tool/code-dispatch')}"]`,
` post["${mermaidCode('tools/post-execute')} waterfall<br/>accept, block, replace, add context"]`,
` final["${mermaidCode('tools/result')} synchronous notification<br/>frozen authoritative outcome"]`,
' context["Buffered additionalContext<br/>context/message after all tool results"]',
' context["Buffered additionalContexts<br/>context/message after all tool results"]',
` toolResult["Session event: ${mermaidCode('tool/result')}<br/>single model-facing outcome"]`,
' allResults["All calls in the step settled<br/>and tool/result events recorded"]',
' presentResult["UI completed card<br/>presentResult(args, result)"]',
@@ -888,7 +905,7 @@ function renderToolPipeline(): string {
' allResults --> context',
'```',
'',
'Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`, while `tools/result` observes the immutable outcome after transforms, lossless-JSON validation, and outer error normalization. This lets hooks span tool families without coupling the tools to one policy service. Code Mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/code-dispatch`, surface denials as binding rejections, and omit `additionalContext` to preserve call/result adjacency.',
'Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`, while `tools/result` observes the immutable outcome after transforms, lossless-JSON validation, and outer error normalization. This lets hooks span tool families without coupling the tools to one policy service. Code Mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/code-dispatch`, surface denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.',
'',
...maintenanceFooter(maintenance),
].join('\n')

View File

@@ -27,6 +27,7 @@ const GROUP_ORDER = [
'compact',
'subagent',
'web',
'spill',
'timeout',
'todo',
'cordis',

View File

@@ -27,6 +27,7 @@ import * as ToolAskUser from '@deepseek-ai/dsh-tool-ask-user'
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import * as ToolFsSearch from '@deepseek-ai/dsh-tool-fs-search'
import * as ToolSkill from '@deepseek-ai/dsh-tool-skill'
import * as ToolTasks from '@deepseek-ai/dsh-tool-tasks'
import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
@@ -149,6 +150,23 @@ const TOOL_PACKAGES: ToolPackage[] = [
note:
'The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The tool schemas above are identical with or without the policy plugin.',
},
{
pkg: '@deepseek-ai/dsh-tool-fs-search',
dir: 'tool-fs-search',
source: 'packages/fs/tool-fs-search/src/index.ts',
requires: ['ctx.tools', 'ctx.bash', 'ctx.systemPrompt'],
writes: ['tool/call', 'tool/result'],
async mount(ctx) {
// The tools inject `bash` (search executes fixed `rg` commands through
// the executor seam, not ctx.fs); boot the local executor to satisfy it.
// `ctx.spillStore` is optional (read via ctx.get) and does not affect the
// schemas, so no spill backend is mounted.
await ctx.plugin(LocalBashExecutor)
await ctx.plugin(ToolFsSearch)
},
note:
'glob and grep are bash-backed discovery tools: they run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments.',
},
{
pkg: '@deepseek-ai/dsh-tool-skill',
dir: 'tool-skill',

View File

@@ -24,6 +24,7 @@ type GateStatus = 'pending' | 'running' | 'passed' | 'failed' | 'skipped'
interface Gate {
id: string
label: string
displayCommand: string
command: string
args: string[]
needs?: string[]
@@ -38,22 +39,39 @@ interface GateResult {
durationMs: number
stdout: string
stderr: string
output: GateOutputChunk[]
exitCode: number | null
error?: string
}
interface GateOutputChunk {
stream: 'stdout' | 'stderr'
text: string
}
interface RunningGate {
gate: Gate
promise: Promise<GateResult>
}
interface ConcurrencyDefault {
workers: number
source: string
}
const root = resolve(import.meta.dirname, '..')
const mode = parseMode(process.argv[2])
const gates = gatesForMode(mode)
const maxConcurrency = concurrencyFromEnv('DSH_GATE_CONCURRENCY', defaultConcurrency(gates.length))
const concurrencyDefault = defaultConcurrency(mode, gates.length)
const concurrencyOverride = process.env.DSH_GATE_CONCURRENCY
const maxConcurrency = concurrencyFromEnv('DSH_GATE_CONCURRENCY', concurrencyDefault.workers)
const verbose = process.env.DSH_GATE_VERBOSE === '1'
const startedAt = performance.now()
console.log(`run-gates: ${mode} running ${gates.length} gate(s) with ${maxConcurrency} worker(s).`)
const concurrencySource = concurrencyOverride === undefined || concurrencyOverride === ''
? concurrencyDefault.source
: '$DSH_GATE_CONCURRENCY'
console.log(`run-gates: ${mode} running ${gates.length} gate(s) with ${maxConcurrency} worker(s) from ${concurrencySource}.`)
const results = await runGates(gates, maxConcurrency)
printSummary(results, performance.now() - startedAt)
@@ -78,8 +96,15 @@ function parseMode(raw: string | undefined): Mode {
}
}
function defaultConcurrency(total: number): number {
return Math.min(total, Math.max(4, availableParallelism()))
function defaultConcurrency(selectedMode: Mode, total: number): ConcurrencyDefault {
const available = availableParallelism()
const modeLimit = selectedMode === 'pre-push' ? Math.min(4, available) : available
return {
workers: Math.min(total, modeLimit),
source: selectedMode === 'pre-push'
? `${available} available CPU(s), pre-push cap 4`
: `${available} available CPU(s)`,
}
}
function concurrencyFromEnv(name: string, fallback: number): number {
@@ -96,6 +121,7 @@ function pnpmScript(id: string, script: string, options: Partial<Gate> = {}): Ga
return {
id,
label: options.label ?? script,
displayCommand: `pnpm run ${script}`,
...pnpmInvocation(['run', script]),
...options,
}
@@ -105,6 +131,7 @@ function pnpmExec(id: string, args: string[], options: Partial<Gate> = {}): Gate
return {
id,
label: options.label ?? `pnpm exec ${args.join(' ')}`,
displayCommand: `pnpm exec ${args.join(' ')}`,
...pnpmInvocation(['exec', ...args]),
...options,
}
@@ -162,7 +189,10 @@ function gatesForMode(selected: Mode): Gate[] {
pnpmScript('snapshot', 'test:snapshot'),
pnpmScript('build', 'build'),
...hygieneLeafGates({ artifactNeeds: ['build'] }),
...docSyncLeafGates(),
...docSyncLeafGates({
docTypecheckNeeds: ['build'],
docTypecheckEnv: { DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1' },
}),
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
]
}
@@ -275,9 +305,15 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
]
}
function docSyncLeafGates(): Gate[] {
function docSyncLeafGates(options: {
docTypecheckNeeds?: string[]
docTypecheckEnv?: Record<string, string | undefined>
} = {}): Gate[] {
const docTypecheckOptions: Partial<Gate> = {}
if (options.docTypecheckNeeds !== undefined) docTypecheckOptions.needs = options.docTypecheckNeeds
if (options.docTypecheckEnv !== undefined) docTypecheckOptions.env = options.docTypecheckEnv
return [
pnpmScript('doc-typecheck', 'doc-typecheck'),
pnpmScript('doc-typecheck', 'doc-typecheck', docTypecheckOptions),
pnpmScript('cordis-catalog', 'verify-cordis-catalog', { label: 'cordis catalog' }),
pnpmScript('export-jsdoc', 'verify-export-jsdoc', { label: 'export jsdoc' }),
pnpmScript('tool-catalog', 'verify-tool-catalog', { label: 'tool catalog' }),
@@ -306,6 +342,7 @@ function demoSmokeGate(options: { needs?: string[] } = {}): Gate {
return {
id: 'demo-smoke',
label: 'demo smoke',
displayCommand: 'pnpm run demo:echo',
...pnpmInvocation(['run', 'demo:echo']),
input: 'echo ci smoke\n',
...dependencyOptions,
@@ -382,6 +419,7 @@ async function runGates(allGates: Gate[], maxActive: number): Promise<GateResult
durationMs: 0,
stdout: '',
stderr: '',
output: [],
exitCode: null,
error: `dependency failed or skipped: ${failedDeps.join(', ')}`,
}
@@ -416,8 +454,10 @@ async function runGate(gate: Gate): Promise<GateResult> {
const started = performance.now()
let stdout = ''
let stderr = ''
const output: GateOutputChunk[] = []
let spawnError: string | undefined
const exitCode = await new Promise<number | null>((resolveExit, reject) => {
const exitCode = await new Promise<number | null>((resolveExit) => {
const child = spawn(gate.command, gate.args, {
cwd: root,
env: { ...process.env, ...gate.env },
@@ -425,19 +465,28 @@ async function runGate(gate: Gate): Promise<GateResult> {
})
child.stdout.setEncoding('utf8')
child.stderr.setEncoding('utf8')
child.stdout.on('data', (chunk: string) => { stdout += chunk })
child.stderr.on('data', (chunk: string) => { stderr += chunk })
child.on('error', reject)
child.stdout.on('data', (chunk: string) => {
stdout += chunk
output.push({ stream: 'stdout', text: chunk })
})
child.stderr.on('data', (chunk: string) => {
stderr += chunk
output.push({ stream: 'stderr', text: chunk })
})
child.on('error', (error) => {
spawnError = `failed to start command: ${error.message}`
resolveExit(null)
})
child.on('close', resolveExit)
if (gate.input !== undefined) child.stdin.end(gate.input)
else child.stdin.end()
})
let status: GateStatus = exitCode === 0 ? 'passed' : 'failed'
let error: string | undefined
let status: GateStatus = exitCode === 0 && spawnError === undefined ? 'passed' : 'failed'
let error = spawnError
if (status === 'passed' && gate.verify !== undefined) {
try {
await gate.verify({ gate, status, durationMs: performance.now() - started, stdout, stderr, exitCode })
await gate.verify({ gate, status, durationMs: performance.now() - started, stdout, stderr, output, exitCode })
} catch (verifyError: unknown) {
status = 'failed'
error = verifyError instanceof Error ? verifyError.message : String(verifyError)
@@ -450,6 +499,7 @@ async function runGate(gate: Gate): Promise<GateResult> {
durationMs: performance.now() - started,
stdout,
stderr,
output,
exitCode,
}
if (error !== undefined) result.error = error
@@ -458,9 +508,16 @@ async function runGate(gate: Gate): Promise<GateResult> {
function printResult(result: GateResult): void {
const seconds = (result.durationMs / 1000).toFixed(2)
console.log(`\n== ${result.status.toUpperCase()} ${result.gate.label} (${seconds}s) ==`)
process.stdout.write(result.stdout)
process.stderr.write(result.stderr)
if (result.status === 'passed' && !verbose) {
console.log(`run-gates: PASS ${result.gate.label} (${seconds}s)`)
return
}
const heading = `${result.status.toUpperCase()} ${result.gate.label} (${seconds}s)`
const writeHeading = result.status === 'passed' ? console.log : console.error
writeHeading(`\n== ${heading} ==`)
if (result.status !== 'passed') console.error(`command: ${result.gate.displayCommand}`)
printOutput(result.output)
if (result.error !== undefined) console.error(result.error)
}
@@ -470,4 +527,22 @@ function printSummary(results: GateResult[], durationMs: number): void {
const skipped = results.filter(result => result.status === 'skipped').length
const seconds = (durationMs / 1000).toFixed(2)
console.log(`\nrun-gates: ${passed} passed, ${failed} failed, ${skipped} skipped in ${seconds}s.`)
const unsuccessful = results.filter(result => result.status === 'failed' || result.status === 'skipped')
if (unsuccessful.length === 0) return
console.error('run-gates: unsuccessful gates:')
for (const result of unsuccessful) {
const duration = (result.durationMs / 1000).toFixed(2)
const reason = result.error ?? (result.exitCode === null ? 'no exit code' : `exit ${result.exitCode}`)
console.error(` - ${result.status.toUpperCase()} ${result.gate.label} (${duration}s, ${reason})`)
console.error(` ${result.gate.displayCommand}`)
}
}
function printOutput(output: GateOutputChunk[]): void {
for (const chunk of output) {
if (chunk.stream === 'stdout') process.stdout.write(chunk.text)
else process.stderr.write(chunk.text)
}
}

View File

@@ -57,6 +57,7 @@ CUSTOM_CORDIS = """\
- id: agent-core
name: '@deepseek-ai/dsh-agent-spine-demo'
config:
workspaceContext: false
tools:
mode: both
- id: sessions

View File

@@ -13,6 +13,7 @@
{ "doc": "docs/core-data-structures/core.md", "symbol": "ToolSchema", "source": "packages/llm/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "LlmCallConfig", "source": "packages/llm/llm/src/call-config.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "InjectOptions", "source": "packages/core/agent/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "Agent", "source": "packages/core/agent/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "HookContext", "source": "packages/core/agent/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "PromptDecision", "source": "packages/core/agent/src/types.ts" },
@@ -33,6 +34,7 @@
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "ContentBlockMap", "source": "packages/llm/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "AppIdentity", "source": "packages/llm/llm/src/attribution.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "ContextEnvelope", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "SessionEventMap", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "EpochHeader", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "TodoItem", "source": "packages/core/session/src/types.ts" },
@@ -48,13 +50,18 @@
{ "doc": "docs/core-data-structures/persistence.md", "symbol": "SessionHeader", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/persistence.md", "symbol": "CreateSessionOptions", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/persistence.md", "symbol": "SessionLocation", "source": "packages/session-persistence/session-persistence/src/index.ts" },
{ "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionEventSurface", "source": "packages/session-query/session-query/src/types.ts" },
{ "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionRecord", "source": "packages/session-query/session-query/src/types.ts" },
{ "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionEventRecord", "source": "packages/session-query/session-query/src/types.ts" },
{ "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionLineageNode", "source": "packages/session-query/session-query/src/types.ts" },
{ "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionLineageTrace", "source": "packages/session-query/session-query/src/types.ts" },
{ "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionQueryErrorCode", "source": "packages/session-query/session-query/src/config.ts" },
{ "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionEventReadRequest", "source": "packages/session-query/session-query/src/types.ts" },
{ "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionEventWindow", "source": "packages/session-query/session-query/src/types.ts" },
{ "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionEventTraceRequest", "source": "packages/session-query/session-query/src/types.ts" },
{ "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionEventTrace", "source": "packages/session-query/session-query/src/types.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "ToolDefinition", "source": "packages/core/tools/src/index.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "SchemaProp", "source": "packages/core/tools/src/schema.ts" },
@@ -63,6 +70,7 @@
{ "doc": "docs/core-data-structures/tools.md", "symbol": "ToolExecutionToken", "source": "packages/core/tools/src/index.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "ToolExecutionInput", "source": "packages/core/tools/src/index.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "ToolExecution", "source": "packages/core/tools/src/index.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "ToolRunContext", "source": "packages/core/tools/src/index.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "ToolGuard", "source": "packages/core/tools/src/index.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "ToolRestriction", "source": "packages/core/tools/src/index.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "ToolExecutionResult", "source": "packages/core/tools/src/index.ts" },
@@ -86,6 +94,8 @@
{ "doc": "docs/core-data-structures/approval.md", "symbol": "ApprovalPolicy", "source": "packages/ui/user-approval/src/index.ts" },
{ "doc": "docs/core-data-structures/approval.md", "symbol": "ApprovalRequest", "source": "packages/ui/user-approval/src/index.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "DshEnvironmentKey", "source": "packages/bash/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "DshEnvironment", "source": "packages/bash/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashExecRequest", "source": "packages/bash/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashExecSpec", "source": "packages/bash/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashRunResult", "source": "packages/bash/bash/src/types.ts" },
@@ -117,6 +127,7 @@
{ "doc": "docs/core-data-structures/filesystem.md", "symbol": "FsTargetKey", "source": "packages/fs/fs/src/types.ts" },
{ "doc": "docs/core-data-structures/filesystem.md", "symbol": "FsVersion", "source": "packages/fs/fs/src/types.ts" },
{ "doc": "docs/core-data-structures/filesystem.md", "symbol": "FsInfo", "source": "packages/fs/fs/src/types.ts" },
{ "doc": "docs/core-data-structures/filesystem.md", "symbol": "FsPathInfo", "source": "packages/fs/fs/src/types.ts" },
{ "doc": "docs/core-data-structures/filesystem.md", "symbol": "FsDirEntry", "source": "packages/fs/fs/src/types.ts" },
{ "doc": "docs/core-data-structures/filesystem.md", "symbol": "FsWriteIntent", "source": "packages/fs/fs/src/types.ts" },
{ "doc": "docs/core-data-structures/filesystem.md", "symbol": "FsWriteOutcome", "source": "packages/fs/fs/src/types.ts" },
@@ -152,6 +163,12 @@
{ "doc": "docs/core-data-structures/web.md", "symbol": "WebFetchResult", "source": "packages/web/web/src/types.ts" },
{ "doc": "docs/core-data-structures/web.md", "symbol": "WebFetchBody", "source": "packages/web/web/src/types.ts" },
{ "doc": "docs/core-data-structures/spill.md", "symbol": "SaveTextSpill", "source": "packages/spill/spill/src/types.ts" },
{ "doc": "docs/core-data-structures/spill.md", "symbol": "SpillOwner", "source": "packages/spill/spill/src/types.ts" },
{ "doc": "docs/core-data-structures/spill.md", "symbol": "SpillSource", "source": "packages/spill/spill/src/types.ts" },
{ "doc": "docs/core-data-structures/spill.md", "symbol": "SpillRef", "source": "packages/spill/spill/src/types.ts" },
{ "doc": "docs/core-data-structures/spill.md", "symbol": "SpillLocator", "source": "packages/spill/spill/src/types.ts" },
{ "doc": "docs/core-data-structures/workflow.md", "symbol": "WorkflowStartRequest", "source": "packages/workflow/workflow/src/types.ts" },
{ "doc": "docs/core-data-structures/workflow.md", "symbol": "WorkflowMeta", "source": "packages/workflow/workflow/src/types.ts" },
{ "doc": "docs/core-data-structures/workflow.md", "symbol": "WorkflowResult", "source": "packages/workflow/workflow/src/types.ts" },

View File

@@ -30,6 +30,7 @@ interface SentenceContract {
const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
'packages/core/scope': 'The package is a model-agnostic registration and lifecycle primitive; model-facing consumers own any context selection.',
'packages/util/brand': 'The package is a type-only primitive erased at compile time.',
'packages/util/paths': 'The package only resolves harness-owned host paths; model-facing consumers own any rendered use.',
}
/**
@@ -54,9 +55,12 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/session-query/session-query': { kind: 'none', reason: 'The trusted query service exposes cloned records only to callers and registers no model surface.' },
'packages/skill/skill': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-skill.' },
'packages/skill/skill-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-skill.' },
'packages/spill/spill': { kind: 'indirect', reason: 'The storage seam delegates model rendering to spill consumers.' },
'packages/spill/spill-local': { kind: 'indirect', reason: 'The storage backend delegates model rendering to spill consumers.' },
'packages/subagent/subagent': { kind: 'indirect', reason: 'The provider registry delegates parent-model rendering to dsh-tool-subagent.' },
'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/agent-loop-testkit': { kind: 'none', reason: 'The test helper mounts services but neither drives nor modifies model 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.' },
@@ -67,7 +71,9 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/examples/jsonrpc-demo': { kind: 'indirect', reason: 'Only the externally configured plugin tree contributes model context.' },
'packages/ui/permission': { kind: 'indirect', reason: 'The service writes mechanism events rendered by dsh-user-approval and dsh-tool-bash.' },
'packages/ui/user-interaction': { kind: 'indirect', reason: 'Model-facing consumers render provider answers and seam errors.' },
'packages/util/home': { kind: 'indirect', reason: 'Only dsh-tool-bash exposes the resolved home to model commands.' },
'packages/util/timeout': { kind: 'indirect', reason: 'Only timeout consumers render timeout outcomes.' },
'packages/util/retention': { kind: 'indirect', reason: 'Only retention consumers render retained content and omission metadata.' },
'packages/web/web': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-web.' },
'packages/web/web-fetch-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-web.' },
'packages/web/web-search-exa': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-web.' },