Merge branch 'feat/plugin-mgmt-1-boot' into feat/plugin-mgmt-2-manager

# Conflicts:
#	apps/cli/src/bin.ts
#	docs/config-catalog.i18n.yaml
#	docs/config-catalog.md
#	docs/config-catalog.zh.md
#	packages/api/remotes/src/client/index.ts
#	packages/boot/app-boot/src/index.ts
#	packages/boot/app-boot/src/profile.ts
#	packages/host/README.i18n.yaml
#	packages/host/README.md
#	packages/host/README.zh.md
#	scripts/verify-package-readme-model-experience.ts
This commit is contained in:
Yichen Jiang committed 2026-09-08 16:52:09 +08:00
commit bb408abe28
2137 files changed
+73685 -9705

No files matched your search

+29 -1
View File
@@ -1,5 +1,5 @@
import { spawnSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
@@ -23,6 +23,32 @@ function run(env: NodeJS.ProcessEnv, ...args: string[]) {
}
describe('Python runtime executable builder CLI', () => {
it('keeps the single-file dispatcher on the Python packaging surface', () => {
const bootstrapPath = resolve(root, 'python/sdk-runtime/runtime-bootstrap.mjs')
const bootstrap = readFileSync(bootstrapPath, 'utf8')
const cliConfig = readFileSync(resolve(root, 'apps/cli/tsdown.config.ts'), 'utf8')
const cliTsconfig = readFileSync(resolve(root, 'apps/cli/tsconfig.json'), 'utf8')
const cliManifest = JSON.parse(readFileSync(resolve(root, 'apps/cli/package.json'), 'utf8')) as {
dependencies?: Record<string, string>
devDependencies?: Record<string, string>
}
const runtimeManifest = JSON.parse(readFileSync(resolve(root, 'python/sdk-runtime/package.json'), 'utf8')) as {
dependencies?: Record<string, string>
}
expect(existsSync(resolve(root, 'apps/cli/src/runtime-bootstrap.ts'))).toBe(false)
expect(cliConfig).not.toContain('runtime-bootstrap')
expect(cliConfig).toContain("clean: ['lib/*.js']")
expect(cliTsconfig).not.toContain('packages/subprocess/subprocess-local')
expect(cliManifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subprocess-local')
expect(cliManifest.devDependencies).toHaveProperty('@deepseek-ai/dsh-subprocess-local')
expect(runtimeManifest.dependencies).toHaveProperty('@deepseek-ai/dsh-subprocess-local')
expect(bootstrap).toContain("import('@deepseek-ai/dsh/lib/bin.js')")
expect(bootstrap).toContain('await runCli()')
expect(bootstrap).toContain("import('@deepseek-ai/dsh-subprocess-local/runner')")
expect(bootstrap).toContain('await runSelectedSubprocessRunner(selection)')
})
it('runs pnpm through its JavaScript entrypoint without a command shell', () => {
const result = run(
{ npm_execpath: 'C:\\tools\\pnpm.cjs' },
@@ -34,6 +60,8 @@ describe('Python runtime executable builder CLI', () => {
expect(result.status).toBe(0)
expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs run verify-runtime-closure`)
expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs --filter dsh-python-runtime-closure deploy`)
expect(result.stdout).not.toContain(resolve(root, 'python/sdk-runtime/runtime-bootstrap.mjs'))
expect(result.stdout).toContain('"bin":"runtime-bootstrap.mjs"')
expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs exec pkg`)
expect(result.stdout).not.toMatch(/pnpm\.cmd/i)
})
+3 -3
View File
@@ -17,8 +17,8 @@ const root = resolve(import.meta.dirname, '..')
/** The closure manifest whose dependencies define the executable. */
const DEPLOY_ROOT_PACKAGE = 'dsh-python-runtime-closure'
/** The sole application launcher inside the deployed closure. */
const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh/lib/bin.js'
/** The sole executable entry inside the deployed closure. */
const ENTRY_BIN = 'runtime-bootstrap.mjs'
/** Python-visible executable basename. */
const OUTPUT_BASENAME = 'deepseek-harness-sdk-runtime'
/** Default Node major; SEA mode requires at least Node 22. */
@@ -406,7 +406,7 @@ class SingleExeBuild {
throw new Error(`build-exe-for-python-sdk: ${manifestPath} missing — pnpm deploy did not produce a staged package.`)
}
if (!existsSync(join(this.staging, ENTRY_BIN))) {
throw new Error(`build-exe-for-python-sdk: ${join(this.staging, ENTRY_BIN)} missing — run without --skip-build so lib/ artifacts exist.`)
throw new Error(`build-exe-for-python-sdk: staged bootstrap ${join(this.staging, ENTRY_BIN)} is missing.`)
}
const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as Record<string, unknown>
await writeFile(manifestPath, `${JSON.stringify({ ...manifest, ...patch }, null, 2)}\n`)
+16 -3
View File
@@ -53,10 +53,16 @@ const experimentalPackageDirectory = /^packages\/experimental\/[^/]+$/
/** npm namespace reserved for private experimental packages. */
const experimentalPackageNamePrefix = '@deepseek-ai/dsh-experimental-'
/** Directories whose packages this repository publishes: one release member each. */
const releaseMemberDirectory = /^(?:packages\/(?!experimental\/)[^/]+\/[^/]+|apps\/[^/]+|vendor\/[^/]+)$/
const releaseMemberDirectory = /^(?:packages\/(?!experimental\/)[^/]+\/[^/]+|apps\/(?!desktop(?:-host)?$)[^/]+|vendor\/[^/]+)$/
/** Installable application assembled by electron-builder rather than published to npm. */
const desktopApplicationDirectory = 'apps/desktop'
const localArtifactDirs = new Set(['node_modules'])
const appPackageFiles: Readonly<Record<string, readonly string[]>> = {
'@deepseek-ai/dsh': ['lib/*.js'],
'@deepseek-ai/dsh-desktop-host': [
'lib/index.js',
'config/desktop.cordis.patch.yml',
],
// Sourcemaps stay out by payload policy; the worker-preview surface
// (dist/preview.html and dist/preview/) backs private experimental
// packages and is not published.
@@ -147,6 +153,7 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
// The glob covers whichever sheets a package emits; sourcemaps stay
// unpublished, as everywhere else in the repository.
'@deepseek-ai/dsh-client-ui-primitives': ['lib/**/*.css'],
'@deepseek-ai/dsh-client-ui-dockkit': ['lib/**/*.css'],
'@deepseek-ai/dsh-client-web': ['lib/**/*.css'],
'@deepseek-ai/dsh-client-ui-theme': ['lib/styles'],
// The CPython side ships as source .py files, published as-is rather than built.
@@ -164,11 +171,17 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
// also shares its generated FFI code through a hashed runtime chunk.
'@deepseek-ai/dsh-sandbox-windows-acl': ['lib/runner.js', 'lib/types-*.js'],
'@deepseek-ai/dsh-skill-badge': ['assets'],
// Ordinary native containment ships a path-loaded runner and its shared
// runner chunk beside the existing node-pty permission repair.
'@deepseek-ai/dsh-subprocess-local': [
'lib/runner.js',
'lib/runner-*.js',
'scripts/ensure-spawn-helper.mjs',
],
// tsdown shares the repository/pack code between the lib entry and the bin
// through a hashed chunk. The committed bin.js is the link target pnpm can
// resolve at install time, before the build produces lib/bin.js.
'@deepseek-ai/dsh-experimental-webworker-packer': ['bin.js', 'lib/repository-*.js'],
'@deepseek-ai/dsh-subprocess-local': ['scripts/ensure-spawn-helper.mjs'],
// The package probe's child process entry, path-loaded beside the lib.
'@deepseek-ai/dsh-app-boot': ['lib/probe-child.js', 'lib/patch-file.js'],
}
@@ -355,7 +368,7 @@ export function checkWorkspaceManifest({ dir, manifest }: WorkspaceManifest): st
}
}
if (dir.startsWith('apps/') && manifest.name?.startsWith('@deepseek-ai/')) {
if (dir.startsWith('apps/') && dir !== desktopApplicationDirectory && manifest.name?.startsWith('@deepseek-ai/')) {
const expectedFiles = appPackageFiles[manifest.name]
if (expectedFiles === undefined) {
errors.push(`${label}: app package has no publication files policy`)
+178
View File
@@ -0,0 +1,178 @@
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { spawnSync } from 'node:child_process'
import { runInNewContext } from 'node:vm'
import * as yaml from 'js-yaml'
import { describe, expect, it } from 'vitest'
interface Step {
name?: string
uses?: string
if?: string
run?: string
env?: Record<string, string>
with?: Record<string, unknown>
}
interface CompatibilityJob {
'runs-on': string
if: string
env: Record<string, string>
strategy: { 'fail-fast': boolean; matrix: { include: Array<{ node: string | number; name: string; runner: string; gate_concurrency: string }> } }
steps: Step[]
}
const workflow = yaml.load(readFileSync(resolve(import.meta.dirname, '../.github/workflows/ci.yml'), 'utf8')) as {
jobs: { 'node-compat': CompatibilityJob; 'python-sdk': { 'runs-on': string } }
}
const job = workflow.jobs['node-compat']
const labels = ['self-hosted', 'linux', 'x64', 'vm-backup']
// This wiring check uses equal-typed, canonical-case fixtures. Actions compares
// strings case-insensitively; JavaScript does not. This is not an Actions evaluator.
function evaluate(expression: string, context: Record<string, unknown>): unknown {
const body = expression.trim().slice(3, -2)
return runInNewContext(body, {
...context, fromJSON: JSON.parse,
}, { timeout: 1000 }) as unknown
}
function route(options: { mode?: string; author?: string; repository?: string; fork?: boolean; actor?: string } = {}): unknown {
return evaluate(job['runs-on'], {
vars: { DSH_CI_FAILOVER_LINUX: options.mode ?? 'selfhosted' },
github: {
repository: 'deepseek-harness/deepseek-harness',
actor: options.actor ?? 'maintainer',
event: { pull_request: {
user: { login: options.author ?? 'maintainer' },
head: { repo: { full_name: options.repository ?? 'deepseek-harness/deepseek-harness', fork: options.fork ?? false } },
} },
},
matrix: { runner: 'ubuntu-latest' },
})
}
describe('Node compatibility self-hosted routing', () => {
it('uses the Linux pool only for opted-in repository-owned PRs', () => {
expect(route()).toEqual(labels)
for (const mode of ['', 'hosted', 'unexpected']) expect(route({ mode })).toBe('ubuntu-latest')
expect(route({ author: 'dependabot[bot]', actor: 'maintainer' })).toBe('ubuntu-latest')
expect(route({ repository: 'outsider/fork', fork: true })).toBe('ubuntu-latest')
expect(route({ repository: 'outsider/fork', fork: false })).toBe('ubuntu-latest')
expect(route({ fork: true })).toBe('ubuntu-latest')
expect(route({ repository: '' })).toBe('ubuntu-latest')
})
it('preserves all three required version jobs and their concurrency', () => {
expect(job.if).toBe("github.event_name == 'pull_request'")
expect(job.strategy['fail-fast']).toBe(false)
expect(job.strategy.matrix.include).toEqual([
{ node: '22.19', name: 'node 22.19', runner: 'ubuntu-latest', gate_concurrency: '1' },
{ node: '24.9', name: 'node 24.9', runner: 'ubuntu-latest', gate_concurrency: '1' },
{ node: 26, name: 'node 26', runner: 'ubuntu-latest', gate_concurrency: '1' },
])
expect(job.env.DSH_GATE_CONCURRENCY).toBe('${{ matrix.gate_concurrency }}')
expect(job.steps.map(step => step.run)).toContain('pnpm run check:node-compat')
expect(job.steps.map(step => step.run)).toContain('pnpm exec vitest run packages/boot/app-boot/tests/loader-shape.compat.spec.ts')
expect(workflow.jobs['python-sdk']['runs-on']).toBe('ubuntu-latest')
})
it('isolates version installs and enables hosted package caching only on hosted runners', () => {
const setup = job.steps.find(step => step.uses === 'actions/setup-node@v6')!
expect(setup.env).toEqual({
NODE_OPTIONS: "${{ runner.environment == 'self-hosted' && '--import=./scripts/ci-compatible-toolcache.mjs' || '' }}",
})
expect(setup.with?.['node-version']).toBe('${{ matrix.node }}')
expect(setup.with?.['package-manager-cache']).toBe(false)
for (const [environment, cache] of [['github-hosted', 'pnpm'], ['self-hosted', '']]) {
const context = { runner: { environment, temp: '/runner/temp', tool_cache: '/runner/toolcache' } }
expect(evaluate(setup.with?.cache as string, context)).toBe(cache)
expect(evaluate(setup.env!.NODE_OPTIONS!, context)).toBe(
environment === 'self-hosted' ? '--import=./scripts/ci-compatible-toolcache.mjs' : '',
)
}
expect(job.steps[0]?.with).toEqual({ 'persist-credentials': false })
expect(job.steps.some(step => step.uses?.startsWith('actions/cache/'))).toBe(false)
})
it('overrides runner exports inside setup-node without affecting later Node processes', () => {
const setup = job.steps.find(step => step.uses === 'actions/setup-node@v6')!
const nodeOptions = evaluate(setup.env!.NODE_OPTIONS!, { runner: { environment: 'self-hosted' } }) as string
const root = mkdtempSync(join(tmpdir(), 'ci-compatible preload-'))
try {
const env = { PATH: process.env.PATH, RUNNER_TEMP: root, RUNNER_TOOL_CACHE: join(root, 'persistent') }
const probe = (options: Record<string, string | undefined>) => {
const child = spawnSync(process.execPath, ['-p', 'process.env.RUNNER_TOOL_CACHE'], {
cwd: resolve(import.meta.dirname, '..'), env: options, encoding: 'utf8', timeout: 10_000,
})
expect(child.error).toBeUndefined()
expect(child.signal).toBeNull()
return child
}
const setupChild = probe({ ...env, NODE_OPTIONS: nodeOptions })
expect(setupChild.status, setupChild.stderr).toBe(0)
expect(setupChild.stdout.trim()).toBe(join(root, 'node-compat-toolcache'))
const normalChild = probe(env)
expect(normalChild.status, normalChild.stderr).toBe(0)
expect(normalChild.stdout.trim()).toBe(env.RUNNER_TOOL_CACHE)
const missingTemp = probe({ ...env, RUNNER_TEMP: undefined, NODE_OPTIONS: nodeOptions })
expect(missingTemp.status).not.toBe(0)
expect(missingTemp.stderr).toContain('requires an absolute RUNNER_TEMP')
expect(job.env).not.toHaveProperty('NODE_OPTIONS')
expect(job.steps.filter(step => step.env?.NODE_OPTIONS)).toEqual([setup])
} finally {
rmSync(root, { recursive: true, force: true })
}
})
it.skipIf(process.platform === 'win32')('rejects a Node executable outside its runner temporary installation', () => {
const step = job.steps.find(candidate => candidate.name === 'Verify isolated Node installation')!
expect(step.if).toBe("runner.environment == 'self-hosted'")
for (const [executable, status] of [
['/runner temp/node-compat-toolcache/node/24.9.0/x64/bin/node', 0],
['/shared/toolcache/node/24.9.0/x64/bin/node', 1],
['/runner temp/node-compat-toolcache-other/node', 1],
] as const) {
const child = spawnSync('bash', ['-e', '-u', '-o', 'pipefail', '-c', 'node() { printf "%s" "$TEST_EXECUTABLE"; }; ' + step.run!], {
env: { PATH: process.env.PATH, RUNNER_TEMP: '/runner temp', TEST_EXECUTABLE: executable }, encoding: 'utf8', timeout: 10_000,
})
expect(child.error).toBeUndefined()
expect(child.signal).toBeNull()
expect(child.status, child.stderr).toBe(status)
}
})
it.skipIf(process.platform === 'win32')('configures generated caches before pnpm without changing HOME or global links', () => {
const index = job.steps.findIndex(step => step.name === 'Isolate compatibility caches')
const step = job.steps[index]!
expect(index).toBeGreaterThan(0)
expect(index).toBeLessThan(job.steps.findIndex(candidate => candidate.uses === 'pnpm/action-setup@v4'))
expect(step.if).toBe("runner.environment == 'self-hosted'")
const root = mkdtempSync(join(tmpdir(), 'ci-compatible-selfhosted-'))
try {
const outputs = ['runner-a', 'runner-b'].map((runner) => {
const envFile = join(root, runner + '.env')
const temp = join(root, runner)
const child = spawnSync('bash', ['-e', '-u', '-o', 'pipefail', '-c', step.run!], {
env: { PATH: process.env.PATH, HOME: join(root, 'shared home'), RUNNER_TEMP: temp, GITHUB_ENV: envFile },
encoding: 'utf8', timeout: 10_000,
})
expect(child.error).toBeUndefined()
expect(child.signal).toBeNull()
expect(child.status, child.stderr).toBe(0)
const output = readFileSync(envFile, 'utf8')
expect(output).toBe([
'NODE_COMPILE_CACHE=' + temp + '/node-compile-cache',
'npm_config_devdir=' + temp + '/node-gyp',
'PNPM_CONFIG_STORE_DIR=' + join(root, 'shared home') + '/.local/share/pnpm/store',
'',
].join('\n'))
return output
})
expect(outputs[0]).not.toBe(outputs[1])
} finally {
rmSync(root, { recursive: true, force: true })
}
})
})
+8
View File
@@ -0,0 +1,8 @@
import assert from 'node:assert/strict'
import { isAbsolute, join } from 'node:path'
// The Actions runner exports RUNNER_TOOL_CACHE after step env. Run inside the
// setup-node process so version installs use runner temp rather than shared state.
const temp = process.env.RUNNER_TEMP
assert(temp && isAbsolute(temp), 'Node compatibility setup requires an absolute RUNNER_TEMP')
process.env.RUNNER_TOOL_CACHE = join(temp, 'node-compat-toolcache')
+129 -30
View File
@@ -36,6 +36,45 @@ describe('CI workflow', () => {
}
})
it.each(['node-24', 'node-24-coverage', 'node-24-consumers'])(
'%s keeps tool and fixture temporary files under runner cleanup',
(jobName) => {
const job = workflowJob(loadWorkflow('.github/workflows/ci.yml'), jobName)
if (!Array.isArray(job.steps)) throw new TypeError(`${jobName} must define steps`)
expect(job.steps[0]).toEqual({
name: 'Use runner-owned temporary storage',
run: [
'echo "TMPDIR=${{ runner.temp }}" >> "$GITHUB_ENV"',
...(jobName === 'node-24-consumers'
? ['echo "PLAYWRIGHT_BROWSERS_PATH=${RUNNER_TEMP%/*}/ms-playwright" >> "$GITHUB_ENV"']
: []),
'',
].join('\n'),
})
if (jobName === 'node-24-consumers') {
const browserCache: unknown = job.steps.find(step => isRecord(step) && isRecord(step.with)
&& step.with.path === '${{ env.PLAYWRIGHT_BROWSERS_PATH }}')
expect(browserCache).toMatchObject({ uses: 'actions/cache/restore@v4' })
}
const store: unknown = job.steps.find(step => isRecord(step) && step.name === 'Configure pnpm store path')
expect(store).toMatchObject({
run: [
'store_root="$HOME/.local/share/pnpm/store"',
'echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"',
'store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)',
'echo "path=$store_path" >> "$GITHUB_OUTPUT"',
'',
].join('\n'),
})
for (const step of job.steps) {
if (isRecord(step) && isRecord(step.env)) {
expect(step.env.TMPDIR).toBeUndefined()
expect(step.env.npm_config_cache).toBeUndefined()
}
}
},
)
it('isolates the python SDK exe pnpm setup destination per job', () => {
const workflow: unknown = yaml.load(readFileSync(resolve(root, '.github/workflows/build-exe-for-python-sdk.yml'), 'utf8'))
if (!isRecord(workflow) || !isRecord(workflow.jobs)) throw new TypeError('build-exe-for-python-sdk.yml must define jobs')
@@ -55,11 +94,10 @@ describe('CI workflow', () => {
}
})
it('keeps required Wine and split native Windows jobs with failover, plus a master-only standby', () => {
it('keeps split native Windows PR jobs with failover, plus a master-only standby', () => {
const workflow = loadWorkflow('.github/workflows/ci.yml')
const masterWorkflow = loadWorkflow('.github/workflows/ci-master.yml')
if (!isRecord(workflow.jobs)
|| !isRecord(workflow.jobs.windows)
|| !isRecord(workflow.jobs['windows-build'])
|| !isRecord(workflow.jobs['windows-coverage'])
|| !isRecord(workflow.jobs['windows-native-tests'])
@@ -71,17 +109,14 @@ describe('CI workflow', () => {
|| !isRecord(workflow.jobs['node-compat'])
|| !isRecord(workflow.jobs['all-checks-passed'])
|| !isRecord(masterWorkflow.jobs)
|| !isRecord(masterWorkflow.jobs['wine-apt-cache'])
|| !isRecord(masterWorkflow.jobs['serial-windows'])) {
throw new TypeError('CI workflow must define windows, windows-build, windows-coverage, windows-native-tests, windows-observational, node-24, node-24-coverage, node-24-bench, node-24-consumers, node-compat, and all-checks-passed; ci-master must define wine-apt-cache and serial-windows')
throw new TypeError('CI workflow must define windows-build, windows-coverage, windows-native-tests, windows-observational, node-24, node-24-coverage, node-24-bench, node-24-consumers, node-compat, and all-checks-passed; ci-master must define serial-windows')
}
const windows = workflow.jobs.windows
const windowsBuild = workflow.jobs['windows-build']
const windowsCoverage = workflow.jobs['windows-coverage']
const windowsNativeTests = workflow.jobs['windows-native-tests']
const windowsObservational = workflow.jobs['windows-observational']
const wineAptCache = masterWorkflow.jobs['wine-apt-cache']
const serialWindows = masterWorkflow.jobs['serial-windows']
const node24 = workflow.jobs['node-24']
const node24Coverage = workflow.jobs['node-24-coverage']
@@ -89,19 +124,9 @@ describe('CI workflow', () => {
const node24Consumers = workflow.jobs['node-24-consumers']
const nodeCompat = workflow.jobs['node-compat']
const aggregate = workflow.jobs['all-checks-passed']
if (!Array.isArray(windows.steps) || !Array.isArray(aggregate.needs)) {
throw new TypeError('Windows job must define steps and the aggregate must define needs')
if (!Array.isArray(aggregate.needs)) {
throw new TypeError('CI aggregate must define needs')
}
const commandSteps = windows.steps.filter((step): step is Record<string, unknown> & { run: string } => (
isRecord(step) && typeof step.run === 'string'
))
// Required PR job: Wine on ubuntu-latest, runs wine-windows-gates.sh.
expect(windows['runs-on']).toBe('ubuntu-latest')
expect(windows.name).toBe('windows node 24 / wine blocking')
expect(windows.if).toBe("github.event_name == 'pull_request'")
expect(commandSteps.some(step => step.run.includes('wine-windows-gates.sh'))).toBe(true)
// The split native jobs all resolve their pool through the Windows switch.
for (const [jobName, job] of [['windows-build', windowsBuild], ['windows-coverage', windowsCoverage], ['windows-native-tests', windowsNativeTests], ['windows-observational', windowsObservational]] as const) {
expect(typeof job['runs-on']).toBe('string')
@@ -183,10 +208,6 @@ describe('CI workflow', () => {
expect(windowsObservational.name).toBe('windows node 24 / observational')
expect(windowsObservational['continue-on-error']).toBe(true)
// wine-apt-cache: master-only, seeds the Wine apt cache, lives in ci-master.
expect(wineAptCache.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'")
expect(wineAptCache['runs-on']).toBe('ubuntu-latest')
// serial-windows: master-only standby, self-hosted, non-blocking, lives in ci-master.
expect(serialWindows.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'")
expect(serialWindows['runs-on']).toEqual(['self-hosted', 'dsh-win-ci', 'windows'])
@@ -220,16 +241,25 @@ describe('CI workflow', () => {
expect(serialGate).toBeDefined()
expect(serialGate!.env).toMatchObject({ DSH_COVERAGE_TEST_TIMEOUT_MS: '90000' })
// Aggregate: Wine and the required split native jobs are needed;
// windows-coverage is temporarily non-blocking while Windows ACP
// half-close tests are stabilized; observational stays out too.
expect(aggregate.needs).toContain('windows')
expect(aggregate.needs).not.toContain('windows')
expect(aggregate.needs).toContain('windows-build')
// The benchmark lane is a required verdict input and runs alone so its
// wall-clock budgets never share a runner with a concurrent aggregate.
expect(aggregate.needs).toContain('node-24-bench')
expect(node24Bench.name).toBe('node 24 / benchmarks')
expect(node24Bench.env).toBeUndefined()
expect(node24Bench.steps).toContainEqual({
name: 'Install benchmark browser and hosted dependencies',
run: 'pnpm --filter @deepseek-ai/dsh-benchmarks exec playwright install --with-deps chromium',
})
expect(JSON.stringify(node24Bench.steps)).not.toContain('DSH_CI_FAILOVER_LINUX')
expect(node24Bench.steps).toContainEqual({
name: 'Run performance benchmarks',
env: { DSH_GATE_VERBOSE: '1' },
run: 'pnpm run check:ci:bench',
})
expect(aggregate.needs).not.toContain('windows-coverage')
expect(aggregate.needs).toContain('windows-native-tests')
expect(aggregate.needs).not.toContain('windows-observational')
@@ -362,9 +392,7 @@ describe('CI workflow', () => {
expect(job.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'")
}
// What bounds the cost of exempting push: a master push may only carry the
// cache seeder and the two drills. Any job reachable on push would start
// accumulating uncancelled runs, so the set is pinned here.
// Pin the post-merge runtime, Wine, and standby inventory.
const NOT_PUSH_REACHABLE = new Set([
"github.event_name == 'workflow_dispatch' && inputs.suite == 'larger-runner-benchmark'",
"github.event_name == 'workflow_dispatch' && inputs.suite == 'consolidated-runner-benchmark'",
@@ -379,7 +407,7 @@ describe('CI workflow', () => {
})
.map(([name]) => name)
.sort()
expect(pushReachable).toEqual(['serial-linux-selfhosted', 'serial-windows', 'wine-apt-cache'])
expect(pushReachable).toEqual(['python-runtime', 'serial-linux-selfhosted', 'serial-windows', 'windows'])
// Why workflow_dispatch must keep cancelling: each benchmark fans out to a
// dozen larger runners at once, in this same group on master. If it stopped
@@ -395,6 +423,36 @@ describe('CI workflow', () => {
}
})
it('redirects the Node compile cache to the data-volume runner temp before the first pnpm call', () => {
const prWorkflow = loadWorkflow('.github/workflows/ci.yml')
const masterWorkflow = loadWorkflow('.github/workflows/ci-master.yml')
const redirectLanes = [
[prWorkflow, 'node-24'],
[prWorkflow, 'node-24-coverage'],
[prWorkflow, 'node-24-consumers'],
[masterWorkflow, 'serial-linux-selfhosted'],
] as const
for (const [workflow, jobKey] of redirectLanes) {
const job = workflowJob(workflow, jobKey)
if (!Array.isArray(job.steps)) throw new TypeError(`${jobKey} must define steps`)
const redirectStepIndex = job.steps.findIndex((step): step is Record<string, unknown> & { run: string } => (
isRecord(step) && typeof step.run === 'string'
&& step.run.includes('NODE_COMPILE_CACHE=${{ runner.temp }}/node-compile-cache')
&& step.run.includes('"$GITHUB_ENV"')
))
// Removing this injection would send every pnpm call in the lane (setup,
// store-path probe, install, and the gate) back to the root partition's
// /tmp; rationale in
// .agents/notes/implemented/process/2026-08-28-ci-node-compile-cache-data-disk.md.
expect(redirectStepIndex, `${jobKey} must inject NODE_COMPILE_CACHE into GITHUB_ENV`).toBeGreaterThan(-1)
const pnpmSetupIndex = job.steps.findIndex((step): step is Record<string, unknown> & { uses: string } => (
isRecord(step) && typeof step.uses === 'string' && step.uses.includes('pnpm/action-setup')
))
expect(pnpmSetupIndex, `${jobKey} must run pnpm/action-setup`).toBeGreaterThan(-1)
expect(redirectStepIndex, `${jobKey} must redirect before pnpm/action-setup runs pnpm`).toBeLessThan(pnpmSetupIndex)
}
})
it('keeps supported LSP source under native Windows coverage', () => {
const config = readFileSync(resolve(root, 'vitest.config.ts'), 'utf8')
@@ -403,7 +461,7 @@ describe('CI workflow', () => {
expect(config).not.toContain('packages/lsp/lsp-stdio/src/instance.ts')
})
it('requires release-shaped Python runtime validation on every published target', () => {
it('requires release-shaped Python runtime validation on Linux and Windows x64', () => {
const workflow = loadWorkflow('.github/workflows/ci.yml')
const pythonRuntime = workflowJob(workflow, 'python-runtime')
const aggregate = workflowJob(workflow, 'all-checks-passed')
@@ -416,7 +474,7 @@ describe('CI workflow', () => {
name: 'python runtime / release-shaped matrix',
uses: './.github/workflows/build-exe-for-python-sdk.yml',
with: {
targets: 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64',
targets: 'node24-linux-x64,node24-win-x64',
ci: true,
},
secrets: {
@@ -716,6 +774,47 @@ describe('Python release workflows', () => {
})
})
describe('Request review workflow', () => {
it('runs trusted routing on pull request review-state updates', () => {
const workflow = loadWorkflow('.github/workflows/request-review.yml')
const event = workflowEvent(workflow, 'pull_request_target')
const job = workflowJob(workflow, 'request-review')
if (!isRecord(workflow.on)) throw new TypeError('request-review workflow must define events')
if (!Array.isArray(job.steps)) throw new TypeError('request-review job must define steps')
const steps = job.steps.filter(isRecord)
const checkout = steps.find(step => step.name === 'Check out trusted review policy')
const request = steps.find(step => step.name === 'Request reviewers')
expect(workflow.name).toBe('request-review')
expect(Object.keys(workflow.on)).toEqual(['pull_request_target'])
expect(event.types).toEqual(['opened', 'synchronize', 'reopened', 'ready_for_review', 'converted_to_draft'])
expect(workflow.permissions).toEqual({ contents: 'read', 'pull-requests': 'write' })
expect(workflow.concurrency).toEqual({
group: 'request-review-${{ github.event.pull_request.number }}',
'cancel-in-progress': true,
})
expect(job).toMatchObject({
name: 'request-review',
'runs-on': 'ubuntu-latest',
'timeout-minutes': 5,
})
expect(job).not.toHaveProperty('if')
expect(checkout).toMatchObject({
uses: 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1',
with: {
ref: '${{ github.event.repository.default_branch }}',
'persist-credentials': false,
},
})
expect(request).toMatchObject({
env: { GITHUB_TOKEN: '${{ github.token }}' },
run: 'node .github/review-ownership/request-review.mjs',
})
expect(JSON.stringify(workflow)).not.toContain('github.event.pull_request.head')
expect(JSON.stringify(workflow)).not.toContain('secrets.')
})
})
describe('Issue lifecycle workflow', () => {
it('runs the lifecycle job on every PR/review event but gates token and board steps', () => {
const lifecycle = loadWorkflow('.github/workflows/issue-lifecycle.yml')
+1
View File
@@ -68,6 +68,7 @@ export class RepositoryCleaner {
const canonicalRoot = await realpath(this.root)
await this.addIfPresent(targets, join(this.root, '.dsh-build'), canonicalRoot)
await this.addIfPresent(targets, join(this.root, 'apps/desktop/.desktop-build'), canonicalRoot)
// These checks cover legacy root-level incremental state emitted by older configs.
await this.addIfPresent(targets, join(this.root, '.typecheck'), canonicalRoot)
+2
View File
@@ -99,6 +99,8 @@ describe('client bundle purity gate', () => {
expect(resolveId('@deepseek-ai/dsh-token-meter/client')).toBeNull()
expect(() => resolveId('@deepseek-ai/dsh-token-meter')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-token-meter/client/internal')).toThrow(/purity/)
expect(resolveId('@deepseek-ai/dsh-host-open-in-app/shared')).toBeNull()
expect(() => resolveId('@deepseek-ai/dsh-host-open-in-app')).toThrow(/purity/)
})
it('admits only the pure spill notice entry, not its Host policy', () => {
@@ -0,0 +1,47 @@
/** Hold the advanced workflow child's first step until its parent records membership. */
export const name = 'python-snapshot-workflow-order'
/**
* @param {import('@deepseek-ai/cordis').Context} ctx - Scenario-local host context.
* @param {{ parentSessionId: string, prompt: string }} config - Exact advanced scenario identities.
*/
export function apply(ctx, config) {
const started = new Set()
const pending = new Map()
let disposed = false
ctx.effect(() => async () => {
disposed = true
const waits = [...pending.values()]
for (const wait of waits) wait.reject(new Error('workflow snapshot barrier disposed'))
await Promise.allSettled(waits.map(wait => wait.done))
started.clear()
})
ctx.on('session/event', (session, event) => {
if (disposed || session.id !== config.parentSessionId || event.type !== 'tool-workflow/agent-start') return
started.add(event.data.childId)
pending.get(event.data.childId)?.resolve()
})
ctx.on('agent/pre-step', async ({ agent, messages, turn, step, signal }, next) => {
if (agent.session.header.parentSession !== config.parentSessionId || turn !== 1 || step !== 1
|| !messages.some(message => message.content.some(block => block.type === 'text' && block.text === config.prompt))) {
return next()
}
signal.throwIfAborted()
if (disposed) throw new Error('workflow snapshot barrier disposed')
if (!started.has(agent.id)) {
const wait = Promise.withResolvers()
const abort = () => { wait.reject(signal.reason) }
signal.addEventListener('abort', abort, { once: true })
wait.done = wait.promise.finally(() => {
signal.removeEventListener('abort', abort)
pending.delete(agent.id)
})
pending.set(agent.id, wait)
await wait.done
}
signal.throwIfAborted()
if (disposed) throw new Error('workflow snapshot barrier disposed')
return next()
})
}
+18 -1
View File
@@ -125,6 +125,7 @@ export const SERVICE_PAGE: Record<string, string> = {
webhookRuntime: 'webhook.md',
workspaceRegistry: 'workspace.md',
workspaceController: 'workspace.md',
workspaceFiles: 'workspace.md',
directoryPickerController: 'workspace.md',
}
@@ -178,6 +179,9 @@ export const SERVICE_WALK_EXEMPTIONS: Record<string, string> = {
slots: 'client-side interface-typed browser service — packages/client/ui-renderer/README.md owns the API',
theme: 'client-side interface-typed browser service — packages/client/ui-theme/README.md owns the API',
workspaces: 'client-side interface-typed browser service — packages/api/workspace-controller/README.md owns the API',
resources: 'client-side resource model (protocol providers, pins, live sources) — packages/client/resources/README.md owns the API',
sidebarRight: 'client-side right-Sidebar navigation face — packages/client/ui-sidebar-right/README.md owns the API',
sidebarRightTabs: 'client-side right-Sidebar tab-type registry — packages/client/ui-sidebar-right/README.md owns the API',
}
/**
@@ -257,6 +261,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
ContentBlock: 'llm-streaming.md',
CreateAgentOptions: 'core.md',
GenerateOptions: 'llm-streaming.md',
Inbox: 'core.md',
InboxItem: 'core.md',
InboxPlacement: 'core.md',
InspectorJsonValue: 'extensions.md',
@@ -411,6 +416,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
CreateGoalRequest: 'goal.md',
EditGoalRequest: 'goal.md',
GoalBlockReason: 'goal.md',
GoalActivationChanged: 'goal.md',
GoalChanged: 'goal.md',
GoalRef: 'goal.md',
GoalView: 'goal.md',
@@ -675,8 +681,11 @@ export const FOUNDATION_TYPE_NAMES: ReadonlySet<string> = new Set([
'Promise',
'Record',
'Readonly',
'Set',
'ReadonlyMap',
'ReadonlySet',
'Request',
'Response',
'Set',
'Uint8Array',
])
@@ -767,6 +776,14 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
WorkflowAgentEndInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts',
WorkflowAgentInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts',
WorkflowResultInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts',
WorkspaceByteRange: 'Host workspace file endpoint contract is owned by packages/api/workspace-files/README.md',
WorkspaceDirectoryListing: 'Host workspace file endpoint contract is owned by packages/api/workspace-files/README.md',
WorkspaceFileBytes: 'Host workspace file endpoint contract is owned by packages/api/workspace-files/README.md',
WorkspaceFileChange: 'Host workspace file endpoint contract is owned by packages/api/workspace-files/README.md',
WorkspaceFileWatchFrame: 'Host workspace file endpoint contract is owned by packages/api/workspace-files/README.md',
WorkspaceFileRange: 'Host workspace file endpoint contract is owned by packages/api/workspace-files/README.md',
WorkspaceFileStat: 'Host workspace file endpoint contract is owned by packages/api/workspace-files/README.md',
WorkspaceFileText: 'Host workspace file endpoint contract is owned by packages/api/workspace-files/README.md',
}
/** Repository data policy consumed by the Cordis catalog projector. */
+1 -1
View File
@@ -10,7 +10,7 @@ const root = resolve(import.meta.dirname, '..')
const CLIENT_OUT = 'packages/extensions/cordis-client-runner/src/client/api-catalog.ts'
const CLIENT_SERVICES: Readonly<Record<string, readonly string[]>> = {
layout: ['toggleSidebar', 'openDetails', 'closeDetails'],
layout: ['toggleSidebar', 'openRightbar', 'closeRightbar'],
locale: ['getLocale', 'getSnapshot', 'subscribe', 'setLocale', 'addLanguage', 'register', 'bind'],
sessions: ['open', 'openSubagent', 'setSubagentCatalogOpen', 'refreshSubagents', 'search', 'fork', 'scope', 'binding'],
slots: ['register', 'inject'],
+7
View File
@@ -192,6 +192,13 @@ const SERVICE_ROLES: ServiceRole[] = [
mode: 'core',
note: 'Projects the user-settings seam onto the generated Remote namespace: the read is always redacted and every refusal is classified here, not on the seam Definition.',
},
{
key: 'workspaceFiles',
pkg: 'api-workspace-files',
title: 'Host workspace file Remote service',
mode: 'core',
note: 'Serves stat, paged text, byte windows, directory listings, and the change feed for files inside a Session\'s workspace root, confined by lstat, containment, and a stat re-check.',
},
{
key: 'workspaceController',
pkg: 'api-workspace-controller',
+1 -1
View File
@@ -450,7 +450,7 @@ describe('docsPages locale routes', () => {
const translated = rootPages.filter(page => page.contentLocale === 'zh-CN')
const fallbacks = rootPages.filter(page => page.contentLocale === 'en-US')
expect(translated).toHaveLength(46)
expect(translated).toHaveLength(48)
expect(translated.every(page => page.source.endsWith('.zh.md'))).toBe(true)
expect(fallbacks).toEqual([])
})
@@ -0,0 +1,167 @@
import { getEventListeners } from 'node:events'
import { Context } from '@deepseek-ai/cordis'
import { agentEvents, type Agent, type PreStepDecision } from '@deepseek-ai/dsh-agent'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import { WorkflowRunId } from '@deepseek-ai/dsh-workflow'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import * as spawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import { MockAdapter, textResponse } from '../packages/core/agent-loop/tests/mock-adapter.ts'
import type {} from '@deepseek-ai/dsh-tool-workflow'
import { afterEach, describe, expect, it, vi } from 'vitest'
// @ts-expect-error Scenario plugins are runtime JavaScript without declaration artifacts.
import * as fixtureModule from './fixtures/python-snapshot-workflow-order.mjs'
const config = { parentSessionId: 'advanced-parent', prompt: 'workflow child prompt' }
const fixture = fixtureModule as unknown as {
name: string
apply(ctx: Context, config: { parentSessionId: string; prompt: string }): void
}
const cleanups: (() => Promise<unknown>)[] = []
afterEach(async () => { for (const cleanup of cleanups.splice(0).reverse()) await cleanup() })
async function harness() {
const ctx = new Context()
const store = ctx.plugin(SessionStore)
await store
cleanups.push(() => store.dispose())
const fiber = ctx.plugin(fixture, config)
await fiber
cleanups.push(() => fiber.dispose())
const parent = ctx.sessions.create(SessionId(config.parentSessionId))
const other = ctx.sessions.create(SessionId('other-parent'))
const session = ctx.sessions.create(SessionId('workflow-child'), { meta: { parentSession: parent.id } })
// The dispatcher needs only the subject identity; the fixture reads its Session.
const agent = { id: session.id, session } as Agent
const controller = new AbortController()
const messages = [createUserMessage({ content: [{ type: 'text', text: config.prompt }], source: { kind: 'user' } })]
const decision: PreStepDecision = { kind: 'enter', messages }
const next = vi.fn(async () => decision)
const start = (owner = parent, childId = agent.id) => owner.append('tool-workflow/agent-start', {
runId: WorkflowRunId('run'), seq: 1, label: 'workflow-child', childId,
})
const step = (overrides = {}) => agentEvents(ctx, agent).waterfall('agent/pre-step', {
turn: 1, step: 1, messages, signal: controller.signal, ...overrides,
}, next)
return { ctx, fiber, parent, other, session, agent, controller, decision, next, start, step }
}
describe('advanced Python snapshot workflow ordering', () => {
it('blocks a real spawned child before its descriptor and first model request', async () => {
const ctx = new Context()
const entered = Promise.withResolvers<Agent>()
const order: string[] = []
const adapter = new MockAdapter([textResponse('child complete')])
const assembly = ctx.plugin({
name: 'workflow-order-driver-test',
async apply(inner: Context) {
await mountAgentLoopTestDependencies(inner)
await inner.plugin(AgentLoop, { agents: [] })
await inner.plugin(SubagentRuntime)
await inner.plugin(spawn, { providerName: 'spawn' })
inner.on('agent/pre-step', ({ agent }, next) => {
if (agent.session.header.parentSession === config.parentSessionId) entered.resolve(agent)
return next()
})
inner.on('session/event', (_session, event) => {
if (event.type === 'tool-workflow/agent-start' || event.type === 'subagent/descriptor') order.push(event.type)
})
await inner.plugin(fixture, config)
},
})
cleanups.push(() => assembly.dispose())
await assembly
ctx.llm.registerAdapter(['mock'], adapter)
const parent = await ctx.agentLoop.create(SessionId(config.parentSessionId), { provider: 'mock', model: 'mock' })
const run = await ctx.subagents.start('spawn', {
parent, prompt: [{ type: 'text', text: config.prompt }], signal: new AbortController().signal,
})
cleanups.push(() => run.dispose())
const child = await entered.promise
expect(child.id).toBe(run.id)
expect(adapter.requests).toHaveLength(0)
expect(child.session.snapshotEvents().some(event => event.type === 'subagent/descriptor')).toBe(false)
parent.session.append('tool-workflow/agent-start', {
runId: WorkflowRunId('run'), seq: 1, label: 'workflow-child', childId: child.id,
})
expect((await run.result).output).toEqual([{ type: 'text', text: 'child complete' }])
expect(adapter.requests).toHaveLength(1)
expect(order).toEqual(['tool-workflow/agent-start', 'subagent/descriptor'])
})
it('holds the child until the exact parent records the exact member', async () => {
const h = await harness()
const pending = h.step()
expect(h.next).not.toHaveBeenCalled()
expect(getEventListeners(h.controller.signal, 'abort')).toHaveLength(1)
h.start(h.other)
h.start(h.parent, SessionId('other-child'))
h.parent.append('tool-workflow/run-start', { runId: WorkflowRunId('run'), name: 'workflow' })
await Promise.resolve()
expect(h.next).not.toHaveBeenCalled()
h.start()
expect(await pending).toBe(h.decision)
expect(h.next).toHaveBeenCalledOnce()
expect(getEventListeners(h.controller.signal, 'abort')).toHaveLength(0)
})
it('retains a start recorded before the child reaches its first step', async () => {
const h = await harness()
h.start()
expect(await h.step()).toBe(h.decision)
expect(h.next).toHaveBeenCalledOnce()
expect(getEventListeners(h.controller.signal, 'abort')).toHaveLength(0)
})
it.each(['prompt', 'parent', 'turn', 'step'])('does not hold an unrelated %s', async (difference) => {
const h = await harness()
const overrides = difference === 'prompt' ? { messages: [] }
: difference === 'turn' ? { turn: 2 }
: difference === 'step' ? { step: 2 } : {}
if (difference === 'parent') {
const session = h.ctx.sessions.create(SessionId('unrelated-child'), { meta: { parentSession: h.other.id } })
Object.assign(h.agent, { session })
}
expect(await h.step(overrides)).toBe(h.decision)
expect(h.next).toHaveBeenCalledOnce()
})
it.each([false, true])('rejects cancellation and detaches the waiter (already aborted: %s)', async (alreadyAborted) => {
const h = await harness()
const reason = new Error('cancelled child')
if (alreadyAborted) h.controller.abort(reason)
const pending = h.step()
const rejected = expect(pending).rejects.toBe(reason)
h.controller.abort(reason)
await rejected
h.start()
expect(h.next).not.toHaveBeenCalled()
expect(getEventListeners(h.controller.signal, 'abort')).toHaveLength(0)
})
it('does not admit a cancelled child when start and cancellation share a tick', async () => {
const h = await harness()
const reason = new Error('cancelled after membership')
const pending = h.step()
const rejected = expect(pending).rejects.toBe(reason)
h.start()
h.controller.abort(reason)
await rejected
expect(h.next).not.toHaveBeenCalled()
expect(getEventListeners(h.controller.signal, 'abort')).toHaveLength(0)
})
it('settles pending waits before disposal completes and removes both listeners', async () => {
const h = await harness()
const pending = h.step()
const rejected = expect(pending).rejects.toThrow('workflow snapshot barrier disposed')
await h.fiber.dispose()
await rejected
h.start()
expect(h.next).not.toHaveBeenCalled()
expect(getEventListeners(h.controller.signal, 'abort')).toHaveLength(0)
expect(await h.step()).toBe(h.decision)
})
})
+5 -5
View File
@@ -48,7 +48,7 @@ interface PlannedVersion {
readonly tag: string | undefined
}
/** One private dsh package whose version follows the publishable family. */
/** One private dsh workspace whose version follows the publishable family. */
interface PrivateDshVersion {
/** Repository-relative manifest path. */
readonly manifestPath: string
@@ -243,13 +243,13 @@ function rootVersion(root: string): string {
}
/**
* Discover private package manifests that share the dsh version without joining
* its publish set.
* Discover private package and application manifests that share the dsh version
* without joining its publish set.
* @param root - repository root.
* @returns Private package manifests sorted by path.
* @returns Private workspace manifests sorted by path.
*/
function privateDshVersions(root: string): PrivateDshVersion[] {
return globSync('packages/*/*/package.json', { cwd: root })
return globSync(['apps/*/package.json', 'packages/*/*/package.json'], { cwd: root })
.map(path => path.replaceAll('\\', '/'))
.sort()
.flatMap((manifestPath) => {
+12 -1
View File
@@ -49,10 +49,20 @@ describe('release families', () => {
expect(members.map(member => member.name)).not.toContain('@deepseek-ai/dsh-experimental-agent-team')
})
it('bumps private dsh packages without adding release tags', () => {
it('excludes private applications from the publish set', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-release-private-'))
roots.push(root)
write(join(root, 'apps/public/package.json'), '{"name":"@deepseek-ai/dsh-public","version":"0.0.1"}\n')
write(join(root, 'apps/private/package.json'), '{"name":"@deepseek-ai/dsh-private","version":"0.0.1","private":true}\n')
expect(releaseFamily('dsh').members(root).map(entry => entry.name)).toEqual(['@deepseek-ai/dsh-public'])
})
it('bumps private dsh workspaces without adding release tags', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-release-version-'))
roots.push(root)
write(join(root, 'package.json'), '{"version":"0.0.1"}\n')
write(join(root, 'apps/desktop/package.json'), '{"version":"0.0.1","private":true}\n')
write(join(root, 'packages/experimental/prototype/package.json'), '{"version":"0.0.1","private":true}\n')
write(join(root, 'packages/core/unselected/package.json'), '{"version":"0.0.1"}\n')
@@ -63,6 +73,7 @@ describe('release families', () => {
expect(planned.map(entry => ({ path: entry.manifestPath, tag: entry.tag }))).toEqual([
{ path: 'package.json', tag: undefined },
{ path: 'packages/core/published/package.json', tag: 'dsh-v0.0.2' },
{ path: 'apps/desktop/package.json', tag: undefined },
{ path: 'packages/experimental/prototype/package.json', tag: undefined },
])
})
+2 -1
View File
@@ -117,7 +117,7 @@ export abstract class ReleaseFamily {
/**
* Discover this family's members.
* @param root - repository root.
* @returns Members sorted by directory, with names validated and deduplicated.
* @returns Publishable members sorted by directory, with names validated and deduplicated.
*/
members(root: string): ReleaseMember[] {
const manifestPaths = globSync([...this.patterns], { cwd: root }).sort()
@@ -128,6 +128,7 @@ export abstract class ReleaseFamily {
for (const manifestPath of manifestPaths) {
const normalized = manifestPath.replaceAll('\\', '/')
const manifest = readManifest(resolve(root, manifestPath))
if (manifest.private === true) continue
const name = requireString(manifest, 'name', normalized)
const version = requireString(manifest, 'version', normalized)
if (name === WORKSPACE_ROOT_PACKAGE) throw new Error(`${normalized} selected the workspace root`)
+3 -1
View File
@@ -10,6 +10,7 @@
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { parseArgs } from 'node:util'
import { pnpmInvocation } from '../pnpm-invocation.ts'
import { releaseFamily, tarballName, type ReleaseFamily, type ReleaseMember } from './families.ts'
import { isEntry, runConcurrent } from './process.ts'
import { PUBLISH_ORDER_FILE, tarballFiles } from './tarball.ts'
@@ -25,7 +26,8 @@ const DEFAULT_OUTPUT = 'dist/npm'
* @returns The tarball filename.
*/
async function packMember(family: ReleaseFamily, member: ReleaseMember, destination: string): Promise<string> {
await runConcurrent('pnpm', ['--dir', member.directory, 'pack', '--pack-destination', destination])
const invocation = pnpmInvocation(['--dir', member.directory, 'pack', '--pack-destination', destination])
await runConcurrent(invocation.command, invocation.args)
const filename = tarballName(member)
const tarball = join(destination, filename)
+1 -1
View File
@@ -27,7 +27,7 @@ export interface PackedIdentity {
* @returns Every path inside the archive.
*/
export function tarballFiles(tarball: string): string[] {
return capture('tar', ['-tzf', tarball]).split('\n').filter(line => line !== '')
return capture('tar', ['-tzf', tarball]).split(/\r?\n/u).filter(line => line !== '')
}
/**
+16
View File
@@ -172,6 +172,12 @@ describe('gate graph validation', () => {
expect(ids).toContain('subsystem-pages')
})
it('keeps the package README Summary limit in the documentation gate', () => {
const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id))
expect(ids).toContain('package-readme-summaries')
})
it('derives the quick documentation aggregate from marked doc-sync leaves', () => {
const full = withPnpmEntrypoint(() => gatesForMode('doc-sync'))
const quick = withPnpmEntrypoint(() => gatesForMode('doc-quick'))
@@ -240,6 +246,15 @@ describe('gate graph validation', () => {
},
)
it.each(['ci-primary', 'ci-static', 'check-all'] as const)(
'keeps review request policy tests in %s',
(mode) => {
const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
expect(ids).toContain('request-review')
},
)
it.each(['ci-primary', 'ci-static', 'check-all', 'hygiene'] as const)(
'keeps hard-coded Client UI copy enforcement in %s',
(mode) => {
@@ -548,6 +563,7 @@ describe('Node 24 lane ownership', () => {
})
expect(subject.find(item => item.id === 'built-bin-smoke')?.args).toEqual(
expect.arrayContaining([
'packages/subprocess/subprocess-local/tests/spawn-runner-built.e2e.ts',
'packages/subagent/subagent-codex/tests/loader-composition.e2e.ts',
'packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts',
'packages/experimental/agent-team/tests/built-lib.e2e.ts',
+4
View File
@@ -267,6 +267,7 @@ export function gatesForMode(selected: Mode): Gate[] {
pnpmScript('client-domain-graph', 'verify-client-domain-graph', { label: 'client domain graph' }),
pnpmScript('test', 'test'),
pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }),
pnpmScript('request-review', 'test:request-review', { label: 'Review request policy' }),
pnpmScript('duplication', 'duplication'),
snapshotGate(),
expectedOutputGate(),
@@ -310,6 +311,7 @@ function ciSharedStaticGates(): Gate[] {
pnpmScript('client-ui-i18n', 'verify-client-ui-i18n', { label: 'client UI i18n' }),
pnpmScript('no-bare-dispatcher', 'verify-no-bare-dispatcher', { label: 'proxy-aware dispatchers' }),
pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }),
pnpmScript('request-review', 'test:request-review', { label: 'Review request policy' }),
]
}
@@ -745,6 +747,7 @@ function docSyncLeafGates(options: {
pnpmScript('package-paths', 'verify-package-paths', { label: 'package paths' }),
pnpmScript('tsconfig-paths', 'verify-tsconfig-paths', { label: 'tsconfig paths' }),
pnpmScript('config-source-ownership', 'verify-config-source-ownership', { label: 'config source ownership' }),
pnpmScript('package-readme-summaries', 'verify-package-readme-summaries', { label: 'package README Summaries', quick: true }),
pnpmScript('package-readme-model-experience', 'verify-package-readme-model-experience', { label: 'package README model experience', quick: true }),
pnpmScript('agent-note-classification', 'verify-agent-note-classification', { label: 'agent note classification', quick: true }),
pnpmScript('agent-note-format', 'verify-agent-note-format', { label: 'agent note format', quick: true }),
@@ -782,6 +785,7 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate {
'apps/cli/tests/built-bin.e2e.ts',
'packages/host/directory-picker-native/tests/built-worker.e2e.ts',
'packages/sdk/server/tests/built-scope-carrier.e2e.ts',
'packages/subprocess/subprocess-local/tests/spawn-runner-built.e2e.ts',
'packages/subagent/subagent-codex/tests/loader-composition.e2e.ts',
'packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts',
'packages/api/remotes/tests/built-lib.e2e.ts',
+149 -25
View File
@@ -11,6 +11,7 @@ import json
import os
import queue
import re
import secrets
import shutil
import subprocess
import sys
@@ -769,7 +770,7 @@ def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--scenario",
choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-spawn-node", "sdk-mcp", "sdk-snapshot", "sdk-restart", "sdk-profile-plugin", "sdk-live", "direct"),
choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-spawn-node", "sdk-mcp", "sdk-snapshot", "sdk-restart", "sdk-profile-plugin", "sdk-live", "runner", "direct"),
default="all",
)
parser.add_argument("--exe", type=Path)
@@ -788,13 +789,20 @@ def main() -> None:
parser.error("--scenario sdk-profile-plugin requires --installed-wheel")
if args.installed_wheel:
args.exe = assert_installed_wheel_environment()
if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-spawn-node", "sdk-snapshot", "sdk-restart", "direct"} and args.exe is None:
parser.error("--exe is required for custom, minimal, snapshot, and direct scenarios")
if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-spawn-node", "sdk-snapshot", "sdk-restart", "runner", "direct"} and args.exe is None:
parser.error("--exe is required for custom, minimal, fs-search, spawn-node, snapshot, restart, runner, and direct scenarios")
if args.update_snapshots and args.scenario not in {"all", "sdk-minimal", "sdk-snapshot", "sdk-restart"}:
parser.error("--update-snapshots requires --scenario sdk-minimal, sdk-snapshot, sdk-restart, or all")
if args.exe is not None and not args.exe.is_file():
parser.error(f"runtime executable does not exist: {args.exe}")
if args.scenario in {"all", "runner"}:
assert args.exe is not None
smoke_packaged_runner(args.exe.resolve())
if args.scenario == "runner":
print("smoke-python-runtime: runner passed")
return
if args.scenario == "sdk-live":
smoke_sdk_live()
print("smoke-python-runtime: sdk-live passed")
@@ -908,8 +916,9 @@ def smoke_sdk_live() -> None:
f"containing {LIVE_API_SENTINEL}. Then reply with exactly {LIVE_API_SENTINEL}.\n{marker}"
)
verify_prompt = (
"Use a tool to read the file created in the previous turn. "
f"If its only line is {LIVE_API_SENTINEL}, reply with exactly {LIVE_API_SENTINEL}."
"The file created in the previous turn now contains a new value written by the test. "
"Use a tool to read its current contents without modifying it. "
"Reply with exactly its current single line, not the value from the previous turn."
)
with DeepSeekHarness(
provider="deepseek-official",
@@ -925,34 +934,46 @@ def smoke_sdk_live() -> None:
request_timeout_seconds=180,
) as harness:
created = harness.run(create_prompt, session_id=session_id)
assert_live_turn("create", created, LIVE_API_SENTINEL)
if not marker.is_file():
raise AssertionError(f"real-model tool turn did not create {marker}")
if marker.read_text(encoding="utf-8").splitlines() != [LIVE_API_SENTINEL]:
raise AssertionError(f"real-model tool turn wrote unexpected text to {marker}")
# Only a fresh filesystem observation can reveal the verification answer.
verification_value = secrets.token_hex(16)
marker.write_text(verification_value + "\n", encoding="utf-8")
verified = harness.run(verify_prompt, session_id=session_id)
assert_live_turn("verify", verified, verification_value)
for label, result in (("create", created), ("verify", verified)):
if result.finish_reason != "completed":
event_types = [event.get("type") for event in result.events]
turn_end_data = next(
(event.get("data") for event in reversed(result.events) if event.get("type") == "turn/end"),
None,
)
turn_end = safe_turn_end(turn_end_data)
raise AssertionError(
f"{label} turn ended with {result.finish_reason!r}; "
f"final={result.final_response!r}; turn_end={turn_end!r}; events={event_types}"
)
if not any(event.get("type") == "tool/call" for event in result.events):
raise AssertionError(
f"{label} turn made no model-requested tool call; "
f"final={result.final_response!r}"
)
if result.final_response.strip() != LIVE_API_SENTINEL:
raise AssertionError(f"{label} turn returned {result.final_response!r}")
if not marker.is_file():
raise AssertionError(f"real-model tool turn did not create {marker}")
if marker.read_text(encoding="utf-8").splitlines() != [LIVE_API_SENTINEL]:
if marker.read_text(encoding="utf-8").splitlines() != [verification_value]:
raise AssertionError(f"real-model tool turn wrote unexpected text to {marker}")
assert_zstd_session_log(sessions)
def assert_live_turn(label: str, result: RunResult, expected: str) -> None:
"""Require a completed model-requested tool turn with the expected response."""
if result.finish_reason != "completed":
event_types = [event.get("type") for event in result.events]
turn_end_data = next(
(event.get("data") for event in reversed(result.events) if event.get("type") == "turn/end"),
None,
)
turn_end = safe_turn_end(turn_end_data)
raise AssertionError(
f"{label} turn ended with {result.finish_reason!r}; "
f"final={result.final_response!r}; turn_end={turn_end!r}; events={event_types}"
)
if not any(event.get("type") == "tool/call" for event in result.events):
raise AssertionError(
f"{label} turn made no model-requested tool call; "
f"final={result.final_response!r}"
)
if result.final_response.strip() != expected:
raise AssertionError(f"{label} turn returned {result.final_response!r}")
def safe_turn_end(value: object) -> object:
"""Project a live-provider failure without retaining credential-bearing text."""
if not isinstance(value, dict):
@@ -1271,6 +1292,11 @@ def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool)
sessions = dsh_home / "sessions"
patch = write_advanced_profile_patch(root, "snapshot.patch.yml", sessions)
feedback_patch = write_profile_patch(root, "feedback.patch.yml", sessions, [{"insert": [
{"id": "snapshot-workflow-order", "name": (
Path(__file__).resolve().parent / "fixtures/python-snapshot-workflow-order.mjs"
).as_uri(), "config": {
"parentSessionId": SNAPSHOT_SESSION_ID, "prompt": SNAPSHOT_WORKFLOW_CHILD_PROMPT,
}},
{"id": "snapshot-message-feedback", "name": "@deepseek-ai/dsh-message-feedback",
"config": {"maxNoteBytes": 1024}},
{"id": "snapshot-feedback-producer", "name": (
@@ -1428,6 +1454,104 @@ def smoke_direct(base_url: str, executable: Path) -> None:
assert_session_log(sessions, root, EXPECTED_TEXT)
def smoke_packaged_runner(executable: Path) -> None:
"""Exercise the private subprocess runner through the single-file entry."""
with tempfile.TemporaryDirectory(prefix="dsh-packaged-runner-") as temporary:
root = Path(temporary).resolve()
target_script = (
"import os,sys; "
"ok = (os.getcwd() == os.environ['PACKAGED_RUNNER_EXPECTED_CWD'] "
"and os.environ.get('DSH_SUBPROCESS_RUNNER') == 'target-collision-restored'); "
"sys.exit(7 if ok else 9)"
)
if not IS_WINDOWS:
request_path = root / "launch-request.json"
target_env = dict(os.environ)
target_env["DSH_SUBPROCESS_RUNNER"] = "target-collision-restored"
target_env["PACKAGED_RUNNER_EXPECTED_CWD"] = str(root)
request_path.write_text(
json.dumps({"cwd": str(root), "env": target_env}),
encoding="utf-8",
)
request_path.chmod(0o600)
environment = dict(os.environ)
environment["DSH_SUBPROCESS_RUNNER"] = str(request_path)
result = subprocess.run(
[str(executable), "--", sys.executable, "-c", target_script],
cwd=root,
env=environment,
capture_output=True,
text=True,
timeout=30,
check=False,
)
if result.returncode != 7 or request_path.exists() or (root / "startup-error.json").exists():
raise AssertionError(
"packaged POSIX runner failed: "
f"exit={result.returncode}; stdout={result.stdout!r}; stderr={result.stderr!r}"
)
return
node = shutil.which("node")
if node is None:
raise AssertionError("packaged Windows runner smoke requires node on PATH")
helper = root / "windows-runner-smoke.mjs"
helper.write_text(
"""import { spawn } from 'node:child_process'
const [runtime, target, cwd, targetScript] = process.argv.slice(2)
const child = spawn(runtime, ['--', target, '-c', targetScript], {
cwd,
env: { ...process.env, DSH_SUBPROCESS_RUNNER: 'windows' },
stdio: ['ignore', 'ignore', 'ignore', 'ipc', 'pipe', 'pipe', 'pipe'],
})
const messages = []
let stdout = ''
let stderr = ''
child.stdio[4].destroy()
child.stdio[5].on('data', chunk => { stdout += chunk.toString() })
child.stdio[6].on('data', chunk => { stderr += chunk.toString() })
child.on('message', message => { messages.push(message) })
const result = await new Promise((resolve, reject) => {
child.once('error', reject)
child.once('spawn', () => {
child.send({
type: 'start',
cwd,
env: {
...process.env,
DSH_SUBPROCESS_RUNNER: 'target-collision-restored',
PACKAGED_RUNNER_EXPECTED_CWD: cwd,
},
}, error => { if (error) reject(error) })
})
child.once('close', (exitCode, signal) => { resolve({ exitCode, signal }) })
})
process.stdout.write(JSON.stringify({ ...result, messages, stdout, stderr }))
""",
encoding="utf-8",
)
helper_result = subprocess.run(
[node, str(helper), str(executable), sys.executable, str(root), target_script],
cwd=root,
capture_output=True,
text=True,
timeout=30,
check=False,
)
if helper_result.returncode != 0:
raise AssertionError(f"packaged Windows runner helper failed: {helper_result.stderr}")
observed = json.loads(helper_result.stdout)
expected = {
"exitCode": 0,
"signal": None,
"messages": [{"type": "target-exit", "exitCode": 7}],
"stdout": "",
"stderr": "",
}
if observed != expected:
raise AssertionError(f"packaged Windows runner returned unexpected facts: {observed}")
def is_idle_notification(message: dict[str, object]) -> bool:
"""Return whether a JSON-RPC notification marks a session idle."""
params = message.get("params")
+42
View File
@@ -0,0 +1,42 @@
import { Context } from '@deepseek-ai/cordis'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { expect, it } from 'vitest'
import * as adapter from './snapshot-shell-path.ts'
it.skipIf(process.platform === 'win32')('translates only the exact fixture command and preserves real shell failures', async () => {
const root = await mkdtemp(join(tmpdir(), 'snapshot-shell-'))
const ctx = new Context()
const disposers: (() => Promise<void>)[] = []
try {
const subprocess = ctx.plugin(LocalSubprocessRuntime)
disposers.push(() => subprocess.dispose())
await subprocess
const bash = ctx.plugin(LocalBashExecutor)
disposers.push(() => bash.dispose())
await bash
const command = "printf 'actual bytes' > /fixture/recorded.txt"
const livePath = join(root, "space and 'quote.txt")
const fork = ctx.plugin(adapter, { command, recordedPath: '/fixture/recorded.txt', livePath })
disposers.push(() => fork.dispose())
await fork
const outcome = await ctx.shell.run(ctx.shell.resolve({ command }))
expect(outcome.timedOut).toBe(false)
expect(outcome.exitCode).toBe(0)
expect(await readFile(livePath, 'utf8')).toBe('actual bytes')
const untouched = await ctx.shell.run(ctx.shell.resolve({ command: "printf '/fixture/recorded.txt'; exit 7" }))
expect(untouched.exitCode).toBe(7)
expect(untouched.stdout.text).toBe('/fixture/recorded.txt')
await fork.dispose()
await rm(livePath)
const restored = await ctx.shell.run(ctx.shell.resolve({ command }))
expect(restored.exitCode).not.toBe(0)
await expect(readFile(livePath)).rejects.toMatchObject({ code: 'ENOENT' })
} finally {
for (const dispose of disposers.reverse()) await dispose()
await rm(root, { recursive: true, force: true })
}
})
+30
View File
@@ -0,0 +1,30 @@
/** Exact recorded shell-command path translation; execution and reported outcomes remain real. */
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-shell'
export const name = 'snapshot-shell-path'
export const inject = ['shell']
/** One recorded command and its isolated live filesystem target. */
export interface Config {
command: string
recordedPath: string
livePath: string
}
/**
* Translate one exact fixture command after tool logging and approval.
* @param ctx - profile context with its real shell executor.
* @param config - recorded command/path and allocated live target.
*/
export function apply(ctx: Context, config: Config): void {
const shell = ctx.shell
// oxlint-disable-next-line typescript/unbound-method -- preserve method identity for restoration; calls bind the receiver.
const run = shell.run
ctx.effect(() => {
shell.run = spec => run.call(shell, spec.command === config.command
? { ...spec, command: spec.command.replaceAll(config.recordedPath, "'" + config.livePath.replaceAll("'", "'\"'\"'") + "'") }
: spec)
return () => { shell.run = run }
})
}
+68
View File
@@ -0,0 +1,68 @@
import { Context } from '@deepseek-ai/cordis'
import { LocalSpillStore } from '@deepseek-ai/dsh-spill-local'
import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local'
import { SessionId } from '@deepseek-ai/dsh-session'
import { ToolCallId } from '@deepseek-ai/dsh-llm'
import { mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { expect, it } from 'vitest'
import * as locators from './snapshot-spill-locators.ts'
it.each([false, true])('keeps concurrent physical spill files private while retaining logical locator length and bytes (reverse allocation=%s)', async (reverseAllocation) => {
const roots: string[] = []
const disposers: (() => Promise<void>)[] = []
const secondAllocated = Promise.withResolvers<undefined>()
try {
const runs = await Promise.all([0, 1].map(async (index) => {
if (reverseAllocation && index === 0) await secondAllocated.promise
const root = await mkdtemp(join(tmpdir(), 'snapshot-locator-'))
roots.push(root)
if (index === 1) secondAllocated.resolve(undefined)
const ctx = new Context()
const storeFiber = ctx.plugin(LocalSpillStore, { root, cleanupPeriodDays: 0 })
disposers.push(() => storeFiber.dispose())
await storeFiber
const fsFiber = ctx.plugin(LocalFileSystem, { cwd: root })
disposers.push(() => fsFiber.dispose())
await fsFiber
const locatorRoot = resolve('/tmp/dsh-acp-snap-123456789')
const fork = ctx.plugin(locators, { root, locatorRoot })
disposers.push(() => fork.dispose())
await fork
const content = `physical UTF-8 内容 ${index}`
const ref = await ctx.spillStore.saveText({
owner: { sessionId: SessionId('same-session') },
source: { kind: 'tool', toolName: 'bash', callId: ToolCallId('same-call'), label: 'result' },
suggestedName: 'bash.txt', content,
})
expect(ref.bytes).toBe(Buffer.byteLength(content))
expect(ref.locator.startsWith(locatorRoot)).toBe(true)
const target = await ctx.fs.resolve(ref.locator)
expect(target.displayPath).toBe(ref.locator)
const physicalPath = ctx.fs.processPath(target)
expect(physicalPath.startsWith(await realpath(root))).toBe(true)
expect(await readFile(physicalPath, 'utf8')).toBe(content)
expect(await ctx.fs.readText(target)).toBe(content)
await expect(ctx.fs.resolve(join(locatorRoot, 'missing.txt'))).rejects.toThrow('not saved by this run')
const ordinary = join(root, 'ordinary.txt')
await writeFile(ordinary, 'ordinary')
expect(await ctx.fs.readText(await ctx.fs.resolve(ordinary))).toBe('ordinary')
await fork.dispose()
const restored = await ctx.fs.resolve(ref.locator)
expect(ctx.fs.processPath(restored)).not.toBe(physicalPath)
expect(await ctx.fs.stat(restored)).toBeUndefined()
return { root, physicalPath, locator: ref.locator, content }
}))
expect(runs[0]?.physicalPath).not.toBe(runs[1]?.physicalPath)
expect(runs[0]?.locator.length).toBe(runs[1]?.locator.length)
// Promise.all preserves input order; allocation completion order may differ.
await rm(runs[0]!.root, { recursive: true, force: true })
expect(await readFile(runs[1]?.physicalPath as string, 'utf8')).toBe(runs[1]?.content)
} finally {
for (const dispose of disposers.reverse()) await dispose()
await Promise.all(roots.map(root => rm(root, { recursive: true, force: true })))
}
})
+59
View File
@@ -0,0 +1,59 @@
/** Fixture-only logical locators over real local spill files; preview budgets retain recorded path lengths. */
import type { Context } from '@deepseek-ai/cordis'
import { join, relative, resolve, sep } from 'node:path'
import type { SpillLocator } from '@deepseek-ai/dsh-spill'
import type {} from '@deepseek-ai/dsh-fs'
export const name = 'snapshot-spill-locators'
export const inject = ['spillStore', 'fs']
/** Live storage and recorded locator prefixes supplied by the snapshot owner. */
export interface Config {
root: string
locatorRoot: string
}
/**
* Translate only locators returned by this fixture's real spill backend.
* @param ctx - profile context with real spill and filesystem providers.
* @param config - per-run storage and stable logical prefix.
*/
export function apply(ctx: Context, config: Config): void {
const root = resolve(config.root)
const locatorRoot = resolve(config.locatorRoot)
const paths = new Map<string, string>()
const store = ctx.spillStore
const fs = ctx.fs
// oxlint-disable-next-line typescript/unbound-method -- preserve method identity for restoration; calls bind the receiver.
const saveText = store.saveText
// oxlint-disable-next-line typescript/unbound-method -- preserve method identity for restoration; calls bind the receiver.
const resolvePath = fs.resolve
ctx.effect(() => {
store.saveText = async (input) => {
const saved = await saveText.call(store, input)
const suffix = relative(root, saved.locator)
if (suffix.startsWith('..') || resolve(root, suffix) !== saved.locator) {
throw new Error('snapshot spill backend returned a locator outside its live root')
}
const locator = join(locatorRoot, suffix) as SpillLocator
paths.set(locator, saved.locator)
return { ...saved, locator }
}
fs.resolve = async (path, opts) => {
const live = paths.get(path)
if (live !== undefined) {
const target = await resolvePath.call(fs, live, opts)
return { ...target, displayPath: path }
}
if (path.startsWith(locatorRoot + sep)) {
throw new Error('snapshot spill locator was not saved by this run')
}
return resolvePath.call(fs, path, opts)
}
return () => {
store.saveText = saveText
fs.resolve = resolvePath
paths.clear()
}
})
}
+78
View File
@@ -0,0 +1,78 @@
import { existsSync } from 'node:fs'
import { chmod, mkdir, mkdtemp, readFile, rm, stat, symlink } from 'node:fs/promises'
import { homedir, tmpdir } from 'node:os'
import { dirname, join, parse } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import { SandboxedFileSystem } from '@deepseek-ai/dsh-fs-sandbox'
import { canonicalPath } from '@deepseek-ai/dsh-sandbox'
import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { describe, expect, it } from 'vitest'
import { assertWorkspaceOutsideTemp, outsideTempWorkspaceParent } from './snapshot-workspace-parent.ts'
// Host disk exhaustion is not simulated: placement and the real write fence are the regression oracles.
describe('snapshot workspace parent', () => {
// Windows directory permissions and root bypass do not enforce POSIX write bits.
it.skipIf(process.platform === 'win32' || process.getuid?.() === 0)('uses home when the temp parent is not writable', async () => {
const base = await mkdtemp(join(outsideTempWorkspaceParent(), '.dsh-snapshot-readonly-'))
try {
const temporary = join(base, '_temp')
await mkdir(temporary)
await chmod(base, 0o500)
expect(outsideTempWorkspaceParent(temporary)).toBe(homedir())
} finally {
await chmod(base, 0o700)
await rm(base, { recursive: true, force: true })
}
})
it('uses home when a temp sibling would require a system directory or inherit its grant', () => {
expect(outsideTempWorkspaceParent('/tmp')).toBe(homedir())
expect(outsideTempWorkspaceParent(parse(tmpdir()).root)).toBe(homedir())
expect(outsideTempWorkspaceParent(join(canonicalPath('/tmp'), 'runner', '_temp'))).toBe(homedir())
})
it('rejects automatically writable temporary workspaces, including symlink aliases', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-snapshot-parent-'))
try {
expect(() => { assertWorkspaceOutsideTemp(root) }).toThrow('must be outside temporary writable root')
const alias = join(root, 'alias')
await symlink(tmpdir(), alias, process.platform === 'win32' ? 'junction' : 'dir')
expect(() => { assertWorkspaceOutsideTemp(alias) }).toThrow('must be outside temporary writable root')
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('allows the allocated workspace but denies sibling writes and cleans the complete tree', async () => {
const base = await mkdtemp(join(outsideTempWorkspaceParent(), 'dsh-snapshot-parent-'))
const ctx = new Context()
const fibers: Awaited<ReturnType<Context['plugin']>>[] = []
try {
const temporary = join(base, '_temp')
await mkdir(temporary)
expect(outsideTempWorkspaceParent(temporary)).toBe(canonicalPath(base))
const workspace = await mkdtemp(join(outsideTempWorkspaceParent(temporary), 'workspace-'))
const outside = join(base, 'outside.txt')
assertWorkspaceOutsideTemp(workspace)
expect(dirname(workspace)).toBe(canonicalPath(base))
expect((await stat(workspace)).dev).toBe((await stat(temporary)).dev)
fibers.push(await ctx.plugin(SessionProjectionRegistry))
fibers.push(await ctx.plugin(SandboxPolicyService, { mode: 'workspace-write', workspaceRoot: workspace }))
fibers.push(await ctx.plugin(SandboxedFileSystem, { cwd: workspace }))
const inside = join(workspace, 'inside.txt')
await ctx.fs.writeText(await ctx.fs.resolve(inside), 'inside')
expect(await readFile(inside, 'utf8')).toBe('inside')
await expect(ctx.fs.writeText(await ctx.fs.resolve(outside), 'outside'))
.rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' })
expect(existsSync(outside)).toBe(false)
} finally {
try {
for (const fiber of fibers.reverse()) await fiber.dispose()
} finally {
await rm(base, { recursive: true, force: true })
}
}
expect(existsSync(base)).toBe(false)
})
})
+46
View File
@@ -0,0 +1,46 @@
/** Workspace placement for snapshots that must not inherit temporary-directory write grants. */
import { accessSync, constants } from 'node:fs'
import { homedir, tmpdir } from 'node:os'
import { dirname, isAbsolute, parse, relative, sep } from 'node:path'
import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox'
function contains(root: string, path: string): boolean {
const suffix = relative(root, path)
return suffix === '' || suffix !== '..' && !suffix.startsWith('..' + sep) && !isAbsolute(suffix)
}
/**
* Select a writable temp sibling parent, or home when that parent is unavailable for writes.
* The caller atomically allocates and owns cleanup of the generated workspace.
* @param tempRoot - platform temporary directory.
* @param home - fallback when temp siblings require a system directory or a non-writable parent.
* @returns existing parent outside the automatic temporary write grants.
*/
export function outsideTempWorkspaceParent(tempRoot = tmpdir(), home = homedir()): string {
const temporary = canonicalPath(tempRoot)
const systemTemporary = canonicalPath('/tmp')
const parent = dirname(temporary)
if (temporary === systemTemporary || parent === parse(parent).root || contains(systemTemporary, parent)) return home
try {
accessSync(parent, constants.W_OK)
} catch (error) {
// A non-writable parent cannot host siblings; allocation failures still propagate from mkdtemp.
const code = (error as NodeJS.ErrnoException).code
if (code === 'EACCES' || code === 'EPERM' || code === 'EROFS') return home
throw error
}
return parent
}
/**
* Reject a workspace whose write could succeed without the session's workspace grant.
* @param cwd - allocated workspace to check, with symlinks resolved before comparison.
* @returns nothing; throws when an automatic temporary write grant contains the workspace.
*/
export function assertWorkspaceOutsideTemp(cwd: string): void {
const path = canonicalPath(cwd)
for (const root of writableRoots({ mode: 'workspace-write', workspaceRoot: '/tmp' })) {
if (contains(root, path)) throw new Error('snapshot workspace ' + cwd + ' must be outside temporary writable root ' + root)
}
}
+121
View File
@@ -0,0 +1,121 @@
/** Scheduling policy for post-merge native runtime carriers and Wine. */
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { load } from 'js-yaml'
import { describe, expect, it } from 'vitest'
import { gatesForMode } from '../run-gates.ts'
const root = resolve(import.meta.dirname, '../..')
const masterPush = "github.event_name == 'push' && github.ref == 'refs/heads/master'"
const runtimeBuilder = './.github/workflows/build-exe-for-python-sdk.yml'
interface Job {
if?: string | boolean
uses?: string
needs?: string[]
with?: Record<string, unknown>
secrets?: Record<string, string>
steps?: Array<{ name?: string; run?: string; if?: string; uses?: string; with?: Record<string, unknown> }>
'runs-on'?: string | string[]
'continue-on-error'?: boolean
}
interface Workflow {
on: Record<string, unknown>
jobs: Record<string, Job>
concurrency?: Record<string, unknown>
}
function workflow(name: string): Workflow {
return load(readFileSync(resolve(root, '.github/workflows', name), 'utf8')) as Workflow
}
function commands(job: Job): string[] {
return (job.steps ?? []).flatMap(step => step.run ? [step.run] : [])
}
describe('master-only platform scheduling', () => {
it('keeps only Linux and Windows x64 runtimes in required PR CI', () => {
const pr = workflow('ci.yml')
expect(Object.keys(pr.on)).toEqual(['pull_request'])
expect(pr.jobs['python-runtime']).toMatchObject({
if: "github.event_name == 'pull_request'",
uses: runtimeBuilder,
with: { ci: true, targets: 'node24-linux-x64,node24-win-x64' },
})
expect(pr.jobs.windows).toBeUndefined()
expect(JSON.stringify(pr.jobs)).not.toMatch(/wine-windows-gates|check:windows-wine/)
const aggregate = pr.jobs['all-checks-passed']!
expect(aggregate.needs).toContain('python-runtime')
expect(aggregate.needs).not.toContain('windows')
expect(aggregate.needs!.every(id => id in pr.jobs)).toBe(true)
expect(aggregate.if).toBe("always() && github.event_name == 'pull_request'")
expect(aggregate.steps).toContainEqual(expect.objectContaining({
if: "contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')",
}))
})
it('runs all three deferred carriers on master pushes with fail-loud API credentials', () => {
const master = workflow('ci-master.yml')
expect(master.on.push).toEqual({ branches: ['master'] })
expect(Object.keys(master.on).sort()).toEqual(['push', 'workflow_dispatch'])
const runtime = master.jobs['python-runtime']!
expect(runtime).toMatchObject({
if: masterPush,
uses: runtimeBuilder,
with: { ci: true, targets: 'node24-linux-arm64,node24-macos-arm64,node24-macos-x64' },
secrets: { DEEPSEEK_API_KEY_EXTERNAL: '${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}' },
})
expect(runtime.needs).toBeUndefined()
expect(runtime['continue-on-error']).toBeUndefined()
const builder = workflow('build-exe-for-python-sdk.yml')
expect(builder.concurrency?.['cancel-in-progress']).toBe(
"${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }}",
)
const build = builder.jobs.build!
const preflight = build.steps!.find(step => step.name === 'Preflight installed-wheel real API test (POSIX)')!
expect(preflight.if).toContain('inputs.ci')
expect(preflight.if).toContain("github.event_name != 'pull_request'")
expect(preflight.if).toContain('github.event.pull_request.head.repo.fork')
expect(preflight.if).toContain("github.event.pull_request.user.login == 'dependabot[bot]'")
expect(preflight.run).toContain('exit 1')
})
it('runs Wine once on hosted master CI and seeds its own apt cache', () => {
const master = workflow('ci-master.yml')
const wine = master.jobs.windows!
expect(wine).toMatchObject({ if: masterPush, 'runs-on': 'ubuntu-latest' })
expect(wine.needs).toBeUndefined()
expect(wine['continue-on-error']).toBeUndefined()
expect(master.jobs['wine-apt-cache']).toBeUndefined()
expect(Object.values(master.jobs).flatMap(commands).filter(command => command.includes('wine-windows-gates.sh')))
.toEqual(['bash scripts/wine-windows-gates.sh'])
expect(wine.steps).toContainEqual(expect.objectContaining({
uses: 'actions/cache@v4', with: { path: '~/wine-debs', key: '${{ steps.wine-cache-key.outputs.key }}' },
}))
expect(commands(wine).join('\n')).toContain('--download-only wine')
expect(wine.steps).toContainEqual(expect.objectContaining({ name: 'Shut down wineserver', if: 'always()' }))
// Graph construction needs a pnpm entrypoint but never launches it.
const previous = process.env.npm_execpath
process.env.npm_execpath = '/test/pnpm.cjs'
try {
for (const mode of ['ci-linux-primary', 'ci-windows-complete'] as const) {
expect(gatesForMode(mode).map(gate => gate.displayCommand).join('\n')).not.toMatch(/wine/i)
}
} finally {
if (previous === undefined) Reflect.deleteProperty(process.env, 'npm_execpath')
else process.env.npm_execpath = previous
}
expect(process.env.npm_execpath).toBe(previous)
})
it('retains the complete release matrix independently of CI scheduling', () => {
const release = workflow('python-release.yml')
const calls = Object.values(release.jobs).filter(job => job.uses === runtimeBuilder)
expect(calls).toHaveLength(1)
expect(calls[0]!.with).toMatchObject({
release: true,
targets: 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64',
})
})
})
+18 -2
View File
@@ -38,6 +38,12 @@ function evaluate(expression: string, context: Record<string, string | boolean>)
return runInNewContext(source, { fromJSON: JSON.parse }, { timeout: 1000 }) as unknown
}
function assertSharedPersistentStore(run: string | undefined): void {
expect(run).toContain('store_root="$HOME/.local/share/pnpm/store"')
expect(run).toContain('echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"')
expect(run).toContain('store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)')
}
const trustedPr = {
'vars.DSH_CI_FAILOVER_LINUX': 'selfhosted',
'github.repository': repository,
@@ -108,8 +114,18 @@ for (const [file, jobIds] of [['release.yml', ['dependencies', 'pack']], ['relea
.toBe('${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}')
expect(job.steps.find(step => step.name === 'Install (immutable)')?.run).toBe('pnpm install --frozen-lockfile')
})
it('uses the persistent store without remote cache reads or writes on self-hosted', () => {
expect(job.steps.find(step => step.name === 'Configure pnpm store path')?.run).toContain('store_root="$HOME/.local/share/pnpm/store"')
it('retains the configured shared npm cache', () => {
expect(JSON.stringify(release)).not.toMatch(/npm_config_cache/i)
})
it.each(['', 'store_root="${RUNNER_TEMP%/*}/pnpm-store"', 'store_root="$RUNNER_TEMP/pnpm-store"'])(
'rejects missing, runner-private, or job-temporary store placement: %s', (replacement) => {
const run = job.steps.find(step => step.name === 'Configure pnpm store path')?.run
?.replace('store_root="$HOME/.local/share/pnpm/store"', replacement)
expect(() => { assertSharedPersistentStore(run) }).toThrow()
},
)
it('uses the shared persistent store without remote cache reads or writes on self-hosted', () => {
assertSharedPersistentStore(job.steps.find(step => step.name === 'Configure pnpm store path')?.run)
const caches = job.steps.filter(step => step.uses?.startsWith('actions/cache'))
expect(caches.map(step => step.uses)).toEqual(['actions/cache/restore@v4'])
for (const step of caches) {
@@ -3,6 +3,8 @@
".agents/notes/AGENTS.md",
".agents/notes/implemented/AGENTS.md",
".agents/notes/implemented/CLAUDE.md",
".agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.md",
".github/review-ownership/README.md",
"docs/AGENTS.md",
"docs/cordis-api/inherited.md",
"docs/i18n/style-samples.md",
+15 -5
View File
@@ -116,6 +116,11 @@
"symbol": "LlmCallConfigAdapterDefaults",
"source": "packages/llm/llm/src/call-config.ts"
},
{
"doc": "docs/subsystems/core.md",
"symbol": "Inbox",
"source": "packages/core/agent/src/runtime-types.ts"
},
{
"doc": "docs/subsystems/core.md",
"symbol": "InboxTarget",
@@ -202,6 +207,11 @@
"symbol": "GoalView",
"source": "packages/goal/goal/src/types.ts"
},
{
"doc": "docs/subsystems/goal.md",
"symbol": "GoalActivationChanged",
"source": "packages/goal/goal/src/types.ts"
},
{
"doc": "docs/subsystems/goal.md",
"symbol": "GoalSnapshotChangeMeta",
@@ -1349,22 +1359,22 @@
{
"doc": "docs/subsystems/subagent.md",
"symbol": "AgentMessageSource",
"source": "packages/subagent/subagent/src/continuation.ts"
"source": "packages/subagent/subagent/src/continuation-messages.ts"
},
{
"doc": "docs/subsystems/subagent.md",
"symbol": "SubagentSettledMessageSource",
"source": "packages/subagent/subagent/src/continuation.ts"
"source": "packages/subagent/subagent/src/continuation-messages.ts"
},
{
"doc": "docs/subsystems/subagent.md",
"symbol": "SubagentSendMessageOptions",
"source": "packages/subagent/subagent/src/continuation.ts"
"source": "packages/subagent/subagent/src/types.ts"
},
{
"doc": "docs/subsystems/subagent.md",
"symbol": "SubagentInterruptAuthority",
"source": "packages/subagent/subagent/src/continuation.ts"
"source": "packages/subagent/subagent/src/types.ts"
},
{
"doc": "docs/subsystems/subagent.md",
@@ -1374,7 +1384,7 @@
{
"doc": "docs/subsystems/subagent.md",
"symbol": "ContinuableStart",
"source": "packages/subagent/subagent/src/continuation.ts"
"source": "packages/subagent/subagent/src/types.ts"
},
{
"doc": "docs/subsystems/subagent.md",
@@ -74,6 +74,15 @@ describe('application entrypoints', () => {
])
})
it('rejects a packaging dispatcher owned by the CLI workspace', () => {
const root = fixture()
write(root, 'apps/cli/src/runtime-bootstrap.ts', '#!/usr/bin/env node\n')
expect(applicationEntrypointViolations(root)).toEqual([
'apps/cli/src/runtime-bootstrap.ts: executable source has no application/build/test classification',
])
})
it('rejects a private Python application carrier outside dsh', () => {
const root = fixture()
write(root, 'packages/sdk/rogue-python-runtime/package.json', JSON.stringify({ private: true }))
+3 -1
View File
@@ -29,7 +29,7 @@ const MANIFEST_BIN_ALLOWLIST = new Map<string, ManifestBin>([
['packages/experimental/webworker-packer/package.json', { 'dsh-pack-vfs-image': './bin.js' }],
])
/** Every executable in a Node application workspace has one explicit role. */
/** Every JavaScript executable in an application or packaging workspace has one explicit role. */
const EXECUTABLE_SOURCE_ALLOWLIST = new Map<string, string>([
['apps/cli/src/bin.ts', 'supported dsh application launcher'],
['packages/context/time-context/tests/fixtures/driver.ts', 'test-only subprocess driver'],
@@ -44,6 +44,7 @@ const EXECUTABLE_SOURCE_ALLOWLIST = new Map<string, string>([
['packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/driver.ts', 'test-only subprocess driver'],
['packages/test-support/loader-smoke/tests/fixtures/headless-driver.ts', 'test-only subprocess driver'],
['packages/test-support/llm-mock-server/src/bin.ts', 'test-only model server'],
['python/sdk-runtime/runtime-bootstrap.mjs', 'private packaging-only runtime dispatcher'],
])
/** Root demos are application wrappers and therefore must visibly select dsh. */
@@ -65,6 +66,7 @@ const SOURCE_PATTERNS = [
'packages/**/*.js',
'packages/**/*.mjs',
'packages/**/*.cjs',
'python/sdk-runtime/*.mjs',
]
const SOURCE_EXCLUDES = [
+16
View File
@@ -62,4 +62,20 @@ describe('Client UI i18n source check', () => {
'export const en = { title: "Hard-coded by design" }',
)).toEqual([])
})
it('rejects Electron dialog, title, prompt, and DOM copy outside locale owners', () => {
const source = `
dialog.showMessageBox({ title: 'Update available', message: 'Install it now?' })
window.setTitle('Desktop plugins')
window.prompt('Target version')
status.textContent = 'Finished'
`
expect(findUiI18nViolations('apps/desktop/src/main.ts', source).map(row => row.text)).toEqual([
'Update available',
'Install it now?',
'Desktop plugins',
'Target version',
'Finished',
])
})
})
+18 -2
View File
@@ -114,7 +114,7 @@ export function findUiI18nViolations(file: string, sourceText: string): UiI18nVi
sourceText,
ts.ScriptTarget.Latest,
true,
file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
file.endsWith('.tsx') ? ts.ScriptKind.TSX : file.endsWith('.js') ? ts.ScriptKind.JS : ts.ScriptKind.TS,
)
const violations = new Map<number, UiI18nViolation>()
@@ -254,13 +254,27 @@ export function findUiI18nViolations(file: string, sourceText: string): UiI18nVi
&& (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))
) collectExpression(node.expression, 'JSX child')
if (file.endsWith('.tsx') && ts.isPropertyAssignment(node)) {
if ((file.endsWith('.tsx') || file.startsWith('apps/desktop/')) && ts.isPropertyAssignment(node)) {
const name = propertyName(node.name)
if (name !== undefined && (COPY_NAME.test(name) || COPY_SUFFIX.test(name))) {
collectExpression(node.initializer, `${name} property`)
}
}
if (file.startsWith('apps/desktop/') && ts.isBinaryExpression(node)
&& node.operatorToken.kind === ts.SyntaxKind.EqualsToken
&& ts.isPropertyAccessExpression(node.left)
&& (node.left.name.text === 'textContent' || node.left.name.text === 'innerText')) {
collectExpression(node.right, `${node.left.name.text} assignment`)
}
if (file.startsWith('apps/desktop/') && ts.isCallExpression(node)
&& ts.isPropertyAccessExpression(node.expression)
&& (node.expression.name.text === 'setTitle' || node.expression.name.text === 'prompt')) {
const copy = node.arguments[0]
if (copy !== undefined) collectExpression(copy, `${node.expression.name.text} argument`)
}
if (ts.isVariableDeclaration(node) && node.initializer !== undefined) {
const name = propertyName(node.name)
if (name !== undefined && (COPY_NAME.test(name) || COPY_SUFFIX.test(name))) {
@@ -314,6 +328,8 @@ function sourceFiles(): string[] {
...[...clientComponentRoots].flatMap(clientRoot =>
globSync(`${clientRoot}/**/*.{ts,tsx}`, { cwd: root })),
...globSync('apps/web/src/**/*.{ts,tsx}', { cwd: root }),
...globSync('apps/desktop/src/{main,update-coordinator}.{ts,tsx}', { cwd: root }),
...globSync('apps/desktop/renderer/*.js', { cwd: root }),
])]
.map(file => file.replaceAll('\\', '/'))
.filter(file => !file.endsWith('.d.ts'))
@@ -84,6 +84,8 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/experimental/agent-team-web-profile': { kind: 'indirect', reason: 'The private Web layer delegates model composition to the Host-side Agent Teams profile.' },
'packages/experimental/client-ui-agent-team': { kind: 'none', reason: 'The browser projection and task controls register no model-facing input.' },
'packages/client/ui-layout': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-dockkit': { kind: 'none', reason: 'Browser-side docking layout engine and components; registers nothing model-facing.' },
'packages/client/ui-sidebar-right': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-sidebar': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-brand-official': { kind: 'none', reason: 'Browser-side presentation occupants; registers nothing model-facing.' },
'packages/client/ui-conversation': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
@@ -109,6 +111,9 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/client/ui-directory-picker-browse': { kind: 'none', reason: 'Browser-side directory-browsing surface; registers nothing model-facing.' },
'packages/client/ui-directory-picker-native': { kind: 'none', reason: 'Browser-side surface driving the host OS chooser; registers nothing model-facing.' },
'packages/client/ui-theme': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-sidebar-textpreview': { kind: 'none', reason: 'Browser-only text viewer; registers no tool, prompt section, or session event, and what the user reads never enters a model request.' },
'packages/client/ui-sidebar-files': { kind: 'none', reason: 'Browser-only workspace file tree; registers no tool, prompt section, or session event.' },
'packages/client/resources': { kind: 'none', reason: 'Browser-side resource model (providers, pinning, useResource); registers nothing model-facing.' },
'packages/client/ui-settings': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-settings-general': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-settings-models': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
@@ -130,6 +135,8 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/host/plugin-inventory': { kind: 'none', reason: 'Host-side read-only Loader projection; registers nothing model-facing.' },
'packages/host/plugin-manager': { kind: 'none', reason: 'The plugins Remote over the shared plugin manager; the rows the manager composes own every model-facing registration.' },
'packages/boot/plugin-manager': { kind: 'none', reason: 'Plugin management over the profile; the rows it composes own every model-facing registration.' },
'packages/host/open-in-app': { kind: 'none', reason: 'Host routes opening desktop applications for a human; registers nothing model-facing.' },
'packages/client/ui-open-in-app': { kind: 'none', reason: 'Browser-side split button opening the workspace directory for a human; registers nothing model-facing.' },
'packages/bundle/base': { kind: 'indirect', reason: 'The bundle is a patch-list carrier; each inserted row\'s package owns its model-facing behavior.' },
'packages/bundle/headless': { kind: 'none', reason: 'The one-shot runner submits the task as an ordinary user message; prompts and tools belong to the composed base and headless bundles.' },
'packages/llm/llm': { kind: 'none', reason: 'The adapter registry forwards already-assembled requests unchanged.' },
@@ -173,6 +180,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/api/session-controller': { kind: 'none', reason: 'Session API and transport owner; invoked Agent commands own any model-visible effect.' },
'packages/api/settings-controller': { kind: 'none', reason: 'Configuration-surface API owner; it registers no prompt, tool, or session event.' },
'packages/api/workspace-controller': { kind: 'none', reason: 'Workspace API and state projection owner; it registers no prompt, tool, or session event.' },
'packages/api/workspace-files': { kind: 'none', reason: 'Workspace file read API and its Client resource provider; it registers no prompt, tool, or session event.' },
'packages/typert/protocol': { kind: 'none', reason: 'Compiler-independent Remote protocol declarations; registers nothing model-facing.' },
'packages/typert/generator': { kind: 'none', reason: 'The build-time generator runs outside any agent runtime and touches no model request.' },
'packages/jobs/jobs': { kind: 'indirect', reason: 'Producer and controller plugins own all model rendering over the job registry.' },
@@ -0,0 +1,46 @@
import { describe, expect, it } from 'vitest'
import {
MAX_PACKAGE_README_SUMMARY_WORDS,
packageReadmeSummaryErrors,
} from './verify-package-readme-summaries.ts'
function readme(summary: string, kind = 'package-reference'): string {
return `---\nkind: "${kind}"\n---\n# Example\n\n## Summary\n\n${summary}\n\n## Table of Contents\n`
}
describe('package README Summary limit', () => {
it('accepts exactly 100 whitespace-delimited words', () => {
const summary = Array.from({ length: MAX_PACKAGE_README_SUMMARY_WORDS }, () => 'word').join(' ')
expect(packageReadmeSummaryErrors('packages/example/example/README.md', readme(summary))).toEqual([])
})
it.each([
'package-group',
'package-reference',
'package-library',
'package-bundle',
])('rejects 101 words and directs the author to the skill and %s template', (kind) => {
const summary = Array.from({ length: MAX_PACKAGE_README_SUMMARY_WORDS + 1 }, () => 'word').join(' ')
expect(packageReadmeSummaryErrors('packages/example/example/README.md', readme(summary, kind))).toEqual([
`packages/example/example/README.md: Summary has 101 words; the limit is 100. Read .agents/skills/dsh-doc/SKILL.md and .agents/skills/dsh-doc/templates/${kind}.md before rewriting it.`,
])
})
it('counts only the Summary body', () => {
const laterSection = Array.from({ length: 101 }, () => 'detail').join(' ')
expect(packageReadmeSummaryErrors(
'packages/example/example/README.md',
`${readme('Short summary.')}\n${laterSection}`,
)).toEqual([])
})
it('rejects a missing Summary instead of silently narrowing the corpus', () => {
expect(packageReadmeSummaryErrors(
'packages/example/example/README.md',
'---\nkind: "package-reference"\n---\n# Example\n',
)).toEqual(['packages/example/example/README.md: missing `## Summary`'])
})
})
@@ -0,0 +1,77 @@
/** Enforce the English package README Summary entry-length limit. */
import { globSync, readFileSync } from 'node:fs'
import { resolve } from 'node:path'
const root = resolve(import.meta.dirname, '..')
/** Maximum `wc -w`-style length of an English package README Summary. */
export const MAX_PACKAGE_README_SUMMARY_WORDS = 100
const PACKAGE_README_PATTERNS = [
'packages/README.md',
'packages/*/README.md',
'packages/*/*/README.md',
] as const
/** `wc -w` equivalent used by the documentation budget gate. */
function countWords(text: string): number {
return text.split(/\s+/u).filter(Boolean).length
}
/** Extract one H2 section body without consuming the next H2. */
function h2Body(source: string, heading: string): string | undefined {
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')
const match = new RegExp(`^## ${escaped}\\s*\\n([\\s\\S]*?)(?=^## |(?![\\s\\S]))`, 'mu').exec(source)
return match?.[1]?.trim()
}
/** Read the package README kind for a diagnostic template link. */
function readKind(source: string): string | undefined {
return /^kind:\s*["']?([a-z-]+)["']?\s*$/mu.exec(source)?.[1]
}
/**
* Report Summary length violations for one English package README.
* @param file - Repository-relative README path.
* @param source - Complete README source.
* @returns Diagnostics for a missing or oversized Summary.
*/
export function packageReadmeSummaryErrors(file: string, source: string): string[] {
const summary = h2Body(source, 'Summary')
if (summary === undefined) return [`${file}: missing \`## Summary\``]
const words = countWords(summary)
if (words <= MAX_PACKAGE_README_SUMMARY_WORDS) return []
const kind = readKind(source)
const template = kind === undefined
? '.agents/skills/dsh-doc/templates/'
: `.agents/skills/dsh-doc/templates/${kind}.md`
return [
`${file}: Summary has ${String(words)} words; the limit is ${String(MAX_PACKAGE_README_SUMMARY_WORDS)}. Read .agents/skills/dsh-doc/SKILL.md and ${template} before rewriting it.`,
]
}
/** Find every authored English package README covered by the kind templates. */
function packageReadmes(): string[] {
return PACKAGE_README_PATTERNS
.flatMap(pattern => globSync(pattern, { cwd: root, exclude: ['**/node_modules/**'] }))
.map(file => file.replaceAll('\\', '/'))
.sort()
}
if (import.meta.main) {
const files = packageReadmes()
const failures = files.length === 0
? ['no English package READMEs found; the scan is empty or narrowed']
: files.flatMap(file => packageReadmeSummaryErrors(file, readFileSync(resolve(root, file), 'utf8')))
if (failures.length > 0) {
console.error('verify-package-readme-summaries: violations found:')
for (const failure of failures) console.error(` ${failure}`)
process.exitCode = 1
} else {
console.log(`verify-package-readme-summaries: ${String(files.length)} English package README Summaries are within ${String(MAX_PACKAGE_README_SUMMARY_WORDS)} words.`)
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Run the blocking Windows gates (workspace build, production site) with real
# win-x64 Node.js under Wine — the same script the pull-request `windows` job
# in ci.yml executes and the optional local gate `pnpm run check:windows-wine`
# win-x64 Node.js under Wine — the same script the master-only `windows` job
# in ci-master.yml executes and the optional local gate `pnpm run check:windows-wine`
# wraps. Owning rationale and fidelity limits:
# .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md
#