Merge remote-tracking branch 'origin/master' into worktree/web-background-tasks-display-258f7e

# Conflicts:
#	docs/subsystems/tasks.i18n.yaml
#	docs/subsystems/tasks.md
#	docs/subsystems/tasks.zh.md
#	packages/host/apiproxy/README.i18n.yaml
#	packages/host/apiproxy/README.md
#	packages/host/apiproxy/README.zh.md
#	packages/host/apiproxy/src/api-proxy.ts
#	packages/tasks/tasks-local/src/index.ts
#	packages/tasks/tasks/README.i18n.yaml
#	packages/tasks/tasks/README.md
#	packages/tasks/tasks/README.zh.md
#	packages/tasks/tasks/src/index.ts
This commit is contained in:
Yichen Jiang
2026-08-11 11:57:33 +08:00
2697 changed files with 39978 additions and 18500 deletions

View File

@@ -1,3 +1,3 @@
# AGENTS.md — Repository scripts
Gate scripts invoke pnpm shell-free, normalize repository-relative glob paths to `/` at ingestion, and keep platform adaptation at the owning gate boundary instead of a shared platform layer.
Gate scripts invoke pnpm shell-free, normalize repository-relative glob paths to `/` at ingestion, and keep platform adaptation in the gate that needs it instead of a shared platform layer.

View File

@@ -4,7 +4,7 @@ import { createHash } from 'node:crypto'
import { basename } from 'node:path'
import { AGENT_NOTE_CLASSES } from './agent-note-tree.ts'
/** Versioned shape of the frozen-content manifest. */
/** Versioned fields in the frozen-content manifest. */
export interface ArchiveManifest {
version: 1
files: Readonly<Record<string, string>>

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 } }

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}")

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),
]

View File

@@ -113,20 +113,42 @@ describe('E2B e2e workflow', () => {
})
describe('Issue lifecycle workflow', () => {
it('uses review signals instead of rerunning when a draft becomes ready', () => {
it('uses explicit review handoff events without rerunning when a draft becomes ready', () => {
const lifecycle = loadWorkflow('.github/workflows/issue-lifecycle.yml')
const lifecyclePullRequest = workflowEvent(lifecycle, 'pull_request')
const lifecycleReview = workflowEvent(lifecycle, 'pull_request_review')
const lifecycleJob = workflowJob(lifecycle, 'lifecycle')
const policy = loadWorkflow('.github/workflows/issue-policy.yml')
const policyPullRequest = workflowEvent(policy, 'pull_request')
expect(lifecyclePullRequest.types).not.toContain('ready_for_review')
expect(lifecyclePullRequest.types).toContain('review_requested')
expect(lifecycleReview.types).toContain('submitted')
expect(lifecycleReview.types).toEqual(['submitted'])
expect(lifecycleJob.if).toBe(
"${{ github.event_name != 'pull_request_review' || (github.event.action == 'submitted' && github.event.review.state == 'changes_requested') }}",
)
expect(policyPullRequest.types).toContain('ready_for_review')
})
})
describe('Git hooks', () => {
it('leaves frozen Agent Note sidecars to the archive verifier', () => {
const lefthook = loadWorkflow('lefthook.yml')
for (const hookName of ['pre-commit', 'pre-merge-commit']) {
const hook = lefthook[hookName]
if (!isRecord(hook) || !Array.isArray(hook.jobs)) {
throw new TypeError(`lefthook must define ${hookName} jobs`)
}
const pairing: unknown = hook.jobs.find(
(job: unknown) => isRecord(job) && job.name === 'translation pairing (staged records)',
)
expect(pairing).toMatchObject({ exclude: ['.agents/notes/archived/**'] })
}
})
})
function loadWorkflow(path: string): Record<string, unknown> {
const workflow: unknown = yaml.load(readFileSync(resolve(root, path), 'utf8'))
if (!isRecord(workflow)) throw new TypeError(`${path} must define a workflow`)
@@ -140,6 +162,13 @@ function workflowEvent(workflow: Record<string, unknown>, event: string): Record
return workflow.on[event]
}
function workflowJob(workflow: Record<string, unknown>, job: string): Record<string, unknown> {
if (!isRecord(workflow.jobs) || !isRecord(workflow.jobs[job])) {
throw new TypeError(`workflow must define the ${job} job`)
}
return workflow.jobs[job]
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}

View File

@@ -1,5 +1,5 @@
/**
* Pins shared client-bundle preset contracts: the module-edge purity gate and
* Pins shared client-bundle preset rules: the module-edge purity gate and
* the physical watch dependencies hidden behind virtual CSS Modules.
*/
import { fileURLToPath } from 'node:url'
@@ -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'])
})
})

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,9 +79,9 @@ 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 shape the projector
* from method and property members alike so a declaration form the projector
* would reject still enters the exhaustiveness scan.
* @param body - The cordis module augmentation block.
* @param sf - Owning source file (for computed-name text extraction).

View File

