Merge latest master into Codex subagent provider

This commit is contained in:
Tianyi Cui
2026-08-06 21:26:31 +08:00
2612 changed files with 56217 additions and 27538 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 { 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',
@@ -133,8 +145,6 @@ function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
// declarations.
...usesEmittedTreeDefaults(manifest) ? ['lib/types/**/*.js'] : [],
'lib/types/**/*.d.ts',
'lib/types/**/*.d.ts.map',
'src',
]
}
@@ -164,7 +174,24 @@ 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/')) {
for (const file of manifest.files ?? []) {
if (isForbiddenPublicationFile(file)) {
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

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

@@ -1,6 +1,6 @@
{
"AGENTS.md": 1775,
"docs/AGENTS.md": 1150,
"docs/AGENTS.md": 1320,
"docs/architecture.md": 2160,
"docs/cordis-primer.md": 600,
"docs/defensive-patterns.md": 550,

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',
@@ -103,8 +105,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',

View File

@@ -598,7 +598,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]) }
@@ -620,9 +621,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',
@@ -1153,20 +1154,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')}*`,
@@ -1189,11 +1192,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`,
@@ -1201,9 +1210,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

@@ -562,7 +562,7 @@ ${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)}

View File

@@ -311,7 +311,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)
@@ -392,7 +392,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',

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

@@ -0,0 +1,54 @@
import { describe, expect, it } from 'vitest'
import { 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()
})
})

View File

@@ -0,0 +1,27 @@
/** Publication payload policy shared by static manifests and packed tarballs. */
/** 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): boolean {
const normalized = payloadPath(file)
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): void {
for (const file of files) {
if (!isForbiddenPublicationFile(file)) 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

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

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

@@ -86,6 +86,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.' },