Merge branch 'master' into worktree/pr-to-issue-lifecycle

This commit is contained in:
Tianyi Cui
2026-08-07 22:25:01 +08:00
committed by GitHub
3159 changed files with 101424 additions and 31375 deletions

Binary file not shown.

View File

@@ -7,6 +7,7 @@
import { existsSync, readdirSync, readFileSync } from 'node:fs'
import { join, relative, resolve } from 'node:path'
import { hasTypeRTRemoteNavigation, isForbiddenPublicationFile } from './publication-payload.ts'
const root = resolve(import.meta.dirname, '..')
// vendor/* is single-level; packages/<group>/<pkg> nests one level deeper
@@ -14,6 +15,7 @@ const root = resolve(import.meta.dirname, '..')
const workspaceGlobs = [
{ dir: 'vendor', depth: 1 },
{ dir: 'packages', depth: 2 },
{ dir: 'apps', depth: 1 },
] as const
const vendoredPackages = new Set([
'cordis',
@@ -28,6 +30,10 @@ const vendoredPackages = new Set([
])
const localArtifactDirs = new Set(['node_modules'])
const appPackageFiles: Readonly<Record<string, readonly string[]>> = {
'@deepseek-ai/dsh': ['lib/*.js', 'config'],
'@deepseek-ai/dsh-frontend': ['dist'],
}
/** The subset of package.json fields this constraint check cares about. */
interface PackageManifest {
@@ -96,7 +102,13 @@ function workspaceManifests(): WorkspaceManifest[] {
}
const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
// Profile bundles publish their dsh.bundle.patch layer beside the lib.
'@deepseek-ai/dsh-base': ['cordis.patch.yml'],
'@deepseek-ai/dsh-web-app': ['cordis.patch.yml'],
'@deepseek-ai/dsh-headless': ['cordis.patch.yml'],
'@deepseek-ai/dsh-client-ui-theme': ['lib/styles'],
'@deepseek-ai/dsh-helper': ['lib/assets'],
'@deepseek-ai/dsh-pty-local': ['scripts/ensure-spawn-helper.mjs'],
'@deepseek-ai/dsh-scripts': [
'lib/dev/tsdown-config.js',
'lib/local-plugin-loader-hooks.js',
@@ -110,6 +122,7 @@ function sameStringList(actual: readonly string[] | undefined, expected: readonl
function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
const extras = manifest.name ? packageFileExtras[manifest.name] ?? [] : []
const typeRTRemoteNavigation = hasTypeRTRemoteNavigation(manifest)
return [
'lib/index.js',
// Every package publishes its invariant ownership companion as a separate
@@ -133,11 +146,37 @@ function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
// declarations.
...usesEmittedTreeDefaults(manifest) ? ['lib/types/**/*.js'] : [],
'lib/types/**/*.d.ts',
'lib/types/**/*.d.ts.map',
'src',
...hasExportPair(manifest, './typert', './lib/typert.host.d.ts', './lib/typert.host.js')
? ['lib/typert.host.js', 'lib/typert.host.d.ts']
: [],
...hasExportPair(manifest, './client/typert', './lib/typert.client.d.ts', './lib/typert.client.js')
? ['lib/typert.client.js', 'lib/typert.client.d.ts']
: [],
...typeRTRemoteNavigation
? [
'lib/typert.remote-client.js',
'lib/typert.remote-client.d.ts',
'lib/typert.remote-client.d.ts.map',
'src',
]
: [],
]
}
/** Whether one conditional export exactly names the generated runtime and declaration pair. */
function hasExportPair(
manifest: PackageManifest,
subpath: string,
types: string,
runtime: string,
): boolean {
const entry = manifest.exports?.[subpath]
return typeof entry === 'object'
&& entry !== null
&& entry.types === types
&& entry.default === runtime
}
/** Runtime target of an export entry: conditional `default`, or the bare-string shorthand. */
function exportDefault(manifest: PackageManifest, subpath: string): string | undefined {
const entry = manifest.exports?.[subpath]
@@ -164,7 +203,25 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
return errors
}
if (manifest.name?.startsWith('@deepseek-ai/dsh-') && manifest.name !== '@deepseek-ai/dsh-root') {
if (manifest.name?.startsWith('@deepseek-ai/')) {
const publicationPolicy = { typeRTRemoteNavigation: hasTypeRTRemoteNavigation(manifest) }
for (const file of manifest.files ?? []) {
if (isForbiddenPublicationFile(file, publicationPolicy)) {
errors.push(`${label}: package.json files must not publish ${JSON.stringify(file)}`)
}
}
}
if (dir.startsWith('apps/') && manifest.name?.startsWith('@deepseek-ai/')) {
const expectedFiles = appPackageFiles[manifest.name]
if (expectedFiles === undefined) {
errors.push(`${label}: app package has no publication files policy`)
} else if (!sameStringList(manifest.files, expectedFiles)) {
errors.push(`${label}: package.json files must be ${JSON.stringify(expectedFiles)}`)
}
}
if (dir.startsWith('packages/') && manifest.name?.startsWith('@deepseek-ai/dsh-')) {
const peer = manifest.peerDependencies?.cordis
const dev = manifest.devDependencies?.cordis

View File

@@ -59,6 +59,13 @@ describe('client bundle purity gate', () => {
expect(resolveId('@deepseek-ai/dsh-brand')).toBeNull()
})
it('lets exact generated Remote contributions inline without admitting their package implementation', () => {
expect(resolveId('@deepseek-ai/dsh-goal/remote')).toBeNull()
expect(() => resolveId('@deepseek-ai/dsh-goal')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-goal/client')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-goal/remote/nested')).toThrow(/purity/)
})
it('throws on any other @deepseek-ai leak', () => {
expect(() => resolveId('@deepseek-ai/dsh-agent')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-client-web')).toThrow(/purity/)

View File

@@ -0,0 +1,108 @@
'use strict';
/**
* Istanbul coverage reporter printing one clickable `path:line:col` record per
* uncovered statement, branch path, and function. Vitest's per-file threshold
* failures name only the file; this reporter supplies the exact locations,
* printed just above those ERROR lines (reports run before threshold checks).
* Files at 100% print nothing, so a green run stays silent.
*
* CommonJS by requirement: istanbul-reports loads custom reporters with a bare
* require() outside the tsx/ESM pipeline (istanbul-reports index.js create()),
* so this file can be neither TypeScript nor ESM. Wired into vitest.config.ts
* by absolute path — require() would resolve a relative specifier against
* istanbul-reports' own directory.
*/
const path = require('node:path');
const { ReportBase } = require('istanbul-lib-report');
/**
* Editor-convention `line:column` of an istanbul location start (istanbul
* columns are 0-based; editors and terminal link handlers expect 1-based).
*/
function pos(loc) {
return `${loc.start.line}:${loc.start.column + 1}`;
}
/** Whether a location carries a usable 1-based start line. */
function usable(loc) {
return Boolean(loc && loc.start && Number.isFinite(loc.start.line) && loc.start.line >= 1);
}
/**
* ` (to line:col)` suffix when the range end adds information beyond the
* start. v8-remapped whole-line statements carry end.column = Infinity; those
* degrade to a line-only suffix, or to nothing on a single line.
*/
function endSuffix(loc) {
const end = loc.end;
if (!end || !Number.isFinite(end.line) || end.line < 1) return '';
if (!Number.isFinite(end.column)) {
return end.line === loc.start.line ? '' : ` (to ${end.line})`;
}
if (end.line === loc.start.line && end.column === loc.start.column) return '';
return ` (to ${end.line}:${end.column + 1})`;
}
class UncoveredLocationsReport extends ReportBase {
constructor(opts = {}) {
super(opts);
// Vitest passes the resolved config root alongside reporter options.
this.projectRoot = opts.projectRoot || process.cwd();
this.records = [];
}
onStart() {
this.records = [];
}
onDetail(node) {
const fc = node.getFileCoverage();
const rel = path.relative(this.projectRoot, fc.path).split(path.sep).join('/');
const items = [];
const add = (loc, text) => items.push({ line: loc.start.line, column: loc.start.column, text });
for (const id of Object.keys(fc.statementMap)) {
if (fc.s[id] !== 0) continue;
const loc = fc.statementMap[id];
if (!usable(loc)) continue;
add(loc, `${rel}:${pos(loc)} uncovered statement${endSuffix(loc)}`);
}
for (const id of Object.keys(fc.fnMap)) {
if (fc.f[id] !== 0) continue;
const fn = fc.fnMap[id];
const loc = usable(fn.decl) ? fn.decl : fn.loc;
if (!usable(loc)) continue;
const name = fn.name ? ` ${fn.name}` : '';
add(loc, `${rel}:${pos(loc)} uncovered function${name}`);
}
for (const id of Object.keys(fc.branchMap)) {
const counts = fc.b[id];
const branch = fc.branchMap[id];
for (let i = 0; i < counts.length; i += 1) {
if (counts[i] !== 0) continue;
// Implicit arms (e.g. a missing else) may carry an empty location;
// fall back to the branch's own span so the record stays clickable.
const loc = usable(branch.locations && branch.locations[i]) ? branch.locations[i] : branch.loc;
if (!usable(loc)) continue;
add(loc, `${rel}:${pos(loc)} uncovered branch (${branch.type}, path ${i + 1}/${counts.length})`);
}
}
if (items.length === 0) return;
items.sort((a, b) => a.line - b.line || a.column - b.column);
for (const item of items) this.records.push(item.text);
}
onEnd() {
if (this.records.length === 0) return;
console.log(`\nUncovered locations (per-file 100% gate): ${this.records.length}`);
for (const record of this.records) console.log(record);
console.log('');
}
}
module.exports = UncoveredLocationsReport;

View File

@@ -6,7 +6,7 @@ import { spawn } from 'node:child_process'
const SURFACES = new Map([
// The browser surface with the cordis toolset layered on: `dsh web --config`
// applies this overlay over the shipped web composition; it owns port 3081.
['web', ['--import', 'tsx', 'apps/cli/src/bin.ts', 'web', '--config', 'examples/web-cordis/cordis.yml']],
['web', ['--import', 'tsx', 'apps/cli/src/bin.ts', 'web', '--patch', 'examples/web-cordis/cordis.yml']],
['acp', ['--import', 'tsx', 'packages/examples/acp-demo/src/bin.ts', '--config', 'examples/acp-agent/cordis-tools.cordis.yml']],
])

View File

@@ -22,13 +22,7 @@ export default defineConfig({
const bundlePath = join(root, 'lib/client.js')
await writeFile(sourcePath, 'export const version = "watch-v1"\n')
bundles = await watchClientPlugins(root, ['.'], 50)
await expect.poll(async () => {
try {
return (await readFile(bundlePath, 'utf8')).includes('watch-v1')
} catch {
return false
}
}, { timeout: 10_000 }).toBe(true)
expect(await readFile(bundlePath, 'utf8')).toContain('watch-v1')
await new Promise(resolve => setTimeout(resolve, 1_000))
await writeFile(sourcePath, `export const version = "watch-v2-${'x'.repeat(100)}"\n`)

View File

@@ -47,21 +47,40 @@ export function discoverPluginDirs(root = repoRoot): string[] {
* @param root - repository or fixture root passed to tsdown.
* @param pluginDirs - workspace-relative package directories to watch.
* @param pollInterval - optional source-watcher polling interval in milliseconds.
* @returns live bundles whose async disposers stop every watcher.
* @returns live bundles after every watcher has completed its initial build.
*/
export async function watchClientPlugins(
root: string,
pluginDirs: readonly string[],
pollInterval?: number,
): Promise<TsdownBundle[]> {
return build({
let resolveInitialBuilds: (() => void) | undefined
const initialBuilds = new Promise<void>((resolve) => { resolveInitialBuilds = resolve })
const initialized = new WeakSet<object>()
const readiness: { expectedBuilds?: number; initializedBuilds: number } = { initializedBuilds: 0 }
const bundles = await build({
cwd: root,
workspace: [...pluginDirs],
watch: true,
hooks: {
'build:done': ({ options }) => {
if (initialized.has(options)) return
initialized.add(options)
readiness.initializedBuilds += 1
if (
readiness.expectedBuilds !== undefined
&& readiness.initializedBuilds >= readiness.expectedBuilds
) resolveInitialBuilds?.()
},
},
...pollInterval !== undefined
? { inputOptions: { watch: { watcher: { usePolling: true, pollInterval } } } }
: {},
})
readiness.expectedBuilds = bundles.length
if (readiness.initializedBuilds >= readiness.expectedBuilds) resolveInitialBuilds?.()
await initialBuilds
return bundles
}
const invokedPath = process.argv[1]

View File

@@ -1,11 +1,11 @@
{
"AGENTS.md": 1775,
"docs/AGENTS.md": 1150,
"AGENTS.md": 1782,
"docs/AGENTS.md": 1320,
"docs/architecture.md": 2160,
"docs/cordis-primer.md": 600,
"docs/defensive-patterns.md": 550,
"docs/testing.md": 1100,
"docs/testing.md": 1150,
"examples/AGENTS.md": 310,
"packages/AGENTS.md": 675,
"packages/README.md": 920
"packages/README.md": 936
}

View File

@@ -126,12 +126,13 @@ function loadFile(abs: string, rel: string, cache: Map<string, FileCtx>): FileCt
}
/** A type declaration a paste can contain. */
type TypeDecl = ts.InterfaceDeclaration | ts.TypeAliasDeclaration
type TypeDecl = ts.InterfaceDeclaration | ts.TypeAliasDeclaration | ts.EnumDeclaration
/** Find an interface/type-alias declaration by name in a file, or null. */
/** Find a pasteable type declaration by name in a file, or null. */
function findTypeDecl(ctx: FileCtx, name: string): TypeDecl | null {
for (const stmt of ctx.sf.statements) {
if ((ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)) && stmt.name.text === name) return stmt
if ((ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt) || ts.isEnumDeclaration(stmt))
&& stmt.name.text === name) return stmt
}
return null
}
@@ -207,7 +208,7 @@ function checkMemberDocs(ctx: FileCtx, decl: TypeDecl, violations: string[]): vo
else ts.forEachChild(type, (n) => { walkNested(n, path) })
}
if (ts.isInterfaceDeclaration(decl)) walkMembers(decl.members, decl.name.text)
else walkNested(decl.type, decl.name.text)
else if (ts.isTypeAliasDeclaration(decl)) walkNested(decl.type, decl.name.text)
}
/** Cross-file resolution context for the schema-path check. */

View File

@@ -28,12 +28,11 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
ContinuationDecision: 'core.md',
ContinuationStop: 'core.md',
GenerateOptions: 'core.md',
InboxItem: 'core.md',
InboxPlacement: 'core.md',
MessageId: 'core.md',
HookContext: 'core.md',
SettleReason: 'core.md',
AdapterRegistrationHandle: 'core.md',
DirectoryRegistrationHandle: 'core.md',
LlmCallConfig: 'core.md',
LlmModelContext: 'core.md',
LlmModelReasoningInfo: 'core.md',
@@ -42,13 +41,16 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
LlmModelInfo: 'core.md',
LlmProviderInfo: 'core.md',
LlmConfigurableProvider: 'core.md',
LlmModelDiscoveryRequest: 'core.md',
LlmDiscoveredModel: 'core.md',
ResolvedRetryPolicy: 'llm-streaming.md',
Message: 'core.md',
MessageSource: 'core.md',
UserMessage: 'session.md',
PromptDecision: 'core.md',
RequestError: 'core.md',
PreStepDecision: 'core.md',
PreStepContext: 'core.md',
RequestErrorAction: 'core.md',
RequestFailureContext: 'core.md',
PreparedReferencedMessage: 'session-reference.md',
SessionReferenceCandidate: 'session-reference.md',
SessionReferenceInput: 'session-reference.md',
@@ -88,6 +90,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
FsWriteIntent: 'filesystem.md',
FsWriteOutcome: 'filesystem.md',
CreateGoalRequest: 'goal.md',
CreateGoalResult: 'goal.md',
EditGoalRequest: 'goal.md',
GoalBlockReason: 'goal.md',
GoalChanged: 'goal.md',
@@ -103,8 +106,11 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
StreamChunk: 'llm-streaming.md',
SkillProviderControl: 'skills.md',
CreateSessionOptions: 'persistence.md',
PrepareSessionOptions: 'persistence.md',
SessionHeader: 'persistence.md',
SessionInspection: 'persistence.md',
SessionLocation: 'persistence.md',
SessionPreparation: 'persistence.md',
SessionPersistenceSnapshot: 'persistence.md',
ConfinedArgv: 'sandbox.md',
SandboxExecutionPolicy: 'sandbox.md',
@@ -271,15 +277,19 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
TypertPackageRecord: 'registry package record is owned by packages/typert/registry/README.md',
TypertSchemaFilter: 'registry schema query filter is owned by packages/typert/registry/README.md',
TypertSchemaRecord: 'registry schema record is owned by packages/typert/registry/README.md',
TypeRTDisposer: 'TypeRT lifecycle contract is owned by packages/typert/type-meta/README.md',
'z.core.JSONSchema.BaseSchema': 'zod projection output is owned by the zod v4 API',
'z.core.ToJSONSchemaParams': 'zod projection parameters are owned by the zod v4 API',
InvariantInstaller: 'service-local contribution contract is owned by packages/support/invariants/README.md',
LocaleDict: 'service-local dictionary shape is owned by packages/client/i18n/src/index.ts',
WebBootGraph: 'web boot graph wire shape is owned by packages/client/modules/src/client/index.ts',
WebRoute: 'route registration contract is owned by packages/host/webserver/src/index.ts',
WebUpgradeRoute:
'upgrade route registration contract is owned by packages/host/webserver/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',
InvariantRegistration: 'service-local lifecycle handle is owned by packages/support/invariants/README.md',
InvokeRemoteRequest: 'gateway invocation contract is owned by packages/api/gateway/README.md',
PresetOption: 'deployment menu metadata is owned by packages/ui/permission/README.md',
PresetSpec: 'deployment preset composition is owned by packages/ui/permission/README.md',
KnobState: 'projection unit state shape is owned by packages/ui/permission/README.md',

View File

@@ -140,8 +140,15 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'typert-registry',
title: 'Runtime type registry',
mode: 'core',
consumers: ['typert-loader'],
note: 'Plugins register live zod contributions directly or through dsh-typert-loader; runtime consumers query schemas and reflection metadata at their own edges.',
consumers: ['typert-loader', 'api-gateway'],
note: 'Plugins register live zod contributions directly or through dsh-typert-loader; the API gateway consumes invocation descriptors and providers, while other runtime consumers query schemas and reflection metadata at their own edges.',
},
{
key: 'typertGateway',
pkg: 'api-gateway',
title: 'TypeRT Host invocation gateway',
mode: 'core',
note: 'Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier.',
},
{
key: 'sessionPersistence',
@@ -320,24 +327,25 @@ const SERVICE_ROLES: ServiceRole[] = [
title: 'Subprocess seam',
mode: 'seam',
implementations: ['subprocess-local'],
consumers: ['bash-local', 'bash-sandbox', 'lsp-local', 'subagent-acp'],
note: 'The bash executors, the LSP host, and the ACP subagent backend spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation.',
consumers: ['bash-local', 'bash-sandbox', 'lsp-local', 'subagent-acp', 'subagent-codex', 'subagent-claude-code'],
note: 'The bash executors, the LSP host, and the out-of-process ACP, Codex, and Claude Code subagent backends spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation.',
},
{
key: 'bash',
pkg: 'bash',
title: 'Bash executor seam',
mode: 'seam',
implementations: ['bash-local', 'bash-sandbox'],
consumers: ['tool-bash', 'hooks-claude', 'hooks-codex'],
note: 'The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors replace bash-local without touching them.',
implementations: ['bash-local', 'bash-sandbox', 'pwsh-local'],
consumers: ['tool-bash', 'tool-pwsh', 'hooks-claude', 'hooks-codex'],
note: 'The model-facing shell tools and hook bridges consume this seam; sandboxed, remote, or PowerShell executors replace bash-local without touching them.',
},
{
key: 'bashEnv',
pkg: 'tool-bash',
pkg: 'bash-env',
title: 'Managed bash environment registry',
mode: 'core',
note: 'Plugins declare effect-scoped DSH_* facts; tool-bash collects one trusted snapshot per execution and the executor rebuilds the namespace.',
consumers: ['tool-bash', 'tool-pwsh'],
note: 'Plugins declare effect-scoped DSH_* facts; each shell tool collects one trusted snapshot per execution and its executor rebuilds the namespace.',
},
{
key: 'pty',
@@ -416,7 +424,7 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'subagent',
title: 'Subagent provider and continuation service',
mode: 'seam',
implementations: ['subagent-spawn', 'subagent-fork', 'subagent-acp'],
implementations: ['subagent-spawn', 'subagent-fork', 'subagent-acp', 'subagent-codex', 'subagent-claude-code', 'subagent-dsh-sdk'],
consumers: ['tool-subagent', 'tool-subagent-control', 'tool-ralph'],
note: 'Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route.',
},
@@ -597,7 +605,8 @@ function parseExampleCordis(rel: string): ExamplePlugin[] {
if (current?.name) plugins.push({ id: current.id, name: current.name })
}
for (const line of text.split('\n')) {
const id = /^-\s+id:\s+(.+?)\s*$/.exec(line)
// Top-level rows (`- id:`) and bundle-patch insert rows (` - id:`).
const id = /^\s*-\s+id:\s+(.+?)\s*$/.exec(line)
if (id?.[1] !== undefined) {
flush()
current = { id: stripYamlScalar(id[1]) }
@@ -619,9 +628,9 @@ const APP_EXAMPLES = [
id: 'dsh_base',
rel: 'apps/cli/composition.md',
title: 'DSH Base Composition',
label: 'apps/cli/config/base.cordis.yml',
config: 'apps/cli/config/base.cordis.yml',
summary: 'The raw CLI applies one required caller-selected patch list over this shared base; Web and headless apply their own shipped overlays.',
label: 'packages/bundle/base/cordis.patch.yml',
config: 'packages/bundle/base/cordis.patch.yml',
summary: 'The dsh-base bundle patch every profile applies first; mode bundles (dsh-web-app, dsh-headless) and the user\'s profile layer patch over it.',
},
{
id: 'headless',
@@ -1152,20 +1161,22 @@ function renderLifecycle(): string {
' participant Session',
' participant SDK as UI or SDK listener',
' User->>Agent: followup(content)',
` Agent-->>SDK: ${mermaidCode('agent/inbox/enqueue')}`,
` Agent-->>SDK: ${mermaidCode('agent/inbox/spliced')}`,
` Agent-->>SDK: ${mermaidCode('agent/inbox/inserted')} { message }`,
' Agent->>Driver: queued work wakes driver',
` Driver-->>SDK: ${mermaidCode('agent/status')} running`,
' Note over Agent,Driver: next-step acceptance window opens',
` Driver->>Hooks: ${mermaidCode('agent/prompt-submit')} waterfall`,
' Hooks-->>Driver: authoritative allow, block, or add context',
' alt prompt blocked or admission failed',
' Driver-->>Driver: append context-only batch or keep steering boundary pending',
' else prompt allowed',
' Note over Agent,Driver: claim pending next-step input plus one queued prompt',
` Driver-->>SDK: ${mermaidCode('agent/inbox/spliced')} pure deletion`,
` Driver-->>SDK: ${mermaidCode('agent/inbox/claimed')} { message, turn } per message`,
` Driver->>Hooks: ${mermaidCode('agent/pre-step')} waterfall`,
' Hooks-->>Driver: authoritative reject or enter(messages)',
' alt proposed step rejected or pre-step failed',
' Driver-->>Driver: claimed batch stays removed, no turn opens',
' else enter proposed step',
` Driver->>Session: ${mermaidCode('turn/start')}`,
` Driver->>Session: ${mermaidCode('user/message')}`,
` Driver->>Prompt: ${mermaidCode('system-prompt/assemble')} waterfall`,
` Driver-->>Driver: ${mermaidCode('agent/step')} serial checkpoint`,
` Driver->>Session: ${mermaidCode('step/start')}`,
` Driver->>Session: ${mermaidCode('user/message')} per entered message`,
` Driver->>Prompt: ${mermaidCode('system-prompt/assemble')} waterfall`,
` Driver->>LLM: ${mermaidCode('agent/request')} waterfall, then ${mermaidCode('llm/stream')} waterfall`,
' LLM-->>Driver: StreamChunk*',
` Driver->>Session: ${mermaidCode('assistant/chunk')}*`,
@@ -1188,11 +1199,17 @@ function renderLifecycle(): string {
` Driver->>Session: ${mermaidCode('tool/result')}`,
' end',
' end',
' Driver->>Session: post-tool context and steering (no prompt-submit)',
` Driver->>Session: ${mermaidCode('step/end')}`,
` Driver->>Hooks: ${mermaidCode('agent/turn-stopping')} serial terminal checkpoint`,
' opt natural stop and next-step inbox empty',
` Driver->>Hooks: ${mermaidCode('agent/turn-stopping')} serial terminal checkpoint`,
' end',
' opt next-step input is pending',
' Driver-->>Driver: claim pending next-step input',
` Driver-->>SDK: ${mermaidCode('agent/inbox/claimed')} { message, turn } per message`,
` Driver->>Hooks: ${mermaidCode('agent/pre-step')} waterfall`,
' Hooks-->>Driver: authoritative reject or enter(messages)',
' end',
' end',
' Note over Agent,Driver: next-step acceptance window closes',
` Driver->>Session: ${mermaidCode('turn/end')}`,
' end',
` Driver-->>SDK: ${mermaidCode('agent/status')} idle`,
@@ -1200,9 +1217,9 @@ function renderLifecycle(): string {
'',
'The `assistant/message` edge records every successful provider call, including content-less and `max-tokens` finishes. Empty content stays out of derived history while the durable anchor retains usage and exact chunk provenance, including an explicit empty source set.',
'',
'`dsh-compact-basic` uses `agent/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.',
'`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/prompt-submit` allow is authoritative; listeners wrapping `next()` preserve downstream content and additional contexts unless replacement is intentional. Steering bypasses that waterfall and joins at its durable checkpoint.',
'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.',
'',
'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.',
'',

View File

@@ -2,7 +2,20 @@ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSyn
import { join, resolve } from 'node:path'
import { tmpdir } from 'node:os'
import { describe, expect, it } from 'vitest'
import { collectPythonDependencies, isPermissive, type Manifest, manifestPatterns, parsePyprojectRequirements, parseVendoredRows, render, tierExternalDeps, virtualManifest } from './gen-third-party-notices.ts'
import {
CLAUDE_AGENT_SDK_PACKAGE,
claudeDistributionFromManifest,
collectPythonDependencies,
isOwnerAuthorizedRuntime,
isPermissive,
type Manifest,
manifestPatterns,
parsePyprojectRequirements,
parseVendoredRows,
render,
tierExternalDeps,
virtualManifest,
} from './gen-third-party-notices.ts'
const root = resolve(import.meta.dirname, '..')
@@ -12,7 +25,9 @@ describe('THIRD_PARTY_NOTICES.md', () => {
// Pre-commit regenerates the file whenever a manifest is staged, so reaching
// this assertion means the notices were committed without that hook.
it('matches what the generator produces from the current manifests', () => {
expect(readFileSync(resolve(root, 'THIRD_PARTY_NOTICES.md'), 'utf8'), 'stale notices — run `pnpm run gen-third-party-notices`').toBe(render())
const generated = render()
expect(generated).toContain('It depends on the third-party software listed below.')
expect(readFileSync(resolve(root, 'THIRD_PARTY_NOTICES.md'), 'utf8'), 'stale notices — run `pnpm run gen-third-party-notices`').toBe(generated)
})
})
@@ -223,7 +238,14 @@ describe('collectPythonDependencies', () => {
describe('isPermissive', () => {
it('accepts the licenses this project ships and rejects copyleft or unknown ones', () => {
expect(['MIT', 'ISC', 'BSD-3-Clause', 'Apache-2.0', 'MIT / Apache-2.0', '(MIT OR CC0-1.0)'].every(isPermissive)).toBe(true)
expect(['LGPL-3.0-only', 'MPL-2.0', 'GPL-3.0-or-later', 'SEE LICENSE IN LICENSE'].some(isPermissive)).toBe(false)
expect([
'LGPL-3.0-only',
'MPL-2.0',
'GPL-3.0-or-later',
'SEE LICENSE IN LICENSE',
'SEE LICENSE IN README.md',
'SEE LICENSE IN LICENSE.md',
].some(isPermissive)).toBe(false)
})
it('requires every operand of an AND, so a copyleft conjunct cannot ride along', () => {
@@ -245,6 +267,66 @@ describe('isPermissive', () => {
})
})
describe('official Claude distribution authorization', () => {
it('authorizes only the direct SDK identity without relabeling its license', () => {
expect(isOwnerAuthorizedRuntime(CLAUDE_AGENT_SDK_PACKAGE)).toBe(true)
expect(isOwnerAuthorizedRuntime(`${CLAUDE_AGENT_SDK_PACKAGE}-linux-x64`))
.toBe(false)
expect(isOwnerAuthorizedRuntime('@anthropic-ai/unrelated')).toBe(false)
expect(isPermissive('SEE LICENSE IN README.md')).toBe(false)
})
it('derives version-independent platform payloads from the official SDK manifest', () => {
expect(claudeDistributionFromManifest({
name: CLAUDE_AGENT_SDK_PACKAGE,
version: '9.8.7',
license: 'future declared terms',
claudeCodeVersion: '6.5.4',
optionalDependencies: {
[`${CLAUDE_AGENT_SDK_PACKAGE}-linux-x64`]: '9.8.7',
[`${CLAUDE_AGENT_SDK_PACKAGE}-darwin-arm64`]: '9.8.7',
},
})).toEqual({
sdkVersion: '9.8.7',
claudeCodeVersion: '6.5.4',
payloads: [
{
name: `${CLAUDE_AGENT_SDK_PACKAGE}-darwin-arm64`,
version: '9.8.7',
},
{
name: `${CLAUDE_AGENT_SDK_PACKAGE}-linux-x64`,
version: '9.8.7',
},
],
})
})
it('rejects a wrong SDK identity, missing payloads, and unrelated optionals', () => {
expect(() => claudeDistributionFromManifest({
name: '@anthropic-ai/unrelated',
version: '1.0.0',
claudeCodeVersion: '1.0.0',
optionalDependencies: {
[`${CLAUDE_AGENT_SDK_PACKAGE}-linux-x64`]: '1.0.0',
},
})).toThrow(`expected ${CLAUDE_AGENT_SDK_PACKAGE} manifest`)
expect(() => claudeDistributionFromManifest({
name: CLAUDE_AGENT_SDK_PACKAGE,
version: '1.0.0',
claudeCodeVersion: '1.0.0',
})).toThrow('declares no optional platform payloads')
expect(() => claudeDistributionFromManifest({
name: CLAUDE_AGENT_SDK_PACKAGE,
version: '1.0.0',
claudeCodeVersion: '1.0.0',
optionalDependencies: {
'@anthropic-ai/unrelated': '1.0.0',
},
})).toThrow('outside its authorized platform-payload identity')
})
})
describe('manifestPatterns', () => {
it('derives globs from the declared members, so a new member area is read', () => {
expect(manifestPatterns(['packages/*/*', 'tools/*'], ['packages/*'])).toEqual([

View File

@@ -49,6 +49,21 @@ const FIRST_PARTY = new Set([
'node-addon-landlock-run-linux-x64',
])
/** Official SDK identity covered by the project's narrow owner authorization. */
export const CLAUDE_AGENT_SDK_PACKAGE = '@anthropic-ai/claude-agent-sdk'
const CLAUDE_PLATFORM_PACKAGE_PREFIX = `${CLAUDE_AGENT_SDK_PACKAGE}-`
const CLAUDE_PLATFORM_DECLARED_LICENSE = 'SEE LICENSE IN LICENSE.md'
/**
* Whether a non-permissive runtime declaration has an identity-scoped owner
* authorization. This does not reclassify its terms as permissive.
* @param name - exact npm package identity.
* @returns true only for the official Claude Agent SDK package.
*/
export function isOwnerAuthorizedRuntime(name: string): boolean {
return name === CLAUDE_AGENT_SDK_PACKAGE
}
/**
* Metadata overrides where the installed manifest is wrong or unreachable.
* Each entry documents why the store cannot answer.
@@ -92,6 +107,7 @@ const BUILD_TIME_TOOLS = [
/** The `package.json` fields this generator reads. */
export interface Manifest {
name?: string
version?: string
private?: boolean
license?: string
dependencies?: Record<string, string>
@@ -164,7 +180,74 @@ function loadWorkspaceManifests(): { manifests: Map<string, Manifest>; names: Se
return { manifests, names }
}
type VirtualManifest = Manifest & { license?: string; repository?: string | { url?: string }; homepage?: string }
type VirtualManifest = Manifest & {
claudeCodeVersion?: string
license?: string
repository?: string | { url?: string }
homepage?: string
}
/** One platform payload declared by the official Claude Agent SDK. */
export interface ClaudePlatformPayload {
readonly name: string
readonly version: string
}
/** Current SDK and CLI distribution facts derived from the installed SDK manifest. */
export interface ClaudeDistribution {
readonly sdkVersion: string
readonly claudeCodeVersion: string
readonly payloads: ClaudePlatformPayload[]
}
function requiredManifestString(
value: string | undefined,
field: string,
): string {
if (value === undefined || value.length === 0) {
throw new Error(`gen-third-party-notices: ${CLAUDE_AGENT_SDK_PACKAGE} has no ${field}.`)
}
return value
}
/**
* Derive the official platform payload set without a version or platform
* allowlist. Only identities in the SDK's own package namespace are covered.
* @param manifest - installed official SDK manifest.
* @returns current SDK, CLI, and optional platform payload facts.
*/
export function claudeDistributionFromManifest(
manifest: VirtualManifest,
): ClaudeDistribution {
if (manifest.name !== CLAUDE_AGENT_SDK_PACKAGE) {
throw new Error(
`gen-third-party-notices: expected ${CLAUDE_AGENT_SDK_PACKAGE} manifest, got ${JSON.stringify(manifest.name)}.`,
)
}
const sdkVersion = requiredManifestString(manifest.version, 'version')
const claudeCodeVersion = requiredManifestString(
manifest.claudeCodeVersion,
'claudeCodeVersion',
)
const entries = Object.entries(manifest.optionalDependencies ?? {})
if (entries.length === 0) {
throw new Error(
`gen-third-party-notices: ${CLAUDE_AGENT_SDK_PACKAGE} declares no optional platform payloads.`,
)
}
const payloads = entries.map(([name, version]) => {
if (!name.startsWith(CLAUDE_PLATFORM_PACKAGE_PREFIX)) {
throw new Error(
`gen-third-party-notices: ${CLAUDE_AGENT_SDK_PACKAGE} optional dependency ${name} is outside its authorized platform-payload identity.`,
)
}
return {
name,
version: requiredManifestString(version, `${name} optional dependency version`),
}
}).sort((left, right) => left.name.localeCompare(right.name))
return { sdkVersion, claudeCodeVersion, payloads }
}
/**
* Resolve one package's manifest inside a pnpm virtual store. The prefix scan
@@ -193,9 +276,8 @@ export function virtualManifest(virtual: string, name: string): VirtualManifest
return undefined
}
/** License and repository URL for an installed external package, from the pnpm store. */
function installedMetadata(name: string): { license: string; repo: string } {
const override = OVERRIDES[name]
/** Resolve one installed external package manifest from either pnpm store. */
function installedManifest(name: string): VirtualManifest | undefined {
let manifest: (Manifest & { license?: string; repository?: string | { url?: string }; homepage?: string }) | undefined
// The nested Landlock workspace installs into its own store, so a package
// only that workspace depends on is unreachable from the root one.
@@ -210,6 +292,13 @@ function installedMetadata(name: string): { license: string; repo: string } {
manifest = virtualManifest(virtual, name)
if (manifest !== undefined) break
}
return manifest
}
/** License and repository URL for an installed external package, from the pnpm store. */
function installedMetadata(name: string): { license: string; repo: string } {
const override = OVERRIDES[name]
const manifest = installedManifest(name)
const license = override?.license ?? manifest?.license
const rawRepo = typeof manifest?.repository === 'string' ? manifest.repository : manifest?.repository?.url ?? manifest?.homepage
const repo = override?.repo ?? normalizeRepo(rawRepo)
@@ -219,6 +308,37 @@ function installedMetadata(name: string): { license: string; repo: string } {
return { license, repo }
}
function collectClaudeDistribution(): ClaudeDistribution {
const manifest = installedManifest(CLAUDE_AGENT_SDK_PACKAGE)
if (manifest === undefined) {
throw new Error(
`gen-third-party-notices: cannot resolve ${CLAUDE_AGENT_SDK_PACKAGE}; run \`pnpm install\`.`,
)
}
const distribution = claudeDistributionFromManifest(manifest)
let installedPayloads = 0
for (const payload of distribution.payloads) {
const installed = installedManifest(payload.name)
if (installed === undefined) continue
installedPayloads += 1
if (
installed.name !== payload.name
|| installed.version !== payload.version
|| installed.license !== CLAUDE_PLATFORM_DECLARED_LICENSE
) {
throw new Error(
`gen-third-party-notices: installed ${payload.name} does not match its SDK-declared version and ${CLAUDE_PLATFORM_DECLARED_LICENSE} license field.`,
)
}
}
if (installedPayloads === 0) {
throw new Error(
'gen-third-party-notices: no SDK-declared Claude platform payload is installed; install optional dependencies before regenerating.',
)
}
return distribution
}
/** Normalize a manifest repository/homepage value to a browsable https URL. */
function normalizeRepo(raw: string | undefined): string | undefined {
if (raw === undefined || raw === '') return undefined
@@ -519,6 +639,26 @@ function renderNpmTable(deps: ExternalDep[]): string {
return lines.join('\n')
}
function renderClaudeDistribution(
distribution: ClaudeDistribution | undefined,
): string {
if (distribution === undefined) return ''
const rows = distribution.payloads.map(payload =>
`| [\`${payload.name}\`](https://www.npmjs.com/package/${payload.name}) | ${payload.version} | ${CLAUDE_PLATFORM_DECLARED_LICENSE} |`,
)
return `
## Official Claude Code platform payloads
The project owner authorizes distribution of every version of the official \`${CLAUDE_AGENT_SDK_PACKAGE}\` package and the official Claude Code CLI/platform payloads that each version declares through \`optionalDependencies\`. This identity-scoped authorization does not classify their declared terms as permissive and does not cover any unrelated runtime package; version, declared-license, and payload-set changes still require the ordinary dependency, lockfile, compatibility, terms, and notices review.
The installed SDK ${distribution.sdkVersion} declares the following optional platform packages. Each carries the official Claude Code ${distribution.claudeCodeVersion} executable; the package identities and versions come from the SDK manifest, while the declared license field is verified against the platform payload installed for the current host.
| Optional platform package | Version | Declared license |
| --- | --- | --- |
${rows.join('\n')}
`
}
/**
* Render the complete notices document.
* @returns the exact bytes `THIRD_PARTY_NOTICES.md` must hold.
@@ -531,11 +671,19 @@ export function render(): string {
const vendored = collectVendored()
const python = collectPython()
const patched = collectPatched()
const claudeDistribution = runtimeDeps.some(
dep => dep.name === CLAUDE_AGENT_SDK_PACKAGE,
)
? collectClaudeDistribution()
: undefined
const nonPermissiveDev = devDeps.filter(dep => !isPermissive(dep.license))
// A copyleft license reaching a shipped surface is a distribution decision,
// not a rendering detail; the notices cannot quietly absorb it.
const nonPermissiveRuntime = runtimeDeps.filter(dep => !isPermissive(dep.license))
const nonPermissiveRuntime = runtimeDeps.filter(dep =>
!isPermissive(dep.license)
&& !isOwnerAuthorizedRuntime(dep.name),
)
if (nonPermissiveRuntime.length > 0) {
throw new Error(`gen-third-party-notices: runtime ${nonPermissiveRuntime.map(dep => `${dep.name} (${dep.license})`).join(', ')} is not a permissive license; review the distribution terms and record the decision before regenerating.`)
}
@@ -546,9 +694,9 @@ export function render(): string {
# Third-Party Notices
DeepSeek Harness is licensed under [BSD 3-Clause](LICENSE). It depends on the third-party open-source software listed below. Each project remains under its own license; nothing in this file changes those terms.
DeepSeek Harness is licensed under [BSD 3-Clause](LICENSE). It depends on the third-party software listed below. Each project remains under its own license; nothing in this file changes those terms.
This file lists **direct** dependencies declared by the workspace. It is generated from the workspace manifests by \`scripts/gen-third-party-notices.ts\`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and \`scripts/gen-third-party-notices.spec.ts\` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run \`pnpm run verify-third-party-notices\` for the standalone check.
This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude platform payload closure. It is generated from the workspace manifests by \`scripts/gen-third-party-notices.ts\`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and \`scripts/gen-third-party-notices.spec.ts\` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run \`pnpm run verify-third-party-notices\` for the standalone check.
The complete npm transitive closure, with exact pinned versions, is recorded in [\`pnpm-lock.yaml\`](pnpm-lock.yaml) — inspect it with \`pnpm licenses list\`. The Python closure is recorded in [\`python/sdk/uv.lock\`](python/sdk/uv.lock), and the Landlock launcher workspace keeps its own in [\`native/landlock-run/pnpm-lock.yaml\`](native/landlock-run/pnpm-lock.yaml).
@@ -562,13 +710,14 @@ ${vendored.map(row => `| \`${row.npmName}\` | [${row.upstream.replace('https://'
## Runtime npm dependencies
External packages that a workspace package resolves at runtime. \`scripts/install.sh\` installs this repository itself, so the tier covers every plugin a user can mount from \`cordis.yml\` — not only what the \`dsh\` CLI/TUI, the Web UI, and the Python SDK runtime load by default.
External packages that a workspace package resolves at runtime. \`scripts/install.sh\` installs this repository itself, so the tier covers every plugin a user can mount from \`cordis.yml\` — not only what the \`dsh\` CLI, Web UI, and Python SDK runtime load by default.
${renderNpmTable(runtimeDeps)}
pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification:
${patchedLines.join('\n')}
${renderClaudeDistribution(claudeDistribution)}
## Development-only npm dependencies

View File

@@ -14,11 +14,14 @@ import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { createScope } from '@deepseek-ai/dsh-scope'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SessionQuerySqlite from '@deepseek-ai/dsh-session-query-sqlite'
import GoalService from '@deepseek-ai/dsh-goal'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
import LocalBashExecutor from '@deepseek-ai/dsh-bash-local'
import * as BashEnvPlugin from '@deepseek-ai/dsh-bash-env'
import { PwshLocalExecutor } from '@deepseek-ai/dsh-pwsh-local'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
@@ -36,6 +39,7 @@ import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
import LocalTaskService from '@deepseek-ai/dsh-tasks-local'
import * as ToolAskUser from '@deepseek-ai/dsh-tool-ask-user'
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
import * as ToolPwsh from '@deepseek-ai/dsh-tool-pwsh'
import * as ToolBashPersistent from '@deepseek-ai/dsh-tool-bash-persistent'
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
@@ -172,7 +176,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
toolsConfig: { mode: 'code' },
async mount() {},
note:
'Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode Agent Note). Under `code` it is the registry\'s only wire contribution; the other visible capabilities are declared in a generated TypeScript SDK section, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.',
'Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode Agent Note). Under `code` it is the registry\'s only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime\'s language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.',
},
{
pkg: '@deepseek-ai/dsh-plan-mode',
@@ -190,16 +194,35 @@ const TOOL_PACKAGES: ToolPackage[] = [
pkg: '@deepseek-ai/dsh-tool-bash',
dir: 'tool-bash',
source: 'packages/bash/tool-bash/src/index.ts',
requires: ['ctx.tools', 'ctx.bash', 'ctx.tasks at call time for run_in_background'],
requires: ['ctx.tools', 'ctx.bash', 'ctx.systemPrompt', 'ctx.bashEnv', 'ctx.tasks at call time for run_in_background'],
writes: ['tool/call', 'tool/result'],
async mount(ctx) {
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(BashEnvPlugin)
await ctx.plugin(LocalBashExecutor)
await ctx.plugin(ToolBash)
},
note:
'The bash tool is the model-facing consumer of the bash executor seam. A `run_in_background` run registers with the generic `ctx.tasks` runtime and is collected/stopped through the `task_*` tools from `@deepseek-ai/dsh-tool-tasks`; the `enableRunInBackground` config (default true) removes the parameter entirely when disabled.',
},
{
pkg: '@deepseek-ai/dsh-tool-pwsh',
dir: 'tool-pwsh',
source: 'packages/bash/tool-pwsh/src/index.ts',
requires: ['ctx.tools', 'ctx.bash', 'ctx.systemPrompt', 'ctx.bashEnv', 'ctx.tasks at call time for run_in_background'],
writes: ['tool/call', 'tool/result'],
async mount(ctx) {
// The pwsh tool consumes the bash executor seam; the schema harvest
// mounts the pwsh-local implementation so the inject resolves without
// executing anything (registration never spawns a process).
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(BashEnvPlugin)
await ctx.plugin(PwshLocalExecutor)
await ctx.plugin(ToolPwsh)
},
note:
'The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.bash`); it mirrors the bash tool call-for-call minus the sandbox surface — `run_in_background` runs register with the generic `ctx.tasks` runtime and are collected/stopped through the `task_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-bash-env`. Each call runs in a fresh process (no persistent PTY session; ConPTY is roadmap work), with native `C:\\...` paths and `$env:NAME` variables.',
},
{
pkg: '@deepseek-ai/dsh-tool-cordis',
dir: 'tool-cordis',
@@ -289,7 +312,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
dir: 'tool-goal',
source: 'packages/goal/tool-goal/src/index.ts',
requires: ['ctx.tools', 'ctx.agents', 'ctx.goals', 'ctx.systemPrompt', 'a calling Agent in an authorized open turn'],
writes: ['tool/call', 'user/message goal snapshot for mutations', 'tool/result'],
writes: ['tool/call', 'goal/change for mutations', 'tool/result'],
async mount(ctx) {
await ctx.plugin(AgentRegistry)
await ctx.plugin(GoalService)
@@ -370,7 +393,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
await ctx.plugin(ToolSubagent, { provider: 'mock' })
},
note:
'The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `apps/cli/config/base.cordis.yml` and `examples/acp-agent/cordis.yml`.',
'The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `packages/bundle/base/cordis.patch.yml` and `examples/acp-agent/cordis.yml`.',
},
{
pkg: '@deepseek-ai/dsh-tool-subagent-control',
@@ -379,19 +402,19 @@ const TOOL_PACKAGES: ToolPackage[] = [
list_agents: 'packages/subagent/tool-subagent-control/src/list-agents.ts',
send_message: 'packages/subagent/tool-subagent-control/src/index.ts',
},
requires: ['ctx.tools', 'ctx.subagents', 'ctx.sessionQuery (list_agents only)'],
requires: ['ctx.tools', 'ctx.subagents', 'ctx.sessionProjections (list_agents catalog rows)'],
writes: ['tool/call', 'tool/result', 'child session events through ctx.subagents'],
async mount(ctx) {
await ctx.plugin(SubagentService)
await ctx.plugin(LocalTaskService)
await ctx.plugin(AgentRegistry)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionQuerySqlite, { path: ':memory:' })
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(ToolSubagentControl)
await ctx.plugin(ToolSubagentListAgents)
},
note:
'The globally named control tools over continuable background subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` once, plus `list_agents` from its separately loaded `/list-agents` plugin (which additionally requires session query).',
'The globally named control tools over continuable background subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows are served through the sessionProjections registry).',
},
{
pkg: '@deepseek-ai/dsh-tool-subagent-report',
@@ -432,10 +455,10 @@ const TOOL_PACKAGES: ToolPackage[] = [
requires: ['ctx.tools', 'owning Agent session'],
writes: ['tool/call', 'todo/write', 'tool/result'],
async mount(ctx) {
await ctx.plugin(ToolTodo)
await ctx.plugin(ToolTodo, { allowParallelInProgress: true })
},
note:
'todo_write is session-owned state; UIs render the latest todo/write event as a checklist.',
'todo_write is session-owned state; UIs render the latest todo/write event as a checklist. `allowParallelInProgress` is required with no default, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task.',
},
{
pkg: '@deepseek-ai/dsh-tool-workflow',
@@ -588,7 +611,7 @@ export function render(catalog: ToolCatalog): string {
'',
'This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any package is missing from the generator\'s boot manifest, so a new tool cannot be silently undocumented. See [the tool-schema-catalog Agent Note](../.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md).',
'',
'Scope: shipped product tools under `packages/*/tool-*`, each booted with its DEFAULT config. The registered tool NAME can be a load-time config (e.g. `tool-subagent`\'s `toolName`), so a deployment may surface a package under a different or additional name — a per-package note records those shipped aliases where they exist. The `examples/` demo tools (e.g. `echo`) are excluded, matching the cordis catalog\'s packages-only scope.',
'Scope: shipped product tools under `packages/*/tool-*`, each booted with its DEFAULT config, except where a Config field is REQUIRED with no default — there the generator must choose, and the per-package note records which branch this page shows. The registered tool NAME can be a load-time config (e.g. `tool-subagent`\'s `toolName`), so a deployment may surface a package under a different or additional name — a per-package note records those shipped aliases where they exist. The `examples/` demo tools (e.g. `echo`) are excluded, matching the cordis catalog\'s packages-only scope.',
'',
'## Tool Package Map',
'',

View File

@@ -14,6 +14,7 @@ import {
} from 'node:fs'
import { spawnSync } from 'node:child_process'
import { dirname, isAbsolute, join, resolve } from 'node:path'
import lefthookPackage from 'lefthook/package.json' with { type: 'json' }
const MINIMUM_GIT = [2, 26, 0]
const HOOKS_DIRECTORY = 'dsh-hooks'
@@ -596,6 +597,7 @@ function refuseScopedHooksPath(entry) {
async function main() {
if (process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true') return
if (typeof lefthookPackage.bin?.lefthook !== 'string') return
const probe = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' })
if (probe.status !== 0) return
const root = stripGitLineTerminator(probe.stdout)

View File

@@ -19,6 +19,9 @@ import { afterEach, describe, expect, it } from 'vitest'
const installer = fileURLToPath(new URL('./install-lefthook.mjs', import.meta.url))
const fixtures: string[] = []
// Multi-worktree cases spawn several Git and Node subprocesses; coverage concurrency can
// legitimately exceed Vitest's default deadline without changing the installer behavior.
const MULTI_PROCESS_TEST_TIMEOUT_MS = 20_000
interface Fixture {
container: string
@@ -260,7 +263,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
git(fixture, fixture.main, ['worktree', 'remove', '--force', fixture.linked])
expect(readFileSync(join(mainHooks, 'pre-commit'), 'utf8')).toBe(mainHookBeforeRemoval)
expect(readFileSync(legacyHook, 'utf8')).toBe('#!/bin/sh\n# legacy hook\n')
})
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
it('replaces the owned hook path Git copies into a newly added worktree', async () => {
const fixture = createFixture()
@@ -284,7 +287,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
'# config=late-linked-worktree-config',
)
expect(readFileSync(join(mainHooks, 'pre-commit'), 'utf8')).toBe(mainHookBefore)
})
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
it('serializes concurrent installs and keeps repeated output stable', async () => {
const fixture = createFixture()
@@ -305,7 +308,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
expect(readFileSync(mainHookPath, 'utf8')).toBe(initialHook)
expect(existsSync(join(commonDirectory(fixture), 'dsh-lefthook-install.lock'))).toBe(false)
expect(existsSync(join(hooksPath(fixture, fixture.main), '.fake-lefthook-running'))).toBe(false)
})
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
it('waits for a concurrent installer to finish publishing its lock record', async () => {
const fixture = createFixture()
@@ -343,7 +346,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
expect(readFileSync(join(movedHooks, '.dsh-lefthook-owned'), 'utf8')).toContain(
JSON.stringify(movedHooks),
)
})
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
it.skipIf(process.platform === 'win32')('refuses a multiply linked ownership marker before relocation rewrites it', async () => {
const fixture = createFixture()
@@ -384,7 +387,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
expect(result.stderr).toContain('non-regular or multiply linked hook entry')
expect(readFileSync(externalHook, 'utf8')).toBe(externalContent)
}
})
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
it('restores the marker-backed stale hook path when relocation reinstall fails', async () => {
const fixture = createFixture()

View File

@@ -46,7 +46,7 @@
# DSH_MASTER master clone directory (default: $DSH_SOURCE/master)
# DSH_CURRENT stable symlink to the active worktree (default: $DSH_SOURCE/current)
# DSH_BIN_DIR directory the `dsh` symlink lands in (default: ~/.local/bin)
# DSH_HOME Harness home holding the personal config (default: ~/.dsh)
# DSH_HOME Harness home holding profiles and user patches (default: ~/.dsh)
# FIXME(install-ts): Move the post-checkout workflow into a tested TypeScript
# entrypoint; keep this POSIX shell file as the curl/source bootstrap.
set -eu

View File

@@ -47,7 +47,7 @@ function fixture(options: {
default: './lib/invariant.js',
},
},
files: ['lib/index.js', 'lib/invariant.js', 'src'],
files: ['lib/index.js', 'lib/invariant.js'],
peerDependencies: options.invariantDependency === false ? {} : {
'@deepseek-ai/dsh-invariants': '^0.0.1',
},

View File

@@ -1,12 +1,14 @@
/** Tests for the documentation website projection adapter. */
import { execFileSync } from 'node:child_process'
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { docsPages, type DocsPage } from '../website/docs.ts'
import { addProjectionFrontmatter, projectedPageContent, rewriteMarkdown } from './project-doc-site.ts'
import {
addProjectionFrontmatter, projectedPageContent, publishableImage, rewriteMarkdown,
} from './project-doc-site.ts'
const roots: string[] = []
const repositoryRoot = resolve(import.meta.dirname, '..')
@@ -63,6 +65,32 @@ describe('website source layout', () => {
})
})
describe('publishableImage', () => {
it('accepts a regular file inside the repository', () => {
const { root } = fixture()
const real = realpathSync(join(root, 'packages/logo.svg'))
expect(publishableImage(join(root, 'packages/logo.svg'), realpathSync(root))).toBe(real)
})
it('refuses a target whose real path escapes the repository', () => {
// Publication copies the bytes onto the site, so a reference reaching a
// build-machine file must not be treated as an image the repository owns.
const { root } = fixture()
const outside = mkdtempSync(join(tmpdir(), 'dsh-doc-site-outside-'))
roots.push(outside)
writeFileSync(join(outside, 'secret.png'), 'not really a png\n')
symlinkSync(join(outside, 'secret.png'), join(root, 'packages/linked.png'))
expect(publishableImage(join(root, 'packages/linked.png'), realpathSync(root))).toBeUndefined()
expect(publishableImage(join(outside, 'secret.png'), realpathSync(root))).toBeUndefined()
})
it('refuses a directory', () => {
const { root } = fixture()
expect(publishableImage(join(root, 'packages'), realpathSync(root))).toBeUndefined()
})
})
describe('rewriteMarkdown', () => {
it('maps published pages and pins unpublished source links', () => {
const { root, pages } = fixture()
@@ -93,7 +121,7 @@ describe('rewriteMarkdown', () => {
})).toBe('[B](./reference-root/b.md)\n')
})
it('uses raw GitHub content for unpublished images', () => {
it('uses raw GitHub content for unpublished images when nothing places them', () => {
const { root, pages } = fixture()
expect(rewriteMarkdown('![logo](../packages/logo.svg)\n', {
locale: 'en',
@@ -105,6 +133,57 @@ describe('rewriteMarkdown', () => {
})).toBe('![logo](https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/abc123/packages/logo.svg)\n')
})
it('hands an image to the placer and uses the URL it returns', () => {
// A raw GitHub URL cannot serve a private repository, so the site build
// carries images itself; the placer is what puts them there. The stand-in
// derives its URL the way the real one does, so a placer that stopped
// returning the basename would fail here rather than pass on a constant.
const { root, pages } = fixture()
const placed: string[] = []
expect(rewriteMarkdown('![logo](../packages/logo.svg)\n', {
locale: 'en',
sourcePath: 'docs/a.md',
route: 'en/a.md',
pages,
repoRoot: root,
repositoryRef: 'abc123',
placeImage: (absPath) => {
const name = absPath.split('/').pop() ?? ''
placed.push(name)
return `./${name}`
},
})).toBe('![logo](./logo.svg)\n')
expect(placed).toEqual(['logo.svg'])
})
it('keeps a placed image\u2019s query or fragment', () => {
// An SVG view fragment and a Vite query both change what the reference
// means, and the GitHub branch has always carried them.
const { root, pages } = fixture()
expect(rewriteMarkdown('![logo](../packages/logo.svg#view)\n', {
locale: 'en',
sourcePath: 'docs/a.md',
route: 'en/a.md',
pages,
repoRoot: root,
repositoryRef: 'abc123',
placeImage: absPath => `./${absPath.split('/').pop() ?? ''}`,
})).toBe('![logo](./logo.svg#view)\n')
})
it('leaves a published page link to the route even when a placer exists', () => {
const { root, pages } = fixture()
expect(rewriteMarkdown('[B](b.md)\n', {
locale: 'en',
sourcePath: 'docs/a.md',
route: 'en/a.md',
pages,
repoRoot: root,
repositoryRef: 'abc123',
placeImage: () => { throw new Error('a page link must not be placed as an asset') },
})).toBe('[B](./reference/b.md)\n')
})
it('does not rewrite Markdown-looking text inside code fences', () => {
const { root, pages } = fixture()
const source = '```md\n[B](b.md)\n```\n'

View File

@@ -5,8 +5,10 @@
* tier, while this adapter rewrites cross-source links for the public site.
*/
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { dirname, extname, posix, relative, resolve, sep } from 'node:path'
import {
copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync,
} from 'node:fs'
import { basename, dirname, extname, posix, relative, resolve, sep } from 'node:path'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { gfmFromMarkdown } from 'mdast-util-gfm'
import { gfm } from 'micromark-extension-gfm'
@@ -38,6 +40,15 @@ export interface RewriteMarkdownOptions {
pages: DocsPage[]
repoRoot: string
repositoryRef: string
/**
* Place one referenced image beside the projected page and return the URL to
* reach it from that page. A GitHub raw URL cannot serve this repository —
* `raw.githubusercontent.com` answers 404 for a private one, and no reader of
* the site is authenticated to it — so an image travels into the generated
* tree and Vite bundles it like any other site asset. Omitted by callers that
* only rewrite text, which then leave images pointing at the repository.
*/
placeImage?: (absPath: string) => string
}
function repoPath(absPath: string, repoRoot: string): string {
@@ -222,9 +233,13 @@ export function rewriteMarkdown(source: string, options: RewriteMarkdownOptions)
? options.locale === 'root' ? 'en' : 'root'
: options.locale
const page = published.get(targetPath)?.get(targetLocale)
const nextUrl = page === undefined
? githubTarget(absPath, line, suffix, options.repositoryRef, options.repoRoot, node.type === 'image')
: routeTarget(options.route, page.route, suffix)
const nextUrl = page !== undefined
? routeTarget(options.route, page.route, suffix)
: node.type === 'image' && options.placeImage !== undefined
// The suffix rides along exactly as the GitHub branch keeps it: an SVG
// view fragment or a Vite query changes what the reference means.
? `${options.placeImage(absPath)}${suffix}`
: githubTarget(absPath, line, suffix, options.repositoryRef, options.repoRoot, node.type === 'image')
const start = node.position?.start.offset
const end = node.position?.end.offset
@@ -291,17 +306,78 @@ export function projectedPageContent(markdown: string, page: DocsPage): string {
return markdown.slice(0, closing + closingDelimiter.length)
}
/** Canonical Markdown files watched by the local VitePress dev server. */
/**
* The repository file one image reference resolves to, or `undefined` when the
* target is not a local file this build may publish.
* @param absPath - resolved image target.
* @param repoRoot - repository root every published image must stay inside.
* @returns the file's real path, or `undefined` when it must not be copied.
*
* Only a regular file whose real path stays inside the repository qualifies.
* Publication copies the bytes into the site, so a reference escaping the
* repository — `../../.ssh/id_rsa`, or a symlink pointing out of the tree —
* would put a build-machine file on the site; `existsSync` alone, which is all
* link resolution needs, does not answer that.
*/
export function publishableImage(absPath: string, repoRoot: string): string | undefined {
const real = realpathSync(absPath)
const inside = real === repoRoot || real.startsWith(`${repoRoot}${sep}`)
return inside && statSync(real).isFile() ? real : undefined
}
/** Every local image a published page references, resolved to its repository file. */
function referencedImages(): string[] {
const found = new Set<string>()
for (const page of docsPages) {
const sourceAbs = resolve(root, page.source)
if (!existsSync(sourceAbs)) continue
rewriteMarkdown(readFileSync(sourceAbs, 'utf8'), {
sourcePath: page.source,
locale: page.locale,
route: page.route,
pages: docsPages,
repoRoot: root,
repositoryRef: 'master',
placeImage: (absPath) => {
const real = publishableImage(absPath, root)
if (real !== undefined) found.add(real)
return ''
},
})
}
return [...found]
}
/**
* Files watched by the local VitePress dev server: every canonical Markdown
* source, plus the images they publish. Without the images, replacing a
* screenshot leaves the previous copy in the generated tree until something
* touches the Markdown beside it.
*/
export function docsSourceFiles(): string[] {
return [...new Set(docsPages.map(page => resolve(root, page.source)))]
return [...new Set([...docsPages.map(page => resolve(root, page.source)), ...referencedImages()])]
}
/** Rebuild the disposable VitePress source tree from the publication manifest. */
export function projectDocs(): void {
const routes = new Set<string>()
/** Projected path to the repository file that claimed it, pages and images alike. */
const claimed = new Map<string, string>()
const repositoryRef = process.env.GITHUB_SHA ?? 'master'
rmSync(generatedRoot, { recursive: true, force: true })
/** Reserve one projected path, refusing a second source for it. */
const claim = (target: string, sourceAbs: string): void => {
const holder = claimed.get(target)
if (holder !== undefined && holder !== sourceAbs) {
throw new Error(
`project-doc-site: ${repoPath(sourceAbs, root)} and ${repoPath(holder, root)}`
+ ` both project to ${relative(generatedRoot, target).split(sep).join('/')}.`,
)
}
claimed.set(target, sourceAbs)
}
for (const page of docsPages) {
if (routes.has(page.route)) throw new Error(`project-doc-site: duplicate route ${JSON.stringify(page.route)}.`)
routes.add(page.route)
@@ -310,6 +386,9 @@ export function projectDocs(): void {
throw new Error(`project-doc-site: source ${JSON.stringify(page.source)} does not exist or is not a file.`)
}
const output = resolve(generatedRoot, page.route)
// Claimed before the images are placed: a page and an image landing on one
// path would otherwise overwrite each other in whichever order they ran.
claim(output, sourceAbs)
mkdirSync(dirname(output), { recursive: true })
const markdown = readFileSync(sourceAbs, 'utf8')
const projected = rewriteMarkdown(markdown, {
@@ -319,6 +398,25 @@ export function projectDocs(): void {
pages: docsPages,
repoRoot: root,
repositoryRef,
placeImage: (absPath) => {
const real = publishableImage(absPath, root)
if (real === undefined) {
throw new Error(
`project-doc-site: ${page.source} references image ${repoPath(absPath, root)},`
+ ' which is not a regular file inside the repository.',
)
}
// Beside the page that references it, under its own basename: each
// locale's route tree gets its own copy, so one relative URL is correct
// from both.
const name = basename(real)
const target = resolve(dirname(output), name)
claim(target, real)
copyFileSync(real, target)
// Encoded because the destination is a Markdown inline target, where an
// unescaped space would end it early.
return `./${encodeURI(name)}`
},
})
writeFileSync(output, addProjectionFrontmatter(projectedPageContent(projected, page), page))
}

View File

@@ -0,0 +1,83 @@
import { describe, expect, it } from 'vitest'
import {
hasTypeRTRemoteNavigation,
isForbiddenPublicationFile,
validateTarballPayload,
} from './publication-payload.ts'
function validateFixtureTarball(files: readonly string[]): () => void {
return () => {
validateTarballPayload(files, 'fixture.tgz')
}
}
describe('publication payload policy', () => {
it.each([
'lib/index.js',
'lib/types/index.d.ts',
'lib/styles/base.css',
])('accepts %s', (file) => {
expect(isForbiddenPublicationFile(file)).toBe(false)
})
it.each([
'src',
'./src',
'src/',
'src/index.ts',
'./src/index.ts',
String.raw`src\index.ts`,
'lib/types/index.d.ts.map',
'./lib/types/index.d.ts.map',
])('rejects static manifest path %s', (file) => {
expect(isForbiddenPublicationFile(file)).toBe(true)
})
it('rejects source members in packed tarballs', () => {
expect(validateFixtureTarball([
'package/package.json',
'package/src/index.ts',
])).toThrow('fixture.tgz publishes source file package/src/index.ts')
})
it('rejects declaration maps in packed tarballs', () => {
expect(validateFixtureTarball([
'package/package.json',
'package/lib/types/index.d.ts.map',
])).toThrow('fixture.tgz publishes declaration map package/lib/types/index.d.ts.map')
})
it('accepts a clean packed tarball', () => {
expect(validateFixtureTarball([
'package/package.json',
'package/lib/index.js',
'package/lib/types/index.d.ts',
'package/lib/styles/base.css',
])).not.toThrow()
})
it('allows only the TypeRT declaration map and its navigable source tree when requested', () => {
const policy = { typeRTRemoteNavigation: true }
expect(isForbiddenPublicationFile('src/index.ts', policy)).toBe(false)
expect(isForbiddenPublicationFile('lib/typert.remote-client.d.ts.map', policy)).toBe(false)
expect(isForbiddenPublicationFile('lib/types/index.d.ts.map', policy)).toBe(true)
expect(() => {
validateTarballPayload([
'package/lib/typert.remote-client.d.ts.map',
'package/src/index.ts',
], 'fixture.tgz', policy)
}).not.toThrow()
})
it('recognizes only the canonical Host-for-Client export pair', () => {
expect(hasTypeRTRemoteNavigation({
exports: {
'./remote': {
types: './lib/typert.remote-client.d.ts',
default: './lib/typert.remote-client.js',
},
},
})).toBe(true)
expect(hasTypeRTRemoteNavigation({ exports: { './remote': './lib/remote.js' } })).toBe(false)
})
})

View File

@@ -0,0 +1,57 @@
/** Publication payload policy shared by static manifests and packed tarballs. */
/** Publication exceptions required for TypeRT declaration-map navigation. */
export interface PublicationPayloadPolicy {
readonly typeRTRemoteNavigation?: boolean
}
/** Whether a package manifest exports generated Host-for-Client metadata with source navigation. */
export function hasTypeRTRemoteNavigation(manifest: unknown): boolean {
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) return false
const exportsField = (manifest as Record<string, unknown>).exports
if (exportsField === null || typeof exportsField !== 'object' || Array.isArray(exportsField)) return false
const remote = (exportsField as Record<string, unknown>)['./remote']
if (remote === null || typeof remote !== 'object' || Array.isArray(remote)) return false
const entry = remote as Record<string, unknown>
return entry.types === './lib/typert.remote-client.d.ts'
&& entry.default === './lib/typert.remote-client.js'
}
/** Normalize a package manifest path or npm tarball member to its payload-relative path. */
function payloadPath(file: string): string {
const normalized = file.replaceAll('\\', '/').replace(/^\.\/+/, '').replace(/\/+$/, '')
return normalized.startsWith('package/') ? normalized.slice('package/'.length) : normalized
}
/** Whether a package payload path exposes source or declaration-map intermediates. */
export function isForbiddenPublicationFile(
file: string,
policy: PublicationPayloadPolicy = {},
): boolean {
const normalized = payloadPath(file)
if (policy.typeRTRemoteNavigation === true
&& (normalized === 'src'
|| normalized.startsWith('src/')
|| normalized === 'lib/typert.remote-client.d.ts.map')) {
return false
}
return normalized === 'src'
|| normalized.startsWith('src/')
|| normalized.endsWith('.d.ts.map')
}
/** Reject source and declaration-map members in a packed npm tarball. */
export function validateTarballPayload(
files: readonly string[],
context: string,
policy: PublicationPayloadPolicy = {},
): void {
for (const file of files) {
if (!isForbiddenPublicationFile(file, policy)) continue
const normalized = payloadPath(file)
if (normalized === 'src' || normalized.startsWith('src/')) {
throw new Error(`${context} publishes source file ${file}`)
}
throw new Error(`${context} publishes declaration map ${file}`)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -189,6 +189,12 @@ describe('Node 24 lane ownership', () => {
expect(subject.find(item => item.id === 'doc-typecheck')?.env).toEqual({
DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1',
})
expect(subject.find(item => item.id === 'built-bin-smoke')?.args).toEqual(
expect.arrayContaining([
'packages/subagent/subagent-codex/tests/loader-composition.e2e.ts',
'packages/subagent/subagent-claude-code/tests/loader-composition.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

@@ -602,7 +602,11 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate {
'apps/cli/tests/built-bin.e2e.ts',
'packages/examples/cli-demo/tests/built-bin.e2e.ts',
'packages/examples/acp-demo/tests/built-bin.e2e.ts',
'packages/host/directory-picker-native/tests/built-worker.e2e.ts',
'packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts',
'packages/subagent/subagent-codex/tests/loader-composition.e2e.ts',
'packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts',
'packages/api/remotes/tests/built-lib.e2e.ts',
// The worker-entry packages' built bundles: the only automated proof
// that lib/index.js resolves its sibling lib/worker.cjs under plain node
// (the e2e lane runs unbuilt, so these files self-skip there).

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
/** Unit tests for the prompt-v4 renderer and three-section response parser. */
/** Unit tests for the prompt-v7 content and unchanged three-section protocol. */
import { readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
@@ -15,6 +15,20 @@ const root = resolve(import.meta.dirname, '..')
const document = readFileSync(join(root, 'docs/i18n/translation-prompt.md'), 'utf8')
const terminology = '| English | 中文 |\n|---|---|\n| agent | agent |'
const retainedExamples = [
['### Colloquial verb → Professional verb', 'The repo pins pnpm@11.7.0 in package.json', '该仓库在 package.json 中固定使用 pnpm@11.7.0'],
['### Run-on sentence → Natural phrasing with pause', 'Read docs/architecture.md before changing anything under packages/.', '在修改 packages/ 目录下的任何内容之前,请先阅读 docs/architecture.md。'],
['### 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', '不对照原文时,更容易察觉别扭的表达'],
['### 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', '面向开发者:请先阅读开发指南'],
['### Code block comments — NEVER translate', '# full-screen TUI coding agent (needs DEEPSEEK_API_KEY)', 'keep exactly as-is, byte-for-byte'],
['### Language switcher — flip direction', 'English | [中文](README.zh.md)', '[English](README.md) | 中文'],
]
describe('translation prompt rendering', () => {
it('renders both directions with every placeholder resolved', () => {
const en = renderTranslationPrompt(document, { sourceLanguage: 'English', sourceFilename: 'guide.md', terminology })
@@ -22,15 +36,31 @@ describe('translation prompt rendering', () => {
expect(en).toContain(terminology)
expect(en).not.toContain('{{')
expect(en).toContain('plain source stays plain (必须)')
expect(en).toContain('When the target language is English, use the "English" column without a Chinese gloss')
expect(en).toContain('for a Chinese target, use an established Chinese rendering')
expect(en).toContain('for an English target, use the established English technical term')
expect(en).toContain('does an English target use established English terminology')
expect(en).toContain('For an English target, use the established English technical term')
expect(en).toContain('does a Chinese target use an established Chinese rendering')
expect(en).toContain('does an English target use the established English technical term')
expect(en).toContain('The parser removes exactly one framing escape')
const zh = renderTranslationPrompt(document, { sourceLanguage: 'Chinese', sourceFilename: 'guide.zh.md', terminology })
expect(zh).toContain('from Chinese to English')
})
it('retains every v4 embedded example', () => {
for (const example of retainedExamples) {
for (const fragment of example) expect(document).toContain(fragment)
}
})
it('states the selected v7 safeguards', () => {
const rendered = renderTranslationPrompt(document, { sourceLanguage: 'English', sourceFilename: 'guide.md', terminology })
expect(rendered).toContain('## Priority')
expect(rendered).toContain('### Faithfulness')
expect(rendered).toContain('do not invent a filename or switcher')
expect(rendered).toContain('Markdown emphasis markers do not create a word boundary')
expect(rendered).toContain('Never invent responsibility merely to avoid a passive construction')
expect(rendered).toContain('Never vary a terminology-table form, defined concept, or contract verb merely for stylistic variety')
expect(rendered).toContain('Return exactly three raw XML sections')
})
it('rejects a template with unknown or missing placeholders', () => {
const alien = document.replaceAll('{{terminology}}', '{{terms_prompt}}')
expect(() => renderTranslationPrompt(alien, { sourceLanguage: 'English', sourceFilename: 'guide.md', terminology })).toThrow(/unsupported placeholder/)

View File

@@ -26,6 +26,21 @@
"symbol": "MessageSourceMap",
"source": "packages/llm/llm/src/message.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "ContextForm",
"source": "packages/llm/llm/src/message.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "ContextSnapshotSection",
"source": "packages/llm/llm/src/message.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "ContextFormed",
"source": "packages/llm/llm/src/message.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "FinishReasonMap",
@@ -46,6 +61,16 @@
"symbol": "LlmModelInfo",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "LlmModelDiscoveryRequest",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "LlmDiscoveredModel",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "LlmModelContext",
@@ -98,33 +123,8 @@
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "SendTarget",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "InboxPlacement",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "InboxItem",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "InboxAction",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "InboxActionResult",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "SendOptions",
"source": "packages/core/agent/src/types.ts"
"symbol": "InboxTarget",
"source": "packages/core/agent/src/inbox.ts"
},
{
"doc": "docs/core-data-structures/core.md",
@@ -134,7 +134,7 @@
{
"doc": "docs/core-data-structures/core.md",
"symbol": "AgentCancelCause",
"source": "packages/core/agent/src/types.ts"
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.md",
@@ -143,7 +143,7 @@
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "PromptDecision",
"symbol": "PreStepDecision",
"source": "packages/core/agent/src/types.ts"
},
{
@@ -151,11 +151,6 @@
"symbol": "RequestErrorAction",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "RequestError",
"source": "packages/core/agent/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "SessionStartSource",
@@ -204,7 +199,7 @@
{
"doc": "docs/core-data-structures/goal.md",
"symbol": "GoalView",
"source": "packages/goal/goal/src/domain.ts"
"source": "packages/goal/goal/src/types.ts"
},
{
"doc": "docs/core-data-structures/goal.md",
@@ -224,12 +219,12 @@
{
"doc": "docs/core-data-structures/goal.md",
"symbol": "CreateGoalRequest",
"source": "packages/goal/goal/src/domain.ts"
"source": "packages/goal/goal/src/types.ts"
},
{
"doc": "docs/core-data-structures/goal.md",
"symbol": "EditGoalRequest",
"source": "packages/goal/goal/src/domain.ts"
"source": "packages/goal/goal/src/types.ts"
},
{
"doc": "docs/core-data-structures/goal.md",
@@ -370,7 +365,7 @@
},
{
"doc": "docs/core-data-structures/session.md",
"symbol": "TurnTriggerMap",
"symbol": "TurnEndCancelCause",
"source": "packages/core/session/src/types.ts"
},
{
@@ -424,6 +419,32 @@
"symbol": "CreateSessionOptions",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/persistence.md",
"symbol": "RestoredSessionOptions",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/persistence.md",
"symbol": "PrepareSessionOptions",
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/core-data-structures/persistence.md",
"symbol": "SessionPreparationOptions",
"source": "packages/core/session/src/preparation.ts"
},
{
"doc": "docs/core-data-structures/persistence.md",
"symbol": "SessionPreparation",
"source": "packages/core/session/src/preparation.ts",
"projection": "public-api"
},
{
"doc": "docs/core-data-structures/persistence.md",
"symbol": "SessionInspection",
"source": "packages/session-persistence/session-persistence/src/index.ts"
},
{
"doc": "docs/core-data-structures/persistence.md",
"symbol": "SessionLocation",
@@ -889,6 +910,11 @@
"symbol": "SandboxPolicyRequest",
"source": "packages/sandbox/sandbox-policy/src/index.ts"
},
{
"doc": "docs/core-data-structures/sandbox.md",
"symbol": "RunnerFailureRule",
"source": "packages/sandbox/sandbox/src/index.ts"
},
{
"doc": "docs/core-data-structures/sandbox.md",
"symbol": "ConfinedArgv",
@@ -1468,6 +1494,66 @@
"doc": "docs/core-data-structures/settings.md",
"symbol": "SettingsPathOp",
"source": "packages/settings/settings/src/index.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypeRTLookupMap",
"source": "packages/typert/type-meta/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypeRTContextMap",
"source": "packages/typert/type-meta/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypeRTLookupDefinition",
"source": "packages/typert/type-meta/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypeRTCodec",
"source": "packages/typert/type-meta/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "InvocationParameterDescriptor",
"source": "packages/typert/type-meta/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "InvocationDescriptor",
"source": "packages/typert/type-meta/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypeRTService",
"source": "packages/typert/type-meta/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypeRTRemoteNamespaceMap",
"source": "packages/typert/type-meta/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "InvokeRemoteRequest",
"source": "packages/api/gateway/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypertGatewayErrorCode",
"source": "packages/api/gateway/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypertGateway",
"source": "packages/api/gateway/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypeRTClientRemote",
"source": "packages/typert/type-meta/src/types.ts"
}
]
}

View File

@@ -149,11 +149,33 @@ function validateExampleResolution(): string[] {
}
function validateAppResolution(): string[] {
const dependencies = readManifest('apps/cli/package.json').dependencies ?? {}
const violations: string[] = []
// App overlays (and any config left under apps/cli/config) resolve from the
// dsh app's own dependency surface — the profile module fallback mirrors it.
const appDependencies = {
...readManifest('apps/cli/package.json').dependencies,
// The fallback also links every bundle's own dependencies (healProfilesModuleFallback).
...Object.fromEntries(globSync('packages/bundle/*/package.json', { cwd: root })
.flatMap(file => Object.entries(readManifest(file).dependencies ?? {}))),
}
const shipped = new Set(globSync('*.cordis.yml', { cwd: resolve(root, 'apps/cli/config') })
.map(file => `apps/cli/config/${file}`))
const references = pluginReferences.filter(reference => shipped.has(reference.file) || appOverlayFiles.has(reference.file))
return missingPluginDependencies(references, dependencies, 'apps/cli/package.json')
const appReferences = pluginReferences.filter(reference => shipped.has(reference.file) || appOverlayFiles.has(reference.file))
violations.push(...missingPluginDependencies(appReferences, appDependencies, 'apps/cli/package.json or a bundle manifest'))
// Each bundle's patch rows must resolve from that bundle's own dependencies:
// per-layer resolution anchors on the bundle package directory.
for (const manifestPath of globSync('packages/bundle/*/package.json', { cwd: root })) {
const bundleDir = manifestPath.replace(/\/package\.json$/, '')
const manifest = readManifest(manifestPath)
const references = pluginReferences.filter(reference => reference.file.startsWith(`${bundleDir}/`))
violations.push(...missingPluginDependencies(
// A bundle may mount its own package (the web-app runtime row).
references.filter(reference => packageNameFromSpecifier(reference.name) !== manifest.name),
manifest.dependencies ?? {},
manifestPath,
))
}
return violations
}
/**

View File

@@ -42,7 +42,9 @@ const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
*/
const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'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.' },
'packages/bash/pwsh-local': { kind: 'indirect', reason: 'The executor backend delegates model rendering to dsh-tool-pwsh.' },
'packages/code-runtime/code-runtime': { kind: 'indirect', reason: 'The service interface delegates model rendering to Code Mode in dsh-tools.' },
'packages/code-runtime/code-runtime-worker': { kind: 'indirect', reason: 'The worker backend delegates model rendering to Code Mode in dsh-tools.' },
'packages/typert/registry': { kind: 'none', reason: 'Runtime type registry; consumers (cordis_inspect, wire faces, gates) own any model-visible projection of registry contents.' },
@@ -55,10 +57,12 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/client/web-react': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/schema-form': { kind: 'none', reason: 'Browser-side form-rendering library; registers no model surface.' },
'packages/client/connection': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/api/remotes': { kind: 'none', reason: 'The Remote BFF selects business methods and identity policy; selected services own any model-visible effect.' },
'packages/client/runtime': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-layout': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-sidebar': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-conversation': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-deliverables': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-slash': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-command': { kind: 'indirect', reason: 'The dispatch paths trigger the host command.execute RPC; each command handler\'s host package owns any model-visible effect.' },
'packages/client/ui-model': { kind: 'indirect', reason: 'Selection routes session.selectModel; the host snapshots the target at the next prompt-assembly boundary and owns the model-visible effect.' },
@@ -84,6 +88,9 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/host/directory-picker-browse': { kind: 'none', reason: 'The GUI-host picking backend registers no model surface.' },
'packages/host/directory-picker-native': { kind: 'none', reason: 'The GUI-host picking backend registers no model surface.' },
'packages/host/webserver': { kind: 'none', reason: 'The HTTP carrier bridges browser and API handler and registers no model surface.' },
'packages/host/frontend-static': { kind: 'none', reason: 'The SPA dist server answers browser asset requests and registers no model surface.' },
'packages/bundle/base': { kind: 'indirect', reason: 'The bundle is a patch-list carrier; each inserted row\'s package owns its model surface.' },
'packages/bundle/headless': { kind: 'none', reason: 'The one-shot runner submits the task as an ordinary user message; prompts and tools belong to the composed base/web bundles.' },
'packages/llm/llm': { kind: 'none', reason: 'The adapter registry forwards already-assembled requests unchanged.' },
'packages/llm/token-meter': { kind: 'indirect', reason: 'The measurement service leaves model-visible changes to its consumers.' },
'packages/lsp/lsp': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-lsp.' },
@@ -119,6 +126,8 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/support/loader-smoke': { kind: 'none', reason: 'The test harness observes child-process streams without changing live requests.' },
'packages/support/llm-mock-server': { kind: 'none', reason: 'The test server substitutes provider wire behavior without invoking a real model.' },
'packages/support/llm-replay': { kind: 'none', reason: 'The keyless adapter invokes no provider model.' },
'packages/api/gateway': { kind: 'none', reason: 'Remote dispatch infrastructure; invoked business methods own any model-visible effect.' },
'packages/typert/type-meta': { kind: 'none', reason: 'Compiler-independent Remote protocol declarations; registers no model surface.' },
'packages/typert/generator': { kind: 'none', reason: 'The build-time generator runs outside any agent runtime and touches no model request.' },
'packages/tasks/tasks': { kind: 'indirect', reason: 'Producer and control-surface plugins own all model rendering over the task registry.' },
'packages/tasks/tasks-local': { kind: 'indirect', reason: 'The registry backend delegates model rendering to producer plugins and dsh-tool-tasks.' },

View File

@@ -204,11 +204,14 @@ cat "$scratch/logs/smoke.log"
grep -q '^smoke: win32 x64' "$scratch/logs/smoke.log" || { echo 'wine-windows-gates: Windows Node smoke did not report win32 x64' >&2; exit 1; }
# ---- the two blocking surfaces, concurrently ------------------------------
# The same shape run-gates gives ci-windows-blocking on native Windows:
# `build` = tsc -b then tsdown, `production site` = the VitePress build. Both
# statuses are captured so one failure cannot hide the other's result.
# The build preserves the face order from package.json: generate Host contracts
# before either aggregate typecheck, then bundle the completed workspace.
# Both statuses are captured so one failure cannot hide the other's result.
build_gate() {
wine_node "$scratch/logs/tsc.log" "$tsc_js" -b --pretty false || return $?
wine_node "$scratch/logs/contracts-tsc.log" "$tsc_js" -b packages/typert/generator --pretty false || return $?
wine_node "$scratch/logs/contracts-tsdown.log" "$tsdown_js" --config tsdown.typert-host.config.ts || return $?
wine_node "$scratch/logs/host-tsc.log" "$tsc_js" -b tsconfig.host.json --pretty false || return $?
wine_node "$scratch/logs/client-tsc.log" "$tsc_js" -b tsconfig.client.json --pretty false || return $?
wine_node "$scratch/logs/tsdown.log" "$tsdown_js"
}
site_gate() {
@@ -235,7 +238,12 @@ report() {
for log in "$@"; do tail -n 200 "$log" >&2 || true; done
fi
}
report 'build (tsc -b, tsdown)' "$build_status" "$scratch/logs/tsc.log" "$scratch/logs/tsdown.log"
report 'build (contract prepass, tsc, tsdown)' "$build_status" \
"$scratch/logs/contracts-tsc.log" \
"$scratch/logs/contracts-tsdown.log" \
"$scratch/logs/host-tsc.log" \
"$scratch/logs/client-tsc.log" \
"$scratch/logs/tsdown.log"
report 'production site (vitepress build)' "$site_status" "$scratch/logs/site.log"
if (( build_status != 0 )); then exit "$build_status"; fi
exit "$site_status"