@@ -1,6 +1,6 @@
/**
* Heavy suites the coverage aggregate runs uninstrumented in a parallel gate.
* Membership contract: a suite qualifies only when every coverage-measured
* Membership rule: a suite qualifies only when every coverage-measured
* file it executes in-process (`coverage.include` spans package src trees;
* typert generator src is threshold-excluded in vitest.config.ts) is already
* fully covered by other suites, so removing it from the instrumented run

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-'))

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 ')}`,
)
}

View File

@@ -1,6 +1,6 @@
/** Map one workspace source alias target to its declaration-build target. */
export function builtDeclarationPath(candidate: string): string {
// Two workspace shapes exist: whole-package entries end in /src, subpath
// Two workspace path forms exist: whole-package entries end in /src, subpath
// wildcards (apiproxy's browser-safe /api and /client channels) in /src/*.
if (candidate.endsWith('/src')) {
return `${candidate.slice(0, -'/src'.length)}/lib/types`

View File

@@ -221,7 +221,7 @@ const { primary: all, derivatives } = partitionPairedMarkdownDerivatives(
const checked = all.filter(b => b.kind === 'check')
const ignored = all.filter(b => b.kind === 'ignore')
// Only compile-eligible fences belong in the opt-out ratio; every other skipped
// kind has an independent verifier named in BlockKind's contract above.
// kind has an independent verifier named in the BlockKind rules above.
const ratioDenominator = checked.length + ignored.length
if (checked.length === 0) {

View File

@@ -2,7 +2,7 @@
* Generate `docs/config-catalog.md` from package entry points, config types,
* JSDoc, and static Schemastery schemas. Every package must classify, referenced
* types must resolve without collisions, and every enumerable schema path must
* exist on the declared config type. External and dynamic shapes stay unknown;
* exist on the declared config type. External and dynamic types stay unknown;
* declared runtime-only fields need not appear in the schema. `--check` verifies
* the committed artifact.
*/
@@ -220,7 +220,7 @@ interface World {
}
/** How a schema key path fared against the declared config type: definitely
* present, definitely absent, or crossing a shape the walk cannot enumerate
* present, definitely absent, or crossing a type the walk cannot enumerate
* (only `missing` is a violation — `unknown` must never mis-report). */
type PathLookup = 'found' | 'missing' | 'unknown'
@@ -307,7 +307,7 @@ const PASSTHROUGH_WRAPPERS = new Set(['Partial', 'Required', 'Readonly', 'NonNul
/**
* Walk a schema key path against a declared type. This is a PRESENCE check,
* not a shape check: it answers "does the declared config type have a member
* not a runtime value check: it answers "does the declared config type have a member
* here", resolving interfaces (heritage included), type aliases, literals,
* intersections, unions, arrays, indexed access, pass-through utility
* wrappers, and type references across package-local and workspace imports.
@@ -412,7 +412,7 @@ function unwrapExpr(expr: ts.Expression): ts.Expression {
* Statically walk a schemastery schema expression to its key paths plus the
* packages whose schemas an intersect composes. A key path is the top-level
* key or a nested path through object/array compositions (`agents[].id`).
* Handles the shapes the repo declares — `z.object({…})` (possibly behind
* Handles the declaration forms the repo uses — `z.object({…})` (possibly behind
* chained calls) and `z.intersect([X.Config, …])` — and hard-errors on
* anything else, so a schema the walk cannot see fails the gate instead of
* silently thinning it. Nested values that are neither `object` nor `array`
@@ -521,7 +521,7 @@ function findSchemaExpr(ctx: FileCtx, pluginClass: ts.ClassDeclaration | null):
function findInject(ctx: FileCtx, pluginClass: ts.ClassDeclaration | null, violations: string[]): string[] {
const fromArray = (expr: ts.Expression, where: string): string[] => {
if (!ts.isArrayLiteralExpression(expr)) {
violations.push(`${where}: inject is not a plain string-array literal; teach the generator the new shape.`)
violations.push(`${where}: inject is not a plain string-array literal; teach the generator the new declaration form.`)
return []
}
return expr.elements.map(el => ts.isStringLiteral(el) ? el.text : el.getText(ctx.sf))
@@ -727,7 +727,7 @@ export function collectConfigCatalog(scanRoot: string = root): CatalogEntry[] {
}
// Fold composed schemas' key paths in, then check each path against the type.
// Only a definite miss fails; shapes the walk cannot enumerate stay unknown.
// Only a definite miss fails; types the walk cannot enumerate stay unknown.
const byName = new Map(entries.map(e => [e.pkg, e]))
for (const entry of entries) {
if (entry.kind !== 'config' || entry.schemaKeys === null || entry.schemaKeys === undefined) continue

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',

View File

@@ -48,6 +48,7 @@ export const SERVICE_PAGE: Record<string, string> = {
agentPresets: 'core.md',
agents: 'core.md',
approval: 'approval.md',
attachments: 'attachment.md',
bash: 'bash.md',
bashEnv: 'bash.md',
clientModuleHost: 'client-modules.md',
@@ -98,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}
@@ -110,16 +111,18 @@ 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',
configuredAgentIdentities: 'not a service: launcher-provided boot-context value (ConfiguredAgentIdentities | undefined) — packages/core/agent-loop/README.md owns the launcher contract',
launcherSessionQueryPath: 'not a service: launcher-provided boot-context value (string | undefined) — packages/session-query/session-query-sqlite/README.md owns the launcher contract',
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',
headlessIo: 'not a service: launcher-provided root accessor value (HeadlessIo | undefined) for the headless bundle runner — packages/bundle/headless/README.md owns the launcher contract',
launcherEnvironment: 'not a service: launcher-provided root accessor value (EnvironmentSnapshot | undefined) — packages/util/environment/README.md owns the launcher contract',
headlessIo: 'not a service: launcher-provided root accessor value (HeadlessIo | undefined) for the headless bundle runner — packages/bundle/headless/README.md owns this launcher contract',
launcherEnvironment: 'not a service: launcher-provided root accessor value (EnvironmentSnapshot | undefined) — packages/util/environment/README.md owns this launcher contract',
lsp: 'interface-typed (LspService); implementing class Lsp is not the declared type name — packages/lsp/lsp/README.md owns the surface',
apiProxy: 'interface-typed (ApiProxy) with the class in api-proxy.ts, not index.ts — packages/host/apiproxy/README.md owns the surface',
appShell: 'client-side interface-typed browser service — packages/client/web/README.md owns the surface',
connection: 'client-side interface-typed browser service — packages/client/connection/README.md owns the surface',
chatFileMentions: 'client-side slot-contract accessor (ChatFileMentions) — packages/client/ui-conversation/README.md owns the surface',
chatFileMentions: 'client-side slot-contract accessor (ChatFileMentions) — packages/client/ui-conversation/README.md owns the API',
command: 'client-side interface-typed browser service — packages/client/ui-command/README.md owns the surface',
conversation: 'client-side interface-typed browser service — packages/client/ui-conversation/README.md owns the surface',
conversationEvents: 'client-side interface-typed registry — packages/client/runtime/README.md owns the surface',
@@ -129,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',
@@ -167,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
@@ -180,6 +182,7 @@ export const EVENT_WALK_EXEMPTIONS: Record<string, string> = {
'credentials/changed': 'client-face registry invalidation signal — packages/client/runtime/README.md owns the surface',
'locale/change': 'client-face locale switch signal — packages/client/locale/README.md owns the surface',
'models/changed': 'client-face registry invalidation signal — packages/client/runtime/README.md owns the surface',
'session/preset-changed': 'client-face per-session catalog invalidation signal — packages/client/runtime/README.md owns the surface',
'settings/changed': 'client-face registry invalidation signal — packages/client/runtime/README.md owns the surface',
'slash/input-begin-command': 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the surface',
'slash/input-consume-token': 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the surface',
@@ -243,6 +246,9 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
ApprovalPolicy: 'approval.md',
ApprovalRequest: 'approval.md',
ApprovalService: 'approval.md',
ImageAttachmentRef: 'attachment.md',
SaveImageAttachment: 'attachment.md',
StoredImageAttachment: 'attachment.md',
BashExecRequest: 'bash.md',
BashExecSpec: 'bash.md',
BashProcess: 'bash.md',
@@ -479,13 +485,13 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
'z.core.ToJSONSchemaParams': 'zod projection parameters are owned by the zod v4 API',
TypeRTDisposer: 'TypeRT lifecycle contract is owned by packages/typert/type-meta/README.md',
InvokeRemoteRequest: 'gateway invocation contract is owned by packages/api/gateway/README.md',
LocaleDict: 'service-local dictionary shape is owned by packages/client/i18n/src/index.ts',
LocaleDict: 'service-local dictionary fields are owned by packages/client/i18n/src/index.ts',
ThemeTokens: 'service-local token dictionary is owned by packages/client/ui-theme/src/index.ts',
Translate: 'service-local bound translator is owned by packages/client/i18n/src/index.ts',
WebUpgradeRoute:
'upgrade route registration contract is owned by packages/host/webserver/src/index.ts',
InvariantRegistration: 'service-local lifecycle handle is owned by packages/support/invariants/README.md',
KnobState: 'projection unit state shape is owned by packages/interaction/permission/README.md',
KnobState: 'projection unit state fields are owned by packages/interaction/permission/README.md',
PermissionSelect: 'permissions projection payload is owned by packages/interaction/permission/src/types.ts',
PromptAssembly: 'assembly result is owned by packages/core/system-prompt/README.md',
Sandbox: 'external E2B SDK handle is owned by packages/e2b/e2b/README.md',
@@ -748,7 +754,7 @@ export function maybeRecordPair(pageRel: string, before: Map<string, Buffer>, sc
// after review, never silently by regeneration.
return false
}
// The record must be exactly the well-formed two-entry shape for THIS pair;
// The record must contain exactly the two valid entries for THIS pair;
// a malformed or renamed-key sidecar is the pairing gate's problem to
// report, never something regeneration silently repairs into validity.
const recorded = parsePairMeta(meta)

View File

@@ -62,6 +62,7 @@ type EventReceiverKind = 'context' | 'agent-dispatch' | 'events-service'
const GROUP_ORDER = [
'util',
'attachment',
'llm',
'core',
'typert',
@@ -95,6 +96,15 @@ const GROUP_ORDER = [
]
const SERVICE_ROLES: ServiceRole[] = [
{
key: 'attachments',
pkg: 'attachment',
title: 'Durable binary attachment storage',
mode: 'seam',
implementations: ['attachment-local'],
consumers: ['host-runtime', 'llm-pi-ai'],
note: 'The host commits accepted images before session events; provider adapters resolve authorized durable references into provider-native content.',
},
{
key: 'llm',
pkg: 'llm',
@@ -258,7 +268,7 @@ const SERVICE_ROLES: ServiceRole[] = [
title: 'Human question/answer seam',
mode: 'seam',
consumers: ['tool-ask-user'],
note: 'UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise.',
note: 'UI front ends provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise.',
},
{
key: 'planMode',
@@ -320,7 +330,7 @@ const SERVICE_ROLES: ServiceRole[] = [
title: 'Default Agent model selection',
mode: 'core',
consumers: ['headless', 'host-apiproxy'],
note: 'Layers the default ModelSelection through settings so direct and Host-backed Agent front doors share one state owner.',
note: 'Layers the default ModelSelection through settings so direct and Host-backed Agent entry points share one state owner.',
},
{
key: 'agentLoop',
@@ -502,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',
@@ -511,7 +521,7 @@ const SERVICE_ROLES: ServiceRole[] = [
mode: 'seam',
implementations: ['workflow-workerthread'],
consumers: ['tool-workflow', 'tool-ralph'],
note: 'One engine per context (bash shape, no named-provider registry); the general workflow and fixed Ralph consumers start runs whose agent() calls fan out through ctx.subagents.',
note: 'One engine per context, as in bash, with no named-provider registry; the general workflow and fixed Ralph consumers start runs whose agent() calls fan out through ctx.subagents.',
},
]
@@ -682,7 +692,7 @@ function renderAppExpansion(lines: string[], appNode: string, pluginName: string
lines.push(` ${appNode} --> ${agentCore}["@deepseek-ai/dsh-agent-spine-demo"]`)
lines.push(` ${appNode} --> ${jsonl}["@deepseek-ai/dsh-session-persistence-jsonl"]`)
if (pluginName === '@deepseek-ai/dsh-acp-demo') {
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'acp')}["@deepseek-ai/dsh-acp<br/>automation-only JSON-RPC stdio<br/>fresh sessions created by client"]`)
lines.push(` ${appNode} --> ${nodeId('entrypoint', 'acp')}["@deepseek-ai/dsh-acp<br/>automation-only JSON-RPC stdio<br/>fresh sessions created by client"]`)
}
lines.push(
` ${agentCore} --> ${nodeId('spine', 'llm')}["ctx.llm"]`,
@@ -734,7 +744,18 @@ type CallSiteIndex = Map<ts.SignatureDeclaration | ts.JSDocSignature, ts.CallExp
*/
const EVENT_API_METHODS = new Set(['on', 'once', 'emit', 'parallel', 'serial', 'waterfall', 'dispatch'])
/** Collect event dispatch/listener relations from real cross-file receiver types. */
/**
* Collect event dispatch/listener relations from real cross-file receiver types.
*
* TODO: the program is seeded from the host aggregate alone (ts-project.ts
* documents why: one program cannot hold both faces' Context merges), so a
* Client package enters only when a host file imports it. Client-face
* listeners on client-face events are therefore under-reported —
* `connection/reset` omits `ui-skill`/`ui-agent-preset`, `models/changed`
* omits `ui-model`, `session/preset-changed` omits `ui-skill`. Closing it
* needs a second Client program whose relations merge into these, not a
* wider seed.
*/
export class EventRelationCollector {
private readonly relations = new Map<string, EventRelation>()
private readonly fileCallSites = new Map<ts.SourceFile, CallSiteIndex>()
@@ -794,7 +815,7 @@ export class EventRelationCollector {
* Return every indexed call resolving to one local helper declaration.
* Fast path: when every same-file reference to the non-exported helper is
* provably a direct callee, module scoping confines all of its calls to that
* file, so only that file is indexed. Any other reference shape may alias
* file, so only that file is indexed. Any other reference form may alias
* the function value outward, so the original full package-source index
* decides instead.
*/
@@ -1134,7 +1155,7 @@ function renderEventRelations(pkgs: Pkg[], events: readonly EventEntry[]): strin
lines.push(`| \`${event.name}\` | \`${event.mode}\` | ${sourceLink(event.source)} | ${relationPackages(relation.dispatchers, pkgsByShort)} | ${listenerPackages(relation.listeners, pkgsByShort)} |`)
}
// Every declared event needs a dispatcher: zero means dead vocabulary or an
// unrecognized semantic dispatch shape. Listener-free extension points remain
// unrecognized semantic dispatch form. Listener-free extension points remain
// valid. Client-declared events are exempt: the relation scan seeds the HOST
// aggregate program only (host+client cannot share one program — the cordis
// Context merges collide), so client dispatch sites are structurally
@@ -1147,8 +1168,8 @@ function renderEventRelations(pkgs: Pkg[], events: readonly EventEntry[]): strin
if (undispatched.length > 0) {
throw new Error(
`event-producer-consumer matrix: no dispatcher found for declared event${undispatched.length > 1 ? 's' : ''} `
+ `${undispatched.map(name => `"${name}"`).join(', ')} — dead vocabulary, or a dispatch shape the semantic scan misses `
+ '(teach scripts/gen-doc-graphs.ts the shape)',
+ `${undispatched.map(name => `"${name}"`).join(', ')} — dead vocabulary, or a dispatch form the semantic scan misses `
+ '(teach scripts/gen-doc-graphs.ts that form)',
)
}
const declared = new Set(events.map(event => event.name))
@@ -1241,9 +1262,9 @@ function renderLifecycle(): string {
'',
'`dsh-compact-basic` uses `agent/pre-step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery works between the closed failed step and failed turn close, and opens a fresh retry turn only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.',
'',
'The returned `agent/pre-step` decision is authoritative; listeners wrapping `next()` preserve downstream messages unless replacement is intentional. Steering and injected context pass through the same waterfall after a later boundary claims their next-step batch.',
'The returned `agent/pre-step` decision is authoritative; listeners wrapping `next()` preserve downstream messages unless replacement is intentional. Steering and injected context pass through the same waterfall after a later claim operation takes their next-step batch.',
'',
'SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination surface for queue/status, prompt interception, request shaping, steering, continuation, and errors.',
'SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination surface for queue/status, prompt interception, request construction, steering, continuation, and errors.',
'',
...maintenanceFooter(maintenance),
].join('\n')
@@ -1253,7 +1274,7 @@ function renderToolPipeline(): string {
const maintenance = 'curated Mermaid flow; exact tool schemas and event signatures live in generated catalogs'
return [
...generatedHeader('Tool Execution Pipeline'),
'This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering fit without changing the loop. The transformable extension points are the `tools/pre-execute`, `tools/execute`, and `tools/post-execute` waterfalls; monotonic guards, definition-owned `finalizeContent`, and `tools/result` are the owner-enforced boundaries around them.',
'This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering run without changing the loop. The `tools/pre-execute` waterfall runs first, monotonic guards run next, and the `tools/execute` and `tools/post-execute` waterfalls follow; the three waterfalls may transform a call. Definition-owned `finalizeContent` and `tools/result` run afterward.',
'',
'```mermaid',
'flowchart TD',
@@ -1359,7 +1380,7 @@ function renderIndex(docs: GraphDoc[]): string {
const maintenance = 'mixed: each linked page declares generated, hybrid, or curated mode'
return [
...generatedHeader('Documentation Graph Index'),
'These diagrams are the relationship layer above the generated catalogs. Use them to navigate package topology, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type shapes still live in the [subsystem pages](subsystems/core.md) (types + the generated `cordis-surface` regions) and [tool-catalog.md](tool-catalog.md).',
'These diagrams show relationships that the generated catalogs do not. Use them to find package relationships, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type definitions still live in the [subsystem pages](subsystems/core.md) (types + the generated `cordis-surface` regions) and [tool-catalog.md](tool-catalog.md).',
'',
'The process decision behind this index is recorded in [the documentation graph Agent Note](../.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.md).',
'',

View File

@@ -13,6 +13,7 @@ import { parseJsDoc, pointer, rawJsDoc, reportViolations } from './jsdoc.ts'
const root = resolve(import.meta.dirname, '..')
const OUT = 'docs/persistence-catalog.md'
const OUT_RUNTIME_TYPES = 'packages/core/session/src/known-event-types.ts'
/** The fenced-block info string for generated declaration blocks (skipped by
* doc-typecheck, since their imported types are not standalone-compilable). */
@@ -359,7 +360,7 @@ export function render(events: AnnotatedLogEventEntry[], envelopeTypes: EventEnv
'',
'This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog Agent Note](../.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md).',
'',
'The envelope declarations below compose each event\'s `type`, monotonic `seq`, epoch-ms `time`, `data`, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](subsystems/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.',
'The envelope declarations below compose each event\'s `type`, monotonic `seq`, epoch-ms `time`, `data`, the optional `ignorable` unknown-type skip marker, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](subsystems/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.',
'',
'## Event envelope',
'',
@@ -382,31 +383,79 @@ export function render(events: AnnotatedLogEventEntry[], envelopeTypes: EventEnv
return lines.join('\n')
}
/** CLI entry: default writes the catalog, `--check` fails if the committed copy
/**
* Render the runtime known-vocabulary module: every event type the packages in
* this repo can write, as a generated `ReadonlySet` the read path checks
* unknown-type refusal against (`SessionEvent.ignorable` contract).
*/
export function renderKnownEventTypes(events: AnnotatedLogEventEntry[]): string {
const names = [...new Set(events.map(e => e.name))].sort()
return [
'/**',
' * GENERATED by `scripts/gen-persistence-catalog.ts` — do not edit by hand; run',
' * `pnpm run gen-persistence-catalog` to regenerate (verified fresh by',
' * `pnpm run verify-persistence-catalog`, part of `doc-sync`).',
' * @module @deepseek-ai/dsh-session/known-event-types',
' */',
'',
'/**',
' * Every `SessionEventMap` member declared in this repository — the event',
' * vocabulary this build understands. The persistence read path refuses to',
' * interpret a log containing a type outside this set unless the event',
' * carries the envelope\'s `ignorable` marker (see `SessionEvent.ignorable`',
' * in `./types.ts`): such a log was likely written by a newer harness, and',
' * silently skipping a required event would reconstruct a wrong session.',
' * Downstream (out-of-repo) plugin events are outside this list by',
' * construction; a registration surface for them is deferred until such a',
' * consumer exists.',
' */',
'export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet<string> = new Set([',
...names.map(name => ` '${name}',`),
'])',
'',
].join('\n')
}
/** One generated artifact: repo-relative target and its freshly-rendered content. */
interface GeneratedArtifact {
readonly out: string
readonly content: string
}
/** CLI entry: default writes the artifacts, `--check` fails if a committed copy
* is stale. Guarded behind an entry-point check so importing this module for
* tests neither regenerates the committed file nor calls process.exit. */
* tests neither regenerates the committed files nor calls process.exit. */
function main(): void {
const content = render(annotateSurface(collectLogEvents(), collectSurfaceEventTypes()), collectEventEnvelopeTypes())
const events = annotateSurface(collectLogEvents(), collectSurfaceEventTypes())
const artifacts: GeneratedArtifact[] = [
{ out: OUT, content: render(events, collectEventEnvelopeTypes()) },
{ out: OUT_RUNTIME_TYPES, content: renderKnownEventTypes(events) },
]
if (process.argv.includes('--check')) {
let committed: string | null = null
try {
committed = readFileSync(resolve(root, OUT), 'utf8')
} catch {
// Only ENOENT (not yet generated) is expected; a present-but-unreadable
// file is not a state this repo produces. Either way the remedy is the
// same — regenerate — so treat a read failure as "stale".
committed = null
}
if (committed === content) {
console.log(`gen-persistence-catalog: ${OUT} is up to date.`)
const stale = artifacts.filter((artifact) => {
let committed: string | null = null
try {
committed = readFileSync(resolve(root, artifact.out), 'utf8')
} catch {
// Only ENOENT (not yet generated) is expected; a present-but-unreadable
// file is not a state this repo produces. Either way the remedy is the
// same — regenerate — so treat a read failure as "stale".
committed = null
}
return committed !== artifact.content
})
if (stale.length === 0) {
console.log(`gen-persistence-catalog: ${artifacts.map(a => a.out).join(', ')} are up to date.`)
process.exit(0)
}
console.error(`gen-persistence-catalog: ${OUT} is stale. Run \`pnpm run gen-persistence-catalog\` and commit ${OUT}.`)
console.error(`gen-persistence-catalog: ${stale.map(a => a.out).join(', ')} stale. Run \`pnpm run gen-persistence-catalog\` and commit the result.`)
process.exit(1)
}
writeFileSync(resolve(root, OUT), content)
console.log(`gen-persistence-catalog: wrote ${OUT}.`)
for (const artifact of artifacts) {
writeFileSync(resolve(root, artifact.out), artifact.content)
console.log(`gen-persistence-catalog: wrote ${artifact.out}.`)
}
}
// Run only when invoked as a script, not when imported by a test.

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. */

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 shape changes, so the generator fails loud', () => {
expect(parseVendoredRows('| `cordis/` | cordis | 4.0.0 | https://example.com | `abc123` |\n')).toEqual([])
it('yields nothing when the table columns change, so the generator fails loud', () => {
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', () => {
@@ -215,7 +219,7 @@ describe('parsePyprojectRequirements', () => {
].join('\n'))).toEqual(['pydantic', 'tomli', 'pytest'])
})
it('accepts dependency-group includes and rejects unsupported requirement shapes', () => {
it('accepts dependency-group includes and rejects unsupported requirement forms', () => {
expect(parsePyprojectRequirements('[dependency-groups]\nbase = ["pytest"]\nall = [{ include-group = "base" }]\n'))
.toEqual(['pytest'])
expect(() => parsePyprojectRequirements('[project]\ndependencies = "pytest"\n')).toThrow(/must be an array/)
@@ -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',

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
}
@@ -475,7 +478,7 @@ function collectPythonRequirementArray(
}
}
/** Read an optional TOML table and reject a present value of another shape. */
/** Read an optional TOML table and reject a present non-table value. */
function optionalTomlTable(value: TomlValueWithoutBigInt | undefined, location: string): TomlTableWithoutBigInt | undefined {
if (value === undefined || isTomlTable(value)) return value
throw new Error(`gen-third-party-notices: ${location} must be a table.`)
@@ -487,7 +490,7 @@ function optionalTomlTable(value: TomlValueWithoutBigInt | undefined, location:
* `[build-system]`, `dependencies` under `[project]`, and every key under
* `[project.optional-dependencies]` and `[dependency-groups]`. A TOML parser
* owns comments, quoted keys, escapes, and array boundaries; unsupported
* requirement shapes fail instead of disappearing from the notices.
* requirement forms fail instead of disappearing from the notices.
* @param text - the complete `pyproject.toml` contents.
* @returns the local project name and declared requirement names.
*/
@@ -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

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'

View File

@@ -7,7 +7,7 @@
* the narrowest safe granularity — code-fence-only splice, changed
* Markdown units, heading sections, whole document — and `--apply` writes
* the computed counterpart for pairs whose change is code-fence-only.
* The briefing contract lives in `scripts/translation-brief.ts`; the
* The briefing rules live in `scripts/translation-brief.ts`; the
* consuming workflow is `.agents/skills/dsh-translate-docs/SKILL.md`.
*/

View File

@@ -1,78 +0,0 @@
// Regression drive for the unified hero composer:
// cold start with zero workspaces -> create a workspace -> type. Asserts the
// composer textarea is the SAME DOM node across the disabled->live flip (a
// remount drops the __heroMark marker property) — the session-maybe
// composer.bar contract.
//
// Prereqs: `pnpm run build`, then a fresh server against empty state:
// rm -rf .storages && DSH_HOME=$(mktemp -d) node --experimental-transform-types \
// --import ./scripts/tspath-loader.ts apps/cli/src/bin.ts web --port 44285 \
// --workspace-root $(mktemp -d)
// Run: node scripts/hero-composer-dom-continuity.mjs
// (BASE_URL overrides the target; screenshots land in .artifacts/.)
import { createRequire } from 'node:module'
// playwright is a devDependency of apps/web only — resolve through its tree.
const require = createRequire(new URL('../apps/web/package.json', import.meta.url))
const { chromium } = require('playwright')
const BASE = process.env.BASE_URL ?? 'http://127.0.0.1:44285'
const SHOTS = new URL('../.artifacts/screenshots/0729-0357-hero-unify/', import.meta.url).pathname
const browser = await chromium.launch()
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } })
page.on('console', msg => { if (msg.type() === 'error') console.log('[console.error]', msg.text()) })
page.on('pageerror', err => { console.log('[pageerror]', err.message) })
await page.goto(BASE)
await page.waitForSelector('textarea', { timeout: 20000 })
await page.screenshot({ path: SHOTS + '01-cold-start.png' })
const initial = await page.evaluate(() => {
const boxes = [...document.querySelectorAll('textarea')]
boxes.forEach((b, i) => { b.__heroMark = 'alive-' + i })
return boxes.map(b => ({ disabled: b.disabled, placeholder: b.placeholder }))
})
console.log('cold-start textareas:', JSON.stringify(initial))
// Open the picker and create a workspace by name (typed-input flow). The name
// must be unique per registry; keystrokes go through pressSequentially so the
// dialog's React onChange enables the submit button.
await page.getByRole('button', { name: 'Choose workspace' }).click()
await page.getByText('Create a new workspace').click()
await page.screenshot({ path: SHOTS + '03-create-form.png' })
const nameBox = page.getByPlaceholder('Workspace name')
await nameBox.click()
const wsName = 'proj-' + Date.now().toString(36)
await nameBox.pressSequentially(wsName, { delay: 30 })
await page.locator('button:text-is("Create workspace")').click()
// Wait for the composer to go live (placeholder flips, textarea enabled).
await page.waitForFunction(() => {
const box = document.querySelector('textarea')
return box !== null && !box.disabled
}, { timeout: 20000 })
await page.screenshot({ path: SHOTS + '04-live.png' })
const after = await page.evaluate(() => {
const boxes = [...document.querySelectorAll('textarea')]
return boxes.map(b => ({
mark: b.__heroMark ?? 'REMOUNTED',
disabled: b.disabled,
placeholder: b.placeholder,
}))
})
console.log('post-pick textareas:', JSON.stringify(after))
// Type into the live composer.
await page.locator('textarea').first().fill('hello from acceptance run')
const typed = await page.evaluate(() => document.querySelector('textarea')?.value)
console.log('typed value:', JSON.stringify(typed))
await page.screenshot({ path: SHOTS + '05-typed.png' })
const survived = after.length === 1 && after[0].mark === 'alive-0'
console.log(survived
? 'DOM-CONTINUITY: PASS (same textarea node across cold-start -> live)'
: 'DOM-CONTINUITY: FAIL ' + JSON.stringify(after))
await browser.close()
process.exit(survived && typed === 'hello from acceptance run' ? 0 : 1)

View File

@@ -15,7 +15,7 @@ interface Profile {
// A one-time audit against eslint.config.mjs blob 696b08282885296830189fdafe7051a356806fc2
// mapped @typescript-eslint/* to typescript/* and four extension rules to their
// Oxlint core equivalents. These fingerprints pin the resulting repository
// contract; they do not re-evaluate that deleted baseline or track its preset.
// snapshot; they do not re-evaluate that deleted baseline or track its preset.
const profiles = {
source: {
count: 88,
@@ -84,7 +84,7 @@ describe('Oxlint repository rule fingerprint', () => {
}
const overrides: readonly unknown[] = parsed.overrides
it('pins the complete override shape', () => {
it('pins every override field', () => {
expect(overrides).toHaveLength(8)
})

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:^',

View File

@@ -19,7 +19,7 @@ interface PackageManifest {
devDependencies?: Record<string, string>
}
/** One package and the files participating in its invariant publication contract. */
/** One package and the files participating in its invariant publication rules. */
export interface PackageInvariantOwner {
readonly dir: string
readonly manifestPath: string
@@ -53,7 +53,7 @@ export function packageInvariantOwners(root: string): PackageInvariantOwner[] {
})
}
/** Return all violations of the package-invariant companion contract. */
/** Return all violations of the package-invariant companion rules. */
export function collectPackageInvariantViolations(root: string): PackageInvariantViolation[] {
const violations: PackageInvariantViolation[] = []
for (const owner of packageInvariantOwners(root)) {
@@ -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:^') {

View File

@@ -19,7 +19,7 @@ fi
archive="${RUNNER_TEMP}/bubblewrap_${BUBBLEWRAP_VERSION}_amd64.deb"
root="${RUNNER_TEMP}/dsh-bubblewrap"
curl --fail --silent --show-error --location --retry 3 --output "$archive" "$BUBBLEWRAP_URL"
curl --fail --silent --show-error --location --retry 3 --retry-all-errors --output "$archive" "$BUBBLEWRAP_URL"
printf '%s %s\n' "$BUBBLEWRAP_SHA256" "$archive" | sha256sum --check --status
mkdir -p "$root"
dpkg-deb --extract "$archive" "$root"

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
scripts/release/bump.ts Normal file
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()

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
scripts/release/families.ts Normal file
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
scripts/release/pack.ts Normal file
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()

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
scripts/release/publish.ts Normal file
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()

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 !== '')
}

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
scripts/release/verify.ts Normal file
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()

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
scripts/rescope-vendor.ts Normal file
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()
}

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' },

View File

@@ -341,7 +341,7 @@ function nodeCompatSmokeGates(options: { cliSmoke?: boolean } = {}): Gate[] {
return gates
}
/** Active Node major used to scope version-specific compatibility contracts. */
/** Active Node major used to select version-specific compatibility checks. */
function runningNodeMajor(): number {
const major = Number.parseInt(process.versions.node.split('.')[0] ?? '', 10)
if (!Number.isSafeInteger(major)) {
@@ -406,7 +406,6 @@ function ciConsumerGates(): Gate[] {
needs: validatedBuild,
}),
builtBinSmokeGate(validatedBuild),
githubRepositoryPluginE2eGate(validatedBuild),
]
}
@@ -473,7 +472,7 @@ function lintGate(options: { needs?: string[] } = {}): Gate {
// The heavy suites run uninstrumented beside the thresholded gate: their
// compiler- and subprocess-bound fixtures pay a multiple of their runtime
// under v8 instrumentation while contributing nothing the thresholds need
// (membership contract in scripts/coverage-exempt.ts).
// (membership rules in scripts/coverage-exempt.ts).
//
// DSH_COVERAGE_MAX_WORKERS is the lane's worker budget, so the two parallel
// gates split it instead of each claiming it whole (the failover pool's
@@ -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'),
@@ -599,6 +599,7 @@ function docSyncLeafGates(options: {
pnpmScript('agent-note-format', 'verify-agent-note-format', { label: 'agent note format' }),
pnpmScript('archived-agent-notes', 'verify-archived-agent-notes', { label: 'archived agent notes' }),
pnpmScript('type-equivalence', 'verify-type-equiv', { label: 'type equivalence' }),
pnpmScript('skill-invocation-metadata', 'verify-skill-invocation-metadata', { label: 'skill invocation metadata' }),
pnpmScript('translation-prompt', 'verify-translation-prompt', { label: 'translation prompt' }),
pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }),
pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets' }),
@@ -638,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.

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

View File

@@ -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"}}}

View File

@@ -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"}}}

View File

@@ -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"}}}

File diff suppressed because one or more lines are too long

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')
}
@@ -137,7 +125,7 @@ describe('global test invariant host', () => {
.toEqual(Object.keys(testInvariantCompanions).sort())
})
it('loads and executes every source companion through the real Loader shape', async () => {
it('loads and executes every source companion through the real Loader setup', async () => {
const owners = new Map(packageInvariantOwners(process.cwd()).map(owner => [owner.sourcePath, owner.packageName]))
const registrations = new Map<string, string>()
const loader = Object.create(Loader.prototype) as Loader
@@ -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)
},
)
})

View File

@@ -6,8 +6,15 @@
*/
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,
ImageAttachmentRef,
SaveImageAttachment,
StoredImageAttachment,
} from '@deepseek-ai/dsh-attachment'
import InvariantService from '@deepseek-ai/dsh-invariants'
declare global {
@@ -17,7 +24,7 @@ declare global {
}
}
/** Loader-safe shape shared by every package invariant companion. */
/** Loader-safe exports shared by every package invariant companion. */
export interface TestInvariantCompanion {
readonly name: string
readonly inject: readonly string[]
@@ -102,6 +109,29 @@ export function usesManualInvariantTree(testPath: string): boolean {
}
const ALL_COMPANION_TESTS = ['/scripts/test-invariants.spec.ts'] as const
const ATTACHMENT_COMPANION = '../packages/attachment/attachment-local/src/invariant.ts'
class TestAttachmentStore extends AttachmentStore {
readonly imageLimits: ImageAttachmentLimits = {
maxImageBytes: 1,
maxImagesPerMessage: 1,
maxMessageImageBytes: 1,
maxImagePixels: 1,
mediaTypes: ['image/png'],
}
validateImage(_input: SaveImageAttachment): Promise<void> {
return Promise.reject(new Error('test invariant attachment store does not validate images'))
}
saveImage(_input: SaveImageAttachment): Promise<ImageAttachmentRef> {
return Promise.reject(new Error('test invariant attachment store does not save images'))
}
readImage(_ref: ImageAttachmentRef): Promise<StoredImageAttachment> {
return Promise.reject(new Error('test invariant attachment store does not read images'))
}
}
/**
* Select the package companions that an ordinary test root must register.
@@ -148,6 +178,9 @@ function startInvariantHost(root: Context): InvariantHost {
const testPath = expect.getState().testPath ?? ''
const companionPaths = testInvariantCompanionPaths(testPath)
const ready = requireActive(serviceFiber, 'invariant service').then(async () => {
const attachmentFiber = companionPaths.includes(ATTACHMENT_COMPANION)
? mount(TestAttachmentStore)
: undefined
const companions = await Promise.all(companionPaths.map(async (path) => {
const load = testInvariantCompanions[path]
if (load === undefined) {
@@ -163,7 +196,12 @@ function startInvariantHost(root: Context): InvariantHost {
fiber: mount(companion),
path,
}))
await Promise.all(companionFibers.map(({ fiber, path }) => requireActive(fiber, path)))
await Promise.all([
...(attachmentFiber === undefined
? []
: [requireActive(attachmentFiber, 'test attachment store')]),
...companionFibers.map(({ fiber, path }) => requireActive(fiber, path)),
])
root.provide(TEST_INVARIANT_READY_SERVICE, true)
})
const host = { byCallback, barrierOwners, ready }
@@ -210,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

View File

@@ -54,7 +54,7 @@ export interface GitIndexBlob {
* @param root - Repository root.
* @param path - Repository-relative path.
* @returns The stage-zero blob, or `undefined` when the path is absent.
* @throws Error when the path is unmerged or has an invalid index shape.
* @throws Error when the path is unmerged or its index entries are not a valid merge state.
*/
export function readGitIndexBlob(root: string, path: string): GitIndexBlob | undefined {
const output = runGit(

View File

@@ -80,7 +80,7 @@ const PAIR_META_LINE = /^([^:#]+\.md): ([0-9a-f]{40})$/
/**
* Parse a `foo.i18n.yaml` consistency record into basename → recorded blob
* hash, or undefined when any non-comment line deviates from the exact
* `<basename>.md: <40-hex>` shape or repeats a key. Consumers must
* `<basename>.md: <40-hex>` format or repeats a key. Consumers must
* additionally require exactly the two expected basenames — a renamed key is
* a malformed record, never a silently-missing entry.
* @param content - Sidecar file text.
@@ -118,7 +118,7 @@ export function renderPairMeta(source: string, sourceHash: string, zh: string, z
].join('\n')
}
/** Validated shape of `scripts/translation-pairing.manifest.json`. */
/** Validated fields of `scripts/translation-pairing.manifest.json`. */
export interface TranslationPairingManifest {
/** Source documents exempt from pairing because they are generated, instructional, or bilingual by construction. */
excluded: string[]

View File

@@ -21,7 +21,7 @@ const retainedExamples = [
['### Stiff passive voice → Active and natural', 'a green gate means the pair was confirmed consistent at these exact contents, not that the confirmation was sound.', '门禁通过意味着这组文档在当前内容上的一致性得到了确认,不代表确认本身正确可靠。'],
['### Invented word → Natural expression', 'A sidecar record of both blob hashes makes consistency checkable', '伴随记录保存两侧 blob hash使一致性可检查'],
['### Em-dash → Colon/period', 'FIXME — an issue that should block a new release.', 'FIXME应当阻塞新版本发布的问题。'],
['### Overly literal → Meaningful rendering', 'awkward phrasing is easier to hear without the source anchoring you', '不对照原文时,更容易察觉别扭的表达'],
['### Overly literal → Meaningful rendering', 'awkward phrasing is easier to notice when you read the translation without comparing it with the source', '不对照原文阅读译文时,更容易察觉别扭的表达'],
['### Terminology — do not translate what should be kept in English', 'typed service seams, and explicit extension points', '类型化的服务 seam 与显式扩展点'],
['### Slang/jargon → Professional phrasing', 'The committed agent workflow lives in .agents/skills/dsh-translate-docs', '仓库内置的 agent 工作流见 .agents/skills/dsh-translate-docs'],
['### "For humans" — translate the intent, not the word', 'For humans, start with the development guide', '面向开发者:请先阅读开发指南'],
@@ -44,7 +44,7 @@ describe('translation prompt rendering', () => {
expect(zh).toContain('from Chinese to English')
})
it('retains every v4 embedded example', () => {
it('contains every embedded example', () => {
for (const example of retainedExamples) {
for (const fragment of example) expect(document).toContain(fragment)
}

View File

@@ -169,7 +169,7 @@ function unescapeResponseBody(value: string): string {
}).join('\n')
}
/** Serialize a response in the exact escaped three-section shape the prompt requests. */
/** Serialize a response in the exact escaped three-section format the prompt requests. */
export function renderTranslationResponse(response: TranslationResponse): string {
return RESPONSE_SECTIONS.map(section => `<${section}>\n${escapeResponseBody(response[section])}\n</${section}>`).join('\n\n')
}
@@ -178,7 +178,7 @@ export function renderTranslationResponse(response: TranslationResponse): string
* Parse the three-section response. Sections must each appear exactly once
* and in order; escaped delimiter lines in Markdown bodies are restored.
* A fenced ```xml wrapper around the whole response is tolerated, matching
* the shape some models echo back from the prompt's own example.
* the wrapper some models copy from the prompt's own example.
*/
export function parseTranslationResponse(text: string): TranslationResponse {
let body = text.trim()

View File

@@ -780,6 +780,31 @@
"symbol": "ApprovalRequest",
"source": "packages/interaction/user-approval/src/index.ts"
},
{
"doc": "docs/subsystems/attachment.md",
"symbol": "ImageMediaType",
"source": "packages/attachment/attachment/src/types.ts"
},
{
"doc": "docs/subsystems/attachment.md",
"symbol": "ImageAttachmentRef",
"source": "packages/attachment/attachment/src/types.ts"
},
{
"doc": "docs/subsystems/attachment.md",
"symbol": "ImageAttachmentLimits",
"source": "packages/attachment/attachment/src/types.ts"
},
{
"doc": "docs/subsystems/attachment.md",
"symbol": "SaveImageAttachment",
"source": "packages/attachment/attachment/src/types.ts"
},
{
"doc": "docs/subsystems/attachment.md",
"symbol": "StoredImageAttachment",
"source": "packages/attachment/attachment/src/types.ts"
},
{
"doc": "docs/subsystems/bash.md",
"symbol": "BashExecRequest",
@@ -1595,6 +1620,11 @@
"symbol": "WebBootGraph",
"source": "packages/client/modules/src/client/manifest.ts"
},
{
"doc": "docs/subsystems/telemetry.md",
"symbol": "TelemetrySharingStatus",
"source": "packages/session/session-telemetry/src/index.ts"
},
{
"doc": "docs/subsystems/telemetry.md",
"symbol": "TelemetrySeverity",

View File

@@ -1,6 +1,6 @@
/**
* Enforce Agent Note lifecycle/class paths and dated filenames. Structural rules
* are shared with `agent-note-tree.ts`; the closed classification contract lives
* are shared with `agent-note-tree.ts`; the closed classification rules live
* in `.agents/notes/README.md`.
*/

View File

@@ -9,7 +9,7 @@ import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { agentNoteRoot, walkAgentNoteTree } from './agent-note-tree.ts'
/** The date the format contract landed; the grandfather comment is valid only before it. */
/** The date these format rules took effect; the grandfather comment is valid only before it. */
const FORMAT_ADOPTED = '2026-07-05'
/** The exact comment a pre-format Agent Note carries in place of `## Alternatives considered`. */

View File

@@ -99,7 +99,7 @@ if (!writeMode) {
}
if (errors.length > 0) {
console.error('verify-archived-agent-notes: archive contract violated:')
console.error('verify-archived-agent-notes: archive rules violated:')
for (const error of errors) console.error(` ${error}`)
process.exit(1)
}

View File

@@ -19,7 +19,7 @@ const SHIPPED_CONFIG_GLOBS = [
'python/*/src/**/cordis.yml',
]
/** Ordinary single-line forms this narrow source-shape check rejects; not full YAML analysis. */
/** Ordinary single-line configuration forms this source check rejects; not full YAML analysis. */
const INLINE_DENY = /^\s*(apiKey|baseURL|apiKeyEnv|authToken|headers)\s*:\s*!!js\b/
/** Return every forbidden inline environment form in shipped configuration. */

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++) {

View File

@@ -75,9 +75,9 @@ function unwrapExpression(e: ts.Expression): ts.Expression {
/**
* Classify inline callable annotations. Mixed callable literals fail closed;
* other annotations are ordinary value shapes.
* other annotations are ordinary value types.
* @param type - the declarator's type annotation.
* @returns the signature to check, 'refuse' for an unclassifiable callable literal, or null for a non-callable shape.
* @returns the signature to check, 'refuse' for an unclassifiable callable literal, or null for a non-callable type.
*/
function callableAnnotation(type: ts.TypeNode): ts.SignatureDeclarationBase | 'refuse' | null {
if (ts.isFunctionTypeNode(type)) return type
@@ -446,7 +446,7 @@ function checkScope(
if (ts.isExportAssignment(stmt)) {
if (stmt.isExportEquals) {
// `export =` has no ESM consumer surface in this repo and the walk
// cannot classify its operand's shape; refuse rather than fail open.
// cannot classify its operand's type; refuse rather than fail open.
w.violations.push(`export-equals assignment (${pointer(w.rel, w.sf, stmt)}) is not a gate-supported export form; use ESM named exports.`)
continue
}

View File

@@ -1,4 +1,4 @@
/** Verify package-owned invariant source and publication contracts. */
/** Verify package-owned invariant source and publication rules. */
import { resolve } from 'node:path'
import {

View File

@@ -42,6 +42,8 @@ const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
* blocks. A package moves on or off this list with its context behavior.
*/
const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/attachment/attachment': { kind: 'indirect', reason: 'The storage seam delegates model request rendering to provider adapters.' },
'packages/attachment/attachment-local': { kind: 'indirect', reason: 'The local backend delegates model request rendering to provider adapters.' },
'packages/bash/bash': { kind: 'indirect', reason: 'The service interface delegates all model rendering to dsh-tool-bash.' },
'packages/bash/bash-env': { kind: 'indirect', reason: 'The env service surfaces managed DSH_* facts through the shell tools (dsh-tool-bash/dsh-tool-pwsh); it registers no prompt or schema of its own.' },
'packages/bash/bash-local': { kind: 'indirect', reason: 'The executor backend delegates model rendering to dsh-tool-bash.' },
@@ -126,12 +128,12 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/util/atomic-write': { kind: 'none', reason: 'Pure filesystem write primitive; registers no model surface.' },
'packages/session/session-telemetry': { kind: 'none', reason: 'The seam observes the session stream and hands redacted copies outward; it registers no model surface.' },
'packages/session/session-telemetry-otel': { kind: 'none', reason: 'The backend forwards seam records into the OTel SDK pipeline and registers no model surface.' },
'packages/session/user-id': { kind: 'none', reason: 'The shared identifier appears only in telemetry metadata and a direct human command response; it registers no model surface.' },
'packages/skill/skill': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-skill.' },
'packages/skill/skill-badge': { kind: 'indirect', reason: 'The bundled provider delegates model rendering to dsh-tool-skill.' },
'packages/skill/skill-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-skill.' },
'packages/spill/spill': { kind: 'indirect', reason: 'The storage seam delegates model rendering to spill consumers.' },
'packages/spill/spill-local': { kind: 'indirect', reason: 'The storage backend delegates model rendering to spill consumers.' },
'packages/subagent/subagent': { kind: 'indirect', reason: 'The provider registry delegates parent-model rendering to dsh-tool-subagent.' },
'packages/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.' },
@@ -145,6 +147,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.' },
@@ -215,12 +218,12 @@ function validateNestedVerbatim(raw: readonly string[], fragments: Set<string>):
return { blocks }
}
/** GitHub-style fragment for the simple ASCII nested titles allowed by this contract. */
/** GitHub-style fragment for the simple ASCII nested titles allowed by these rules. */
function headingFragment(title: string): string {
return title.toLowerCase().replaceAll('`', '').replaceAll(/[^a-z0-9 _-]/g, '').trim().replaceAll(/\s+/g, '-')
}
/** A direct stable system-prompt contribution, as named by the README contract. */
/** A direct stable system-prompt contribution, as named by the README rules. */
function isDirectSystemPromptSurface(title: string): boolean {
return /\bsystem prompt\b/i.test(title)
}

View File

@@ -0,0 +1,53 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { collectSkillInvocationMetadataViolations } from './verify-skill-invocation-metadata.ts'
const roots: string[] = []
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
function fixtureRoot(): string {
const root = mkdtempSync(join(tmpdir(), 'dsh-skill-invocation-metadata-'))
roots.push(root)
return root
}
function writeSkill(root: string, name: string, frontmatter: string, policy = ''): void {
const directory = join(root, '.agents/skills', name)
mkdirSync(join(directory, 'agents'), { recursive: true })
writeFileSync(join(directory, 'SKILL.md'), `---\nname: ${name}\ndescription: Test skill\n${frontmatter}---\n\nTest.\n`)
writeFileSync(
join(directory, 'agents/openai.yaml'),
`interface:\n display_name: "Test"\n${policy}`,
)
}
describe('cross-product skill invocation metadata gate', () => {
it('accepts aligned default and manual-only policies', () => {
const root = fixtureRoot()
writeSkill(root, 'default-skill', '')
writeSkill(
root,
'manual-skill',
'disable-model-invocation: true\nuser-invocable: true\n',
'policy:\n allow_implicit_invocation: false\n',
)
expect(collectSkillInvocationMetadataViolations(root)).toEqual([])
})
it('rejects either direction of a manual-only policy mismatch', () => {
const root = fixtureRoot()
writeSkill(root, 'claude-only', 'disable-model-invocation: true\n')
writeSkill(root, 'codex-only', '', 'policy:\n allow_implicit_invocation: false\n')
expect(collectSkillInvocationMetadataViolations(root)).toEqual([
'.agents/skills/claude-only: Claude Code manual-only=true but Codex manual-only=false',
'.agents/skills/codex-only: Claude Code manual-only=false but Codex manual-only=true',
])
})
})

View File

@@ -0,0 +1,122 @@
/**
* Keep Claude Code and Codex invocation metadata aligned for repository skills.
* @module scripts/verify-skill-invocation-metadata
*/
import { existsSync, readFileSync, readdirSync } from 'node:fs'
import { resolve } from 'node:path'
import { load } from 'js-yaml'
const ROOT = resolve(import.meta.dirname, '..')
/** Return an object-shaped YAML value, or undefined for every other shape. */
function asRecord(value: unknown): Record<string, unknown> | undefined {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? value as Record<string, unknown>
: undefined
}
/** Parse a skill's YAML frontmatter as an object. */
function parseSkillFrontmatter(source: string): Record<string, unknown> {
const lines = source.split('\n')
if (lines[0] !== '---') throw new Error('SKILL.md must start with YAML frontmatter')
const end = lines.indexOf('---', 1)
if (end < 0) throw new Error('SKILL.md frontmatter is not closed')
const metadata = asRecord(load(lines.slice(1, end).join('\n')))
if (metadata === undefined) throw new Error('SKILL.md frontmatter must be a YAML object')
return metadata
}
/** Find repository skill directories that carry Codex product metadata. */
function skillDirectories(root: string): string[] {
const skillsRoot = resolve(root, '.agents/skills')
if (!existsSync(skillsRoot)) return []
return readdirSync(skillsRoot, { withFileTypes: true })
.filter(entry => entry.isDirectory() && existsSync(resolve(skillsRoot, entry.name, 'agents/openai.yaml')))
.map(entry => entry.name)
.sort()
}
/**
* Report cross-product invocation-policy mismatches for repository skills.
* @param root - Repository root containing `.agents/skills`.
* @returns diagnostics for malformed metadata or policies that expose a skill differently.
*/
export function collectSkillInvocationMetadataViolations(root: string): string[] {
const violations: string[] = []
for (const skill of skillDirectories(root)) {
const relativeRoot = `.agents/skills/${skill}`
const skillFile = resolve(root, relativeRoot, 'SKILL.md')
const openaiFile = resolve(root, relativeRoot, 'agents/openai.yaml')
if (!existsSync(skillFile)) {
violations.push(`${relativeRoot}: agents/openai.yaml has no sibling SKILL.md`)
continue
}
let frontmatter: Record<string, unknown>
let openai: Record<string, unknown>
try {
frontmatter = parseSkillFrontmatter(readFileSync(skillFile, 'utf8'))
}
catch (error) {
violations.push(`${relativeRoot}/SKILL.md: ${error instanceof Error ? error.message : String(error)}`)
continue
}
try {
const parsed = asRecord(load(readFileSync(openaiFile, 'utf8')))
if (parsed === undefined) throw new Error('agents/openai.yaml must be a YAML object')
openai = parsed
}
catch (error) {
violations.push(`${relativeRoot}/agents/openai.yaml: ${error instanceof Error ? error.message : String(error)}`)
continue
}
const disableModelInvocation = frontmatter['disable-model-invocation']
if (disableModelInvocation !== undefined && typeof disableModelInvocation !== 'boolean') {
violations.push(`${relativeRoot}/SKILL.md: disable-model-invocation must be a boolean`)
continue
}
const userInvocable = frontmatter['user-invocable']
if (userInvocable !== undefined && typeof userInvocable !== 'boolean') {
violations.push(`${relativeRoot}/SKILL.md: user-invocable must be a boolean`)
continue
}
const policy = asRecord(openai.policy)
const allowImplicitInvocation = policy?.allow_implicit_invocation
if (allowImplicitInvocation !== undefined && typeof allowImplicitInvocation !== 'boolean') {
violations.push(`${relativeRoot}/agents/openai.yaml: policy.allow_implicit_invocation must be a boolean`)
continue
}
const claudeManualOnly = disableModelInvocation === true
const codexManualOnly = allowImplicitInvocation === false
if (claudeManualOnly !== codexManualOnly) {
violations.push(
`${relativeRoot}: Claude Code manual-only=${String(claudeManualOnly)}`
+ ` but Codex manual-only=${String(codexManualOnly)}`,
)
}
if (claudeManualOnly && userInvocable === false) {
violations.push(`${relativeRoot}/SKILL.md: a manual-only skill must remain user-invocable`)
}
}
return violations
}
if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) {
const skills = skillDirectories(ROOT)
const violations = collectSkillInvocationMetadataViolations(ROOT)
if (violations.length > 0) {
process.stderr.write('verify-skill-invocation-metadata: violations found:\n')
for (const violation of violations) process.stderr.write(` ${violation}\n`)
process.exit(1)
}
process.stdout.write(
`verify-skill-invocation-metadata: ${String(skills.length)} cross-product skill policy pair(s) aligned.\n`,
)
}

View File

@@ -291,6 +291,6 @@ if (errors.length === 0) {
process.exit(0)
}
console.error('verify-translation-pairing: bilingual pairing contract violated (see docs/i18n/README.md):')
console.error('verify-translation-pairing: bilingual pairing rules violated (see docs/i18n/README.md):')
for (const message of errors) console.error(` ${message}`)
process.exit(1)