Merge remote-tracking branch 'origin/master' into feat/read-image-context

# Conflicts:
#	apps/cli/tests/web-agent-presets.e2e.ts
#	packages/fs/tool-fs/package.json
This commit is contained in:
creatixchu
2026-08-11 10:12:14 +08:00
1962 changed files with 25048 additions and 14905 deletions
+95 -3
View File
@@ -8,7 +8,7 @@
import { spawn } from 'node:child_process'
import { existsSync, statSync } from 'node:fs'
import { chmod, copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { chmod, copyFile, cp, lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
import { basename, dirname, join, resolve, sep } from 'node:path'
import { parseArgs } from 'node:util'
@@ -16,8 +16,8 @@ const root = resolve(import.meta.dirname, '..')
/** The closure manifest whose dependencies define the executable. */
const DEPLOY_ROOT_PACKAGE = 'dsh-jsonrpc-agent-pkg'
/** The app entry inside the deployed closure. */
const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js'
/** The closed-runtime app entry inside the deployed closure. */
const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js'
const OUTPUT_BASENAME = 'dsh-jsonrpc-agent-pkg'
/** Default Node major; SEA mode requires at least Node 22. */
const DEFAULT_NODE_RANGE = 'node24'
@@ -28,6 +28,8 @@ const OUT_DIR = 'dist-exe'
const PYTHON_RUNTIME_DIR = 'python/sdk-runtime/src/deepseek_harness_runtime/runtime'
/** The deployed closure doubles as the node-mode carrier. */
const PYTHON_NODE_SUBDIR = 'node'
/** Legacy deploy may hoist peer-specialized workspace packages back here. */
const DEPLOY_SOURCE_NODE_MODULES = 'python/sdk-runtime/node_modules'
/** Documentation excluded from the generated runtime directory. */
const DEPLOY_ONLY_DOCS = ['README.md', 'README.zh.md', 'README.i18n.yaml']
@@ -256,6 +258,8 @@ class SingleExeBuild {
'--config.link-workspace-packages=true',
this.staging,
])
await this.restoreLegacyHoists()
await this.materializeStagedLinks()
if (this.cli.dryRun) {
for (const name of DEPLOY_ONLY_DOCS) console.log(`build-exe-for-python-sdk: [dry-run] rm -f ${join(this.staging, name)}`)
} else {
@@ -263,6 +267,94 @@ class SingleExeBuild {
}
}
/**
* Restore direct packages that pnpm's legacy hoister places beside the deploy
* source instead of in the target. The runtime manifest supplies every peer,
* so package-local node_modules trees are omitted to preserve one flat Cordis
* instance and a symlink-free packaged payload.
*/
private async restoreLegacyHoists(): Promise<void> {
if (this.cli.dryRun) {
console.log('build-exe-for-python-sdk: [dry-run] restore direct dependencies omitted by legacy deploy')
return
}
const manifestPath = join(this.staging, 'package.json')
const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as {
dependencies?: Record<string, string>
}
const sourceNodeModules = resolve(root, DEPLOY_SOURCE_NODE_MODULES)
const restored: string[] = []
for (const dependency of Object.keys(manifest.dependencies ?? {}).sort()) {
const destination = join(this.staging, 'node_modules', dependency)
if (existsSync(destination)) continue
const source = join(sourceNodeModules, dependency)
if (!existsSync(source)) {
throw new Error(
`build-exe-for-python-sdk: deployed dependency ${dependency} is absent from both ${destination} and ${source}.`,
)
}
await mkdir(dirname(destination), { recursive: true })
const nestedNodeModules = join(source, 'node_modules')
await cp(source, destination, {
recursive: true,
dereference: true,
filter: path => path !== nestedNodeModules && !path.startsWith(nestedNodeModules + sep),
})
restored.push(dependency)
}
const stillMissing = Object.keys(manifest.dependencies ?? {})
.filter(dependency => !existsSync(join(this.staging, 'node_modules', dependency)))
if (stillMissing.length > 0) {
throw new Error(`build-exe-for-python-sdk: staged dependencies remain missing: ${stillMissing.join(', ')}.`)
}
if (restored.length > 0) {
console.log(`build-exe-for-python-sdk: restored legacy deploy hoists: ${restored.join(', ')}`)
}
}
/** Replace deploy-time package links with files and reject any remaining link. */
private async materializeStagedLinks(): Promise<void> {
if (this.cli.dryRun) {
console.log('build-exe-for-python-sdk: [dry-run] materialize staged package links')
return
}
const nodeModules = join(this.staging, 'node_modules')
let remaining = await this.findSymlink(nodeModules)
while (remaining !== undefined) {
const segments = remaining.slice(nodeModules.length + 1).split(sep)
const binIndex = segments.lastIndexOf('.bin')
if (binIndex >= 0) {
await rm(join(nodeModules, ...segments.slice(0, binIndex + 1)), { recursive: true, force: true })
remaining = await this.findSymlink(nodeModules)
continue
}
const destination = remaining
const source = await realpath(destination)
const nestedNodeModules = join(source, 'node_modules')
await rm(destination, { recursive: true, force: true })
await cp(source, destination, {
recursive: true,
dereference: true,
filter: path => path !== nestedNodeModules && !path.startsWith(nestedNodeModules + sep),
})
remaining = await this.findSymlink(nodeModules)
}
}
/** Return the first symbolic link below a directory, if one exists. */
private async findSymlink(directory: string): Promise<string | undefined> {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const path = join(directory, entry.name)
const metadata = await lstat(path)
if (metadata.isSymbolicLink()) return path
if (metadata.isDirectory()) {
const nested = await this.findSymlink(path)
if (nested !== undefined) return nested
}
}
return undefined
}
/** Add the executable entry and pkg assets to the staged manifest. */
async injectPkgConfig(): Promise<void> {
const patch = { bin: ENTRY_BIN, pkg: { assets: ASSET_GLOBS } }
+39 -8
View File
@@ -17,6 +17,8 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SDK_DISTRIBUTION = "deepseek-harness-sdk"
RUNTIME_DISTRIBUTION = "deepseek-harness-runtime-bin"
PLATFORMS = {
"linux-x64": ("manylinux_2_28_x86_64", "dsh-jsonrpc-agent-pkg-linux-x64"),
"linux-arm64": ("manylinux_2_28_aarch64", "dsh-jsonrpc-agent-pkg-linux-arm64"),
@@ -41,6 +43,8 @@ def main() -> None:
args = parser.parse_args()
version = repository_version()
validate_release_tag(args.tag, version)
# Wheels carry the PEP 440 spelling; the tag keeps the repository spelling.
wheel_version = pep440_version(version)
if args.package == "runtime" and (args.platform is None or args.runtime_exe is None):
parser.error("runtime builds require --platform and --runtime-exe")
if args.package == "sdk" and (args.platform is not None or args.runtime_exe is not None):
@@ -51,19 +55,19 @@ def main() -> None:
with tempfile.TemporaryDirectory(prefix="dsh-python-release-") as temporary:
staging = Path(temporary) / args.package
if args.package == "sdk":
stage_sdk(staging, version)
stage_sdk(staging, wheel_version)
environment = None
expected = output_dir / f"deepseek_harness-{version}-py3-none-any.whl"
expected = output_dir / f"deepseek_harness_sdk-{wheel_version}-py3-none-any.whl"
else:
platform_tag, executable_name = PLATFORMS[args.platform]
stage_runtime(staging, version, args.runtime_exe.resolve(), executable_name)
stage_runtime(staging, wheel_version, args.runtime_exe.resolve(), executable_name)
environment = {"DSH_RUNTIME_PLATFORM_TAG": platform_tag}
expected = output_dir / f"deepseek_harness_runtime_bin-{version}-py3-none-{platform_tag}.whl"
expected = output_dir / f"deepseek_harness_runtime_bin-{wheel_version}-py3-none-{platform_tag}.whl"
command = ["uv", "build", "--wheel", "--out-dir", str(output_dir), str(staging)]
subprocess.run(command, cwd=ROOT, env=None if environment is None else {**os.environ, **environment}, check=True)
if not expected.is_file():
raise RuntimeError(f"build did not produce expected wheel: {expected}")
verify_wheel(expected, args.package, version, None if args.platform is None else PLATFORMS[args.platform])
verify_wheel(expected, args.package, wheel_version, None if args.platform is None else PLATFORMS[args.platform])
print(expected)
@@ -74,13 +78,35 @@ def repository_version(root: Path = ROOT) -> str:
except (OSError, json.JSONDecodeError) as error:
raise ValueError(f"could not read repository version from {package_json}") from error
version = payload.get("version") if isinstance(payload, dict) else None
if not isinstance(version, str) or re.fullmatch(r"\d+\.\d+\.\d+", version) is None:
if not isinstance(version, str) or re.fullmatch(r"\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?", version) is None:
raise ValueError(
f"{package_json} version must be stable X.Y.Z, got {version!r}"
f"{package_json} version must be X.Y.Z with an optional prerelease segment, got {version!r}"
)
return version
def pep440_version(version: str) -> str:
"""The Python spelling of a repository version.
A release candidate is `0.0.1-rc.1` in the repository and `0.0.1rc1` under
PEP 440. Build backends normalize to the latter, so the wheel filename and
metadata carry it: comparing them against the repository spelling would
reject every prerelease build.
"""
stable, separator, prerelease = version.partition("-")
if not separator:
return stable
match = re.fullmatch(r"(a|b|c|rc|alpha|beta|pre|preview)\.?(\d+)", prerelease)
if match is None:
raise ValueError(
f"prerelease segment {prerelease!r} has no PEP 440 spelling; use rc.N, alpha.N, or beta.N"
)
identifier = {"alpha": "a", "beta": "b", "c": "rc", "pre": "rc", "preview": "rc"}.get(
match.group(1), match.group(1)
)
return f"{stable}{identifier}{match.group(2)}"
def validate_release_tag(tag: str | None, version: str) -> None:
if tag is None:
return
@@ -160,6 +186,11 @@ def verify_wheel(
raise RuntimeError(f"{wheel} has wrong WHEEL tags: {wheel_metadata.get_all('Tag')}")
if metadata.get("Version") != version:
raise RuntimeError(f"{wheel} has version {metadata.get('Version')}, expected {version}")
expected_distribution = SDK_DISTRIBUTION if package == "sdk" else RUNTIME_DISTRIBUTION
if metadata.get("Name") != expected_distribution:
raise RuntimeError(
f"{wheel} has distribution name {metadata.get('Name')}, expected {expected_distribution}"
)
runtime_files = [
name for name in archive.namelist() if "/runtime/dsh-jsonrpc-agent-pkg-" in name
]
@@ -177,7 +208,7 @@ def verify_wheel(
raise RuntimeError(f"SDK wheel unexpectedly contains runtime executables: {runtime_files}")
if package == "sdk":
requirements = metadata.get_all("Requires-Dist") or []
expected_requirement = f"deepseek-harness-runtime-bin=={version}"
expected_requirement = f"{RUNTIME_DISTRIBUTION}=={version}"
if expected_requirement not in requirements:
raise RuntimeError(f"{wheel} does not pin {expected_requirement}; found {requirements}")
+84 -21
View File
@@ -21,15 +21,15 @@ const workspaceGlobs = [
{ dir: 'apps', depth: 1 },
] as const
const vendoredPackages = new Set([
'cordis',
'cosmokit',
'schemastery',
'@cordisjs/plugin-loader',
'@cordisjs/plugin-include',
'@cordisjs/plugin-group',
'@cordisjs/plugin-timer',
'@cordisjs/plugin-hmr',
'@cordisjs/plugin-logger-console',
'@deepseek-ai/cordis',
'@deepseek-ai/cosmokit',
'@deepseek-ai/schemastery',
'@deepseek-ai/cordis-plugin-loader',
'@deepseek-ai/cordis-plugin-include',
'@deepseek-ai/cordis-plugin-group',
'@deepseek-ai/cordis-plugin-timer',
'@deepseek-ai/cordis-plugin-hmr',
'@deepseek-ai/cordis-plugin-logger-console',
])
const publicLandlockPackages = new Set([
'@deepseek-ai/node-addon-landlock-run',
@@ -41,6 +41,14 @@ const publicationSourceAllowlist: Readonly<Record<string, readonly string[]>> =
'@deepseek-ai/node-addon-landlock-run': ['src/main.c'],
}
const repositoryUrl = 'git+https://github.com/deepseek-harness/deepseek-harness.git'
/**
* Source home the published packages point consumers at. It differs from
* {@link repositoryUrl}, which the Landlock packages keep because npm resolves
* their trusted publishing against the repository that runs the workflow.
*/
const publishedRepositoryUrl = 'git+https://github.com/deepseek-ai/deepseek-harness.git'
/** Directories whose packages this repository publishes: one release member each. */
const releaseMemberDirectory = /^(?:packages\/[^/]+\/[^/]+|apps\/[^/]+|vendor\/[^/]+)$/
const localArtifactDirs = new Set(['node_modules'])
const appPackageFiles: Readonly<Record<string, readonly string[]>> = {
@@ -72,6 +80,8 @@ interface PackageManifest {
repository?: { type?: string; url?: string; directory?: string }
peerDependencies?: Record<string, string>
devDependencies?: Record<string, string>
dependencies?: Record<string, string>
optionalDependencies?: Record<string, string>
}
/** One workspace manifest and its repo-relative path. */
@@ -126,9 +136,13 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
'@deepseek-ai/dsh-headless': ['cordis.patch.yml'],
'@deepseek-ai/dsh-client-ui-theme': ['lib/styles'],
'@deepseek-ai/dsh-helper': ['lib/assets'],
// The Python runtime uses a distinct closed-resolution bin; the public CLI
// keeps config-owned bare-package resolution through lib/bin.js.
'@deepseek-ai/dsh-jsonrpc-demo': ['lib/packaged-bin.js'],
// The argv-prefix runner entry ships beside the lib as its own bundle;
// sandbox-local resolves it through the package's ./runner export.
'@deepseek-ai/dsh-sandbox-windows-acl': ['lib/runner.js'],
// sandbox-local resolves it through the package's ./runner export. tsdown
// 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'],
'@deepseek-ai/dsh-subprocess-local': ['scripts/ensure-spawn-helper.mjs'],
'@deepseek-ai/dsh-scripts': [
@@ -161,6 +175,9 @@ function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
...exportDefault(manifest, './loader') === './lib/loader.js' ? ['lib/loader.js'] : [],
// web-react's store subpath ships its own bundle (single-entry builds; no shared chunk).
...exportDefault(manifest, './store') === './lib/store/index.js' ? ['lib/store/index.js'] : [],
// A surface bundle's startup row is its own bundle: the Loader imports it
// as a row module, so it cannot ride inside the package entry.
...exportDefault(manifest, './startup') === './lib/startup.js' ? ['lib/startup.js'] : [],
...extras,
// Subpaths whose runtime default is the tsc-emitted tree (lib/types/*.js —
// browser-safe source channels rehomed off src so plain Node can import
@@ -225,8 +242,8 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
if (manifest.private === true) {
errors.push(`${label}: published Landlock package must not set "private": true`)
}
if (manifest.publishConfig?.access !== 'public') {
errors.push(`${label}: published Landlock package must set publishConfig.access to "public"`)
if (manifest.publishConfig?.access !== 'restricted') {
errors.push(`${label}: published Landlock package must set publishConfig.access to "restricted"`)
}
const expectedDirectory = dir
if (manifest.repository?.type !== 'git'
@@ -234,6 +251,21 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
|| manifest.repository.directory !== expectedDirectory) {
errors.push(`${label}: published Landlock package repository must use ${repositoryUrl} with directory ${expectedDirectory} for trusted publishing`)
}
} else if (releaseMemberDirectory.test(dir)) {
// Release members state that they are publishable: npm refuses a private
// package, the scope is published privately, and the repository field is
// how a consumer of a private package finds its source.
if (manifest.private === true) {
errors.push(`${label}: release member must not set "private": true`)
}
if (manifest.publishConfig?.access !== 'restricted') {
errors.push(`${label}: release member must set publishConfig.access to "restricted"`)
}
if (manifest.repository?.type !== 'git'
|| manifest.repository.url !== publishedRepositoryUrl
|| manifest.repository.directory !== dir) {
errors.push(`${label}: release member repository must use ${publishedRepositoryUrl} with directory ${dir}`)
}
} else if (manifest.private !== true) {
errors.push(`${label}: package.json must set "private": true`)
}
@@ -271,13 +303,13 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
}
if (dir.startsWith('packages/') && manifest.name?.startsWith('@deepseek-ai/dsh-')) {
const peer = manifest.peerDependencies?.cordis
const dev = manifest.devDependencies?.cordis
const peer = manifest.peerDependencies?.['@deepseek-ai/cordis']
const dev = manifest.devDependencies?.['@deepseek-ai/cordis']
if (!peer) errors.push(`${label}: cordis must be a peerDependency`)
if (!dev) errors.push(`${label}: cordis must also be a devDependency`)
if (!peer) errors.push(`${label}: @deepseek-ai/cordis must be a peerDependency`)
if (!dev) errors.push(`${label}: @deepseek-ai/cordis must also be a devDependency`)
if (peer && dev && peer !== dev) {
errors.push(`${label}: cordis peer (${peer}) and dev (${dev}) ranges must match`)
errors.push(`${label}: @deepseek-ai/cordis peer (${peer}) and dev (${dev}) ranges must match`)
}
if (manifest.version !== repositoryVersion) {
errors.push(`${label}: package.json version must match root version ${repositoryVersion ?? '(missing)'}`)
@@ -346,13 +378,44 @@ function checkHierarchyShape(): string[] {
}
function checkRepositoryVersion(): string[] {
if (repositoryVersion && /^\d+\.\d+\.\d+$/.test(repositoryVersion)) return []
return ['package.json: version must be stable X.Y.Z']
// The root carries the dsh release family's version, so a prerelease such as
// 0.0.1-rc.1 is a valid state between `release:dsh` and its publication.
if (repositoryVersion && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(repositoryVersion)) return []
return ['package.json: version must be X.Y.Z with an optional prerelease segment']
}
/** Dependency sections whose ranges reach a published tarball or a local install. */
const dependencySections = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'] as const
/**
* Require the `workspace:` protocol for every reference to a workspace member.
*
* A hand-written range says nothing about the version the workspace actually
* carries, and `pnpm pack` leaves it alone: `^0.0.1` published from version
* `0.0.2` names a version that does not exist. The protocol makes pack
* substitute the member's real version, so no release step rewrites ranges.
* @param manifests - every workspace manifest.
* @returns One error per reference that names a workspace member without the protocol.
*/
function checkWorkspaceProtocol(manifests: readonly WorkspaceManifest[]): string[] {
const members = new Set(manifests.map(entry => entry.manifest.name).filter(name => name !== undefined))
const errors: string[] = []
for (const { dir, manifest } of manifests) {
for (const section of dependencySections) {
for (const [name, range] of Object.entries(manifest[section] ?? {})) {
if (!members.has(name) || range.startsWith('workspace:')) continue
errors.push(`${manifest.name ?? dir}: ${section}.${name} must use the workspace: protocol, got ${range}`)
}
}
}
return errors
}
const manifests = workspaceManifests()
const errors = [
...checkRepositoryVersion(),
...workspaceManifests().flatMap(checkWorkspace),
...manifests.flatMap(checkWorkspace),
...checkWorkspaceProtocol(manifests),
...checkHierarchyShape(),
...collectProjectReferenceFaceViolations(root),
]
+2 -2
View File
@@ -97,9 +97,9 @@ describe('client bundle purity gate', () => {
it('carries exactly one documented temporary exemption: runtime/client (store engine pending rehoming)', () => {
expect(resolveId('@deepseek-ai/dsh-client-runtime/client')).toBeNull()
const dshClientChannels = CLIENT_EXTERNALS.filter(
const clientChannels = CLIENT_EXTERNALS.filter(
entry => entry.startsWith('@deepseek-ai/') && entry.endsWith('/client'))
expect(dshClientChannels).toEqual(['@deepseek-ai/dsh-client-runtime/client'])
expect(clientChannels).toEqual(['@deepseek-ai/dsh-client-runtime/client'])
})
})
+6 -6
View File
@@ -11,12 +11,12 @@ import ts from 'typescript'
/** Cheap textual prefilter for a cordis module merge, quote-style agnostic
* (the AST match below reads `stmt.name.text` and never sees the quotes). */
const MERGE_HEAD = /declare module ['"](?:cordis|\.\/context\.ts)['"]/
const MERGE_HEAD = /declare module ['"](?:@deepseek-ai\/cordis|\.\/context\.ts)['"]/
/**
* Parse every file matching `patterns` (repo-relative, sorted, `/`-normalized)
* that textually contains a cordis module merge, yielding one entry per merge
* BLOCK — a file may legally hold several `declare module 'cordis'` blocks
* BLOCK — a file may legally hold several `declare module '@deepseek-ai/cordis'` blocks
* (the Typert analyzer reads them all), so the exhaustiveness scan must too.
* Files without a merge are skipped.
* @param scanRoot - Repository root the patterns are resolved against.
@@ -39,14 +39,14 @@ export function contextMergeFiles(
return out
}
/** Every cordis module-merge body in `sf`: `declare module 'cordis'` (harness
/** Every cordis module-merge body in `sf`: `declare module '@deepseek-ai/cordis'` (harness
* packages) or `declare module './context.ts'` (vendor core), in source order.
* Module-local: consumers walk blocks through {@link contextMergeFiles}. */
function cordisModuleBodies(sf: ts.SourceFile): ts.ModuleBlock[] {
const bodies: ts.ModuleBlock[] = []
for (const stmt of sf.statements) {
if (!ts.isModuleDeclaration(stmt) || !ts.isStringLiteral(stmt.name)) continue
if (stmt.name.text !== 'cordis' && stmt.name.text !== './context.ts') continue
if (stmt.name.text !== '@deepseek-ai/cordis' && stmt.name.text !== './context.ts') continue
if (stmt.body && ts.isModuleBlock(stmt.body)) bodies.push(stmt.body)
}
return bodies
@@ -60,7 +60,7 @@ export function cordisModuleBody(sf: ts.SourceFile): ts.ModuleBlock | null {
}
/**
* Every `key: Type` property a `declare module 'cordis'` Context merge
* Every `key: Type` property a `declare module '@deepseek-ai/cordis'` Context merge
* declares in one module body.
* @param body - The cordis module augmentation block.
* @param sf - Owning source file (for text extraction).
@@ -79,7 +79,7 @@ export function contextKeyMap(body: ts.ModuleBlock, sf: ts.SourceFile): Map<stri
}
/**
* Every event name a `declare module 'cordis'` Events merge declares in one
* Every event name a `declare module '@deepseek-ai/cordis'` Events merge declares in one
* module body. Names are the literal member keys (`'agent/created'`), read
* from method and property members alike so a declaration form the projector
* would reject still enters the exhaustiveness scan.
+21 -2
View File
@@ -1,9 +1,28 @@
import { mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { expect, it } from 'vitest'
import type { TsdownBundle } from 'tsdown'
import { watchClientPlugins } from './dev-web.ts'
import { discoverPluginDirs, watchClientPlugins } from './dev-web.ts'
it('discovers dsh.client packages with sibling roles', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-dev-web-discovery-'))
try {
const current = join(root, 'packages', 'client', 'current')
await mkdir(current, { recursive: true })
await writeFile(join(current, 'package.json'), JSON.stringify({
dsh: {
bundle: { patch: './cordis.patch.yml' },
client: { platform: 'web' },
profile: { bundles: [] },
},
}))
expect(discoverPluginDirs(root)).toEqual(['packages/client/current'])
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('rebuilds a client-plugin bundle after its source changes', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-dev-web-watch-'))
+8 -6
View File
@@ -1,5 +1,5 @@
/**
* Watch-build for client-plugin HMR: runs every dshClient plugin package
* Watch-build for client-plugin HMR: runs every `dsh.client` plugin package
* through the tsdown JS API in watch mode. Reload signaling is not this
* script's business — the host webserver stat-polls the bundles it serves and
* broadcasts `rebuilt` frames itself (`dsh web --dev`), so any process that
@@ -27,7 +27,7 @@ const repoRoot = fileURLToPath(new URL('..', import.meta.url))
/**
* Discover the watch workspace by declaration: every packages/<group>/<name>
* whose package.json carries `dshClient` with platform "web" is a client
* whose package.json carries `dsh.client` with platform "web" is a client
* plugin bundle emitter. Scanned once at startup — a package added while
* watching means restarting this script.
* @param root - repository root containing the grouped package directories.
@@ -36,8 +36,10 @@ const repoRoot = fileURLToPath(new URL('..', import.meta.url))
export function discoverPluginDirs(root = repoRoot): string[] {
const dirs: string[] = []
for (const manifestPath of globSync('packages/*/*/package.json', { cwd: root }).sort()) {
const manifest = JSON.parse(readFileSync(join(root, manifestPath), 'utf8')) as { dshClient?: { platform?: unknown } }
if (manifest.dshClient?.platform === 'web') dirs.push(dirname(manifestPath).split(sep).join('/'))
const manifest = JSON.parse(readFileSync(join(root, manifestPath), 'utf8')) as {
dsh?: { client?: { platform?: unknown } }
}
if (manifest.dsh?.client?.platform === 'web') dirs.push(dirname(manifestPath).split(sep).join('/'))
}
return dirs
}
@@ -88,7 +90,7 @@ const isMain = invokedPath !== undefined && import.meta.url === pathToFileURL(re
if (isMain) {
const pluginDirs = discoverPluginDirs()
if (pluginDirs.length === 0) {
console.error('dev-web: no dshClient (platform "web") packages found under packages/')
console.error('dev-web: no dsh.client (platform "web") packages found under packages/')
process.exit(1)
}
@@ -106,7 +108,7 @@ if (isMain) {
await watchClientPlugins(repoRoot, pluginDirs, pollInterval)
console.log(
`dev-web: watching ${String(pluginDirs.length)} dshClient plugin packages`
`dev-web: watching ${String(pluginDirs.length)} dsh.client plugin packages`
+ `${pollInterval !== undefined ? ` (polling ${String(pollInterval)}ms)` : ''}:\n ${pluginDirs.join('\n ')}`,
)
}
+5 -5
View File
@@ -128,7 +128,7 @@ describe('cordis-walk scan reach', () => {
const dir = join(root, 'packages/client/ui-x/src/client')
mkdirSync(dir, { recursive: true })
writeFileSync(join(dir, 'index.ts'), [
"declare module 'cordis' {",
"declare module '@deepseek-ai/cordis' {",
' interface Events {',
" 'x/changed'(): void",
' }',
@@ -153,12 +153,12 @@ describe('cordis-walk scan reach', () => {
// backstop must not stop at the first one, skip the double-quoted legal
// form, or ignore .tsx sources.
writeFileSync(join(dir, 'split.ts'), [
"declare module 'cordis' {",
"declare module '@deepseek-ai/cordis' {",
' interface Context {',
' first: FirstService',
' }',
'}',
'declare module "cordis" {',
'declare module "@deepseek-ai/cordis" {',
' interface Events {',
" 'second/changed'(): void",
' }',
@@ -167,7 +167,7 @@ describe('cordis-walk scan reach', () => {
'',
].join('\n'))
writeFileSync(join(dir, 'view.tsx'), [
"declare module 'cordis' {",
"declare module '@deepseek-ai/cordis' {",
' interface Context {',
' fromTsx: TsxService',
' }',
@@ -189,7 +189,7 @@ describe('cordis-walk scan reach', () => {
it('reads string-literal and identifier member names from an Events merge', () => {
const sf = ts.createSourceFile('x.ts', [
"declare module 'cordis' {",
"declare module '@deepseek-ai/cordis' {",
' interface Events {',
" 'scope/list'(items: string[]): void",
' plain(): void',
+4 -3
View File
@@ -99,7 +99,7 @@ export const SERVICE_PAGE: Record<string, string> = {
/**
* Context keys declared in `interface Context` merges that the rendering
* projection cannot see, each with the reason and its documentation owner.
* The scan that enforces this list reads EVERY `declare module 'cordis'`
* The scan that enforces this list reads EVERY `declare module '@deepseek-ai/cordis'`
* Context merge under `packages/x/x/src/**` — any depth, not only root
* `index.ts` files with a same-named service class — so a new service can
* never silently join this blind spot: it either enters {@link SERVICE_PAGE}
@@ -111,6 +111,8 @@ export const SERVICE_PAGE: Record<string, string> = {
*/
export const SERVICE_WALK_EXEMPTIONS: Record<string, string> = {
agent: 'not a service: the DX accessor field on Agent.ctx (root accessor defaulting to undefined) — docs/subsystems/core.md owns the Agent handle',
appExit: 'not a service: launcher-provided bounded process-exit callback — packages/boot/cmdline/README.md owns the launcher contract',
cmdlineArgs: 'not a service: launcher-provided immutable app argument accessor — packages/boot/cmdline/README.md owns the launcher contract',
configuredAgentIdentities: 'not a service: launcher-provided boot-context value (ConfiguredAgentIdentities | undefined) — packages/core/agent-loop/README.md owns this launcher contract',
launcherSessionQueryPath: 'not a service: launcher-provided boot-context value (string | undefined) — packages/session-query/session-query-sqlite/README.md owns this launcher contract',
dshHomePath: 'not a service: boot-provided root accessor function (typeof dshHomePath | undefined) for Loader !!js config expressions — packages/boot/app-boot/README.md owns the boot contract',
@@ -130,7 +132,6 @@ export const SERVICE_WALK_EXEMPTIONS: Record<string, string> = {
models: 'client-side interface-typed browser service — packages/client/ui-model/README.md owns the surface',
modules: 'client-side interface-typed browser service — packages/client/modules/README.md owns the surface',
remote: 'client-side interface-typed gateway accessor (ClientRemote) — packages/api/gateway/README.md owns the surface',
sessionHistory: 'client-side interface-typed browser service — packages/client/runtime/README.md owns the surface',
slash: 'client-side interface-typed browser service — packages/client/ui-slash/README.md owns the surface',
slots: 'client-side interface-typed browser service — packages/client/runtime/README.md owns the surface',
theme: 'client-side interface-typed browser service — packages/client/ui-theme/README.md owns the surface',
@@ -168,7 +169,7 @@ export const EVENT_SCOPE_PAGE: Record<string, string> = {
* Event names declared in `interface Events` merges that the rendering
* projection cannot see, each with the reason and its documentation owner.
* The mirror of {@link SERVICE_WALK_EXEMPTIONS} for events: an independent
* scan reads EVERY `declare module 'cordis'` Events merge under
* scan reads EVERY `declare module '@deepseek-ai/cordis'` Events merge under
* `packages/x/x/src/**`, so a declared event either renders onto a subsystems
* page (via {@link EVENT_SCOPE_PAGE}) or names itself here — never vanishes
* silently. Keys are full event names, not scopes: client-face events share
+1 -1
View File
@@ -512,7 +512,7 @@ const SERVICE_ROLES: ServiceRole[] = [
title: 'Client plugin graph host',
mode: 'core',
consumers: ['hmr'],
note: 'Composes the __DSH_BOOT__ entry graph from an incremental dshClient scan, serves plugin bundles, and notifies rebuilt/graph-changed subscribers.',
note: 'Composes the __DSH_BOOT__ entry graph from an incremental dsh.client scan, serves plugin bundles, and notifies rebuilt/graph-changed subscribers.',
},
{
key: 'workflows',
+2 -2
View File
@@ -309,14 +309,14 @@ class ScopedEventGenerator {
}
}
/** Return whether an Events interface is inside declare module 'cordis'. */
/** Return whether an Events interface is inside declare module '@deepseek-ai/cordis'. */
function isCordisModuleInterface(node: ts.InterfaceDeclaration): boolean {
const block = node.parent
const declaration = block.parent
return ts.isModuleBlock(block)
&& ts.isModuleDeclaration(declaration)
&& ts.isStringLiteral(declaration.name)
&& declaration.name.text === 'cordis'
&& declaration.name.text === '@deepseek-ai/cordis'
}
/** Return whether a parameter is the explicit TypeScript this receiver. */
+7 -3
View File
@@ -134,13 +134,17 @@ describe('parseVendoredRows', () => {
const rows = parseVendoredRows(readFileSync(resolve(root, 'vendor/README.md'), 'utf8'))
expect(rows.length).toBeGreaterThan(0)
expect(rows).toContainEqual({ npmName: 'cordis', upstream: 'https://github.com/cordiverse/cordis' })
expect(rows).toContainEqual({
npmName: '@deepseek-ai/cordis',
upstreamName: 'cordis',
upstream: 'https://github.com/cordiverse/cordis',
})
// The upstream column carries a trailing package path for some rows; it is not part of the URL.
expect(rows.every(row => /^https:\/\/\S+$/.test(row.upstream))).toBe(true)
})
it('yields nothing when the table columns change, so the generator fails loud', () => {
expect(parseVendoredRows('| `cordis/` | cordis | 4.0.0 | https://example.com | `abc123` |\n')).toEqual([])
expect(parseVendoredRows('| `cordis/` | `@deepseek-ai/cordis` | cordis | 4.0.0 | https://example.com | `abc123` |\n')).toEqual([])
})
it('covers every vendored directory, so no package can drop out of the notices', () => {
@@ -227,7 +231,7 @@ describe('collectPythonDependencies', () => {
it('excludes normalized local project names without exempting a third-party prefix', () => {
const pyprojects = [
'[project]\nname = "deepseek-harness-runtime-bin"\ndependencies = ["pydantic"]\n',
'[project]\nname = "deepseek-harness"\ndependencies = ["DeepSeek.Harness_Runtime-Bin", "deepseek-unrelated"]\n',
'[project]\nname = "deepseek-harness-sdk"\ndependencies = ["DeepSeek.Harness_Runtime-Bin", "deepseek-unrelated"]\n',
]
expect(() => collectPythonDependencies(pyprojects)).toThrow(
'python dependency deepseek-unrelated is missing from PYTHON_METADATA',
+12 -9
View File
@@ -83,7 +83,7 @@ const OVERRIDES: Record<string, { license?: string; repo?: string }> = {
* the generator fails when a manifest names a package this map misses.
*/
const PYTHON_METADATA: Record<string, { license: string; repo: string; role: string }> = {
pydantic: { license: 'MIT', repo: 'https://github.com/pydantic/pydantic', role: 'runtime dependency of `deepseek-harness`' },
pydantic: { license: 'MIT', repo: 'https://github.com/pydantic/pydantic', role: 'runtime dependency of `deepseek-harness-sdk`' },
hatchling: { license: 'MIT', repo: 'https://github.com/pypa/hatch', role: 'build backend' },
pytest: { license: 'MIT', repo: 'https://github.com/pytest-dev/pytest', role: 'test-only' },
}
@@ -387,6 +387,8 @@ export function tierExternalDeps(manifests: Map<string, Manifest>, names: Set<st
/** A vendored package row parsed out of the `vendor/README.md` manifest table. */
export interface VendoredRow {
npmName: string
/** The name this package carries upstream; MIT attribution names the fork's origin, not our scope. */
upstreamName: string
upstream: string
}
@@ -398,11 +400,12 @@ export interface VendoredRow {
export function parseVendoredRows(text: string): VendoredRow[] {
const rows: VendoredRow[] = []
for (const line of text.split('\n')) {
const match = /^\| \x60\S+\/\x60 \| \x60([^\x60]+)\x60 \| \S+ \| (https:\/\/\S+?)(?: \([^)]*\))? \| \x60[0-9a-f]+\x60 \|$/.exec(line)
const match = new RegExp(String.raw`^\| \x60\S+\/\x60 \| \x60([^\x60]+)\x60 \| \x60([^\x60]+)\x60 \| \S+ \| `
+ String.raw`(https:\/\/\S+?)(?: \([^)]*\))? \| \x60[0-9a-f]+\x60 \|$`).exec(line)
if (match === null) continue
const [, npmName, upstream] = match
if (npmName === undefined || upstream === undefined) continue
rows.push({ npmName, upstream })
const [, npmName, upstreamName, upstream] = match
if (npmName === undefined || upstreamName === undefined || upstream === undefined) continue
rows.push({ npmName, upstreamName, upstream })
}
return rows
}
@@ -696,11 +699,11 @@ The complete npm transitive closure, including the Landlock launcher workspace,
## Vendored source (\`vendor/\`)
The Cordis framework and its foundation libraries are source-vendored into this repository rather than consumed from npm. All are MIT-licensed; each directory preserves its upstream \`LICENSE\` file. Exact upstream commits and local modifications are recorded in [\`vendor/README.md\`](vendor/README.md).
The Cordis framework and its foundation libraries are source-vendored into this repository rather than consumed from npm, and republished under the \`@deepseek-ai\` scope. All are MIT-licensed; each directory preserves its upstream \`LICENSE\` file. Exact upstream commits and local modifications are recorded in [\`vendor/README.md\`](vendor/README.md).
| Package | Upstream | License |
| --- | --- | --- |
${vendored.map(row => `| \`${row.npmName}\` | [${row.upstream.replace('https://', '')}](${row.upstream}) | MIT |`).join('\n')}
| Package | Upstream name | Upstream | License |
| --- | --- | --- | --- |
${vendored.map(row => `| \`${row.npmName}\` | \`${row.upstreamName}\` | [${row.upstream.replace('https://', '')}](${row.upstream}) | MIT |`).join('\n')}
## Runtime npm dependencies
+1 -1
View File
@@ -8,7 +8,7 @@
import { globSync, readFileSync, writeFileSync } from 'node:fs'
import { basename, resolve } from 'node:path'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
+1 -1
View File
@@ -49,7 +49,7 @@ function fixture(options: {
},
files: ['lib/index.js', 'lib/invariant.js'],
peerDependencies: options.invariantDependency === false ? {} : {
'@deepseek-ai/dsh-invariants': '^0.0.1',
'@deepseek-ai/dsh-invariants': 'workspace:^',
},
devDependencies: options.invariantDependency === false ? {} : {
'@deepseek-ai/dsh-invariants': 'workspace:^',
+2 -2
View File
@@ -96,11 +96,11 @@ function checkManifest(
addViolation(violations, owner.manifestPath, 'files must publish lib/invariant.js')
}
if (owner.packageName === '@deepseek-ai/dsh-invariants') return
if (manifest.peerDependencies?.['@deepseek-ai/dsh-invariants'] !== '^0.0.1') {
if (manifest.peerDependencies?.['@deepseek-ai/dsh-invariants'] !== 'workspace:^') {
addViolation(
violations,
owner.manifestPath,
'@deepseek-ai/dsh-invariants must be a ^0.0.1 peerDependency',
'@deepseek-ai/dsh-invariants must be a workspace:^ peerDependency',
)
}
if (manifest.devDependencies?.['@deepseek-ai/dsh-invariants'] !== 'workspace:^') {
+3 -1
View File
@@ -258,7 +258,9 @@ class WorkspacePackageSet {
const name = expectString(manifest, 'name', manifestPath)
const version = expectString(manifest, 'version', manifestPath)
const isVendored = manifestPath.startsWith('vendor/')
if (!isVendored && !name.startsWith('@deepseek-ai/')) {
// Vendored packages are rescoped too (vendor/README.md), so publication
// never carries an upstream name that would squat it on the registry.
if (!name.startsWith('@deepseek-ai/')) {
throw new Error(`${manifestPath} must name an @deepseek-ai package`)
}
if (name === '@deepseek-ai/dsh-root') {
+398
View File
@@ -0,0 +1,398 @@
/**
* Bump one release family's version and commit it, so the published version is
* readable from the repository rather than derived inside CI
* ([rationale](../../.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)).
*
* The dsh family shares one version across its members and the workspace root:
* `major`, `minor`, `patch`, or an explicit `x.y.z` (including a prerelease such
* as `0.0.1-rc.1`). The vendored family has one version line per package and
* publishes only what changed since that package's own `vendor-<package>-v*`
* tag, which is the record of the commit it last published from.
*
* The version lands in the manifests, the lockfile follows, and a human creates
* the tag after the commit merges. CI never writes to the repository.
*/
import { readFileSync, writeFileSync } from 'node:fs'
import { join, matchesGlob } from 'node:path'
import { parseArgs } from 'node:util'
import { releaseFamily, type ReleaseFamily, type ReleaseMember } from './families.ts'
import { attempt, capture, isEntry } from './process.ts'
/** Files npm publishes whether or not `files` lists them. */
const ALWAYS_PUBLISHED = ['package.json', 'README*', 'LICENSE*', 'LICENCE*'] as const
/**
* Inputs that decide what a built payload contains. A package whose `files`
* selects `lib/` publishes build output that git does not track, so a change to
* the sources or the build configuration changes the tarball while no published
* path appears in the diff.
*/
const BUILD_INPUTS = ['src/**', 'tsconfig*.json', 'tsdown.config.*', 'build.config.*'] as const
/** Release types the dsh family accepts besides an explicit version. */
const RELEASE_TYPES = ['major', 'minor', 'patch'] as const
/** The workspace root manifest, which carries the dsh family's version. */
const ROOT_MANIFEST = 'package.json'
/** One manifest the bump rewrites, and the tag its new version will carry. */
interface PlannedVersion {
/** Repository-relative manifest path. */
readonly manifestPath: string
/** Label for the log line. */
readonly label: string
/** The version the manifest currently carries. */
readonly from: string
/** The version to write. */
readonly to: string
/** The tag this version publishes from, or undefined for the workspace root. */
readonly tag: string | undefined
}
/**
* Split a version into its release numbers, discarding any prerelease segment.
* @param version - the current version.
* @returns Major, minor, and patch.
*/
function releaseNumbers(version: string): [number, number, number] {
const match = /^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/.exec(version)
if (match === null) throw new Error(`cannot read release numbers from version ${version}`)
return [Number(match[1]), Number(match[2]), Number(match[3])]
}
/**
* Order two versions by their release numbers alone.
* @param left - one version.
* @param right - the other version.
* @returns Negative when `left` is lower, positive when higher, zero when equal.
*/
function compareReleaseNumbers(left: string, right: string): number {
const [leftMajor, leftMinor, leftPatch] = releaseNumbers(left)
const [rightMajor, rightMinor, rightPatch] = releaseNumbers(right)
return leftMajor - rightMajor || leftMinor - rightMinor || leftPatch - rightPatch
}
/**
* The prerelease segment of a version, or undefined when it has none.
* @param version - the version to read.
* @returns The segment after the first `-`.
*/
function prereleaseOf(version: string): string | undefined {
const index = version.indexOf('-')
return index === -1 ? undefined : version.slice(index + 1)
}
/**
* Order two versions by semver precedence.
*
* Git's version sort cannot stand in for this: `--sort=v:refname` places
* `4.0.1-rc.1` above `4.0.1`, while semver gives a prerelease lower precedence
* than the release it precedes. Prerelease identifiers compare field by field,
* numeric fields numerically, so `rc.10` outranks `rc.1`.
* @param left - one version.
* @param right - the other version.
* @returns Negative when `left` is lower, positive when higher, zero when equal.
*/
export function compareVersions(left: string, right: string): number {
const numbers = compareReleaseNumbers(left, right)
if (numbers !== 0) return numbers
const leftPre = prereleaseOf(left)
const rightPre = prereleaseOf(right)
if (leftPre === undefined || rightPre === undefined) {
if (leftPre === rightPre) return 0
return leftPre === undefined ? 1 : -1
}
const leftFields = leftPre.split('.')
const rightFields = rightPre.split('.')
for (let index = 0; index < Math.max(leftFields.length, rightFields.length); index += 1) {
const leftField = leftFields[index]
const rightField = rightFields[index]
// A shorter identifier list has lower precedence when all its fields match.
if (leftField === undefined) return -1
if (rightField === undefined) return 1
if (leftField === rightField) continue
const leftNumeric = /^\d+$/.test(leftField)
const rightNumeric = /^\d+$/.test(rightField)
if (leftNumeric && rightNumeric) return Number(leftField) - Number(rightField)
// Numeric fields have lower precedence than alphanumeric ones.
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1
return leftField < rightField ? -1 : 1
}
return 0
}
/**
* The next dsh version.
* @param current - the family's current shared version.
* @param request - `major`, `minor`, `patch`, or an explicit version.
* @returns The target version.
*/
function nextSharedVersion(current: string, request: string): string {
if (!RELEASE_TYPES.includes(request as typeof RELEASE_TYPES[number])) {
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(request)) {
throw new Error(`usage: release:dsh <major|minor|patch|x.y.z>, got ${request}`)
}
return request
}
const [major, minor, patch] = releaseNumbers(current)
if (request === 'major') return `${String(major + 1)}.0.0`
if (request === 'minor') return `${String(major)}.${String(minor + 1)}.0`
return `${String(major)}.${String(minor)}.${String(patch + 1)}`
}
/**
* The version a vendored package publishes next.
*
* The baseline is the higher of the manifest version and the last published
* version: a vendor re-sync restores upstream's version, which is lower than
* what this repository already published, and incrementing that would name a
* version the registry already carries.
*
* A prerelease does not consume its own release numbers. Publishing
* `4.0.1-rc.1` leaves `4.0.1` free, so the next stable version is `4.0.1`
* rather than `4.0.2`, and a second prerelease keeps those numbers too.
* @param current - the package's manifest version.
* @param published - the version its newest tag names, when it has one.
* @param prerelease - prerelease identifier to append, for a rehearsal publication.
* @returns The target version.
*/
export function nextVendorVersion(
current: string,
published: string | undefined,
prerelease?: string,
): string {
const ahead = published !== undefined && compareReleaseNumbers(published, current) > 0
const baseline = ahead ? published : current
const [major, minor, patch] = releaseNumbers(baseline)
// Reuse the numbers when the published version that set them is a prerelease
// of them; increment when a stable release already holds them.
const reuse = ahead && published.includes('-')
const numbers = reuse
? `${String(major)}.${String(minor)}.${String(patch)}`
: `${String(major)}.${String(minor)}.${String(patch + 1)}`
return prerelease === undefined ? numbers : `${numbers}-${prerelease}`
}
/**
* Whether a repository-relative path reaches the member's published payload.
* @param member - the member the path belongs to.
* @param path - repository-relative path.
* @returns True when `files`, npm's always-published set, or a build input selects it.
*/
export function reachesPayload(member: ReleaseMember, path: string): boolean {
const relative = path.slice(member.directory.length + 1)
const files = member.manifest.files
const selected = Array.isArray(files) ? files.filter((entry): entry is string => typeof entry === 'string') : []
const built = selected.some(pattern => pattern.startsWith('lib'))
const patterns = [...ALWAYS_PUBLISHED, ...selected, ...built ? BUILD_INPUTS : []]
return patterns.some(pattern =>
matchesGlob(relative, pattern) || matchesGlob(relative, `${pattern}/**`) || relative === pattern)
}
/**
* The newest version a member published, read from its tags.
* @param family - the member's family.
* @param member - the member.
* @returns The version, or undefined when the member never published.
*/
function lastPublishedVersion(family: ReleaseFamily, member: ReleaseMember): string | undefined {
const prefix = family.tagPrefixFor(member)
const versions = capture('git', ['tag', '--list', `${prefix}*`])
.split('\n').filter(line => line !== '').map(tag => tag.slice(prefix.length))
if (versions.length === 0) return undefined
return versions.reduce((newest, candidate) => compareVersions(candidate, newest) > 0 ? candidate : newest)
}
/**
* Confirm the registry carries the version a tag names.
*
* A tag is a commit pointer, not proof of publication: a tag pushed for a
* publication that then failed would otherwise read as "already published" and
* skip the package indefinitely. Querying a private package needs credentials,
* so an unauthenticated machine reports the gap instead of failing.
* @param name - package name.
* @param version - the version the tag names.
*/
function confirmPublished(name: string, version: string): void {
const result = attempt('npm', ['view', `${name}@${version}`, 'version'])
if (result.status === 0) return
const output = `${result.stdout}${result.stderr}`
if (output.includes('ENEEDAUTH') || output.includes('E401') || output.includes('E403')) {
console.log(`release bump: cannot reach the registry for ${name}@${version}; skipping the tag check`)
return
}
if (output.includes('E404') || output.includes('404 Not Found')) {
throw new Error(
`${name}@${version} is tagged but absent from the registry.`
+ '\nThe tag was pushed for a publication that did not complete: re-run that publish, or delete the tag.',
)
}
throw new Error(`npm view ${name}@${version} failed:\n${output}`)
}
/**
* Write a version into a manifest, preserving formatting and key order.
* @param root - repository root.
* @param manifestPath - repository-relative manifest path.
* @param from - the version the manifest currently carries.
* @param to - the target version.
*/
function writeVersion(root: string, manifestPath: string, from: string, to: string): void {
const path = join(root, manifestPath)
const text = readFileSync(path, 'utf8')
const line = `"version": "${from}"`
if (!text.includes(line)) throw new Error(`${manifestPath}: cannot locate ${line}`)
writeFileSync(path, text.replace(line, `"version": "${to}"`))
}
/**
* Read the workspace root version.
* @param root - repository root.
* @returns The root manifest version.
*/
function rootVersion(root: string): string {
const manifest: unknown = JSON.parse(readFileSync(join(root, ROOT_MANIFEST), 'utf8'))
const version = (manifest as Record<string, unknown>).version
if (typeof version !== 'string') throw new Error('package.json must declare a string version')
return version
}
/**
* Plan the dsh family's rewrite: one version for every member and the root.
* @param family - the dsh family.
* @param root - repository root.
* @param members - the family's members.
* @param request - `major`, `minor`, `patch`, or an explicit version.
* @returns The manifests to rewrite and the shared target version.
*/
function planShared(
family: ReleaseFamily,
root: string,
members: readonly ReleaseMember[],
request: string,
): { planned: PlannedVersion[]; version: string } {
const [first] = members
if (first === undefined) throw new Error(`release family ${family.id} has no members`)
const version = nextSharedVersion(first.version, request)
// The workspace root carries the family version too: the workspace constraint
// requires every member's version to equal the root's.
const planned: PlannedVersion[] = [
{ manifestPath: ROOT_MANIFEST, label: ROOT_MANIFEST, from: rootVersion(root), to: version, tag: undefined },
]
for (const member of members) {
planned.push({
manifestPath: join(member.directory, 'package.json'),
label: member.directory,
from: member.version,
to: version,
tag: family.tagFor({ ...member, version }),
})
}
return { planned, version }
}
/**
* Plan the vendored family's rewrite: every package whose payload changed since
* it last published.
* @param family - the vendored family.
* @param members - the family's members.
* @param prerelease - prerelease identifier to append, for a rehearsal publication.
* @returns The manifests to rewrite.
*/
function planPerPackage(
family: ReleaseFamily,
members: readonly ReleaseMember[],
prerelease: string | undefined,
): PlannedVersion[] {
const planned: PlannedVersion[] = []
for (const member of members) {
const published = lastPublishedVersion(family, member)
if (published !== undefined) {
confirmPublished(member.name, published)
const since = `${family.tagPrefixFor(member)}${published}`
const changed = capture('git', ['diff', '--name-only', `${since}..HEAD`, '--', member.directory])
.split('\n').filter(line => line !== '')
if (!changed.some(path => reachesPayload(member, path))) continue
}
const to = nextVendorVersion(member.version, published, prerelease)
planned.push({
manifestPath: join(member.directory, 'package.json'),
label: member.directory,
from: member.version,
to,
tag: family.tagFor({ ...member, version: to }),
})
}
return planned
}
/**
* Bump the family named by `--family` and commit; `--dry-run` only reports the
* plan. `--prerelease rc.1` makes the vendored family publish a rehearsal
* version, which never takes the stable dist-tag.
*/
function main(): void {
const { values, positionals } = parseArgs({
options: {
family: { type: 'string' },
prerelease: { type: 'string' },
'dry-run': { type: 'boolean', default: false },
},
allowPositionals: true,
})
if (values.family === undefined) throw new Error('usage: bump.ts --family <dsh|vendor> [version]')
const family = releaseFamily(values.family)
const root = process.cwd()
const members = family.members(root)
family.verifyVersions(members)
let planned: PlannedVersion[]
let sharedVersion: string | undefined
if (family.id === 'dsh') {
const request = positionals[0]
if (request === undefined) throw new Error('usage: release:dsh <major|minor|patch|x.y.z>')
if (values.prerelease !== undefined) {
throw new Error('release:dsh takes the prerelease in its version argument, as in 0.0.1-rc.1')
}
const shared = planShared(family, root, members, request)
planned = shared.planned
sharedVersion = shared.version
} else {
if (positionals.length > 0) throw new Error('release:vendor takes no version: each package increments its own patch')
if (values.prerelease !== undefined && !/^[0-9A-Za-z.-]+$/.test(values.prerelease)) {
throw new Error(`--prerelease must be a semver prerelease identifier, got ${values.prerelease}`)
}
planned = planPerPackage(family, members, values.prerelease)
}
if (planned.length === 0) {
console.log(`release bump: family ${family.id}, nothing changed since publication`)
return
}
const dryRun = values['dry-run']
if (!dryRun) {
for (const entry of planned) writeVersion(root, entry.manifestPath, entry.from, entry.to)
capture('pnpm', ['install', '--lockfile-only'])
}
const summary = sharedVersion
?? planned.map(entry => `${entry.label.replace('vendor/', '')} ${entry.to}`).join(', ')
console.log(`release bump: family ${family.id} -> ${summary}`)
for (const entry of planned) console.log(` ${entry.label}: ${entry.from} -> ${entry.to}`)
if (dryRun) {
console.log('release bump: dry run, nothing written')
return
}
capture('git', ['add', 'pnpm-lock.yaml', ...planned.map(entry => entry.manifestPath)])
capture('git', ['commit', '-m', `release(${family.id}): ${summary}`])
console.log('release bump: committed. After this merges to master, tag it:')
for (const tag of [...new Set(planned.map(entry => entry.tag).filter(tag => tag !== undefined))]) {
console.log(` git tag ${tag} <merge commit> && git push origin ${tag}`)
}
}
if (isEntry(import.meta.url)) main()
+176
View File
@@ -0,0 +1,176 @@
/** Release family discovery, publish order, tag naming, and the bump judgements. */
import { describe, expect, it } from 'vitest'
import { releaseFamily, type ReleaseMember } from './families.ts'
import { compareVersions, nextVendorVersion, reachesPayload } from './bump.ts'
/**
* A release member standing in for a manifest on disk.
* @param directory - repository-relative package directory.
* @param name - package name.
* @param manifest - manifest fields the subject reads.
* @returns The member.
*/
function member(directory: string, name: string, manifest: Record<string, unknown> = {}): ReleaseMember {
return { directory, name, version: '0.0.1', manifest }
}
describe('release families', () => {
it('names one tag for the whole dsh family and one per vendored package', () => {
const dsh = releaseFamily('dsh')
const vendor = releaseFamily('vendor')
const cli = member('apps/cli', '@deepseek-ai/dsh')
const cordis = { ...member('vendor/cordis', '@deepseek-ai/cordis'), version: '4.0.1' }
expect(dsh.tagFor(cli)).toBe('dsh-v0.0.1')
expect(vendor.tagFor(cordis)).toBe('vendor-cordis-v4.0.1')
// The prefix is constructed, not recovered from a tag: a version with a
// hyphen would defeat any suffix-stripping.
expect(vendor.tagPrefixFor({ ...cordis, version: '4.0.0-rc.7' })).toBe('vendor-cordis-v')
expect(vendor.tagFor({ ...cordis, version: '4.0.0-rc.7' })).toBe('vendor-cordis-v4.0.0-rc.7')
})
it('rejects a family whose members disagree on the shared version', () => {
const dsh = releaseFamily('dsh')
const members = [member('apps/cli', '@deepseek-ai/dsh'), { ...member('apps/web', '@deepseek-ai/dsh-frontend'), version: '0.0.2' }]
expect(() => { dsh.verifyVersions(members) }).toThrow(/must share one version/)
expect(() => { dsh.verifyVersions([members[0]!]) }).not.toThrow()
})
it('accepts independent vendored versions and rejects an unpublishable one', () => {
const vendor = releaseFamily('vendor')
const members = [
{ ...member('vendor/cordis', '@deepseek-ai/cordis'), version: '4.0.1' },
{ ...member('vendor/cosmokit', '@deepseek-ai/cosmokit'), version: '1.8.2' },
]
expect(() => { vendor.verifyVersions(members) }).not.toThrow()
expect(() => { vendor.verifyVersions([{ ...members[0]!, version: 'latest' }]) }).toThrow(/unpublishable version/)
})
it('publishes a dependency before its consumer, and orders ties by name', () => {
const dsh = releaseFamily('dsh')
const members = [
member('packages/a/consumer', '@deepseek-ai/dsh-consumer', { dependencies: { '@deepseek-ai/dsh-library': 'workspace:^' } }),
member('packages/a/library', '@deepseek-ai/dsh-library'),
member('packages/a/zebra', '@deepseek-ai/dsh-zebra'),
]
expect(dsh.publishOrder(members).map(entry => entry.name)).toEqual([
'@deepseek-ai/dsh-library',
'@deepseek-ai/dsh-consumer',
'@deepseek-ai/dsh-zebra',
])
})
it('reports a runtime dependency cycle instead of emitting an arbitrary order', () => {
const dsh = releaseFamily('dsh')
const members = [
member('packages/a/left', '@deepseek-ai/dsh-left', { dependencies: { '@deepseek-ai/dsh-right': 'workspace:^' } }),
member('packages/a/right', '@deepseek-ai/dsh-right', { dependencies: { '@deepseek-ai/dsh-left': 'workspace:^' } }),
]
expect(() => { dsh.publishOrder(members) }).toThrow(/dependency cycle/)
})
it('applies the harness payload policy to dsh and keeps upstream payloads for vendored packages', () => {
const dsh = releaseFamily('dsh')
const vendor = releaseFamily('vendor')
const harness = member('packages/a/library', '@deepseek-ai/dsh-library')
const vendored = member('vendor/cordis', '@deepseek-ai/cordis')
expect(() => { dsh.validatePayload(harness, ['package/lib/index.js', 'package/src/index.ts']) })
.toThrow(/publishes source file/)
expect(() => { vendor.validatePayload(vendored, ['package/lib/index.js', 'package/src/index.ts']) }).not.toThrow()
expect(() => { vendor.validatePayload(vendored, []) }).toThrow(/empty tarball/)
})
it('drives the installed entry only for the family that publishes one', () => {
expect(releaseFamily('dsh').installedEntry).toEqual({ packageName: '@deepseek-ai/dsh', binPath: 'lib/bin.js' })
expect(releaseFamily('vendor').installedEntry).toBeUndefined()
})
it('rejects an unknown family identifier', () => {
expect(() => { releaseFamily('native') }).toThrow(/unknown release family/)
})
})
describe('vendored version baseline', () => {
it('drops an upstream prerelease segment and increments the patch', () => {
expect(nextVendorVersion('4.0.0-rc.7', undefined)).toBe('4.0.1')
expect(nextVendorVersion('1.0.0-rc.5', undefined)).toBe('1.0.1')
expect(nextVendorVersion('1.8.1', undefined)).toBe('1.8.2')
})
it('increments from the last published version when a re-sync restored a lower one', () => {
// Upstream moved rc.7 -> rc.8 after this repository published 4.0.1;
// incrementing the manifest alone would name 4.0.1 a second time.
expect(nextVendorVersion('4.0.0-rc.8', '4.0.1')).toBe('4.0.2')
expect(nextVendorVersion('4.1.0', '4.0.1')).toBe('4.1.1')
})
it('appends a rehearsal prerelease without consuming its release numbers', () => {
// A rehearsal burns 4.0.1-rc.1 and leaves 4.0.1 free, so the stable release
// that follows takes those same numbers instead of skipping to 4.0.2.
expect(nextVendorVersion('4.0.0-rc.7', undefined, 'rc.1')).toBe('4.0.1-rc.1')
expect(nextVendorVersion('4.0.0-rc.7', '4.0.1-rc.1', 'rc.2')).toBe('4.0.1-rc.2')
expect(nextVendorVersion('4.0.0-rc.7', '4.0.1-rc.1')).toBe('4.0.1')
expect(nextVendorVersion('4.0.0-rc.7', '4.0.1')).toBe('4.0.2')
})
})
describe('version precedence', () => {
it('ranks a release above the prerelease it follows', () => {
// git --sort=v:refname disagrees, placing 4.0.1-rc.1 above 4.0.1, which is
// why the newest published version is chosen here rather than by git.
expect(compareVersions('4.0.1', '4.0.1-rc.1')).toBeGreaterThan(0)
expect(compareVersions('4.0.1-rc.1', '4.0.1')).toBeLessThan(0)
})
it('compares numeric prerelease fields numerically', () => {
expect(compareVersions('4.0.1-rc.10', '4.0.1-rc.1')).toBeGreaterThan(0)
expect(compareVersions('4.0.1-rc.2', '4.0.1-rc.10')).toBeLessThan(0)
})
it('ranks a numeric field below an alphanumeric one, and a shorter list below a longer', () => {
expect(compareVersions('4.0.1-1', '4.0.1-alpha')).toBeLessThan(0)
expect(compareVersions('4.0.1-rc', '4.0.1-rc.1')).toBeLessThan(0)
expect(compareVersions('4.0.2', '4.0.1')).toBeGreaterThan(0)
expect(compareVersions('4.0.1-rc.1', '4.0.1-rc.1')).toBe(0)
})
})
describe('payload change judgement', () => {
const sourceShipping = member('vendor/cosmokit', '@deepseek-ai/cosmokit', {
files: ['lib/index.js', 'lib/types/**/*.d.ts', 'src'],
})
const buildOutputOnly = member('vendor/cordis', '@deepseek-ai/cordis', {
files: ['lib/index.js', 'lib/types/**/*.d.ts', 'bin.js'],
})
it('counts the manifest and the files npm always publishes', () => {
expect(reachesPayload(sourceShipping, 'vendor/cosmokit/package.json')).toBe(true)
expect(reachesPayload(sourceShipping, 'vendor/cosmokit/README.md')).toBe(true)
expect(reachesPayload(sourceShipping, 'vendor/cosmokit/src/index.ts')).toBe(true)
})
it('counts build inputs for a package whose payload is build output', () => {
// cordis publishes lib/ only, and lib/ is not tracked: without this, a real
// source change reads as "nothing changed" and the next publish fails on a
// version whose bytes moved.
expect(reachesPayload(buildOutputOnly, 'vendor/cordis/src/context.ts')).toBe(true)
expect(reachesPayload(buildOutputOnly, 'vendor/cordis/tsconfig.json')).toBe(true)
})
it('ignores paths no tarball carries', () => {
expect(reachesPayload(sourceShipping, 'vendor/cosmokit/tests/unit.spec.ts')).toBe(false)
expect(reachesPayload(sourceShipping, 'vendor/cosmokit/CHANGELOG.md')).toBe(false)
// The README pattern is deliberately loose: over-reporting a change costs one
// unnecessary patch bump, while under-reporting fails the next publish on a
// version whose bytes moved.
expect(reachesPayload(sourceShipping, 'vendor/cosmokit/README.i18n.yaml')).toBe(true)
expect(reachesPayload(member('packages/a/library', '@deepseek-ai/dsh-library', { files: ['lib/index.js'] }),
'packages/a/library/tests/library.spec.ts')).toBe(false)
})
})
+310
View File
@@ -0,0 +1,310 @@
/**
* The three independent publish sequences this repository releases from
* (`packages/` + `apps/`, `vendor/`, and `native/`) and the two this module
* owns: `dsh` and `vendor`. Each family carries its own version baseline, tag
* naming, and publish set, so releasing one never republishes another
* ([rationale](../../.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)).
*
* The family dimension lives here only. A new sequence adds a subclass and a
* `releaseFamilies()` entry; nothing else in the release scripts branches on it.
*/
import { globSync, readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { hasTypeRTRemoteNavigation, validateTarballPayload } from '../publication-payload.ts'
/** Dependency sections that constrain publish order: a consumer must publish after its dependency. */
const ORDER_SECTIONS = ['dependencies', 'optionalDependencies'] as const
/** The workspace root manifest, which is never a release member. */
const WORKSPACE_ROOT_PACKAGE = '@deepseek-ai/dsh-root'
/** One publishable package of a release family. */
export interface ReleaseMember {
/** Repository-relative package directory, for example `packages/core/session`. */
readonly directory: string
/** Package name from its manifest. */
readonly name: string
/** Package version from its manifest. */
readonly version: string
/** The parsed manifest, for payload policy and publication checks. */
readonly manifest: Readonly<Record<string, unknown>>
}
/**
* Read and parse a JSON file.
* @param path - absolute file path.
* @returns The parsed object.
*/
function readManifest(path: string): Record<string, unknown> {
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`${path} is not a JSON object`)
}
return parsed as Record<string, unknown>
}
/**
* Read a required string field.
* @param manifest - parsed manifest.
* @param field - field name.
* @param context - manifest path for the error message.
* @returns The field value.
*/
function requireString(manifest: Record<string, unknown>, field: string, context: string): string {
const value = manifest[field]
if (typeof value !== 'string' || value === '') throw new Error(`${context} must declare a string ${field}`)
return value
}
/** The executable a family's installed artifacts are driven through. */
export interface InstalledEntry {
/** Package that carries the executable. */
readonly packageName: string
/** Path to the executable inside that package. */
readonly binPath: string
}
/** A release sequence: its members, its version baseline, and its tag naming. */
export abstract class ReleaseFamily {
/** Workflow-facing identifier, also the `--family` argument. */
abstract readonly id: string
/** Glob patterns, relative to the repository root, that select this family's manifests. */
abstract readonly patterns: readonly string[]
/** Git tag prefix this family publishes from. */
abstract readonly tagPrefix: string
/**
* Discover this family's members.
* @param root - repository root.
* @returns Members sorted by directory, with names validated and deduplicated.
*/
members(root: string): ReleaseMember[] {
const manifestPaths = globSync([...this.patterns], { cwd: root }).sort()
if (manifestPaths.length === 0) throw new Error(`release family ${this.id} matched no manifests`)
const members: ReleaseMember[] = []
const seen = new Set<string>()
for (const manifestPath of manifestPaths) {
const normalized = manifestPath.replaceAll('\\', '/')
const manifest = readManifest(resolve(root, manifestPath))
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`)
if (!name.startsWith('@deepseek-ai/')) throw new Error(`${normalized} must name an @deepseek-ai package`)
if (seen.has(name)) throw new Error(`${name} appears twice in release family ${this.id}`)
seen.add(name)
members.push({
directory: normalized.slice(0, normalized.length - '/package.json'.length),
name,
version,
manifest,
})
}
return members
}
/**
* Order members so every package publishes after the family members it depends on.
* @param members - this family's members.
* @returns The same members in publish order; ties break by name for determinism.
*/
publishOrder(members: readonly ReleaseMember[]): ReleaseMember[] {
const byName = new Map(members.map(member => [member.name, member]))
const ordered: ReleaseMember[] = []
const placed = new Set<string>()
const visiting = new Set<string>()
const visit = (member: ReleaseMember, path: readonly string[]): void => {
if (placed.has(member.name)) return
if (visiting.has(member.name)) {
throw new Error(`dependency cycle in release family ${this.id}: ${[...path, member.name].join(' -> ')}`)
}
visiting.add(member.name)
for (const dependency of this.orderEdges(member, byName)) {
visit(dependency, [...path, member.name])
}
visiting.delete(member.name)
placed.add(member.name)
ordered.push(member)
}
for (const member of [...members].sort((left, right) => left.name.localeCompare(right.name))) {
visit(member, [])
}
return ordered
}
/**
* The family members one member depends on at runtime.
* @param member - the dependent member.
* @param byName - every family member by package name.
* @returns Dependencies inside this family, sorted by name.
*/
private orderEdges(member: ReleaseMember, byName: ReadonlyMap<string, ReleaseMember>): ReleaseMember[] {
const edges: ReleaseMember[] = []
for (const section of ORDER_SECTIONS) {
const dependencies = member.manifest[section]
if (dependencies === null || typeof dependencies !== 'object' || Array.isArray(dependencies)) continue
for (const name of Object.keys(dependencies)) {
const dependency = byName.get(name)
if (dependency !== undefined && dependency.name !== member.name) edges.push(dependency)
}
}
return edges.sort((left, right) => left.name.localeCompare(right.name))
}
/**
* Assert this family's version baseline holds across its members.
* @param members - this family's members.
*/
abstract verifyVersions(members: readonly ReleaseMember[]): void
/**
* The tag prefix a member's versions are tagged under. Every tag for that
* member starts with it, which is how the last published version is found.
* @param member - the member being published.
* @returns The prefix, ending in `-v`.
*/
abstract tagPrefixFor(member: ReleaseMember): string
/**
* The tag a member publishes from.
* @param member - the member being published.
* @returns The full tag name, without `refs/tags/`.
*/
tagFor(member: ReleaseMember): string {
return `${this.tagPrefixFor(member)}${member.version}`
}
/**
* Check what a member's packed tarball carries.
* @param member - the packed member.
* @param files - every path inside its tarball.
*/
abstract validatePayload(member: ReleaseMember, files: readonly string[]): void
/**
* The executable that proves this family's artifacts install and run, or
* `undefined` for a family that publishes no executable.
*/
abstract readonly installedEntry: InstalledEntry | undefined
}
/** `packages/*` and `apps/*`: one shared version across the whole family. */
class DshFamily extends ReleaseFamily {
readonly id = 'dsh'
readonly patterns = ['packages/*/*/package.json', 'apps/*/package.json'] as const
readonly tagPrefix = 'dsh-v'
/**
* Require one version across the family, the way a single tag can name it.
* @param members - this family's members.
*/
verifyVersions(members: readonly ReleaseMember[]): void {
const versions = new Set(members.map(member => member.version))
if (versions.size !== 1) {
const detail = members.map(member => `${member.directory}: ${member.version}`).join('\n')
throw new Error(`dsh release members must share one version:\n${detail}`)
}
}
/**
* The single family prefix: every member shares one version, so one tag names it.
* @returns `dsh-v`.
*/
tagPrefixFor(): string {
return this.tagPrefix
}
/**
* Reject source and declaration-map members, the repository's publication policy.
* @param member - the packed member.
* @param files - every path inside its tarball.
*/
validatePayload(member: ReleaseMember, files: readonly string[]): void {
validateTarballPayload(files, member.name, {
typeRTRemoteNavigation: hasTypeRTRemoteNavigation(member.manifest),
})
}
readonly installedEntry = { packageName: '@deepseek-ai/dsh', binPath: 'lib/bin.js' }
}
/** `vendor/*`: every package keeps its own version line, so every package has its own tag. */
class VendorFamily extends ReleaseFamily {
readonly id = 'vendor'
readonly patterns = ['vendor/*/package.json'] as const
readonly tagPrefix = 'vendor-'
/**
* Accept independent versions; only reject a version this repository cannot publish.
* @param members - this family's members.
*/
verifyVersions(members: readonly ReleaseMember[]): void {
for (const member of members) {
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(member.version)) {
throw new Error(`${member.directory} has an unpublishable version: ${member.version}`)
}
}
}
/**
* A prefix per member, because one vendor release can carry several versions.
* @param member - the member being published.
* @returns `vendor-<unscoped name>-v`.
*/
tagPrefixFor(member: ReleaseMember): string {
return `${this.tagPrefix}${member.name.replace('@deepseek-ai/', '')}-v`
}
/**
* Require the payload the vendored manifest declares, including upstream's
* `src` tree and declaration maps.
*
* The harness policy that rejects both does not apply here: these manifests
* export `./src/*` for source navigation, so dropping `src` would publish a
* package whose export map points at absent files. What must hold instead is
* that every path the manifest selects is present, which `files` already
* decides and `pnpm pack` already enforces.
* @param member - the packed member.
* @param files - every path inside its tarball.
*/
validatePayload(member: ReleaseMember, files: readonly string[]): void {
if (files.length === 0) throw new Error(`${member.name} packed an empty tarball`)
}
/** No installed-entry probe: these are libraries a consumer imports, with no executable. */
readonly installedEntry = undefined
}
/** Every release family this module owns, in workflow order. */
function releaseFamilies(): readonly ReleaseFamily[] {
return [new DshFamily(), new VendorFamily()]
}
/**
* Resolve a family by its `--family` identifier.
* @param id - family identifier.
* @returns The family.
*/
export function releaseFamily(id: string): ReleaseFamily {
const family = releaseFamilies().find(candidate => candidate.id === id)
if (family === undefined) {
const known = releaseFamilies().map(candidate => candidate.id).join(', ')
throw new Error(`unknown release family ${id}; expected one of ${known}`)
}
return family
}
/**
* The npm tarball filename `pnpm pack` writes for a member.
* @param member - the packed member.
* @returns The tarball filename.
*/
export function tarballName(member: ReleaseMember): string {
const unscoped = member.name.startsWith('@') ? member.name.slice(1).replace('/', '-') : member.name
return `${unscoped}-${member.version}.tgz`
}
+61
View File
@@ -0,0 +1,61 @@
/**
* Pack one release family's whole publish set into a single directory, in
* publish order, and record that order for the publish step.
*
* The pack step is the release boundary: it runs without credentials, produces
* every tarball from one commit, and hands the publish step exactly those bytes
* ([rationale](../../.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)).
*/
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { parseArgs } from 'node:util'
import { releaseFamily, tarballName, type ReleaseFamily, type ReleaseMember } from './families.ts'
import { isEntry, run } from './process.ts'
import { PUBLISH_ORDER_FILE, tarballFiles } from './tarball.ts'
/** Where pack output lands when `--out` is omitted. */
const DEFAULT_OUTPUT = 'dist/npm'
/**
* Pack one member and check what its tarball carries.
* @param family - the release family being packed.
* @param member - the member to pack.
* @param destination - absolute output directory.
* @returns The tarball filename.
*/
function packMember(family: ReleaseFamily, member: ReleaseMember, destination: string): string {
run('pnpm', ['--dir', member.directory, 'pack', '--pack-destination', destination])
const filename = tarballName(member)
const tarball = join(destination, filename)
if (!existsSync(tarball)) throw new Error(`${member.name} produced no tarball at ${tarball}`)
family.validatePayload(member, tarballFiles(tarball))
return filename
}
/** Pack the family named by `--family` into `--out`. */
function main(): void {
const { values } = parseArgs({
options: { family: { type: 'string' }, out: { type: 'string' } },
allowPositionals: false,
})
if (values.family === undefined) throw new Error('usage: pack.ts --family <dsh|vendor> [--out dist/npm]')
const family = releaseFamily(values.family)
const root = process.cwd()
const destination = resolve(root, values.out ?? DEFAULT_OUTPUT)
const members = family.publishOrder(family.members(root))
family.verifyVersions(members)
rmSync(destination, { recursive: true, force: true })
mkdirSync(destination, { recursive: true })
const order: string[] = []
for (const member of members) order.push(packMember(family, member, destination))
writeFileSync(join(destination, PUBLISH_ORDER_FILE), `${order.join('\n')}\n`)
console.log(`release pack: family ${family.id}, ${String(order.length)} tarball(s) in ${values.out ?? DEFAULT_OUTPUT}`)
}
if (isEntry(import.meta.url)) main()
+82
View File
@@ -0,0 +1,82 @@
/**
* Process helpers shared by the release scripts: the release steps drive `git`,
* `pnpm`, `npm`, and `tar`, and each needs one of three failure behaviours.
*/
import { spawnSync } from 'node:child_process'
import { realpathSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
/** Where and with what environment a release step runs a command. */
export interface RunOptions {
/** Working directory; defaults to the current one. */
readonly cwd?: string
/** Child environment; defaults to this process's. */
readonly env?: NodeJS.ProcessEnv
}
/** What a command produced, for a caller that decides what a failure means. */
export interface CommandResult {
/** Exit status, or null when a signal ended the process. */
readonly status: number | null
/** Captured standard output. */
readonly stdout: string
/** Captured standard error. */
readonly stderr: string
}
/**
* Run a command and capture its output without judging the exit status.
* @param command - executable name.
* @param args - command arguments.
* @param options - working directory and environment.
* @returns The exit status and captured streams.
*/
export function attempt(command: string, args: readonly string[], options: RunOptions = {}): CommandResult {
const result = spawnSync(command, [...args], { cwd: options.cwd, env: options.env, encoding: 'utf8' })
if (result.error !== undefined) throw result.error
return { status: result.status, stdout: result.stdout, stderr: result.stderr }
}
/**
* Run a command, capture its standard output, and fail on a non-zero exit.
* @param command - executable name.
* @param args - command arguments.
* @param options - working directory and environment.
* @returns The trimmed standard output.
*/
export function capture(command: string, args: readonly string[], options: RunOptions = {}): string {
const result = attempt(command, args, options)
if (result.status !== 0) {
throw new Error(`${command} ${args.join(' ')} exited with ${String(result.status)}:\n${result.stdout}\n${result.stderr}`)
}
return result.stdout.trim()
}
/**
* Run a command with inherited streams, so its progress reaches the log, and
* fail on a non-zero exit.
* @param command - executable name.
* @param args - command arguments.
* @param options - working directory and environment.
*/
export function run(command: string, args: readonly string[], options: RunOptions = {}): void {
const result = spawnSync(command, [...args], { cwd: options.cwd, env: options.env, stdio: 'inherit' })
if (result.error !== undefined) throw result.error
if (result.status !== 0) throw new Error(`${command} ${args.join(' ')} exited with ${String(result.status)}`)
}
/**
* Whether this module is the process entry point.
*
* The release scripts are both commands and modules: a test imports their pure
* logic, and importing a module runs its body, so an unguarded `main()` would
* run the wrong command with the wrong arguments.
* @param moduleUrl - the caller's `import.meta.url`.
* @returns True when Node started this module.
*/
export function isEntry(moduleUrl: string): boolean {
const invoked = process.argv[1]
if (invoked === undefined) return false
return realpathSync(invoked) === realpathSync(fileURLToPath(moduleUrl))
}
+101
View File
@@ -0,0 +1,101 @@
/**
* Publish one packed release family from the tarballs the pack step produced.
*
* Publication is decided per package against the registry, never from a list of
* "what this release includes": a version the registry lacks is published, a
* version whose published tarball has the same integrity is skipped, and a
* version whose published tarball differs fails the run — that last case means
* the content changed without a version bump
* ([rationale](../../.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)).
*
* Skipping on identical integrity is what makes re-running the publish step over
* the same artifact safe.
*/
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { parseArgs } from 'node:util'
import { releaseFamily } from './families.ts'
import { attempt, isEntry, run } from './process.ts'
import { packedIdentity, readPublishOrder } from './tarball.ts'
/** npm access level for every package this repository publishes. */
const ACCESS = 'restricted'
/** What the registry knows about one version. */
type RegistryState =
| { readonly kind: 'absent' }
| { readonly kind: 'present'; readonly integrity: string }
/**
* The subresource integrity string npm records for a tarball.
* @param tarball - absolute tarball path.
* @returns A `sha512-<base64>` string.
*/
function integrityOf(tarball: string): string {
return `sha512-${createHash('sha512').update(readFileSync(tarball)).digest('base64')}`
}
/**
* Ask the registry whether a version exists, and with what integrity.
* @param name - package name.
* @param version - package version.
* @returns The registry state for that version.
*/
function registryState(name: string, version: string): RegistryState {
const result = attempt('npm', ['view', `${name}@${version}`, 'dist.integrity', '--json'])
if (result.status !== 0) {
const output = `${result.stdout}${result.stderr}`
if (output.includes('E404') || output.includes('404 Not Found')) return { kind: 'absent' }
throw new Error(`npm view ${name}@${version} failed:\n${output}`)
}
const parsed: unknown = JSON.parse(result.stdout)
if (typeof parsed !== 'string' || parsed === '') {
throw new Error(`registry reported no dist.integrity for ${name}@${version}`)
}
return { kind: 'present', integrity: parsed }
}
/** Publish the family named by `--family` from the directory named by `--from`. */
function main(): void {
const { values } = parseArgs({
options: { family: { type: 'string' }, from: { type: 'string' } },
allowPositionals: false,
})
if (values.family === undefined || values.from === undefined) {
throw new Error('usage: publish.ts --family <dsh|vendor> --from <packed directory>')
}
const family = releaseFamily(values.family)
const directory = resolve(process.cwd(), values.from)
let published = 0
let skipped = 0
for (const filename of readPublishOrder(directory)) {
const tarball = join(directory, filename)
const { name, version } = packedIdentity(tarball)
const state = registryState(name, version)
if (state.kind === 'present') {
const local = integrityOf(tarball)
if (state.integrity !== local) {
throw new Error(
`${name}@${version} is already published with different content`
+ `\n registry: ${state.integrity}\n packed: ${local}`
+ '\nBump the version, or investigate why the build is not reproducible.',
)
}
console.log(`release publish: ${name}@${version} already published, skipping`)
skipped += 1
continue
}
// A prerelease version never takes the latest dist-tag.
const tagArgs = version.includes('-') ? ['--tag', 'next'] : []
run('npm', ['publish', tarball, '--access', ACCESS, ...tagArgs])
published += 1
}
console.log(`release publish: family ${family.id}, ${String(published)} published, ${String(skipped)} already present`)
}
if (isEntry(import.meta.url)) main()
+53
View File
@@ -0,0 +1,53 @@
/**
* Reading packed npm tarballs and the order file that accompanies them.
*
* The release steps after pack treat a directory of tarballs as the unit of
* work, so they read what a tarball declares rather than what the checkout
* currently says.
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { capture } from './process.ts'
/** Name of the file recording the order in which a packed family uploads. */
export const PUBLISH_ORDER_FILE = 'publish-order.txt'
/** What a packed tarball calls itself. */
export interface PackedIdentity {
/** Package name from the packed manifest. */
readonly name: string
/** Package version from the packed manifest. */
readonly version: string
}
/**
* List a tarball's members.
* @param tarball - absolute tarball path.
* @returns Every path inside the archive.
*/
export function tarballFiles(tarball: string): string[] {
return capture('tar', ['-tzf', tarball]).split('\n').filter(line => line !== '')
}
/**
* Read a packed tarball's own manifest.
* @param tarball - absolute tarball path.
* @returns The name and version the tarball declares.
*/
export function packedIdentity(tarball: string): PackedIdentity {
const manifest: unknown = JSON.parse(capture('tar', ['-xOzf', tarball, 'package/package.json']))
if (manifest === null || typeof manifest !== 'object') throw new Error(`${tarball} has no manifest`)
const { name, version } = manifest as Record<string, unknown>
if (typeof name !== 'string' || typeof version !== 'string') throw new Error(`${tarball} manifest lacks name/version`)
return { name, version }
}
/**
* Read a packed directory's upload order.
* @param directory - absolute path of a pack output directory.
* @returns Tarball filenames in upload order.
*/
export function readPublishOrder(directory: string): string[] {
return readFileSync(join(directory, PUBLISH_ORDER_FILE), 'utf8').split('\n').filter(line => line !== '')
}
+120
View File
@@ -0,0 +1,120 @@
/**
* Install packed tarballs into a throwaway consumer outside the repository and
* drive the installed executable with plain Node.
*
* Every tarball the installed tree needs comes from `--from`, so the only
* registry traffic is for external dependencies. That matters beyond hermetic
* verification: the harness packages declare the vendored framework as a peer,
* and those packages live in another release sequence that this credential-free
* job cannot fetch from a private registry — so a dsh verification passes the
* vendored family's pack output too, while publishing only its own
* ([rationale](../../.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)).
*
* What this proves is that `files` selected a complete payload and that the
* published dependency ranges resolve. A workspace link or a stale `lib/` in the
* checkout cannot stand in for a missing file here.
*/
import { mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { parseArgs } from 'node:util'
import { releaseFamily } from './families.ts'
import { capture, isEntry } from './process.ts'
import { packedIdentity } from './tarball.ts'
/**
* Environment for the installed artifact: no host Node hooks, no host DeepSeek
* Harness home, and no ambient npm user agent that would confuse npm.
* @param consumerRoot - the throwaway consumer directory.
* @returns The child environment.
*/
function consumerEnvironment(consumerRoot: string): NodeJS.ProcessEnv {
const environment = { ...process.env }
delete environment.npm_config_user_agent
delete environment.NPM_CONFIG_USER_AGENT
delete environment.NODE_OPTIONS
delete environment.NODE_PATH
environment.DSH_HOME = resolve(consumerRoot, '.dsh')
environment.DSH_AGENTS_HOME = resolve(consumerRoot, '.agents')
environment.DSH_TELEMETRY_DISABLED = '1'
return environment
}
/**
* Every packed tarball in the given directories, as `file:` dependency entries.
*
* The directories are read by their contents rather than a pack order file: a
* directory here can hold tarballs packed only to satisfy a cross-sequence
* dependency, which no release order describes.
* @param directories - absolute directories holding packed tarballs.
* @returns Package name to tarball file URL, and the version each carries.
*/
function packedDependencies(directories: readonly string[]): Map<string, { url: string; version: string }> {
const dependencies = new Map<string, { url: string; version: string }>()
for (const directory of directories) {
const tarballs = readdirSync(directory).filter(name => name.endsWith('.tgz')).sort()
if (tarballs.length === 0) throw new Error(`${directory} holds no packed tarball`)
for (const filename of tarballs) {
const tarball = join(directory, filename)
const { name, version } = packedIdentity(tarball)
dependencies.set(name, { url: pathToFileURL(tarball).href, version })
}
}
return dependencies
}
/** Install every tarball under `--from` and drive the `--family` entry. */
function main(): void {
const { values } = parseArgs({
options: { family: { type: 'string' }, from: { type: 'string', multiple: true } },
allowPositionals: false,
})
if (values.family === undefined || values.from === undefined || values.from.length === 0) {
throw new Error('usage: verify-packed-install.ts --family <dsh|vendor> --from <packed directory> [--from ...]')
}
const family = releaseFamily(values.family)
const entry = family.installedEntry
if (entry === undefined) {
console.log(`release verify-packed-install: family ${family.id} publishes no executable, nothing to drive`)
return
}
const root = process.cwd()
const packed = packedDependencies(values.from.map(directory => resolve(root, directory)))
const expected = packed.get(entry.packageName)
if (expected === undefined) throw new Error(`${entry.packageName} is not among the packed tarballs`)
const consumerRoot = mkdtempSync(join(tmpdir(), `dsh-packed-${family.id}-`))
try {
writeFileSync(join(consumerRoot, 'package.json'), `${JSON.stringify({
name: `dsh-packed-install-${family.id}`,
version: '0.0.0',
private: true,
dependencies: Object.fromEntries([...packed].map(([name, entryPacked]) => [name, entryPacked.url])),
}, null, 2)}\n`)
const environment = consumerEnvironment(consumerRoot)
console.log(`release verify-packed-install: installing ${String(packed.size)} tarball(s) into ${consumerRoot}`)
// Optional dependencies are omitted: the Landlock platform packages behind
// them need a musl toolchain and one build per architecture, and a consumer
// that cannot install them must still start — which is what optional means
// here. Their entry package is a plain dependency of dsh-sandbox-local, so
// its tarball is supplied through --from.
capture('npm', ['install', '--no-audit', '--no-fund', '--package-lock=false', '--omit=optional'],
{ cwd: consumerRoot, env: environment })
const bin = join(consumerRoot, 'node_modules', ...entry.packageName.split('/'), entry.binPath)
const version = capture(process.execPath, [bin, '--version'], { cwd: consumerRoot, env: environment })
if (version !== expected.version) {
throw new Error(`installed ${entry.packageName} --version reported ${JSON.stringify(version)}, expected ${expected.version}`)
}
console.log(`release verify-packed-install: installed ${entry.packageName} reports ${version}`)
} finally {
rmSync(consumerRoot, { recursive: true, force: true })
}
}
if (isEntry(import.meta.url)) main()
+70
View File
@@ -0,0 +1,70 @@
/**
* Verify a release family's version baseline, and — when publishing — that the
* run comes from the family's tag and its members are publishable.
*
* Publication happens only from GitHub Actions, so the tag and publishability
* checks are gates on the workflow, not advisory local warnings
* ([rationale](../../.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)).
*/
import { parseArgs } from 'node:util'
import { isEntry } from './process.ts'
import { releaseFamily, type ReleaseFamily, type ReleaseMember } from './families.ts'
/**
* Assert every member may be published: npm refuses a `private` package.
* @param members - the family's members.
*/
function verifyPublishable(members: readonly ReleaseMember[]): void {
const priv = members.filter(member => member.manifest.private === true)
if (priv.length > 0) {
throw new Error(`publishing requires removing "private": true from:\n${priv.map(member => member.directory).join('\n')}`)
}
}
/**
* Assert the workflow runs from a tag this family publishes from, and that the
* tag names a version the family actually carries.
* @param family - the release family.
* @param members - the family's members.
* @param ref - the `GITHUB_REF` value.
*/
function verifyTag(family: ReleaseFamily, members: readonly ReleaseMember[], ref: string): void {
const prefix = 'refs/tags/'
if (!ref.startsWith(prefix)) {
throw new Error(`publishing release family ${family.id} requires running from a ${family.tagPrefix}* tag, got ${ref || '(no ref)'}`)
}
const tag = ref.slice(prefix.length)
if (!tag.startsWith(family.tagPrefix)) {
throw new Error(`tag ${tag} does not belong to release family ${family.id} (expected ${family.tagPrefix}*)`)
}
const expected = members.map(member => family.tagFor(member))
if (!expected.includes(tag)) {
throw new Error(`tag ${tag} names no version this family carries; its members would tag as:\n${[...new Set(expected)].join('\n')}`)
}
}
/** Run the verification for the family named by `--family`. */
function main(): void {
const { values } = parseArgs({
options: { family: { type: 'string' } },
allowPositionals: false,
})
if (values.family === undefined) throw new Error('usage: verify.ts --family <dsh|vendor>')
const family = releaseFamily(values.family)
const members = family.members(process.cwd())
family.verifyVersions(members)
const publishing = process.env.RELEASE_PUBLISH === 'true'
if (publishing) {
verifyPublishable(members)
verifyTag(family, members, process.env.GITHUB_REF ?? '')
}
const versions = [...new Set(members.map(member => member.version))]
const summary = versions.length === 1 ? versions[0] : `${String(versions.length)} versions`
console.log(`release verify: family ${family.id}, ${String(members.length)} member(s), ${summary}${publishing ? ', publish gates passed' : ''}`)
}
if (isEntry(import.meta.url)) main()
+41
View File
@@ -0,0 +1,41 @@
/**
* Acceptance-path coverage for the rescope codemod's exact-edit classifier: a
* duplicated insertion — what a non-idempotent apply produces — must be
* rejected rather than applied again.
*/
import { describe, expect, it } from 'vitest'
import { exactEditState } from './rescope-vendor.ts'
const ANCHOR = '\n## Sync procedure'
const INSERTED = `\n15. **rescope**: one log entry.\n${ANCHOR}`
describe('exactEditState', () => {
it('classifies an insertion by its target form, so a duplicate is invalid', () => {
expect(exactEditState(`log\n${ANCHOR}\n`, ANCHOR, INSERTED, 1)).toBe('pending')
expect(exactEditState(`log${INSERTED}\n`, ANCHOR, INSERTED, 1)).toBe('applied')
// The anchor survives an insertion, so counting the source form would have
// called this pending and inserted the entry a second time.
expect(exactEditState(`log${INSERTED}${INSERTED}\n`, ANCHOR, INSERTED, 1)).toBe('invalid')
expect(exactEditState('log\n', ANCHOR, INSERTED, 1)).toBe('invalid')
})
it('classifies a deletion by its source form, and requires its remainder to survive', () => {
const remainder = 'exclude:\n'
const withEntries = 'exclude:\n - cordis@4\n'
expect(exactEditState(withEntries, withEntries, remainder, 1)).toBe('pending')
expect(exactEditState(remainder, withEntries, remainder, 1)).toBe('applied')
// Upstream dropped the whole field: the source form is gone, but so is the
// remainder, so this is a moved site rather than a completed deletion.
expect(exactEditState('unrelated:\n', withEntries, remainder, 1)).toBe('invalid')
})
it('requires a replacement to leave no source form and the exact target count', () => {
expect(exactEditState('a = 1\n', 'a = 1', 'b = 2', 1)).toBe('pending')
expect(exactEditState('b = 2\n', 'a = 1', 'b = 2', 1)).toBe('applied')
expect(exactEditState('b = 2\nb = 2\n', 'a = 1', 'b = 2', 1)).toBe('invalid')
// A moved or partially applied site: neither state is complete.
expect(exactEditState('a = 1\nb = 2\n', 'a = 1', 'b = 2', 1)).toBe('invalid')
expect(exactEditState('x\n', 'a = 1', 'b = 2', 1)).toBe('invalid')
})
})
+771
View File
@@ -0,0 +1,771 @@
/**
* Rescope the vendored Cordis packages into the `@deepseek-ai` scope, and undo
* that rescope with `--reverse`. Every harness package declares `cordis` as a
* peer dependency, so publication carries this framework layer too; publishing
* it under the upstream names would squat them on the registry
* ([rationale](../.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.md),
* [name mapping](../docs/rescope.md)).
*
* The generic pass rewrites ONLY delimited, complete package-name tokens:
* `'old'` / `"old"` / `` `old` `` / `'old/subpath'`, plus a YAML `name: old`
* scalar. A match needs a quote (or `name: `) immediately left and the matching
* quote — optionally after a `/subpath` — immediately right, which excludes
* `cordis.yml`, the Loader's `cordis:` builtin prefix, `cordis-config-entry`,
* `@deepseek-ai/dsh-tool-cordis`, and `cordiverse/cordis`, and makes the
* rewrite idempotent because the scoped name's `cordis` is preceded by `/`.
* Markdown follows the rename inside every fence, and in `docs/` prose too:
* a tutorial that teaches an unresolvable name is wrong, while prose elsewhere
* records what was true when it was written.
*
* Sites the token rule cannot express (dot-notation access, unquoted object
* keys, regex literals, the vendored-manifest table) are listed in
* {@link EXACT_EDITS} with an exact hit count, so an upstream change to one of
* them fails loudly instead of being silently skipped.
*
* Usage: `pnpm run rescope-vendor [--apply|--check] [--reverse]`. Without a
* mode it reports what would change. `--check` asserts the post-state: no
* residue, every exact edit landed, every postcondition holds, and a second
* `--apply` would be a no-op.
*/
import { execFileSync } from 'node:child_process'
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = resolve(import.meta.dirname, '..')
/** One vendored package's directory, upstream npm name, and rescoped name. */
interface Rename {
readonly directory: string
readonly upstream: string
readonly scoped: string
}
/** The mapping this codemod applies; `vendor/README.md` carries the same table. */
const RENAMES: readonly Rename[] = [
{ directory: 'cordis', upstream: 'cordis', scoped: '@deepseek-ai/cordis' },
{ directory: 'cosmokit', upstream: 'cosmokit', scoped: '@deepseek-ai/cosmokit' },
{ directory: 'schemastery', upstream: 'schemastery', scoped: '@deepseek-ai/schemastery' },
{ directory: 'loader', upstream: '@cordisjs/plugin-loader', scoped: '@deepseek-ai/cordis-plugin-loader' },
{ directory: 'include', upstream: '@cordisjs/plugin-include', scoped: '@deepseek-ai/cordis-plugin-include' },
{ directory: 'group', upstream: '@cordisjs/plugin-group', scoped: '@deepseek-ai/cordis-plugin-group' },
{ directory: 'timer', upstream: '@cordisjs/plugin-timer', scoped: '@deepseek-ai/cordis-plugin-timer' },
{ directory: 'hmr', upstream: '@cordisjs/plugin-hmr', scoped: '@deepseek-ai/cordis-plugin-hmr' },
{ directory: 'logger-console', upstream: '@cordisjs/plugin-logger-console', scoped: '@deepseek-ai/cordis-plugin-logger-console' },
]
const EXTENSIONS = ['.ts', '.tsx', '.js', '.mjs', '.cjs', '.tpl', '.json', '.yml', '.yaml', '.md'] as const
/** An exact-string edit the token rule cannot express, with its required hit count. */
interface ExactEdit {
readonly id: string
readonly file: string
readonly find: string
readonly replace: string
readonly expect: number
}
/**
* A file where an upstream name also appears as a vendor DIRECTORY name or an
* upstream runtime identifier: the generic pass is disabled for the listed
* names and {@link EXACT_EDITS} renames the real package-name occurrences.
*/
interface GenericSkip {
readonly file: string
readonly upstream: readonly string[]
}
const GENERIC_SKIPS: readonly GenericSkip[] = [
// `vendorPackages` lists vendor/ directory names, joined with 'vendor' below it.
{ file: 'packages/examples/acp-demo/tests/built-bin.e2e.ts', upstream: ['cordis', 'cosmokit', 'schemastery'] },
// Mixes join(root, 'vendor', 'cordis') paths with real manifest names.
{ file: 'packages/scaffold/helper/tests/documents.spec.ts', upstream: ['cordis'] },
// `Symbol.for('schemastery')` and the `vendor:` metadata field are upstream identifiers.
{ file: 'vendor/schemastery/src/index.ts', upstream: ['schemastery'] },
// Asserts the vendored-manifest table, which gains an upstream-name column.
{ file: 'scripts/gen-third-party-notices.spec.ts', upstream: RENAMES.map(rename => rename.upstream) },
// `cordis` is also an agent-preset id — the directory name under
// apps/cli/config/agent-presets/ — so in these files the bare name is
// product data, not a package reference. Renaming it changed which preset
// the creator flow stages and which id the roster reports.
{ file: 'packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx', upstream: ['cordis'] },
{ file: 'packages/client/ui-agent-preset/src/client/index.ts', upstream: ['cordis'] },
{ file: 'packages/client/ui-agent-preset/tests/apply.spec.ts', upstream: ['cordis'] },
{ file: 'packages/client/ui-agent-preset/tests/locales.spec.ts', upstream: ['cordis'] },
{ file: 'packages/client/ui-agent-preset/tests/section.spec.tsx', upstream: ['cordis'] },
{ file: 'apps/cli/tests/web-agent-presets.e2e.ts', upstream: ['cordis'] },
{ file: 'apps/web/tests/agent-preset-authoring.e2e.ts', upstream: ['cordis'] },
{ file: 'packages/preset/agent-presets/tests/session.spec.ts', upstream: ['cordis'] },
// The preset's own composition: its header comment and its system prompt name
// the preset a model mounts, so the scoped name would send the model after an
// id no roster reports.
{ file: 'apps/cli/config/agent-presets/cordis/agent.cordis.yml', upstream: ['cordis'] },
// GROUP_ORDER holds `packages/<group>/` directory names, not package names.
{ file: 'scripts/gen-module-graph.ts', upstream: ['cordis'] },
{ file: 'scripts/gen-doc-graphs.ts', upstream: ['cordis'] },
]
/** A string that must appear exactly `count` times once the rescope has run. */
interface PostCondition {
readonly file: string
readonly text: string
readonly count: number
}
const POSTCONDITIONS: readonly PostCondition[] = [
{ file: 'vendor/cordis/package.json', text: '"name": "@deepseek-ai/cordis"', count: 1 },
{ file: 'vendor/hmr/package.json', text: '"name": "@deepseek-ai/cordis-plugin-hmr"', count: 1 },
{ file: 'scripts/cordis-walk.ts', text: '@deepseek-ai\\/cordis', count: 1 },
{ file: 'scripts/cordis-walk.ts', text: '!== \'@deepseek-ai/cordis\'', count: 1 },
{ file: 'scripts/gen-scoped-events.ts', text: '=== \'@deepseek-ai/cordis\'', count: 1 },
{ file: 'packages/typert/generator/src/analyzer.ts', text: '!== \'@deepseek-ai/cordis\'', count: 2 },
{ file: 'scripts/check-workspace-constraints.ts', text: '?.[\'@deepseek-ai/cordis\']', count: 2 },
{ file: 'packages/scaffold/helper/src/project/npm-dependency-policy.ts', text: '\'@deepseek-ai/cordis\': \'^4.0.0-rc.7\'', count: 1 },
{ file: 'packages/scaffold/helper/src/plugins/local-plugin-blueprint.ts', text: '\'@deepseek-ai/cordis\': cordisSpec', count: 2 },
{ file: 'packages/boot/app-boot/tsdown.config.ts', text: '[\'@deepseek-ai/cordis-plugin-include\']', count: 1 },
{ file: 'tsconfig.base.json', text: '"@deepseek-ai/cordis-plugin-loader": ["./vendor/loader/src"]', count: 1 },
// One insertion, once: a duplicated log entry is what a non-idempotent apply produced.
{ file: 'vendor/README.md', text: '17. **`@deepseek-ai` rescope**', count: 1 },
{ file: 'knip.json', text: '@cordisjs', count: 0 },
{ file: 'pnpm-workspace.yaml', text: 'cordis@4.0.0-rc.7', count: 0 },
// The preset ids in this table are product data, not package names.
{ file: 'packages/client/ui-agent-preset/tests/locales.spec.ts', text: '[\'cordis\', \'presetCordisName\'', count: 1 },
// The preset id the shipped composition documents to its own model.
{ file: 'apps/cli/config/agent-presets/cordis/agent.cordis.yml', text: 'The `cordis` agent preset', count: 1 },
{ file: 'apps/cli/config/agent-presets/cordis/agent.cordis.yml', text: 'corrupting the `cordis` preset', count: 1 },
// The vendor-directory paths in these fixtures must survive the rename.
{ file: 'packages/scaffold/helper/tests/documents.spec.ts', text: 'join(root, \'vendor\', \'cordis\')', count: 2 },
{ file: 'packages/examples/acp-demo/tests/built-bin.e2e.ts', text: '\'cordis\', \'loader\', \'include\', \'timer\', \'hmr\', \'logger-console\',', count: 1 },
]
/**
* Every exact edit, in application order. Each `find` is written against the
* PRE-rename text because these run before the generic pass, so no `find` may
* quote a neighbouring line the generic pass would rewrite.
*/
const EXACT_EDITS: readonly ExactEdit[] = [
{
id: 'cordis-walk-merge-head',
file: 'scripts/cordis-walk.ts',
find: 'const MERGE_HEAD = /declare module [\'"](?:cordis|\\.\\/context\\.ts)[\'"]/',
replace: 'const MERGE_HEAD = /declare module [\'"](?:@deepseek-ai\\/cordis|\\.\\/context\\.ts)[\'"]/',
expect: 1,
},
{
id: 'constraints-manifest-lookup',
file: 'scripts/check-workspace-constraints.ts',
find: ` const peer = manifest.peerDependencies?.cordis
const dev = manifest.devDependencies?.cordis
if (!peer) errors.push(\`\${label}: cordis must be a peerDependency\`)
if (!dev) errors.push(\`\${label}: cordis must also be a devDependency\`)
if (peer && dev && peer !== dev) {
errors.push(\`\${label}: cordis peer (\${peer}) and dev (\${dev}) ranges must match\`)`,
replace: ` const peer = manifest.peerDependencies?.['@deepseek-ai/cordis']
const dev = manifest.devDependencies?.['@deepseek-ai/cordis']
if (!peer) errors.push(\`\${label}: @deepseek-ai/cordis must be a peerDependency\`)
if (!dev) errors.push(\`\${label}: @deepseek-ai/cordis must also be a devDependency\`)
if (peer && dev && peer !== dev) {
errors.push(\`\${label}: @deepseek-ai/cordis peer (\${peer}) and dev (\${dev}) ranges must match\`)`,
expect: 1,
},
{
id: 'scaffold-dependency-policy',
file: 'packages/scaffold/helper/src/project/npm-dependency-policy.ts',
find: ' cordis: \'^4.0.0-rc.7\',',
replace: ' \'@deepseek-ai/cordis\': \'^4.0.0-rc.7\',',
expect: 1,
},
{
id: 'scaffold-plugin-blueprint',
file: 'packages/scaffold/helper/src/plugins/local-plugin-blueprint.ts',
find: ` cordis: cordisSpec,
},
devDependencies: {
cordis: cordisSpec,
},`,
replace: ` '@deepseek-ai/cordis': cordisSpec,
},
devDependencies: {
'@deepseek-ai/cordis': cordisSpec,
},`,
expect: 1,
},
{
id: 'scaffold-link-workspace-lookup',
file: 'packages/scaffold/create-sdk/tests/link-workspace.e2e.ts',
find: 'manifest.dependencies.cordis',
replace: 'manifest.dependencies[\'@deepseek-ai/cordis\']',
expect: 1,
},
{
id: 'documents-spec-manifest-name',
file: 'packages/scaffold/helper/tests/documents.spec.ts',
find: 'JSON.stringify({ name: \'cordis\' })',
replace: 'JSON.stringify({ name: \'@deepseek-ai/cordis\' })',
expect: 1,
},
{
id: 'documents-spec-peer-key',
file: 'packages/scaffold/helper/tests/documents.spec.ts',
find: 'peerDependencies: { cordis: \'^4\' },',
replace: 'peerDependencies: { \'@deepseek-ai/cordis\': \'^4\' },',
expect: 1,
},
{
id: 'documents-spec-closure-order',
file: 'packages/scaffold/helper/tests/documents.spec.ts',
find: ' \'@deepseek-ai/dsh-helper\', \'@deepseek-ai/dsh-scripts\', \'cordis\',',
replace: ' \'@deepseek-ai/cordis\', \'@deepseek-ai/dsh-helper\', \'@deepseek-ai/dsh-scripts\',',
expect: 1,
},
{
id: 'documents-spec-lookups',
file: 'packages/scaffold/helper/tests/documents.spec.ts',
find: ` expect(manifest.npmDependency('cordis')?.spec).toMatch(/^link:/)
expect(pnpmWorkspace.serialize()).toContain('autoInstallPeers: false')
expect(workspace.packageDirectory('cordis')).toBe(join(root, 'vendor', 'cordis'))
expect(await readFile(join(root, 'vendor', 'cordis', 'package.json'), 'utf8')).toContain('cordis')`,
replace: ` expect(manifest.npmDependency('@deepseek-ai/cordis')?.spec).toMatch(/^link:/)
expect(pnpmWorkspace.serialize()).toContain('autoInstallPeers: false')
expect(workspace.packageDirectory('@deepseek-ai/cordis')).toBe(join(root, 'vendor', 'cordis'))
expect(await readFile(join(root, 'vendor', 'cordis', 'package.json'), 'utf8')).toContain('@deepseek-ai/cordis')`,
expect: 1,
},
{
id: 'documents-spec-policy-lookup',
file: 'packages/scaffold/helper/tests/documents.spec.ts',
find: ' expect(resolveNpmDependency(\'cordis\', \'devDependencies\', \'0.0.1\')).toEqual({',
replace: ' expect(resolveNpmDependency(\'@deepseek-ai/cordis\', \'devDependencies\', \'0.0.1\')).toEqual({',
expect: 1,
},
{
// The rescoped name is already covered by the `@deepseek-ai/.+` pattern beside it.
id: 'knip-logger-console',
file: 'knip.json',
find: ` "ignoreDependencies": [
"@cordisjs/plugin-logger-console",
"@deepseek-ai/.+"
]
},
"packages/util/home": {`,
replace: ` "ignoreDependencies": [
"@deepseek-ai/.+"
]
},
"packages/util/home": {`,
expect: 1,
},
{
id: 'knip-bundle-base',
file: 'knip.json',
find: ` "packages/bundle/base": {
"ignoreDependencies": [
"@deepseek-ai/.+",
"@cordisjs/.+"
]`,
replace: ` "packages/bundle/base": {
"ignoreDependencies": [
"@deepseek-ai/.+"
]`,
expect: 1,
},
{
// Rescoped packages are never fetched from a registry, so the exclusion is dead config.
id: 'pnpm-release-age',
file: 'pnpm-workspace.yaml',
find: `minimumReleaseAgeExclude:
# Cordis release candidates are source-vendored and pinned in vendor/README.md
# during the same-day sync that updates package manifests and the lockfile.
- '@cordisjs/plugin-loader@1.0.0-rc.5'
- cordis@4.0.0-rc.7
`,
replace: 'minimumReleaseAgeExclude:\n',
expect: 1,
},
{
id: 'publication-set-scope-assertion',
file: 'scripts/publish-npm-baseline.ts',
find: ' if (!isVendored && !name.startsWith(\'@deepseek-ai/\')) {',
replace: ` // Vendored packages are rescoped too (vendor/README.md), so publication
// never carries an upstream name that would squat it on the registry.
if (!name.startsWith('@deepseek-ai/')) {`,
expect: 1,
},
{
id: 'vendor-readme-preamble',
file: 'vendor/README.md',
find: 'All vendored packages keep their **original npm names** and are marked `private: true` — they are never published from this repo. `pnpm-workspace.yaml#linkWorkspacePackages` makes matching upstream semver ranges resolve these pinned workspaces, including imports from built `lib/`; disabling it substitutes npm copies behind the same names.',
replace: 'All vendored packages are **renamed into the `@deepseek-ai` scope** (`cordis` → `@deepseek-ai/cordis`, `@cordisjs/plugin-<x>` → `@deepseek-ai/cordis-plugin-<x>`): every harness package declares `cordis` as a peer dependency, so publishing the harness publishes this framework layer too, and a publication under the upstream names would squat them on the registry. Directory names and upstream version numbers are deliberately unchanged, so the manifest below still reads as an upstream snapshot. `pnpm-workspace.yaml#linkWorkspacePackages` makes those preserved semver ranges resolve these pinned workspaces, including imports from built `lib/`.',
expect: 1,
},
{
id: 'vendor-readme-schemastery-note',
file: 'vendor/README.md',
find: 'whose lazy `require(\'cosmokit\')` can race',
replace: 'whose lazy `require(\'@deepseek-ai/cosmokit\')` can race',
expect: 1,
},
{
id: 'vendor-readme-table-head',
file: 'vendor/README.md',
find: '| Directory | npm name | Version | Upstream repo | Commit |\n|---|---|---|---|---|',
replace: '| Directory | npm name | Upstream name | Version | Upstream repo | Commit |\n|---|---|---|---|---|---|',
expect: 1,
},
{
id: 'vendor-readme-local-modification-log',
file: 'vendor/README.md',
find: '\n## Sync procedure',
replace: '17. **`@deepseek-ai` rescope**: every vendored manifest `name`, every internal dependency entry among the vendored set, and every module specifier that reaches them use the scoped names in the manifest table\'s `npm name` column. Directory names, version numbers, and dependency ranges are unchanged, and no upstream runtime identifier is renamed — `Symbol.for(\'schemastery\')` and Schemastery\'s `vendor:` metadata field keep their upstream values. Re-apply with `pnpm run rescope-vendor --apply` after a sync; the table\'s two name columns are the mapping, restated for consumers in [docs/rescope.md](../docs/rescope.md).\n\n## Sync procedure',
expect: 1,
},
{
// A plain fence listing the bundle's mounted tree: a bare token, no quotes.
id: 'agent-spine-demo-mounted-tree',
file: 'packages/examples/agent-spine-demo/README.md',
find: '@cordisjs/plugin-timer timer service',
replace: '@deepseek-ai/cordis-plugin-timer timer service',
expect: 1,
},
{
id: 'agent-spine-demo-mounted-tree-zh',
file: 'packages/examples/agent-spine-demo/README.zh.md',
find: '@cordisjs/plugin-timer timer service',
replace: '@deepseek-ai/cordis-plugin-timer timer service',
expect: 1,
},
{
// The root contract claimed vendored packages keep their upstream names.
id: 'root-agents-vendored-name-contract',
file: 'AGENTS.md',
find: 'vendored packages keep upstream names and are `private: true`. `cordis` is a peerDependency (+ dev) of every harness package.',
replace: 'vendored packages are rescoped ([mapping](docs/rescope.md)) and `private: true`. `@deepseek-ai/cordis` is a peerDependency (+ dev) of every harness package.',
expect: 1,
},
{
// The client purity gate reads `@deepseek-ai/` as "another plugin package".
// The rescope moves the vendored framework and its libraries into that
// namespace, where the gate would reject the library imports client
// bundles have always inlined, so it needs their names.
id: 'client-purity-vendored-libraries',
file: 'packages/client/tsdown.client.ts',
find: '/** Generated descriptor/codec contribution with no shared runtime identity. */',
replace: `/**
* Vendored framework libraries: rescoped into @deepseek-ai, so the gate below
* would read them as plugin packages. They carry no cross-plugin runtime
* identity to share — the framework itself is a platform module (external),
* while these are ordinary libraries a browser bundle inlines.
*/
const VENDORED_LIBRARY = /^@deepseek-ai\\/(cosmokit|schemastery)(\\/|$)/
/** Generated descriptor/codec contribution with no shared runtime identity. */`,
expect: 1,
},
{
id: 'client-purity-vendored-libraries-predicate',
file: 'packages/client/tsdown.client.ts',
find: ' if (INLINE_SAFE.test(source) || GENERATED_REMOTE.test(source)) return null // wire contribution: inline is the point',
replace: ` if (VENDORED_LIBRARY.test(source)) return null // vendored library: inline, no shared identity
if (INLINE_SAFE.test(source) || GENERATED_REMOTE.test(source)) return null // wire contribution: inline is the point`,
expect: 1,
},
{
// The step-1 file tree told the reader to keep the upstream name, one
// paragraph above the invariant that says to rescope it.
id: 'vendoring-cookbook-tree-comment',
file: 'docs/cookbook/adding-a-vendored-package.md',
find: ' package.json # from upstream; set "private": true, keep name/exports/type',
replace: ' package.json # from upstream; set "private": true, rescope the name, keep exports/type',
expect: 1,
},
{
id: 'vendoring-cookbook-tree-comment-zh',
file: 'docs/cookbook/adding-a-vendored-package.zh.md',
find: ' package.json # from upstream; set "private": true, keep name/exports/type',
replace: ' package.json # from upstream; set "private": true, rescope the name, keep exports/type',
expect: 1,
},
{
// The checklist told the next vendoring to keep upstream's name.
id: 'vendoring-cookbook-name-invariant',
file: 'docs/cookbook/adding-a-vendored-package.md',
find: "keep upstream's `name`/`version`/`exports`/`type`",
replace: "rescope the `name` ([mapping](../rescope.md)) while keeping upstream's `version`/`exports`/`type`",
expect: 1,
},
{
id: 'vendoring-cookbook-name-invariant-zh',
file: 'docs/cookbook/adding-a-vendored-package.zh.md',
find: '保留上游的 `name`/`version`/`exports`/`type`',
replace: '改写 `name` 的 scope[映射](../rescope.md)),保留上游的 `version`/`exports`/`type`',
expect: 1,
},
{
// The real package references in files whose other `cordis` strings are preset ids.
id: 'agent-preset-spec-framework-import',
file: 'packages/client/ui-agent-preset/tests/apply.spec.ts',
find: "import { Context } from 'cordis'",
replace: "import { Context } from '@deepseek-ai/cordis'",
expect: 1,
},
{
id: 'web-agent-presets-e2e-framework-import',
file: 'apps/cli/tests/web-agent-presets.e2e.ts',
find: "import { Context } from 'cordis'",
replace: "import { Context } from '@deepseek-ai/cordis'",
expect: 1,
},
{
id: 'notices-vendored-row-type',
file: 'scripts/gen-third-party-notices.ts',
find: `export interface VendoredRow {
npmName: string
upstream: string
}`,
replace: `export interface VendoredRow {
npmName: string
/** The name this package carries upstream; MIT attribution names the fork's origin, not our scope. */
upstreamName: string
upstream: string
}`,
expect: 1,
},
{
id: 'notices-vendored-row-parse',
file: 'scripts/gen-third-party-notices.ts',
find: ` const match = /^\\| \\x60\\S+\\/\\x60 \\| \\x60([^\\x60]+)\\x60 \\| \\S+ \\| (https:\\/\\/\\S+?)(?: \\([^)]*\\))? \\| \\x60[0-9a-f]+\\x60 \\|$/.exec(line)
if (match === null) continue
const [, npmName, upstream] = match
if (npmName === undefined || upstream === undefined) continue
rows.push({ npmName, upstream })`,
replace: ` const match = new RegExp(String.raw\`^\\| \\x60\\S+\\/\\x60 \\| \\x60([^\\x60]+)\\x60 \\| \\x60([^\\x60]+)\\x60 \\| \\S+ \\| \`
+ String.raw\`(https:\\/\\/\\S+?)(?: \\([^)]*\\))? \\| \\x60[0-9a-f]+\\x60 \\|$\`).exec(line)
if (match === null) continue
const [, npmName, upstreamName, upstream] = match
if (npmName === undefined || upstreamName === undefined || upstream === undefined) continue
rows.push({ npmName, upstreamName, upstream })`,
expect: 1,
},
{
id: 'notices-vendored-section',
file: 'scripts/gen-third-party-notices.ts',
find: 'The Cordis framework and its foundation libraries are source-vendored into this repository rather than consumed from npm. All are MIT-licensed',
replace: 'The Cordis framework and its foundation libraries are source-vendored into this repository rather than consumed from npm, and republished under the \\`@deepseek-ai\\` scope. All are MIT-licensed',
expect: 1,
},
{
id: 'notices-vendored-table',
file: 'scripts/gen-third-party-notices.ts',
find: `| Package | Upstream | License |
| --- | --- | --- |
\${vendored.map(row => \`| \\\`\${row.npmName}\\\` | [\${row.upstream.replace('https://', '')}](\${row.upstream}) | MIT |\`).join('\\n')}`,
replace: `| Package | Upstream name | Upstream | License |
| --- | --- | --- | --- |
\${vendored.map(row => \`| \\\`\${row.npmName}\\\` | \\\`\${row.upstreamName}\\\` | [\${row.upstream.replace('https://', '')}](\${row.upstream}) | MIT |\`).join('\\n')}`,
expect: 1,
},
{
id: 'notices-spec-row-fixture',
file: 'scripts/gen-third-party-notices.spec.ts',
find: ' expect(rows).toContainEqual({ npmName: \'cordis\', upstream: \'https://github.com/cordiverse/cordis\' })',
replace: ` expect(rows).toContainEqual({
npmName: '@deepseek-ai/cordis',
upstreamName: 'cordis',
upstream: 'https://github.com/cordiverse/cordis',
})`,
expect: 1,
},
{
id: 'notices-spec-shape-fixture',
file: 'scripts/gen-third-party-notices.spec.ts',
find: 'parseVendoredRows(\'| `cordis/` | cordis | 4.0.0 | https://example.com | `abc123` |\\n\')',
replace: 'parseVendoredRows(\'| `cordis/` | `@deepseek-ai/cordis` | cordis | 4.0.0 | https://example.com | `abc123` |\\n\')',
expect: 1,
},
{
// The framework peer is no longer a registry name, so the rehearsal must install this
// repository's vendored copies; cosmokit comes along as cordis's own dependency.
id: 'packed-install-vendored-peer',
file: 'packages/sandbox/sandbox-local/tests/packed-install.e2e.ts',
find: ` 'packages/support/invariants',
]`,
replace: ` 'packages/support/invariants',
// The framework and the vendored packages the closure declares outright:
// rescoped into @deepseek-ai, so the consumer installs this repository's
// copies. Schemastery is a hard dependency of three members above, not a
// peer, so npm resolves it while installing them.
'vendor/cordis',
'vendor/cosmokit',
'vendor/schemastery',
]`,
expect: 1,
},
{
id: 'packed-install-registry-spec',
file: 'packages/sandbox/sandbox-local/tests/packed-install.e2e.ts',
find: ` // Peer ranges resolve to the tarballs; Cordis is pinned to their peer range. Do not omit optional
// dependencies because the launcher selects its OS/CPU package through one.
writeFileSync(join(consumerDir, 'package.json'), JSON.stringify({ name: 'dsh-packed-consumer', private: true, type: 'module' }))
const install = spawnSync('npm', ['install', '--no-audit', '--no-fund', ...tarballs, 'cordis@4.0.0-rc.7'], {`,
replace: ` // Peer ranges resolve to the tarballs, the framework peer included. Do not omit optional
// dependencies because the launcher selects its OS/CPU package through one.
writeFileSync(join(consumerDir, 'package.json'), JSON.stringify({ name: 'dsh-packed-consumer', private: true, type: 'module' }))
const install = spawnSync('npm', ['install', '--no-audit', '--no-fund', ...tarballs], {`,
expect: 1,
},
{
id: 'packed-install-module-doc',
file: 'packages/sandbox/sandbox-local/tests/packed-install.e2e.ts',
find: ` * Keyless publish-path rehearsal. It packs the provider, its workspace peers, and the current
* repository's Landlock entry/platform packages, then installs those exact tarballs in an external
* plain-Node consumer. The host launcher comes from the exact local tarballs, so no registry copy,
* tsx, path mapping, or workspace resolution can hide missing files, dependency errors, or lost
* executable modes.`,
replace: ` * Keyless publish-path rehearsal. It packs the provider, its workspace peers, the vendored framework
* peer, and the current repository's Landlock entry/platform packages, then installs those exact
* tarballs in an external plain-Node consumer. The host launcher comes from the exact local tarballs,
* so no registry copy, tsx, path mapping, or workspace resolution can hide missing files, dependency
* errors, or lost executable modes.`,
expect: 1,
},
// The manifest table's name column plus the new upstream-name column, one edit per row.
...RENAMES.map(rename => ({
id: `vendor-readme-row-${rename.directory}`,
file: 'vendor/README.md',
find: `| \`${rename.directory}/\` | \`${rename.upstream}\` | `,
replace: `| \`${rename.directory}/\` | \`${rename.scoped}\` | \`${rename.upstream}\` | `,
expect: 1,
})),
]
/** Files the rescope must never rewrite. */
function excluded(file: string): boolean {
if (file === 'scripts/rescope-vendor.ts') return true // the mapping itself
if (file.startsWith('.agents/notes/')) return true // notes record what was true when written
// Recorded model payloads quote documentation verbatim, so they must mirror the
// sources on disk — including the notes this rescope leaves alone.
if (file.startsWith('scripts/snapshots/')) return true
// The mapping documents state both names on purpose.
if (file === 'docs/rescope.md' || file === 'docs/rescope.zh.md') return true
if (file.endsWith('.i18n.yaml')) return true // blob-hash records, re-recorded by the pairing gate
if (file === 'pnpm-lock.yaml') return true // regenerated by pnpm install
if (/^vendor\/[^/]+\/(README\.md|LICENSE)$/.test(file)) return true // upstream files kept verbatim
return !EXTENSIONS.some(extension => file.endsWith(extension))
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
/** One name's rewrite, precompiled for both delimited forms. */
interface Pattern {
readonly upstream: string
readonly from: string
readonly to: string
readonly token: RegExp
readonly yamlName: RegExp
}
function patterns(reverse: boolean): Pattern[] {
return RENAMES
.map(rename => ({
upstream: rename.upstream,
from: reverse ? rename.scoped : rename.upstream,
to: reverse ? rename.upstream : rename.scoped,
}))
.sort((left, right) => right.from.length - left.from.length)
.map(rename => ({
...rename,
token: new RegExp(`(['"\`])${escapeRegExp(rename.from)}((?:/[^'"\`\\s]*)?)\\1`, 'g'),
yamlName: new RegExp(`^(\\s*(?:-\\s*)?name:[ \\t]+)${escapeRegExp(rename.from)}([ \\t]*(?:#.*)?)$`, 'gm'),
}))
}
function skipped(file: string, pattern: Pattern): boolean {
return GENERIC_SKIPS.some(skip => skip.file === file && skip.upstream.includes(pattern.upstream))
}
function rewriteLine(line: string, file: string, all: readonly Pattern[]): string {
let out = line
for (const pattern of all) {
if (skipped(file, pattern)) continue
out = out.replace(pattern.token, (_match, quote: string, subpath: string) => `${quote}${pattern.to}${subpath}${quote}`)
out = out.replace(pattern.yamlName, (_match, prefix: string, suffix: string) => `${prefix}${pattern.to}${suffix}`)
}
return out
}
/**
* Rewrite a file's eligible lines.
*
* Markdown splits in two. Every fence is code a reader copies or a
* configuration they mount, so every fence follows the rename regardless of its
* info string. Prose follows it only under `docs/`, where a sentence quoting
* `` `cordis` `` teaches a name this repository no longer resolves; elsewhere
* prose is a record of what was true when it was written, and the same spelling
* can mean something else entirely — the Python SDK's `cordis` option, or the
* unvendored `@cordisjs/plugin-http`.
*/
function rewrite(text: string, file: string, all: readonly Pattern[]): { text: string; lines: number } {
const markdown = file.endsWith('.md')
const prose = markdown && file.startsWith('docs/')
let insideFence = false
let lines = 0
const out = text.split('\n').map((line) => {
if (markdown) {
if (/^\s*```/.test(line)) {
insideFence = !insideFence
return line
}
if (!insideFence && !prose) return line
}
const next = rewriteLine(line, file, all)
if (next !== line) lines += 1
return next
})
return { text: out.join('\n'), lines }
}
function classify(file: string): string {
if (/^vendor\/[^/]+\/package\.json$/.test(file)) return 'vendor manifest name'
if (file.endsWith('package.json')) return 'package.json dependencies'
if (/\.(ts|tsx|js|mjs|cjs|tpl)$/.test(file)) return 'code specifiers'
if (/\.(yml|yaml)$/.test(file)) return 'YAML plugin names'
if (file.endsWith('.json')) return 'JSON configuration'
return 'Markdown fences and docs prose'
}
/**
* One exact edit's state in the text it targets. `pending` means the source
* form is present and the target form absent; `applied` means the reverse;
* anything else — a partial application, a moved site, or a DUPLICATED
* insertion — is `invalid`, so it fails the run instead of being applied again.
*/
export type ExactEditState = 'pending' | 'applied' | 'invalid'
/**
* Classify one exact edit against its target text.
*
* An insertion keeps its anchor (`replace` contains `find`) and a deletion
* keeps its remainder (`find` contains `replace`), so neither can be judged by
* the source form alone: the surviving side counts the target form instead.
* @param text - the complete current text of the edited file.
* @param find - the source form, already oriented for the running direction.
* @param replace - the target form, already oriented for the running direction.
* @param expect - how many occurrences one complete application produces.
* @returns Whether the edit is pending, already applied, or invalid.
*/
export function exactEditState(text: string, find: string, replace: string, expect: number): ExactEditState {
const hits = text.split(find).length - 1
const landed = text.split(replace).length - 1
if (replace.includes(find)) {
if (landed === expect) return 'applied'
return landed === 0 && hits === expect ? 'pending' : 'invalid'
}
if (find.includes(replace)) {
if (hits === 0) return landed === expect ? 'applied' : 'invalid'
return hits === expect ? 'pending' : 'invalid'
}
if (hits === 0 && landed === expect) return 'applied'
return hits === expect && landed === 0 ? 'pending' : 'invalid'
}
function main(): void {
const args = process.argv.slice(2)
const mode = args.includes('--apply') ? 'apply' : args.includes('--check') ? 'check' : 'dry'
const reverse = args.includes('--reverse')
const all = patterns(reverse)
const files = execFileSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf8' })
.split('\0')
.filter(file => file !== '' && !excluded(file))
const counts = new Map<string, { files: number; lines: number }>()
const failures: string[] = []
const outstanding: string[] = []
// Classify every exact edit before writing anything: a single invalid site
// means the mapping and the tree disagree, and a half-applied tree is worse
// than an untouched one.
const planned: { edit: ExactEdit; path: string; find: string; replace: string }[] = []
for (const edit of EXACT_EDITS) {
const path = resolve(root, edit.file)
const before = readFileSync(path, 'utf8')
const find = reverse ? edit.replace : edit.find
const replace = reverse ? edit.find : edit.replace
const state = exactEditState(before, find, replace, edit.expect)
if (state === 'invalid') {
failures.push(`exact edit ${edit.id}: ${edit.file} is neither pending nor cleanly applied (duplicated, partial, or moved)`)
continue
}
if (mode === 'check') {
if (state !== 'applied') failures.push(`exact edit ${edit.id} did not land in ${edit.file}`)
continue
}
if (state === 'pending') planned.push({ edit, path, find, replace })
}
if (failures.length > 0) {
for (const failure of failures) console.error(`rescope-vendor: ${failure}`)
console.error(`rescope-vendor: ${String(failures.length)} problem(s); nothing was written.`)
process.exitCode = 1
return
}
if (mode === 'apply') {
// Re-read per edit: two edits can target one file, and a stale snapshot
// would let the second write discard the first.
for (const { path, find, replace } of planned) {
writeFileSync(path, readFileSync(path, 'utf8').split(find).join(replace))
}
}
for (const file of files) {
const path = resolve(root, file)
const before = readFileSync(path, 'utf8')
const { text: after, lines } = rewrite(before, file, all)
if (after === before) continue
outstanding.push(file)
const kind = classify(file)
const current = counts.get(kind) ?? { files: 0, lines: 0 }
counts.set(kind, { files: current.files + 1, lines: current.lines + lines })
if (mode === 'apply') writeFileSync(path, after)
}
console.log(`rescope-vendor: ${mode}${reverse ? ' --reverse' : ''} over ${String(files.length)} tracked files`)
for (const kind of [...counts.keys()].sort()) {
const { files: count, lines } = counts.get(kind) ?? { files: 0, lines: 0 }
console.log(` ${kind.padEnd(24)} ${String(count).padStart(4)} file(s), ${String(lines)} line(s)`)
}
if (mode !== 'dry') {
for (const check of POSTCONDITIONS) {
if (reverse) break
const path = resolve(root, check.file)
const hits = existsSync(path) ? readFileSync(path, 'utf8').split(check.text).length - 1 : -1
if (hits !== check.count) {
failures.push(`postcondition: ${check.file} has ${String(hits)} occurrence(s) of ${JSON.stringify(check.text)}, expected ${String(check.count)}`)
}
}
// The generic pass above already told us which files would still change,
// which in check mode is exactly the residue-and-idempotency signal.
if (mode === 'check') {
for (const file of outstanding) failures.push(`residue: ${file} still carries a pre-rescope name token`)
}
}
if (failures.length > 0) {
for (const failure of failures) console.error(`rescope-vendor: ${failure}`)
console.error(`rescope-vendor: ${String(failures.length)} problem(s); the mapping or an upstream site moved.`)
process.exitCode = 1
} else if (mode === 'check') {
console.log('rescope-vendor: post-state verified — no residue, every exact edit landed, idempotent.')
} else if (mode === 'apply') {
console.log('rescope-vendor: applied. Run `pnpm install`, `pnpm run gen-third-party-notices`, and re-record the touched bilingual pairs.')
}
}
// Importing this module for its exported classifier must not run the codemod.
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
main()
}
+1 -13
View File
@@ -227,7 +227,7 @@ describe('Node 24 lane ownership', () => {
const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers'))
expect(defaultConcurrency('ci-consumers', subject.length, 4)).toEqual({
workers: 11,
workers: 10,
source: 'ci-consumers gate count',
})
expect(subject.map(item => item.id)).toEqual([
@@ -241,7 +241,6 @@ describe('Node 24 lane ownership', () => {
'doc-typecheck',
'node-next-types',
'built-bin-smoke',
'github-repository-plugin-e2e',
])
expect(subject.find(item => item.id === 'publint')?.needs).toEqual(['build'])
expect(subject.find(item => item.id === 'built-package-invariants')?.needs).toEqual(['publint'])
@@ -252,7 +251,6 @@ describe('Node 24 lane ownership', () => {
'doc-typecheck',
'node-next-types',
'built-bin-smoke',
'github-repository-plugin-e2e',
]) {
expect(subject.find(item => item.id === id)?.needs).toEqual(['built-package-invariants'])
}
@@ -266,16 +264,6 @@ describe('Node 24 lane ownership', () => {
'packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts',
]),
)
const githubRepositoryPlugin = subject.find(item => item.id === 'github-repository-plugin-e2e')
expect(githubRepositoryPlugin).toMatchObject({
label: 'GitHub repository Plugin dsh run',
env: {
DSH_REQUIRE_GITHUB_REPOSITORY_PLUGIN_E2E: '1',
},
})
expect(githubRepositoryPlugin?.args).toEqual(
expect.arrayContaining(['apps/cli/tests/github-repository-plugin.built.e2e.ts']),
)
expect(subject.find(item => item.id === 'web-snapshot')).toMatchObject({
displayCommand: 'DSH_SNAPSHOT=replay pnpm run test:web:built',
env: { DSH_SNAPSHOT: 'replay' },
+2 -16
View File
@@ -406,7 +406,6 @@ function ciConsumerGates(): Gate[] {
needs: validatedBuild,
}),
builtBinSmokeGate(validatedBuild),
githubRepositoryPluginE2eGate(validatedBuild),
]
}
@@ -518,7 +517,7 @@ function coverageGates(): Gate[] {
}
// Example and package snapshots boot their bins in `lib` mode (built artifacts under plain Node,
// plugins via real exports); repository-script snapshots execute their real source entry path.
// plugins via real exports); script snapshots execute their real source entry path.
// Callers wait either on `build` or on a validation gate that transitively owns that build.
function snapshotGate(needs: string[] = ['build']): Gate {
return pnpmScript('snapshot', 'test:snapshot', {
@@ -554,6 +553,7 @@ function flagEnabled(envName: string): boolean {
function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
const artifactOptions = options.artifactNeeds === undefined ? {} : { needs: options.artifactNeeds }
return [
pnpmScript('rescope-vendor', 'rescope-vendor:check', { label: 'vendor rescope' }),
pnpmScript('knip', 'knip'),
pnpmScript('publint', 'publint', artifactOptions),
pnpmScript('constraints', 'constraints'),
@@ -639,20 +639,6 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate {
})
}
function githubRepositoryPluginE2eGate(needs: string[]): Gate {
return pnpmExec('github-repository-plugin-e2e', [
'vitest',
'run',
'--config',
'vitest.e2e.config.ts',
'apps/cli/tests/github-repository-plugin.built.e2e.ts',
], {
label: 'GitHub repository Plugin dsh run',
needs,
env: { DSH_REQUIRE_GITHUB_REPOSITORY_PLUGIN_E2E: '1' },
})
}
/**
* Reject a gate list whose graph cannot be executed unambiguously.
* @param gates - complete aggregate to validate.
+79 -100
View File
@@ -17,7 +17,7 @@ from pathlib import Path
from typing import TYPE_CHECKING, Callable
if TYPE_CHECKING:
from deepseek_harness import TurnResult
from deepseek_harness import RunResult
EXPECTED_TEXT = "runtime smoke ok"
@@ -25,10 +25,14 @@ CODE_PROMPT = "Use run_code to compute the packaged worker smoke value."
CODE_WORKER_TEXT = "code worker smoke ok"
WORKFLOW_PROMPT = "Use workflow to compute the packaged worker smoke value without agents."
WORKFLOW_WORKER_TEXT = "workflow worker smoke ok"
PERSISTENT_TOOLS_PROMPT = "Exercise the packaged persistent Bash and string-replacement editor."
PERSISTENT_TOOLS_TEXT = "persistent tools smoke ok"
PERSISTENT_EDITOR_PATH_PREFIX = "Editor path: "
PERSISTENT_BASH_COMMAND = (
MINIMAL_PROMPT = "Exercise the packaged minimal agent's persistent Bash and string-replacement editor."
MINIMAL_TEXT = "minimal agent smoke ok"
MINIMAL_EDITOR_PATH_PREFIX = "Editor path: "
MINIMAL_SYSTEM_PROMPT = "You are a helpful software engineer assistant."
MINIMAL_CORDIS = (
Path(__file__).resolve().parent.parent / "examples" / "jsonrpc-agent" / "minimal.cordis.yml"
)
MINIMAL_BASH_COMMAND = (
"counter=$(( ${counter:-0} + 1 )); export counter; "
"printf 'COUNT=%s CWD=%s\\n' \"$counter\" \"$PWD\"; "
"if [ \"$counter\" -eq 1 ]; then cd /tmp; fi"
@@ -103,51 +107,6 @@ CUSTOM_CORDIS = """\
- id: cordis-tool
name: '@deepseek-ai/dsh-tool-cordis'
"""
PERSISTENT_TOOLS_CORDIS = """\
- id: jsonrpc
name: '@deepseek-ai/dsh-jsonrpc'
- id: llm
name: '@deepseek-ai/dsh-llm-deepseek'
config:
apiKey: !!js process.env.DEEPSEEK_API_KEY
baseURL: !!js process.env.DEEPSEEK_BASE_URL
- id: sandbox
name: '@deepseek-ai/dsh-sandbox-local'
- id: sandbox-policy
name: '@deepseek-ai/dsh-sandbox-policy'
config:
mode: danger-full-access
workspaceRoot: !!js process.env.DSH_CWD
- id: pty
name: '@deepseek-ai/dsh-pty'
- id: pty-local
name: '@deepseek-ai/dsh-pty-local'
- id: fs
name: '@deepseek-ai/dsh-fs-local'
config:
cwd: !!js process.env.DSH_CWD
- id: agent-core
name: '@deepseek-ai/dsh-agent-spine-demo'
config:
includeHarnessIdentity: false
persona: 'You are a helpful software engineer assistant.'
workspaceContext: false
skills:
enabled: false
toolBash: false
toolTasks: false
- id: sessions
name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: !!js process.env.DSH_SESSION_ROOT
compression: 'none'
- id: persistent-bash
name: '@deepseek-ai/dsh-tool-bash-persistent'
- id: str-replace-editor
name: '@deepseek-ai/dsh-tool-str-replace-editor'
"""
class MockModelHandler(BaseHTTPRequestHandler):
"""Return deterministic text, worker, and orchestration completions."""
@@ -182,9 +141,9 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
if latest.get("role") == "tool":
call_id, tool_name = latest_tool_call(messages)
tool_text = message_text(latest.get("content"))
persistent = persistent_tool_followup(body, call_id, tool_name, tool_text)
if persistent is not None:
return persistent
minimal = minimal_tool_followup(body, call_id, tool_name, tool_text)
if minimal is not None:
return minimal
advanced = advanced_tool_followup(body, call_id, tool_name, tool_text)
if advanced is not None:
return advanced
@@ -196,16 +155,35 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
return text_chunks(WORKFLOW_WORKER_TEXT)
raise AssertionError(f"unexpected tool follow-up: {tool_name}")
prompt = message_text(latest.get("content"))
if prompt.startswith(f"{PERSISTENT_TOOLS_PROMPT}\n{PERSISTENT_EDITOR_PATH_PREFIX}"):
minimal_prompt = next(
(
message_text(message.get("content"))
for message in reversed(messages)
if isinstance(message, dict)
and message.get("role") == "user"
and message_text(message.get("content")).startswith(
f"{MINIMAL_PROMPT}\n{MINIMAL_EDITOR_PATH_PREFIX}"
)
),
None,
)
if minimal_prompt is not None:
names = advertised_tool_names(body)
if names != {"bash", "str_replace_editor"}:
raise AssertionError(f"persistent tools smoke advertised unexpected tools: {names}")
raise AssertionError(f"minimal agent smoke advertised unexpected tools: {names}")
system_prompts = [
message_text(message.get("content"))
for message in messages
if isinstance(message, dict) and message.get("role") == "system"
]
if system_prompts != [MINIMAL_SYSTEM_PROMPT]:
raise AssertionError(f"minimal agent smoke assembled unexpected system prompts: {system_prompts}")
return tool_call_chunks(
"persistent-bash-1",
"minimal-bash-1",
"bash",
{"command": PERSISTENT_BASH_COMMAND},
{"command": MINIMAL_BASH_COMMAND},
)
prompt = message_text(latest.get("content"))
if prompt == SNAPSHOT_DIRECT_CHILD_PROMPT:
return text_chunks("DIRECT_CHILD_OK")
if prompt == SNAPSHOT_WORKFLOW_CHILD_PROMPT:
@@ -240,24 +218,24 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
return text_chunks(EXPECTED_TEXT)
def persistent_tool_followup(
def minimal_tool_followup(
body: dict[str, object],
call_id: str,
tool_name: str,
tool_text: str,
) -> list[dict[str, object]] | None:
"""Verify packaged PTY persistence, then invoke the packaged editor."""
if not call_id.startswith("persistent-"):
"""Verify the checked-in minimal composition's PTY and editor."""
if not call_id.startswith("minimal-"):
return None
if call_id == "persistent-bash-1" and tool_name == "bash":
if call_id == "minimal-bash-1" and tool_name == "bash":
if "COUNT=1" not in tool_text:
raise AssertionError(f"first persistent bash call lost its output: {tool_text}")
return tool_call_chunks(
"persistent-bash-2",
"minimal-bash-2",
"bash",
{"command": PERSISTENT_BASH_COMMAND},
{"command": MINIMAL_BASH_COMMAND},
)
if call_id == "persistent-bash-2" and tool_name == "bash":
if call_id == "minimal-bash-2" and tool_name == "bash":
if "COUNT=2 CWD=/tmp" not in tool_text:
raise AssertionError(f"persistent bash did not retain state: {tool_text}")
messages = body.get("messages")
@@ -265,18 +243,18 @@ def persistent_tool_followup(
raise AssertionError("persistent editor smoke request has no messages")
editor_path = next(
(
text.split(PERSISTENT_EDITOR_PATH_PREFIX, 1)[1].strip()
text.split(MINIMAL_EDITOR_PATH_PREFIX, 1)[1].strip()
for message in messages
if isinstance(message, dict) and message.get("role") == "user"
for text in [message_text(message.get("content"))]
if PERSISTENT_EDITOR_PATH_PREFIX in text
if MINIMAL_EDITOR_PATH_PREFIX in text
),
None,
)
if editor_path is None:
raise AssertionError("persistent editor smoke prompt has no editor path")
return tool_call_chunks(
"persistent-editor",
"minimal-editor",
"str_replace_editor",
{
"command": "create",
@@ -284,11 +262,11 @@ def persistent_tool_followup(
"file_text": "created by packaged editor\n",
},
)
if call_id == "persistent-editor" and tool_name == "str_replace_editor":
if call_id == "minimal-editor" and tool_name == "str_replace_editor":
if "New file created successfully" not in tool_text:
raise AssertionError(f"packaged editor did not create its file: {tool_text}")
return text_chunks(PERSISTENT_TOOLS_TEXT)
raise AssertionError(f"unexpected persistent-tools follow-up: {call_id} {tool_name}: {tool_text}")
return text_chunks(MINIMAL_TEXT)
raise AssertionError(f"unexpected minimal-agent follow-up: {call_id} {tool_name}: {tool_text}")
def advanced_tool_followup(
@@ -470,14 +448,14 @@ def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--scenario",
choices=("all", "sdk-default", "sdk-custom", "sdk-persistent", "sdk-snapshot", "direct"),
choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-snapshot", "direct"),
default="all",
)
parser.add_argument("--exe", type=Path)
parser.add_argument("--update-snapshots", action="store_true")
args = parser.parse_args()
if args.scenario in {"all", "sdk-custom", "sdk-persistent", "sdk-snapshot", "direct"} and args.exe is None:
parser.error("--exe is required for custom, persistent, snapshot, and direct scenarios")
if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-snapshot", "direct"} and args.exe is None:
parser.error("--exe is required for custom, minimal, snapshot, and direct scenarios")
if args.update_snapshots and args.scenario not in {"all", "sdk-snapshot"}:
parser.error("--update-snapshots requires --scenario sdk-snapshot or all")
if args.exe is not None and not args.exe.is_file():
@@ -489,9 +467,9 @@ def main() -> None:
if args.scenario in {"all", "sdk-custom"}:
assert args.exe is not None
smoke_sdk_custom(model.url, args.exe.resolve())
if args.scenario in {"all", "sdk-persistent"}:
if args.scenario in {"all", "sdk-minimal"}:
assert args.exe is not None
smoke_sdk_persistent_tools(model.url, args.exe.resolve())
smoke_sdk_minimal(model.url, args.exe.resolve())
if args.scenario in {"all", "sdk-snapshot"}:
assert args.exe is not None
smoke_sdk_snapshot(model.url, args.exe.resolve(), args.update_snapshots)
@@ -519,7 +497,6 @@ def smoke_sdk_default(base_url: str) -> None:
request_timeout_seconds=60,
) as harness:
result = harness.run("reply with the smoke text", session_id="default-smoke")
assert result.status == "ok", result
assert result.final_response == EXPECTED_TEXT, result.final_response
assert_zstd_session_log(sessions)
@@ -546,46 +523,40 @@ def smoke_sdk_custom(base_url: str, executable: Path) -> None:
text_result = harness.run("reply with the smoke text", session_id="custom-smoke")
code_result = harness.run(CODE_PROMPT, session_id="custom-smoke")
workflow_result = harness.run(WORKFLOW_PROMPT, session_id="custom-smoke")
assert text_result.status == "ok", text_result
assert text_result.final_response == EXPECTED_TEXT, text_result.final_response
assert code_result.status == "ok", code_result
assert code_result.final_response == CODE_WORKER_TEXT, code_result.final_response
assert workflow_result.status == "ok", workflow_result
assert workflow_result.final_response == WORKFLOW_WORKER_TEXT, workflow_result.final_response
assert_session_log(sessions, root, EXPECTED_TEXT, CODE_WORKER_TEXT, WORKFLOW_WORKER_TEXT)
def smoke_sdk_persistent_tools(base_url: str, executable: Path) -> None:
"""Exercise native PTY state and the editor through the packaged executable."""
def smoke_sdk_minimal(base_url: str, executable: Path) -> None:
"""Exercise the checked-in minimal composition through the packaged executable."""
from deepseek_harness import DeepSeekHarness
with tempfile.TemporaryDirectory(prefix="dsh-sdk-persistent-tools-") as temporary:
with tempfile.TemporaryDirectory(prefix="dsh-sdk-minimal-") as temporary:
root = Path(temporary).resolve()
editor_path = root / "created.txt"
prompt = f"{PERSISTENT_TOOLS_PROMPT}\n{PERSISTENT_EDITOR_PATH_PREFIX}{editor_path}"
prompt = f"{MINIMAL_PROMPT}\n{MINIMAL_EDITOR_PATH_PREFIX}{editor_path}"
sessions = root / "sessions"
cordis = root / "cordis.yml"
cordis.write_text(PERSISTENT_TOOLS_CORDIS)
with DeepSeekHarness(
provider="deepseek",
provider="deepseek-official",
model="smoke-model",
cwd=str(root),
session_root=str(sessions),
cordis=str(cordis),
cordis=str(MINIMAL_CORDIS),
runtime_bin=str(executable),
api_key="sk-keyless-smoke",
base_url=base_url,
request_timeout_seconds=60,
) as harness:
result = harness.run(prompt, session_id="persistent-tools-smoke")
result = harness.run(prompt, session_id="minimal-agent-smoke")
assert result.status == "ok", result
event_text = json.dumps(result.events)
if PERSISTENT_TOOLS_TEXT not in event_text:
raise AssertionError(f"packaged tools run emitted no final response: {result.events}")
if MINIMAL_TEXT not in event_text:
raise AssertionError(f"minimal agent run emitted no final response: {result.events}")
if editor_path.read_text() != "created by packaged editor\n":
raise AssertionError(f"packaged editor wrote unexpected content: {editor_path.read_text()!r}")
assert_session_log(sessions, root, PERSISTENT_TOOLS_TEXT, "COUNT=1", "COUNT=2 CWD=/tmp")
assert_session_log(sessions, root, MINIMAL_TEXT, "COUNT=1", "COUNT=2 CWD=/tmp")
def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool) -> None:
@@ -610,7 +581,6 @@ def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool)
) as harness:
result = harness.run(SNAPSHOT_PROMPT, session_id=SNAPSHOT_SESSION_ID)
assert result.status == "ok", result
assert result.final_response == SNAPSHOT_FINAL_TEXT, result.final_response
methods = [notification.method for notification in result.notifications]
if methods.count("subagent.started") != 2 or methods.count("subagent.finished") != 2:
@@ -657,8 +627,8 @@ def smoke_direct(base_url: str, executable: Path) -> None:
"params": {"sessionId": "direct-smoke", "contentBlocks": [{"type": "text", "text": "reply with the smoke text"}]},
})
messages = peer.read_until(lambda message: message.get("id") == "prompt")
if not any(message.get("method") == "session.finished" and message.get("params", {}).get("status") == "ok" for message in messages):
messages.extend(peer.read_until(lambda message: message.get("method") == "session.finished"))
if not any(is_idle_notification(message) for message in messages):
messages.extend(peer.read_until(is_idle_notification))
event_text = json.dumps(messages)
if EXPECTED_TEXT not in event_text:
raise AssertionError(f"direct runtime emitted no final response: {messages}")
@@ -669,6 +639,16 @@ def smoke_direct(base_url: str, executable: Path) -> None:
assert_session_log(sessions, root, EXPECTED_TEXT)
def is_idle_notification(message: dict[str, object]) -> bool:
"""Return whether a JSON-RPC notification marks a session idle."""
params = message.get("params")
return (
message.get("method") == "session.status"
and isinstance(params, dict)
and params.get("status") == "idle"
)
class RuntimePeer:
def __init__(self, argv: list[str], cwd: Path, environment: dict[str, str]) -> None:
self.process = subprocess.Popen(
@@ -776,7 +756,7 @@ def read_session_logs(sessions: Path) -> dict[str, list[dict[str, object]]]:
return logs
def snapshot_child_ids(result: "TurnResult") -> list[str]:
def snapshot_child_ids(result: "RunResult") -> list[str]:
"""Return the two child session ids in their SDK notification order."""
child_ids: list[str] = []
for notification in result.notifications:
@@ -794,7 +774,7 @@ def snapshot_child_ids(result: "TurnResult") -> list[str]:
def build_snapshot_files(
result: "TurnResult",
result: "RunResult",
logs: dict[str, list[dict[str, object]]],
child_ids: list[str],
cwd: Path,
@@ -809,7 +789,6 @@ def build_snapshot_files(
result_value = {
"session_id": result.session_id,
"status": result.status,
"final_response": result.final_response,
"events": result.events,
"notifications": [
@@ -834,7 +813,7 @@ def build_snapshot_files(
return files
def snapshot_agent_id(result: "TurnResult", child_id: str) -> str:
def snapshot_agent_id(result: "RunResult", child_id: str) -> str:
"""Find the successful subagent id paired with one child session."""
for notification in result.notifications:
if notification.method != "subagent.finished":
File diff suppressed because it is too large Load Diff
@@ -1,14 +1,18 @@
{"type":"session","version":0,"id":"{{child-1}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{parent}}","delegationDepth":1}
{"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"session/title","seq":2,"time":0,"data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[1],"source":{"kind":"fallback"}}}
{"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}}
{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"smoke-model","reasoningEffort":"high"},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","snapshot_double","subagent","task_kill","task_list","task_output","workflow"]},"reason":"initial"}}
{"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"DIRECT_CHILD_OK"}}}
{"type":"assistant/chunk","seq":7,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}}}
{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":10,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[5,6,7,8,9],"surfaceOp":"append"}
{"type":"step/end","seq":11,"time":0,"data":{"turn":1,"step":1}}
{"type":"turn/end","seq":12,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
{"type":"session","version":0,"id":"{{child-1}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{parent}}","origin":"subagent","delegationDepth":1}
{"type":"agent/inbox/spliced","seq":0,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}}
{"type":"turn/start","seq":1,"time":0,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":2,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"subagent/descriptor","seq":3,"time":0,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check direct child"}}
{"type":"step/start","seq":4,"time":0,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":5,"time":0,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":0,"data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","snapshot_double","subagent","task_kill","task_list","task_output","workflow"]},"reason":"initial"}}
{"type":"request/context","seq":8,"time":0,"data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"DIRECT_CHILD_OK"}}}
{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}}}
{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":14,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
{"type":"step/end","seq":15,"time":0,"data":{"turn":1,"step":1}}
{"type":"turn/end","seq":16,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
@@ -1,14 +1,18 @@
{"type":"session","version":0,"id":"{{child-2}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{parent}}","delegationDepth":1}
{"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"session/title","seq":2,"time":0,"data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[1],"source":{"kind":"fallback"}}}
{"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}}
{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"smoke-model","reasoningEffort":"high"},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","snapshot_double","subagent","task_kill","task_list","task_output","workflow"]},"reason":"initial"}}
{"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"WORKFLOW_CHILD_OK"}}}
{"type":"assistant/chunk","seq":7,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}}}
{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":10,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[5,6,7,8,9],"surfaceOp":"append"}
{"type":"step/end","seq":11,"time":0,"data":{"turn":1,"step":1}}
{"type":"turn/end","seq":12,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
{"type":"session","version":0,"id":"{{child-2}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{parent}}","origin":"subagent","delegationDepth":1}
{"type":"agent/inbox/spliced","seq":0,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}}
{"type":"turn/start","seq":1,"time":0,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":2,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"subagent/descriptor","seq":3,"time":0,"data":{"version":2,"mode":"one-shot","provider":"spawn"}}
{"type":"step/start","seq":4,"time":0,"data":{"turn":1,"step":1}}
{"type":"user/message","seq":5,"time":0,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"session/title","seq":6,"time":0,"data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":7,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","snapshot_double","subagent","task_kill","task_list","task_output","workflow"]},"reason":"initial"}}
{"type":"request/context","seq":8,"time":0,"data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"WORKFLOW_CHILD_OK"}}}
{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}}}
{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":14,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
{"type":"step/end","seq":15,"time":0,"data":{"turn":1,"step":1}}
{"type":"turn/end","seq":16,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
@@ -1,68 +1,71 @@
{"type":"session","version":0,"id":"{{parent}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
{"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"session/title","seq":2,"time":0,"data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[1],"source":{"kind":"fallback"}}}
{"type":"agent/inbox/spliced","seq":0,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}}
{"type":"turn/start","seq":1,"time":0,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":2,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}}
{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"smoke-model","reasoningEffort":"high"},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","subagent","task_kill","task_list","task_output","workflow"]},"reason":"initial"}}
{"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-mount","name":"cordis_mount","argumentsDelta":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}}}
{"type":"assistant/chunk","seq":7,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-mount","name":"cordis_mount","arguments":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}}}}
{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":10,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-mount","name":"cordis_mount","arguments":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}],"source":{"kind":"model","provider":"deepseek","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[5,6,7,8,9],"surfaceOp":"append"}
{"type":"tool/call","seq":11,"time":0,"data":{"turn":1,"step":1,"callId":"advanced-mount","name":"cordis_mount","arguments":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}}
{"type":"tool/result","seq":12,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-mount"},"content":[{"type":"tool-result","toolCallId":"advanced-mount","content":[{"type":"text","text":"Temporary Plugin dyn-1 is running (plugin \"<anonymous>\"; available until unmounted or DSH restarts)."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[11],"surfaceOp":"append"}
{"type":"step/end","seq":13,"time":0,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":14,"time":0,"data":{"turn":1,"step":2}}
{"type":"request/header","seq":15,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"smoke-model","reasoningEffort":"high"},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","snapshot_double","subagent","task_kill","task_list","task_output","workflow"]},"reason":"change"}}
{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":17,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-code","name":"run_code","argumentsDelta":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}}
{"type":"assistant/chunk","seq":18,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}}}
{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":21,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[16,17,18,19,20],"surfaceOp":"append"}
{"type":"tool/call","seq":22,"time":0,"data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}
{"type":"tool/code-dispatch-start","seq":23,"time":0,"data":{"parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21}}}
{"type":"tool/code-dispatch","seq":24,"time":0,"data":{"parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"content":[{"type":"text","text":"42"}]}}
{"type":"tool/result","seq":25,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"}
{"type":"step/end","seq":26,"time":0,"data":{"turn":1,"step":2}}
{"type":"step/start","seq":27,"time":0,"data":{"turn":1,"step":3}}
{"type":"assistant/chunk","seq":28,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":29,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-direct-child","name":"subagent","argumentsDelta":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}
{"type":"assistant/chunk","seq":30,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}}
{"type":"assistant/chunk","seq":31,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","seq":32,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":33,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[28,29,30,31,32],"surfaceOp":"append"}
{"type":"tool/call","seq":34,"time":0,"data":{"turn":1,"step":3,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}
{"type":"tool/result","seq":35,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[34],"surfaceOp":"append"}
{"type":"step/end","seq":36,"time":0,"data":{"turn":1,"step":3}}
{"type":"step/start","seq":37,"time":0,"data":{"turn":1,"step":4}}
{"type":"assistant/chunk","seq":38,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":39,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-workflow","name":"workflow","argumentsDelta":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}}
{"type":"assistant/chunk","seq":40,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}}}
{"type":"assistant/chunk","seq":41,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","seq":42,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":43,"time":0,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[38,39,40,41,42],"surfaceOp":"append"}
{"type":"tool/call","seq":44,"time":0,"data":{"turn":1,"step":4,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}
{"type":"tool/result","seq":45,"time":0,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[44],"surfaceOp":"append"}
{"type":"step/end","seq":46,"time":0,"data":{"turn":1,"step":4}}
{"type":"step/start","seq":47,"time":0,"data":{"turn":1,"step":5}}
{"type":"assistant/chunk","seq":48,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":49,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-unmount","name":"cordis_unmount","argumentsDelta":"{\"id\": \"dyn-1\"}"}}}
{"type":"assistant/chunk","seq":50,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}}}}
{"type":"assistant/chunk","seq":51,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","seq":52,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":53,"time":0,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}],"source":{"kind":"model","provider":"deepseek","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[48,49,50,51,52],"surfaceOp":"append"}
{"type":"tool/call","seq":54,"time":0,"data":{"turn":1,"step":5,"callId":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}}
{"type":"tool/result","seq":55,"time":0,"data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-unmount"},"content":[{"type":"tool-result","toolCallId":"advanced-unmount","content":[{"type":"text","text":"Temporary Plugin dyn-1 was unmounted and removed."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[54],"surfaceOp":"append"}
{"type":"step/end","seq":56,"time":0,"data":{"turn":1,"step":5}}
{"type":"step/start","seq":57,"time":0,"data":{"turn":1,"step":6}}
{"type":"request/header","seq":58,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"smoke-model","reasoningEffort":"high"},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","subagent","task_kill","task_list","task_output","workflow"]},"reason":"change"}}
{"type":"assistant/chunk","seq":59,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":60,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":0,"text":"ADVANCED_EXECUTABLE_OK"}}}
{"type":"assistant/chunk","seq":61,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}}}
{"type":"assistant/chunk","seq":62,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","seq":63,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":64,"time":0,"data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[59,60,61,62,63],"surfaceOp":"append"}
{"type":"step/end","seq":65,"time":0,"data":{"turn":1,"step":6}}
{"type":"turn/end","seq":66,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
{"type":"user/message","seq":4,"time":0,"data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"session/title","seq":5,"time":0,"data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":6,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","subagent","task_kill","task_list","task_output","workflow"]},"reason":"initial"}}
{"type":"request/context","seq":7,"time":0,"data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-mount","name":"cordis_mount","argumentsDelta":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}}}
{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-mount","name":"cordis_mount","arguments":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}}}}
{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":13,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-mount","name":"cordis_mount","arguments":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"}
{"type":"tool/call","seq":14,"time":0,"data":{"turn":1,"step":1,"callId":"advanced-mount","name":"cordis_mount","arguments":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}}
{"type":"tool/result","seq":15,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-mount"},"content":[{"type":"tool-result","toolCallId":"advanced-mount","content":[{"type":"text","text":"Temporary Plugin dyn-1 is running (plugin \"<anonymous>\"; available until unmounted or DSH restarts)."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
{"type":"step/end","seq":16,"time":0,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":17,"time":0,"data":{"turn":1,"step":2}}
{"type":"request/header","seq":18,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","snapshot_double","subagent","task_kill","task_list","task_output","workflow"]},"reason":"change"}}
{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-code","name":"run_code","argumentsDelta":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}}
{"type":"assistant/chunk","seq":21,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}}}
{"type":"assistant/chunk","seq":22,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","seq":23,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":24,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
{"type":"tool/call","seq":25,"time":0,"data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}
{"type":"tool/code-dispatch-start","seq":26,"time":0,"data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21}}}
{"type":"tool/code-dispatch","seq":27,"time":0,"data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"content":[{"type":"text","text":"42"}]}}
{"type":"tool/result","seq":28,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[25],"surfaceOp":"append"}
{"type":"step/end","seq":29,"time":0,"data":{"turn":1,"step":2}}
{"type":"step/start","seq":30,"time":0,"data":{"turn":1,"step":3}}
{"type":"assistant/chunk","seq":31,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":32,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-direct-child","name":"subagent","argumentsDelta":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}
{"type":"assistant/chunk","seq":33,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}}
{"type":"assistant/chunk","seq":34,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","seq":35,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":36,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[31,32,33,34,35],"surfaceOp":"append"}
{"type":"tool/call","seq":37,"time":0,"data":{"turn":1,"step":3,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}
{"type":"tool/result","seq":38,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[37],"surfaceOp":"append"}
{"type":"step/end","seq":39,"time":0,"data":{"turn":1,"step":3}}
{"type":"step/start","seq":40,"time":0,"data":{"turn":1,"step":4}}
{"type":"assistant/chunk","seq":41,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":42,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-workflow","name":"workflow","argumentsDelta":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}}
{"type":"assistant/chunk","seq":43,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}}}
{"type":"assistant/chunk","seq":44,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","seq":45,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":46,"time":0,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[41,42,43,44,45],"surfaceOp":"append"}
{"type":"tool/call","seq":47,"time":0,"data":{"turn":1,"step":4,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}
{"type":"tool/result","seq":48,"time":0,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[47],"surfaceOp":"append"}
{"type":"step/end","seq":49,"time":0,"data":{"turn":1,"step":4}}
{"type":"step/start","seq":50,"time":0,"data":{"turn":1,"step":5}}
{"type":"assistant/chunk","seq":51,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":52,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-unmount","name":"cordis_unmount","argumentsDelta":"{\"id\": \"dyn-1\"}"}}}
{"type":"assistant/chunk","seq":53,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}}}}
{"type":"assistant/chunk","seq":54,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","seq":55,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":56,"time":0,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[51,52,53,54,55],"surfaceOp":"append"}
{"type":"tool/call","seq":57,"time":0,"data":{"turn":1,"step":5,"callId":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}}
{"type":"tool/result","seq":58,"time":0,"data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-unmount"},"content":[{"type":"tool-result","toolCallId":"advanced-unmount","content":[{"type":"text","text":"Temporary Plugin dyn-1 was unmounted and removed."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[57],"surfaceOp":"append"}
{"type":"step/end","seq":59,"time":0,"data":{"turn":1,"step":5}}
{"type":"step/start","seq":60,"time":0,"data":{"turn":1,"step":6}}
{"type":"request/header","seq":61,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","subagent","task_kill","task_list","task_output","workflow"]},"reason":"change"}}
{"type":"assistant/chunk","seq":62,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":63,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":0,"text":"ADVANCED_EXECUTABLE_OK"}}}
{"type":"assistant/chunk","seq":64,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}}}
{"type":"assistant/chunk","seq":65,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","seq":66,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":67,"time":0,"data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[62,63,64,65,66],"surfaceOp":"append"}
{"type":"step/end","seq":68,"time":0,"data":{"turn":1,"step":6}}
{"type":"turn/end","seq":69,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
@@ -8,11 +8,11 @@
},
{
"role": "user",
"content": "# DeepSeek Harness\n\nEnglish | [中文](README.zh.md)\n\nDeepSeek Harness (`dsh`) is an open-source coding agent built on the DeepSeek Harness SDK.\n\nIt uses an architecture where **everything is a plugin**.\n\n## Internal testing notice\n\nDeepSeek Harness is under internal testing. Features and interfaces may change.\n\nThe internal build uploads all Session Logs by default to help diagnose reported problems. Set `DSH_TELEMETRY_DISABLED=1` to disable telemetry. Send feedback through the internal WeChat group.\n\n## Install\n\nClone the repository, then run the installer:\n\n```sh\ngit clone <repo-url>\ncd deepseek-harness\nscripts/install.sh\n```\n\nThe installer requires `git` and Node `^22.19 || >=24`, offers to install `pnpm` when it is missing, prompts for a DeepSeek API key, builds the required repository artifacts, and launches the Web UI.\n\nThe default active checkout is `~/.dsh/source/current`, and the launcher is linked into `~/.local/bin`. Re-run the installer to update. [`scripts/install.sh`](scripts/install.sh) owns alternate locations, update mechanics, and recovery options.\n\n## Use DeepSeek Harness\n\n### Web UI\n\nFor the recommended local interface, choose Web UI when the installer finishes. To start it later, or after updating the active checkout, build the repository and run:\n\n```sh\n(cd ~/.dsh/source/current && pnpm run build)\ndsh web\n```\n\nThe path above is the installer's default. If you set `DSH_SOURCE` or `DSH_CURRENT`, or reused an existing checkout, replace `~/.dsh/source/current` with that checkout path; see [`scripts/install.sh`](scripts/install.sh) for details. The Web UI is served at `http://127.0.0.1:3080` by default.\n\n### Profiles\n\n`dsh` boots profiles — ordered stacks of plugin-bundle patch layers under your own overrides in `$DSH_HOME/profiles/<name>`:\n\n```sh\ndsh --profile web # the browser UI (same as: dsh web)\ndsh plugin --profile tui add <package> # install a plugin into a custom profile\ndsh --profile tui # boot it\n```\n\nThe [CLI reference](apps/cli/README.md#profiles) describes profile layout, layer semantics, and config dump commands.\n\n### Headless\n\nRun one task, print the final answer, and exit:\n\n```sh\ndsh run \"summarize this workspace\"\n```\n\n### Automation and SDKs\n\nFrom a source checkout with `DEEPSEEK_API_KEY` in the environment or its root `.env`, start the ACP automation server:\n\n```sh\npnpm run demo:acp\n```\n\nThe [Python SDK](python/README.md) drives a bundled JSON-RPC runtime. The [examples](examples/README.md) cover the runnable headless, ACP, JSON-RPC, Code Mode, and self-referential compositions.\n\n## Why DeepSeek Harness\n\nBuilt-in capabilities cover file reading, editing, and search; shell and persistent PTY execution; reusable skills; task tracking, goals, plans, todos, and background tasks; subagents and workflows; sandboxing and approvals; settings and credentials; persistent, resumable, forkable, and queryable sessions; LSP and web access; context compaction; and telemetry. Each composition selects the subset appropriate to its surface. The Web UI includes Plan Mode.\n\n- **Everything is a plugin.** Models, tools, policies, storage, context management, and interfaces are composable [Cordis plugins](docs/user/develop/basic/index.md), so deployments can extend or replace behavior without forking the agent loop. See the [architecture](docs/architecture.md) for the underlying design.\n- **Runs are reconstructable.** Anything visible to the model is logged in the authoritative session stream; persistence, resume/fork/query, replay, telemetry, and UIs derive from the same events. See the [session-log architecture](docs/architecture.md#session-log).\n- **Code Mode (opt-in).** It exposes a `run_code` tool and a generated TypeScript SDK; only program output re-enters model context. See [Code Mode](packages/core/tools/README.md#code-mode).\n- **Self-referential Cordis tools are opt-in.** They let the agent inspect its live runtime and mount or unmount plugins while it runs. See the [Cordis tools](packages/self-modification/tool-cordis/README.md).\n\n## Community\n\nFollow <a href=\"https://x.com/Deepseekharness\">DeepSeek Harness on Twitter</a> for project updates.\n\n## Development\n\nStart with the [development guide](docs/development.md) and read the [architecture](docs/architecture.md) before changing packages.\n\nFor agents, follow [AGENTS.md](AGENTS.md).\n\nDeepSeek Harness is currently in internal testing.\n\n## License\n\n[BSD 3-Clause](LICENSE)\n\nThird-party dependencies and their licenses are disclosed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).\n"
"content": "# DeepSeek Harness\n\nEnglish | [中文](README.zh.md)\n\nDeepSeek Harness (`dsh`) is an open-source coding agent built on the DeepSeek Harness SDK.\n\nIt uses an architecture where **everything is a plugin**.\n\n## Internal testing notice\n\nDeepSeek Harness is under internal testing. Features and interfaces may change.\n\nThe internal build uploads all Session Logs by default to help diagnose reported problems. Set `DSH_TELEMETRY_DISABLED=1` to disable telemetry. Send feedback through the internal WeChat group.\n\n## Install\n\nClone the repository, then run the installer:\n\n```sh\ngit clone <repo-url>\ncd deepseek-harness\nscripts/install.sh\n```\n\nThe installer requires `git` and Node `^22.19 || >=24`, offers to install `pnpm` when it is missing, prompts for a DeepSeek API key, builds the required repository artifacts, and launches the Web UI.\n\nThe default active checkout is `~/.dsh/source/current`, and the launcher is linked into `~/.local/bin`. Re-run the installer to update. [`scripts/install.sh`](scripts/install.sh) owns alternate locations, update mechanics, and recovery options.\n\n## Use DeepSeek Harness\n\n### Web UI\n\nFor the recommended local interface, choose Web UI when the installer finishes. To start it later, or after updating the active checkout, build the repository and run:\n\n```sh\n(cd ~/.dsh/source/current && pnpm run build)\ndsh web\n```\n\nThe path above is the installer's default. If you set `DSH_SOURCE` or `DSH_CURRENT`, or reused an existing checkout, replace `~/.dsh/source/current` with that checkout path; see [`scripts/install.sh`](scripts/install.sh) for details. The Web UI is served at `http://127.0.0.1:3080` by default.\n\n### Profiles\n\n`dsh` boots profiles — ordered stacks of plugin-bundle patch layers under your own overrides in `$DSH_HOME/profiles/<name>`:\n\n```sh\ndsh --profile web # the browser UI (same as: dsh web)\ndsh plugin --profile tui add <package> # install a plugin into a custom profile\ndsh --profile tui # boot it\n```\n\nThe [CLI reference](apps/cli/README.md#profiles) describes profile layout, layer semantics, and config dump commands.\n\n### Headless\n\nRun one task, print the final answer, and exit:\n\n```sh\ndsh --profile headless \"summarize this workspace\"\n```\n\n### Automation and SDKs\n\nFrom a source checkout with `DEEPSEEK_API_KEY` in the environment or its root `.env`, start the ACP automation server:\n\n```sh\npnpm run demo:acp\n```\n\nThe [Python SDK](python/README.md) drives a bundled JSON-RPC runtime. The [examples](examples/README.md) cover the runnable headless, ACP, JSON-RPC, Code Mode, and self-referential compositions.\n\n## Why DeepSeek Harness\n\nBuilt-in capabilities cover file reading, editing, and search; shell and persistent PTY execution; reusable skills; task tracking, goals, plans, todos, and background tasks; subagents and workflows; sandboxing and approvals; settings and credentials; persistent, resumable, forkable, and queryable sessions; LSP and web access; context compaction; and telemetry. Each composition selects the subset appropriate to its surface. The Web UI includes Plan Mode.\n\n- **Everything is a plugin.** Models, tools, policies, storage, context management, and interfaces are composable [Cordis plugins](docs/user/develop/basic/index.md), so deployments can extend or replace behavior without forking the agent loop. See the [architecture](docs/architecture.md) for the underlying design.\n- **Runs are reconstructable.** Anything visible to the model is logged in the authoritative session stream; persistence, resume/fork/query, replay, telemetry, and UIs derive from the same events. See the [session-log architecture](docs/architecture.md#session-log).\n- **Code Mode (opt-in).** It exposes a `run_code` tool and a generated TypeScript SDK; only program output re-enters model context. See [Code Mode](packages/core/tools/README.md#code-mode).\n- **Self-referential Cordis tools are opt-in.** They let the agent inspect its live runtime and mount or unmount plugins while it runs. See the [Cordis tools](packages/self-modification/tool-cordis/README.md).\n\n## Community\n\nFollow <a href=\"https://x.com/Deepseekharness\">DeepSeek Harness on Twitter</a> for project updates.\n\n## Development\n\nStart with the [development guide](docs/development.md) and read the [architecture](docs/architecture.md) before changing packages.\n\nFor agents, follow [AGENTS.md](AGENTS.md).\n\nDeepSeek Harness is currently in internal testing.\n\n## License\n\n[BSD 3-Clause](LICENSE)\n\nThird-party dependencies and their licenses are disclosed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).\n"
},
{
"role": "assistant",
"content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 coding agent(智能体)。\n\n它采用了**一切皆插件**的架构。\n\n## 内测声明\n\nDeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化。\n\n为帮助诊断上报的问题,内测版本默认上传所有会话日志。设置 `DSH_TELEMETRY_DISABLED=1` 可关闭遥测。请通过内部企业微信群反馈问题和建议。\n\n## 安装\n\n克隆仓库,然后运行安装器:\n\n```sh\ngit clone <repo-url>\ncd deepseek-harness\nscripts/install.sh\n```\n\n安装器要求系统已安装 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时可代为安装,并会提示输入 DeepSeek API 密钥,然后构建所需的仓库产物并启动 Web UI。\n\n默认生效的检出位于 `~/.dsh/source/current`,启动器链接到 `~/.local/bin`。再次运行安装器即可更新。其他位置、更新机制和恢复选项由 [`scripts/install.sh`](scripts/install.sh) 负责。\n\n## 使用 DeepSeek Harness\n\n### Web UI\n\n推荐在本地使用 Web UI;安装结束时,选择 Web UI 即可。以后需要启动时,或更新当前生效的检出后,请构建仓库并运行:\n\n```sh\n(cd ~/.dsh/source/current && pnpm run build)\ndsh web\n```\n\n上述路径是安装器的默认位置。如果你设置过 `DSH_SOURCE` 或 `DSH_CURRENT`,或者复用了已有检出,请把 `~/.dsh/source/current` 换成该检出路径;详情见 [`scripts/install.sh`](scripts/install.sh)。Web UI 默认通过 `http://127.0.0.1:3080` 提供服务。\n\n### Profile\n\n`dsh` 启动 profile:按序叠放的插件组合包 patch 层,之上再叠加你在 `$DSH_HOME/profiles/<name>` 中的自有覆盖层:\n\n```sh\ndsh --profile web # the browser UI (same as: dsh web)\ndsh plugin --profile tui add <package> # install a plugin into a custom profile\ndsh --profile tui # boot it\n```\n\nprofile 布局、层语义与配置输出命令详见 [CLI(命令行界面)参考](apps/cli/README.md#profiles)。\n\n### Headless\n\n运行一项任务,打印最终答案后退出:\n\n```sh\ndsh run \"summarize this workspace\"\n```\n\n### 自动化与 SDK\n\n在源码检出中通过环境变量或根目录 `.env` 设置 `DEEPSEEK_API_KEY`,然后启动 ACPAgent Client Protocol)自动化服务器:\n\n```sh\npnpm run demo:acp\n```\n\n[Python SDK](python/README.md) 驱动随附的 JSON-RPC 运行时。[示例](examples/README.md)涵盖可运行的 headless、ACP、JSON-RPC、Code Mode 和自指组合。\n\n## 为什么选择 DeepSeek Harness\n\n内置功能涵盖文件读取、编辑与搜索、shell 和持久 PTY 执行、可复用 skill(技能)、任务跟踪、目标、计划、待办事项与后台任务、subagent 与工作流、沙箱与审批、设置与凭据、可持久化、恢复、fork 与查询的会话、LSP 与 Web 访问、上下文压缩(context compaction),以及遥测。每个组合只选用适合其使用方式的能力子集。Web UI 包含 Plan Mode。\n\n- **一切皆插件。** 模型、工具、策略、存储、上下文管理和界面均为可组合的 [Cordis 插件](docs/user/develop/basic/index.md),部署方无需 fork agent loop(智能体循环)即可扩展或替换行为。底层设计见[架构文档](docs/architecture.md)。\n- **运行可重建。** 凡是模型可见的内容,都会记录在权威会话流中;持久化、恢复/fork/查询、回放、遥测和 UI 均从同一组事件派生。参见[会话日志架构](docs/architecture.md#session-log)。\n- **Code Mode(需显式启用)。** 它会提供 `run_code` 工具和生成的 TypeScript SDK,只有程序输出会重新进入模型上下文。参见 [Code Mode](packages/core/tools/README.md#code-mode)。\n- **自指 Cordis 工具需显式启用。** 这些工具可让 agent 检查自身的实时运行时,并在运行中挂载或卸载插件。参见 [Cordis 工具](packages/self-modification/tool-cordis/README.md)。\n\n## 社区\n\n扫描二维码,或打开 <a href=\"https://wj.qq.com/s2/27234598/03eb/\">DeepSeek Harness 微信社区申请页面</a> 申请加入。\n\n<p>\n <img src=\"assets/community-wecom-survey.png\" alt=\"DeepSeek Harness 微信社区二维码\" width=\"240\">\n</p>\n\n## 开发\n\n请先阅读[开发指南](docs/development.md);修改包之前,请阅读[架构文档](docs/architecture.md)。\n\n面向 agent:遵循 [AGENTS.md](AGENTS.md)。\n\nDeepSeek Harness 目前处于内测阶段。\n\n## 许可证\n\n[BSD 3-Clause](LICENSE)\n\n第三方依赖及其许可证在 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) 中披露。\n"
"content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 coding agent(智能体)。\n\n它采用了**一切皆插件**的架构。\n\n## 内测声明\n\nDeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化。\n\n为帮助诊断上报的问题,内测版本默认上传所有会话日志。设置 `DSH_TELEMETRY_DISABLED=1` 可关闭遥测。请通过内部企业微信群反馈问题和建议。\n\n## 安装\n\n克隆仓库,然后运行安装器:\n\n```sh\ngit clone <repo-url>\ncd deepseek-harness\nscripts/install.sh\n```\n\n安装器要求系统已安装 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时可代为安装,并会提示输入 DeepSeek API 密钥,然后构建所需的仓库产物并启动 Web UI。\n\n默认生效的检出位于 `~/.dsh/source/current`,启动器链接到 `~/.local/bin`。再次运行安装器即可更新。其他位置、更新机制和恢复选项由 [`scripts/install.sh`](scripts/install.sh) 负责。\n\n## 使用 DeepSeek Harness\n\n### Web UI\n\n推荐在本地使用 Web UI;安装结束时,选择 Web UI 即可。以后需要启动时,或更新当前生效的检出后,请构建仓库并运行:\n\n```sh\n(cd ~/.dsh/source/current && pnpm run build)\ndsh web\n```\n\n上述路径是安装器的默认位置。如果你设置过 `DSH_SOURCE` 或 `DSH_CURRENT`,或者复用了已有检出,请把 `~/.dsh/source/current` 换成该检出路径;详情见 [`scripts/install.sh`](scripts/install.sh)。Web UI 默认通过 `http://127.0.0.1:3080` 提供服务。\n\n### Profile\n\n`dsh` 启动 profile:按序叠放的插件组合包 patch 层,之上再叠加你在 `$DSH_HOME/profiles/<name>` 中的自有覆盖层:\n\n```sh\ndsh --profile web # the browser UI (same as: dsh web)\ndsh plugin --profile tui add <package> # install a plugin into a custom profile\ndsh --profile tui # boot it\n```\n\nprofile 布局、层语义与配置输出命令详见 [CLI(命令行界面)参考](apps/cli/README.md#profiles)。\n\n### Headless\n\n运行一项任务,打印最终答案后退出:\n\n```sh\ndsh --profile headless \"summarize this workspace\"\n```\n\n### 自动化与 SDK\n\n在源码检出中通过环境变量或根目录 `.env` 设置 `DEEPSEEK_API_KEY`,然后启动 ACPAgent Client Protocol)自动化服务器:\n\n```sh\npnpm run demo:acp\n```\n\n[Python SDK](python/README.md) 驱动随附的 JSON-RPC 运行时。[示例](examples/README.md)涵盖可运行的 headless、ACP、JSON-RPC、Code Mode 和自指组合。\n\n## 为什么选择 DeepSeek Harness\n\n内置功能涵盖文件读取、编辑与搜索、shell 和持久 PTY 执行、可复用 skill(技能)、任务跟踪、目标、计划、待办事项与后台任务、subagent 与工作流、沙箱与审批、设置与凭据、可持久化、恢复、fork 与查询的会话、LSP 与 Web 访问、上下文压缩(context compaction),以及遥测。每个组合只选用适合其使用方式的能力子集。Web UI 包含 Plan Mode。\n\n- **一切皆插件。** 模型、工具、策略、存储、上下文管理和界面均为可组合的 [Cordis 插件](docs/user/develop/basic/index.md),部署方无需 fork agent loop(智能体循环)即可扩展或替换行为。底层设计见[架构文档](docs/architecture.md)。\n- **运行可重建。** 凡是模型可见的内容,都会记录在权威会话流中;持久化、恢复/fork/查询、回放、遥测和 UI 均从同一组事件派生。参见[会话日志架构](docs/architecture.md#session-log)。\n- **Code Mode(需显式启用)。** 它会提供 `run_code` 工具和生成的 TypeScript SDK,只有程序输出会重新进入模型上下文。参见 [Code Mode](packages/core/tools/README.md#code-mode)。\n- **自指 Cordis 工具需显式启用。** 这些工具可让 agent 检查自身的实时运行时,并在运行中挂载或卸载插件。参见 [Cordis 工具](packages/self-modification/tool-cordis/README.md)。\n\n## 社区\n\n扫描二维码,或打开 <a href=\"https://wj.qq.com/s2/27234598/03eb/\">DeepSeek Harness 微信社区申请页面</a> 申请加入。\n\n<p>\n <img src=\"assets/community-wecom-survey.png\" alt=\"DeepSeek Harness 微信社区二维码\" width=\"240\">\n</p>\n\n## 开发\n\n请先阅读[开发指南](docs/development.md);修改包之前,请阅读[架构文档](docs/architecture.md)。\n\n面向 agent:遵循 [AGENTS.md](AGENTS.md)。\n\nDeepSeek Harness 目前处于内测阶段。\n\n## 许可证\n\n[BSD 3-Clause](LICENSE)\n\n第三方依赖及其许可证在 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) 中披露。\n"
},
{
"role": "user",
+21 -62
View File
@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import { Context, FiberState, Service, ValidationError } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import z from 'schemastery'
import { Context, FiberState, Service, ValidationError } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import z from '@deepseek-ai/schemastery'
import InvariantService from '@deepseek-ai/dsh-invariants'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import { packageInvariantOwners } from './package-invariants.ts'
@@ -13,7 +13,7 @@ import {
usesManualInvariantTree,
} from './test-invariants.ts'
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
testInvariantProbe: TestInvariantProbe
}
@@ -39,18 +39,6 @@ function requiredConfig() {
})
}
function queuedReadinessConfig(
ctx: Context,
onPublished: (dispose: () => void) => void,
) {
return z.transform(z.any(), () => {
queueMicrotask(() => {
onPublished(ctx.provide(TEST_INVARIANT_READY_SERVICE, true))
})
return {}
}, true)
}
function invalidConfigApply(): never {
throw new Error('invalid plugin apply executed')
}
@@ -189,84 +177,55 @@ describe('global test invariant host', () => {
expect(apply).not.toHaveBeenCalled()
})
it('disposes invalid config when readiness refresh wins the rejection-handler race', async () => {
it('disposes invalid config after delayed invariant readiness', async () => {
await withDelayedFirstCompanion(
async ({ started, release }) => {
const ctx = new Context()
const apply = vi.fn(invalidConfigApply)
let disposeQueuedReadiness: (() => void) | undefined
const plugin = {
apply,
Config: z.intersect([
queuedReadinessConfig(ctx, (dispose) => {
disposeQueuedReadiness = dispose
}),
requiredConfig(),
]),
Config: requiredConfig(),
}
const fiber = ctx.plugin(plugin, {})
const firstError = await rejectionOf(fiber)
expectRequiredConfigValidation(firstError)
expect(fiber.state).toBe(FiberState.DISPOSED)
const returnedError = rejectionOf(fiber)
await started
expect(fiber.state).toBe(FiberState.PENDING)
expect(apply).not.toHaveBeenCalled()
await started
if (disposeQueuedReadiness === undefined) throw new Error('queued readiness was not published')
disposeQueuedReadiness()
release()
await ctx.plugin(TestInvariantProbe)
const secondError = await rejectionOf(fiber)
expect(secondError).toBe(firstError)
expectRequiredConfigValidation(await returnedError)
expect(fiber.state).toBe(FiberState.DISPOSED)
expect(apply).not.toHaveBeenCalled()
},
)
})
it('retains a valid plugin failure when readiness wins the initial-probe race', async () => {
it('retains a valid plugin failure after delayed invariant readiness', async () => {
await withDelayedFirstCompanion(
async ({ started, release }) => {
const ctx = new Context()
const failure = new Error('valid plugin apply failed')
const applied = deferred()
const apply = vi.fn(function validConfigApply() {
applied.resolve()
throw failure
})
let disposeQueuedReadiness: (() => void) | undefined
const plugin = {
apply,
Config: queuedReadinessConfig(ctx, (dispose) => {
disposeQueuedReadiness = dispose
}),
Config: z.object({}),
}
const fiber = ctx.plugin(plugin, {})
const returnedError = rejectionOf(fiber)
try {
await Promise.all([started, applied.promise])
expect(fiber.state).toBe(FiberState.FAILED)
expect(apply).toHaveBeenCalledOnce()
expect(ctx.registry.has(plugin)).toBe(true)
expect(ctx.registry.get(plugin)?.fibers).toHaveLength(1)
await started
expect(fiber.state).toBe(FiberState.PENDING)
expect(apply).not.toHaveBeenCalled()
if (disposeQueuedReadiness === undefined) throw new Error('queued readiness was not published')
Reflect.deleteProperty(fiber.inject, TEST_INVARIANT_READY_SERVICE)
disposeQueuedReadiness()
release()
expect(await returnedError).toBe(failure)
expect(fiber.state).toBe(FiberState.FAILED)
expect(apply).toHaveBeenCalledOnce()
expect(ctx.registry.has(plugin)).toBe(true)
expect(ctx.registry.get(plugin)?.fibers).toHaveLength(1)
} finally {
Reflect.deleteProperty(fiber.inject, TEST_INVARIANT_READY_SERVICE)
disposeQueuedReadiness?.()
release()
}
release()
expect(await returnedError).toBe(failure)
expect(fiber.state).toBe(FiberState.FAILED)
expect(apply).toHaveBeenCalledOnce()
expect(ctx.registry.has(plugin)).toBe(true)
expect(ctx.registry.get(plugin)?.fibers).toHaveLength(1)
},
)
})
+15 -12
View File
@@ -6,8 +6,8 @@
*/
import { expect } from 'vitest'
import { FiberState, Inject, RegistryService } from 'cordis'
import type { Context, Plugin } from 'cordis'
import { FiberState, Inject, RegistryService, ValidationError } from '@deepseek-ai/cordis'
import type { Context, Plugin } from '@deepseek-ai/cordis'
import { AttachmentStore } from '@deepseek-ai/dsh-attachment'
import type {
ImageAttachmentLimits,
@@ -248,22 +248,25 @@ function withInvariantReadiness(plugin: Plugin, callback: PluginCallback): Plugi
function joinInvariantStartup(
fiber: PluginFiber,
invariantReady: Promise<void>,
disposeInitialFailure = false,
disposePendingValidationFailure = false,
): PluginFiber {
// RegistryService returns a thenable wrapper whose context still points to
// the raw Fiber. Calling inherited await() on the wrapper would return and
// assimilate that thenable, accidentally following later plugin startup.
const rawFiber = fiber.ctx.fiber
const initialized = disposeInitialFailure
? rawFiber.await().catch(async (error: unknown) => {
// Config validation is the only failure recorded while a gated fiber
// is initially PENDING. Dispose it even if queued readiness publication
// changes its state before this rejection handler runs.
await rawFiber.dispose()
const readiness = invariantReady.then(async () => {
try {
return await rawFiber.await()
} catch (error) {
// Config resolves only after the readiness injection activates. Dispose
// validation failures owned by an initially pending target; ordinary
// callback failures remain inspectable.
if (disposePendingValidationFailure && error instanceof ValidationError) {
await rawFiber.dispose()
}
throw error
})
: Promise.resolve()
const readiness = initialized.then(() => invariantReady).then(() => rawFiber.await())
}
})
const joined = Object.create(fiber) as PluginFiber
joined.then = readiness.then.bind(readiness)
return joined
+2 -2
View File
@@ -165,7 +165,7 @@ function validateEntry(value: unknown, file: string, path: string): void {
}
recordPlugin(value, file)
validateMetadata(value, file, path)
if ((value.group === true || value.name === '@cordisjs/plugin-group') && isUnknownArray(value.config)) {
if ((value.group === true || value.name === '@deepseek-ai/cordis-plugin-group') && isUnknownArray(value.config)) {
for (let index = 0; index < value.config.length; index++) {
validateEntry(value.config[index], file, `${path}.config[${index}]`)
}
@@ -175,7 +175,7 @@ function validateEntry(value: unknown, file: string, path: string): void {
validateEntry(value.insert[index], file, `${path}.insert[${index}]`)
}
}
if (value.name !== '@cordisjs/plugin-include') return
if (value.name !== '@deepseek-ai/cordis-plugin-include') return
const config = value.config
if (!isRecord(config) || !isUnknownArray(config.patches)) return
for (let index = 0; index < config.patches.length; index++) {
@@ -133,7 +133,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/skill/skill-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-skill.' },
'packages/spill/spill': { kind: 'indirect', reason: 'The storage seam delegates model rendering to spill consumers.' },
'packages/spill/spill-local': { kind: 'indirect', reason: 'The storage backend delegates model rendering to spill consumers.' },
'packages/subagent/subagent': { kind: 'indirect', reason: 'The provider registry delegates parent-model rendering to dsh-tool-subagent.' },
'packages/support/acp-snapshot': { kind: 'none', reason: 'The test harness observes and normalizes transcripts without changing live requests.' },
'packages/support/agent-loop-testkit': { kind: 'none', reason: 'The test helper mounts services but neither drives nor modifies model requests.' },
'packages/support/invariants': { kind: 'none', reason: 'The observer validates requests but never rewrites their context.' },
@@ -147,6 +146,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/tasks/tasks-local': { kind: 'indirect', reason: 'The registry backend delegates model rendering to producer plugins and dsh-tool-tasks.' },
'packages/examples/acp-demo': { kind: 'indirect', reason: 'The app bundle delegates request composition to dsh-agent-spine-demo and dsh-acp.' },
'packages/boot/app-boot': { kind: 'indirect', reason: 'Only the loaded plugin tree contributes model context.' },
'packages/boot/cmdline': { kind: 'none', reason: 'Resolves the process command line before any session exists; configured rows own every model-visible consequence.' },
'packages/examples/jsonrpc-demo': { kind: 'indirect', reason: 'Only the externally configured plugin tree contributes model context.' },
'packages/interaction/permission': { kind: 'indirect', reason: 'The service writes mechanism events rendered by dsh-user-approval and dsh-tool-bash.' },
'packages/interaction/user-interaction': { kind: 'indirect', reason: 'Model-facing consumers render provider answers and seam errors.' },