Merge origin/master into worktree/web-plugin-config

Three seams: the tsconfig path map gained a mapping on each side and keeps
both; the event-producer matrix is generated, so it was regenerated rather
than hand-merged row by row.
This commit is contained in:
Yichen Jiang
2026-08-11 18:27:53 +08:00
1460 changed files with 20030 additions and 19474 deletions

View File

@@ -135,7 +135,6 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
'@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'],
// The Python runtime uses a distinct closed-resolution bin; the public CLI
// keeps config-owned bare-package resolution through lib/bin.js.
'@deepseek-ai/dsh-jsonrpc-demo': ['lib/packaged-bin.js'],
@@ -145,11 +144,6 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
'@deepseek-ai/dsh-sandbox-windows-acl': ['lib/runner.js', 'lib/types-*.js'],
'@deepseek-ai/dsh-skill-badge': ['assets'],
'@deepseek-ai/dsh-subprocess-local': ['scripts/ensure-spawn-helper.mjs'],
'@deepseek-ai/dsh-scripts': [
'lib/dev/tsdown-config.js',
'lib/local-plugin-loader-hooks.js',
'lib/assets',
],
}
function sameStringList(actual: readonly string[] | undefined, expected: readonly string[]): boolean {

View File

@@ -27,46 +27,61 @@ describe('CI workflow', () => {
}
})
it('keeps Wine blocking while native Windows reports independently', () => {
it('keeps a required Wine Windows job, a non-blocking native Windows job with failover, and a master-only standby', () => {
const workflow = loadWorkflow('.github/workflows/ci.yml')
if (!isRecord(workflow.jobs)
|| !isRecord(workflow.jobs.windows)
|| !isRecord(workflow.jobs['windows-native'])
|| !isRecord(workflow.jobs['wine-apt-cache'])
|| !isRecord(workflow.jobs['serial-windows'])
|| !isRecord(workflow.jobs['all-checks-passed'])) {
throw new TypeError('CI workflow must define Wine, native Windows, and aggregate jobs')
throw new TypeError('CI workflow must define windows, windows-native, wine-apt-cache, serial-windows, and all-checks-passed jobs')
}
const windows = workflow.jobs.windows
const windowsNative = workflow.jobs['windows-native']
const wineAptCache = workflow.jobs['wine-apt-cache']
const serialWindows = workflow.jobs['serial-windows']
const aggregate = workflow.jobs['all-checks-passed']
if (!Array.isArray(windows.steps) || !Array.isArray(windowsNative.steps) || !Array.isArray(aggregate.needs)) {
throw new TypeError('Windows jobs must define steps and the aggregate must define needs')
if (!Array.isArray(windows.steps) || !Array.isArray(aggregate.needs)) {
throw new TypeError('Windows job must define steps and the aggregate must define needs')
}
const nativeCommandSteps = windowsNative.steps.filter((step): step is Record<string, unknown> & { run: string } => (
const commandSteps = windows.steps.filter((step): step is Record<string, unknown> & { run: string } => (
isRecord(step) && typeof step.run === 'string'
))
// Required PR job: Wine on ubuntu-latest, runs wine-windows-gates.sh.
expect(windows['runs-on']).toBe('ubuntu-latest')
expect(windows.name).toBe('windows node 24 / wine blocking')
expect(windows.if).toBe("github.event_name == 'pull_request'")
expect(JSON.stringify(windows)).toContain('bash scripts/wine-windows-gates.sh')
expect(workflow.jobs).toHaveProperty('wine-apt-cache')
expect(windowsNative['runs-on']).toBe('dsh-windows-2025-16core')
expect(commandSteps.some(step => step.run.includes('wine-windows-gates.sh'))).toBe(true)
// windows-native: non-blocking native job with failover, runs windows-complete.
expect(typeof windowsNative['runs-on']).toBe('string')
expect(windowsNative['runs-on']).toContain('DSH_CI_FAILOVER')
expect(windowsNative['runs-on']).toContain('self-hosted')
expect(windowsNative['runs-on']).toContain('dsh-win-ci')
expect(windowsNative['runs-on']).toContain('dsh-windows-2025-16core')
expect(windowsNative.name).toBe('windows node 24 / native complete')
expect(windowsNative['timeout-minutes']).toBe(60)
expect(windowsNative.if).toBe("github.event_name == 'pull_request'")
expect(windowsNative.env).toMatchObject({
DSH_COVERAGE_MAX_WORKERS: '2',
DSH_GATE_CONCURRENCY: '2',
DSH_PUBLINT_CONCURRENCY: '8',
})
expect(windowsNative).not.toHaveProperty('continue-on-error')
expect(nativeCommandSteps).toHaveLength(3)
expect(nativeCommandSteps.every(step => step.shell === 'pwsh')).toBe(true)
const nativeCommandSteps = (windowsNative.steps as unknown[]).filter((step): step is Record<string, unknown> & { run: string } => (
isRecord(step) && typeof step.run === 'string'
))
expect(nativeCommandSteps.map(step => step.run)).toContain('pnpm run check:ci:windows-complete')
expect(JSON.stringify(windowsNative)).not.toMatch(/wine/i)
// wine-apt-cache: master-only, seeds the Wine apt cache.
expect(wineAptCache.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'")
expect(wineAptCache['runs-on']).toBe('ubuntu-latest')
// serial-windows: master-only standby, self-hosted, non-blocking.
expect(serialWindows.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'")
expect(serialWindows['runs-on']).toEqual(['self-hosted', 'dsh-win-ci', 'windows'])
expect(serialWindows.name).toBe('serial / windows (self-hosted standby)')
// Aggregate: Wine `windows` required, native `windows-native` excluded.
expect(aggregate.needs).toContain('windows')
expect(aggregate.needs).not.toContain('windows-native')
expect(aggregate.needs).not.toContain('serial-windows')
})
it('keeps supported LSP source under native Windows coverage', () => {

View File

@@ -23,7 +23,7 @@ export interface CordisCoreApiPage {
sections: CordisCoreApiSection[]
}
/** Explicit editorial grouping for the pinned Cordis core surface. */
/** Explicit editorial grouping for the pinned Cordis core API. */
export const CORDIS_CORE_API_PAGES: CordisCoreApiPage[] = [
{
out: 'docs/cordis-api/context.md',

View File

@@ -819,7 +819,7 @@ export function render(entries: CatalogEntry[]): string {
'',
'# Plugin Config Catalog',
'',
'Every `config:` block a `cordis.yml` entry can set: for each loadable harness package, the verbatim config declaration (JSDoc included) its `apply` function or service constructor receives, with every referenced type pasted alongside (package-local types) or linked (everything else). The paste is the plugin\'s full declared config type — a field the runtime schema deliberately excludes is a runtime-only seam (its own JSDoc says so) and is not settable from `cordis.yml`. This is the **deployment**-axis reference — the wiring a plugin author works against is the generated `cordis-surface` region on each [subsystem page](subsystems/core.md), the model-facing tool schemas are the [tool catalog](tool-catalog.md), and [subsystems/](subsystems/core.md) documents the types these declarations reference.',
'Every `config:` block a `cordis.yml` entry can set: for each loadable harness package, the verbatim config declaration (JSDoc included) its `apply` function or service constructor receives, with every referenced type pasted alongside (package-local types) or linked (everything else). The paste is the plugin\'s full declared config type — a field the runtime schema deliberately excludes is a runtime-only seam (its own JSDoc says so) and is not settable from `cordis.yml`. This is the **deployment**-axis reference — the wiring a plugin author works against is the generated Cordis API region on each [subsystem page](subsystems/core.md), the model-facing tool schemas are the [tool catalog](tool-catalog.md), and [subsystems/](subsystems/core.md) documents the types these declarations reference.',
'',
'This file is GENERATED from source (`scripts/gen-config-catalog.ts`) and verified fresh by `pnpm run verify-config-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks use a `ts config-catalog` fence (skipped by doc-typecheck, since a lone declaration referencing imports is not standalone-compilable). The generator also cross-checks the runtime schemastery schema against the pasted declaration — every schema-validated key, nested keys included, must be locatable on the declared config type — so the paste cannot hide a loader-accepted field.',
'',
@@ -832,7 +832,7 @@ export function render(entries: CatalogEntry[]): string {
lines.push(
'## Loadable plugins with no config',
'',
'These load from a `cordis.yml` entry with no `config:` block; they declare no config surface.',
'These load from a `cordis.yml` entry with no `config:` block; they declare no configuration API.',
'',
...entries.filter(e => e.kind === 'no-config').map(e => renderTerse(e, '')),
'',

View File

@@ -2,7 +2,7 @@
* Generate the per-subsystem Cordis service/event reference regions from the
* Typert catalog projection. Every harness `ctx.<key>` service and event scope
* maps to exactly one `docs/subsystems/` page through the curated tables below;
* the generator injects each page's surface between its GENERATED markers —
* the generator injects each page's Cordis API reference between its GENERATED markers —
* byte-identically into both language sides of the pair — and re-records a
* pair's `.i18n.yaml` only when nothing outside the region changed. The
* projection enforces event modes, JSDoc parameter/return completeness, and
@@ -40,7 +40,7 @@ export { REGION_BEGIN, REGION_END }
* The owning subsystems page for every harness `ctx.<key>` service the
* projection discovers. Fail-closed both ways: a discovered key absent here
* and an entry whose key the projection no longer discovers are both hard
* errors, so the partition can never silently drift from the service surface.
* errors, so the partition can never silently drift from the service API.
*/
export const SERVICE_PAGE: Record<string, string> = {
agentLoop: 'core.md',
@@ -63,6 +63,7 @@ export const SERVICE_PAGE: Record<string, string> = {
httpServer: 'http-server.md',
invariants: 'invariants.md',
llm: 'llm-streaming.md',
messageFeedback: 'feedback.md',
permission: 'permission.md',
planMode: 'plan.md',
pty: 'pty.md',
@@ -104,7 +105,7 @@ export const SERVICE_PAGE: Record<string, string> = {
* `index.ts` files with a same-named service class — so a new service can
* never silently join this blind spot: it either enters {@link SERVICE_PAGE}
* or names itself here. Client-face keys (the projection analyzes the host
* face only) name the package README that owns their surface.
* face only) name the package README that owns their API.
* TODO(cordis-catalog-interface-services): the interface-typed and
* non-index-declared entries would all render once the projection resolves a
* Context key through its declaring file's imports to the class declaration.
@@ -118,24 +119,24 @@ export const SERVICE_WALK_EXEMPTIONS: Record<string, string> = {
dshHomePath: 'not a service: boot-provided root accessor function (typeof dshHomePath | undefined) for Loader !!js config expressions — packages/boot/app-boot/README.md owns the boot contract',
headlessIo: 'not a service: launcher-provided root accessor value (HeadlessIo | undefined) for the headless bundle runner — packages/bundle/headless/README.md owns this launcher contract',
launcherEnvironment: 'not a service: launcher-provided root accessor value (EnvironmentSnapshot | undefined) — packages/util/environment/README.md owns this launcher contract',
lsp: 'interface-typed (LspService); implementing class Lsp is not the declared type name — packages/lsp/lsp/README.md owns the surface',
apiProxy: 'interface-typed (ApiProxy) with the class in api-proxy.ts, not index.ts — packages/host/apiproxy/README.md owns the surface',
appShell: 'client-side interface-typed browser service — packages/client/web/README.md owns the surface',
connection: 'client-side interface-typed browser service — packages/client/connection/README.md owns the surface',
lsp: 'interface-typed (LspService); implementing class Lsp is not the declared type name — packages/lsp/lsp/README.md owns the API',
apiProxy: 'interface-typed (ApiProxy) with the class in api-proxy.ts, not index.ts — packages/host/apiproxy/README.md owns the API',
appShell: 'client-side interface-typed browser service — packages/client/web/README.md owns the API',
connection: 'client-side interface-typed browser service — packages/client/connection/README.md owns the API',
chatFileMentions: 'client-side slot-contract accessor (ChatFileMentions) — packages/client/ui-conversation/README.md owns the API',
command: 'client-side interface-typed browser service — packages/client/ui-command/README.md owns the surface',
conversation: 'client-side interface-typed browser service — packages/client/ui-conversation/README.md owns the surface',
conversationEvents: 'client-side interface-typed registry — packages/client/runtime/README.md owns the surface',
conversationViews: 'client-side interface-typed registry — packages/client/runtime/README.md owns the surface',
layout: 'client-side interface-typed browser service — packages/client/ui-layout/README.md owns the surface',
locale: 'client-side interface-typed browser service — packages/client/locale/README.md owns the surface',
models: 'client-side interface-typed browser service — packages/client/ui-model/README.md owns the surface',
modules: 'client-side interface-typed browser service — packages/client/modules/README.md owns the surface',
remote: 'client-side interface-typed gateway accessor (ClientRemote) — packages/api/gateway/README.md owns the surface',
slash: 'client-side interface-typed browser service — packages/client/ui-slash/README.md owns the surface',
slots: 'client-side interface-typed browser service — packages/client/runtime/README.md owns the surface',
theme: 'client-side interface-typed browser service — packages/client/ui-theme/README.md owns the surface',
workspaces: 'client-side interface-typed browser service — packages/client/runtime/README.md owns the surface',
command: 'client-side interface-typed browser service — packages/client/ui-command/README.md owns the API',
conversation: 'client-side interface-typed browser service — packages/client/ui-conversation/README.md owns the API',
conversationEvents: 'client-side interface-typed registry — packages/client/runtime/README.md owns the API',
conversationViews: 'client-side interface-typed registry — packages/client/runtime/README.md owns the API',
layout: 'client-side interface-typed browser service — packages/client/ui-layout/README.md owns the API',
locale: 'client-side interface-typed browser service — packages/client/locale/README.md owns the API',
models: 'client-side interface-typed browser service — packages/client/ui-model/README.md owns the API',
modules: 'client-side interface-typed browser service — packages/client/modules/README.md owns the API',
remote: 'client-side interface-typed gateway accessor (ClientRemote) — packages/api/gateway/README.md owns the API',
slash: 'client-side interface-typed browser service — packages/client/ui-slash/README.md owns the API',
slots: 'client-side interface-typed browser service — packages/client/runtime/README.md owns the API',
theme: 'client-side interface-typed browser service — packages/client/ui-theme/README.md owns the API',
workspaces: 'client-side interface-typed browser service — packages/client/runtime/README.md owns the API',
}
/**
@@ -177,19 +178,19 @@ export const EVENT_SCOPE_PAGE: Record<string, string> = {
* so a scope-level exemption would mask a host-face regression.
*/
export const EVENT_WALK_EXEMPTIONS: Record<string, string> = {
'commands/changed': 'client-face registry invalidation signal — packages/client/runtime/README.md owns the surface',
'connection/reset': 'client-face transport signal — packages/client/runtime/README.md owns the surface',
'credentials/changed': 'client-face registry invalidation signal — packages/client/runtime/README.md owns the surface',
'locale/change': 'client-face locale switch signal — packages/client/locale/README.md owns the surface',
'models/changed': 'client-face registry invalidation signal — packages/client/runtime/README.md owns the surface',
'session/preset-changed': 'client-face per-session catalog invalidation signal — packages/client/runtime/README.md owns the surface',
'settings/changed': 'client-face registry invalidation signal — packages/client/runtime/README.md owns the surface',
'slash/input-begin-command': 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the surface',
'slash/input-consume-token': 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the surface',
'slash/input-insert-reference': 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the surface',
'slash/input-insert-text': 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the surface',
'slots/changed': 'client-face slot invalidation signal — packages/client/runtime/README.md owns the surface',
'theme/change': 'client-face theme switch signal — packages/client/ui-theme/README.md owns the surface',
'commands/changed': 'client-face registry invalidation signal — packages/client/runtime/README.md owns the API',
'connection/reset': 'client-face transport signal — packages/client/runtime/README.md owns the API',
'credentials/changed': 'client-face registry invalidation signal — packages/client/runtime/README.md owns the API',
'locale/change': 'client-face locale switch signal — packages/client/locale/README.md owns the API',
'models/changed': 'client-face registry invalidation signal — packages/client/runtime/README.md owns the API',
'session/preset-changed': 'client-face per-session catalog invalidation signal — packages/client/runtime/README.md owns the API',
'settings/changed': 'client-face registry invalidation signal — packages/client/runtime/README.md owns the API',
'slash/input-begin-command': 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the API',
'slash/input-consume-token': 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the API',
'slash/input-insert-reference': 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the API',
'slash/input-insert-text': 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the API',
'slots/changed': 'client-face slot invalidation signal — packages/client/runtime/README.md owns the API',
'theme/change': 'client-face theme switch signal — packages/client/ui-theme/README.md owns the API',
}
/**
@@ -229,6 +230,25 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
ResolvedRetryPolicy: 'llm-streaming.md',
Message: 'llm-streaming.md',
MessageSource: 'llm-streaming.md',
MessageFeedbackDeleteRequest: 'feedback.md',
MessageFeedbackDeleteResult: 'feedback.md',
MessageFeedbackDeleteValue: 'feedback.md',
MessageFeedbackFailure: 'feedback.md',
MessageFeedbackItem: 'feedback.md',
MessageFeedbackListRequest: 'feedback.md',
MessageFeedbackListResult: 'feedback.md',
MessageFeedbackListValue: 'feedback.md',
MessageFeedbackNoteBlank: 'feedback.md',
MessageFeedbackNoteTooLarge: 'feedback.md',
MessageFeedbackPutRequest: 'feedback.md',
MessageFeedbackPutResult: 'feedback.md',
MessageFeedbackRating: 'feedback.md',
MessageFeedbackRejected: 'feedback.md',
MessageFeedbackSessionNotFound: 'feedback.md',
MessageFeedbackSuccess: 'feedback.md',
MessageFeedbackTargetNotFound: 'feedback.md',
MessageFeedbackVersion: 'feedback.md',
MessageFeedbackVersionConflict: 'feedback.md',
UserMessage: 'session.md',
PreStepDecision: 'core.md',
PreStepContext: 'core.md',
@@ -289,7 +309,6 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
CommandDescriptor: 'commands.md',
CommandId: 'commands.md',
CommandResult: 'commands.md',
CommandSurface: 'commands.md',
LlmAdapter: 'llm-streaming.md',
PreparedLlmCall: 'llm-streaming.md',
LlmService: 'llm-streaming.md',
@@ -302,6 +321,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
SessionLocation: 'persistence.md',
SessionPreparation: 'persistence.md',
SessionPersistenceSnapshot: 'persistence.md',
SessionRawArtifact: 'persistence.md',
ConfinedArgv: 'sandbox.md',
SandboxExecutionPolicy: 'sandbox.md',
SandboxMode: 'sandbox.md',
@@ -384,6 +404,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
TaskRead: 'tasks.md',
TaskSnapshot: 'tasks.md',
TaskStart: 'tasks.md',
TasksChangedListener: 'tasks.md',
TokenMeasurement: 'token-meter.md',
CodeDispatchLog: 'tools.md',
PostToolDecision: 'tools.md',
@@ -462,6 +483,7 @@ export const FOUNDATION_TYPE_NAMES: ReadonlySet<string> = new Set([
'Promise',
'Record',
'Readonly',
'Uint8Array',
])
/** Project types deliberately documented outside the subsystems catalog. */
@@ -540,8 +562,8 @@ export const CORDIS_CATALOG_POLICY: CordisCatalogPolicy = {
/**
* Splice a page's generated cordis-surface region into its Markdown content.
* The page must contain exactly one cordis-surface region (the markers are
* Splice a page's generated Cordis API region into its Markdown content.
* The page must contain exactly one `cordis-surface` marker region (the markers are
* part of the hand-owned page skeleton once, then owned by the generator);
* zero or several is a partition error the caller reports with the page path.
* The match is on THIS generator's exact markers, not the generic region
@@ -587,7 +609,7 @@ export interface WalkPartitionMaps {
}
/**
* Judge the rendered surface and the independent AST scan against the curated
* Judge the rendered API and the independent AST scan against the curated
* partition maps, fail-closed in both directions for services AND events: a
* rendered key/scope must be mapped to a page, a mapped key/scope must still
* render, and — the backstop — a DECLARED key/event the projection cannot see
@@ -595,7 +617,7 @@ export interface WalkPartitionMaps {
* direction guards the scan itself: everything rendered must also be declared
* to the scan, so a scan blind spot cannot decay silently. Pure so the
* acceptance paths are provable without running the projection.
* @param input - rendered surface plus the declared-key/event scans.
* @param input - rendered API plus the declared-key/event scans.
* @param maps - the curated page maps and walk exemptions.
* @returns one message per violation, empty when the partition holds.
*/
@@ -646,7 +668,7 @@ export function walkPartitionProblems(input: WalkPartitionInput, maps: WalkParti
// in a Context/Events merge the scan must also reach, so a rendered key or
// event the scan cannot see means the SCAN regressed (glob, prefilter, or
// block walk) — a partial blind spot that exemption staleness alone would
// never surface.
// never appear.
for (const key of input.renderedKeys.keys()) {
if (!input.declaredKeys.has(key)) problems.push(`ctx.${key} is rendered by the projection but the independent scan finds no Context merge declaring it; the scan has a blind spot (glob, prefilter, or module-block walk) — fix the scan, not the maps.`)
}

View File

@@ -135,7 +135,7 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'session',
title: 'In-memory session store',
mode: 'core',
consumers: ['agent-loop', 'agent', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent-inprocess', 'invariants'],
consumers: ['agent-loop', 'agent', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent-inprocess', 'invariants', 'message-feedback'],
note: 'Owns append-only Session instances and emits the durable session event feed.',
},
{
@@ -167,7 +167,7 @@ const SERVICE_ROLES: ServiceRole[] = [
title: 'Durable session persistence seam',
mode: 'seam',
implementations: ['session-persistence-jsonl', 'session-persistence-sqlite'],
consumers: ['agent-loop', 'tool-bash', 'hooks-claude', 'hooks-codex', 'session-query', 'session-query-sqlite'],
consumers: ['agent-loop', 'tool-bash', 'hooks-claude', 'hooks-codex', 'session-query', 'session-query-sqlite', 'message-feedback'],
note: 'Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time.',
},
{
@@ -211,9 +211,16 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'storage-domain',
title: 'Domain data facility',
mode: 'core',
consumers: ['workspace'],
consumers: ['workspace', 'message-feedback'],
note: 'Waits for every configured backend, then publishes the domain form as one lifecycle-bound service for typed durable state.',
},
{
key: 'messageFeedback',
pkg: 'message-feedback',
title: 'Lifecycle-bound message feedback',
mode: 'core',
note: 'Owns local per-assistant-message feedback, lifecycle and target validation, per-item compare-and-set, and the Host unary Remote contract without entering Session history or telemetry.',
},
{
key: 'workspace',
pkg: 'workspace',
@@ -388,7 +395,7 @@ const SERVICE_ROLES: ServiceRole[] = [
mode: 'seam',
implementations: ['pty-local'],
consumers: ['tool-pty'],
note: 'The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-pty exposes the owner-scoped model surface.',
note: 'The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-pty exposes the owner-scoped model tools.',
},
{
key: 'sandbox',
@@ -469,7 +476,7 @@ const SERVICE_ROLES: ServiceRole[] = [
mode: 'seam',
implementations: ['tasks-local'],
consumers: ['tool-bash', 'tool-pty', 'tool-subagent', 'tool-tasks'],
note: 'Producers (background bash, PTY sends, and subagent delegations) register running work; tool-tasks is the model-facing control surface that reads, lists, and kills it; tasks-local is the process-local registry.',
note: 'Producers (background bash, PTY sends, and subagent delegations) register running work; tool-tasks is the model-facing controller that reads, lists, and kills it; tasks-local is the process-local registry.',
},
{
key: 'web',
@@ -1264,7 +1271,7 @@ function renderLifecycle(): string {
'',
'The returned `agent/pre-step` decision is authoritative; listeners wrapping `next()` preserve downstream messages unless replacement is intentional. Steering and injected context pass through the same waterfall after a later claim operation takes their next-step batch.',
'',
'SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination surface for queue/status, prompt interception, request construction, steering, continuation, and errors.',
'SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination API for queue/status, prompt interception, request construction, steering, continuation, and errors.',
'',
...maintenanceFooter(maintenance),
].join('\n')
@@ -1328,7 +1335,7 @@ function renderToolPipeline(): string {
' allResults --> context',
'```',
'',
'Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`. The registry losslessly snapshots the candidate result and normalizes a snapshot failure before the visible definition\'s snapshotted `finalizeContent` callback enforces its synchronous content-only invariant. `tools/result` then observes the immutable, lossless-JSON outcome. This lets hooks span tool families without coupling the tools to one policy service. Code Mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/code-dispatch`, surface denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.',
'Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`. The registry losslessly snapshots the candidate result and normalizes a snapshot failure before the visible definition\'s snapshotted `finalizeContent` callback enforces its synchronous content-only invariant. `tools/result` then observes the immutable, lossless-JSON outcome. This lets hooks span tool families without coupling the tools to one policy service. Code Mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/code-dispatch`, return denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.',
'',
...maintenanceFooter(maintenance),
].join('\n')
@@ -1380,7 +1387,7 @@ function renderIndex(docs: GraphDoc[]): string {
const maintenance = 'mixed: each linked page declares generated, hybrid, or curated mode'
return [
...generatedHeader('Documentation Graph Index'),
'These diagrams show relationships that the generated catalogs do not. Use them to find package relationships, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type definitions still live in the [subsystem pages](subsystems/core.md) (types + the generated `cordis-surface` regions) and [tool-catalog.md](tool-catalog.md).',
'These diagrams show relationships that the generated catalogs do not. Use them to find package relationships, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type definitions still live in the [subsystem pages](subsystems/core.md) (types + the generated Cordis API regions) and [tool-catalog.md](tool-catalog.md).',
'',
'The process decision behind this index is recorded in [the documentation graph Agent Note](../.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.md).',
'',

View File

@@ -27,8 +27,8 @@ const ALL_KINDS = ['dependencies', 'devDependencies', 'optionalDependencies', 'p
* root manifest), test infrastructure, the documentation site, the runnable
* demo leaves, and the native launcher's build workspace. A runtime
* declaration by anything outside these areas is a disclosure-relevant
* runtime dependency, because `scripts/install.sh` installs the repository
* itself and any plugin package can be mounted from a user's `cordis.yml`.
* runtime dependency because any plugin package can be mounted from a user's
* `cordis.yml`.
*/
const DEV_ONLY_AREAS = [
'package.json',
@@ -369,7 +369,7 @@ function collectNpmDeps(): ExternalDep[] {
*/
export function tierExternalDeps(manifests: Map<string, Manifest>, names: Set<string>): Map<string, boolean> {
const tiers = new Map<string, boolean>()
// `tsx` is runtime by fiat: `bin/dsh` execs the CLI through its ESM hook.
// `tsx` is runtime by fiat: the root source-run scripts execute through its ESM hook.
tiers.set('tsx', true)
for (const [path, manifest] of manifests) {
const devOnly = DEV_ONLY_AREAS.some(area => (area.endsWith('/') ? path.startsWith(area) : path === area))
@@ -707,7 +707,7 @@ ${vendored.map(row => `| \`${row.npmName}\` | \`${row.upstreamName}\` | [${row.u
## 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, Web UI, and Python SDK runtime load by default.
External packages that a workspace package resolves at runtime. 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

@@ -24,13 +24,15 @@ 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 { AttachmentStore } from '@deepseek-ai/dsh-attachment'
import type { ImageAttachmentLimits, ImageAttachmentRef, SaveImageAttachment, StoredImageAttachment } from '@deepseek-ai/dsh-attachment'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import PlanModeService from '@deepseek-ai/dsh-plan-mode'
import WebService from '@deepseek-ai/dsh-web'
import * as WebSearchExa from '@deepseek-ai/dsh-web-search-exa'
import * as WebFetchLocal from '@deepseek-ai/dsh-web-fetch-local'
import SubagentService from '@deepseek-ai/dsh-subagent'
import type { SubagentProvider } from '@deepseek-ai/dsh-subagent'
import type { SubagentProvider, SubagentReportDelivery } from '@deepseek-ai/dsh-subagent'
import * as ToolSubagentControl from '@deepseek-ai/dsh-tool-subagent-control'
import * as ToolSubagentListAgents from '@deepseek-ai/dsh-tool-subagent-control/list-agents'
import * as ToolSubagentReport from '@deepseek-ai/dsh-tool-subagent-report'
@@ -60,6 +62,29 @@ import VmWorkflowEngine from '@deepseek-ai/dsh-workflow-workerthread'
import * as ToolRalph from '@deepseek-ai/dsh-tool-ralph'
import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
/** Attachment seam marker that makes the attachments-conditional `read_image` schema harvestable. */
class CatalogAttachmentStore extends AttachmentStore {
readonly imageLimits: ImageAttachmentLimits = Object.freeze({
maxImageBytes: 1,
maxImagesPerMessage: 1,
maxMessageImageBytes: 1,
maxImagePixels: 1,
mediaTypes: Object.freeze(['image/png'] as const),
})
override validateImage(_input: SaveImageAttachment): Promise<void> {
return Promise.reject(new Error('gen-tool-catalog: attachment validation is unreachable during schema harvest'))
}
override saveImage(_input: SaveImageAttachment): Promise<ImageAttachmentRef> {
return Promise.reject(new Error('gen-tool-catalog: attachment writes are unreachable during schema harvest'))
}
override readImage(_ref: ImageAttachmentRef): Promise<StoredImageAttachment> {
return Promise.reject(new Error('gen-tool-catalog: attachment reads are unreachable during schema harvest'))
}
}
const root = resolve(import.meta.dirname, '..')
const OUT = 'docs/tool-catalog.md'
@@ -117,7 +142,7 @@ interface ToolPackage {
* name to its own source.
*/
source: string | Readonly<Record<string, string>>
/** Services or owning runtime surfaces the package requires at execution time. */
/** Services or owning runtimes the package requires at execution time. */
requires: string[]
/** Session events or other visible state the tools write or affect. */
writes: string[]
@@ -131,14 +156,14 @@ interface ToolPackage {
/**
* Config for the caller's `ToolRegistry` mount. The registry itself ships a
* model-facing tool (`run_code`, registered under a non-native `mode`), so
* ITS catalog entry boots the registry in the mode that surfaces it;
* ITS catalog entry boots the registry in the mode that exposes it;
* every other entry uses the default (native) registry.
*/
toolsConfig?: ToolsConfig
/**
* A deployment note rendered after the package's tools, for a fact that
* booting the package alone cannot show. The registered tool NAME can be a
* load-time config (`tool-subagent`'s `toolName`), so one package may surface
* load-time config (`tool-subagent`'s `toolName`), so one package may appear
* under several names across deployments — the boot yields the package
* DEFAULT, and this note records the shipped alternatives the model sees.
*/
@@ -221,7 +246,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
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), with native `C:\\...` paths and `$env:NAME` variables.',
'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 sandbox controls — `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), with native `C:\\...` paths and `$env:NAME` variables.',
},
{
pkg: '@deepseek-ai/dsh-tool-cordis',
@@ -259,22 +284,24 @@ const TOOL_PACKAGES: ToolPackage[] = [
await ctx.plugin(ToolStrReplaceEditor)
},
note:
'Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal surface.',
'Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal API.',
},
{
pkg: '@deepseek-ai/dsh-tool-fs',
dir: 'tool-fs',
source: 'packages/fs/tool-fs/src/index.ts',
requires: ['ctx.tools', 'ctx.fs', 'ctx.systemPrompt'],
writes: ['tool/call', 'fs/write-intent or fs/edit-intent for mutations', 'fs/observed after read presence/absence or successful mutation', 'tool/result'],
requires: ['ctx.tools', 'ctx.fs', 'ctx.systemPrompt', 'ctx.attachments (read_image registration)', 'ctx.llm + an image-capable route (read_image execution)'],
writes: ['tool/call', 'fs/write-intent or fs/edit-intent for mutations', 'fs/observed after read presence/absence or successful file operation', 'durable attachment (read_image)', 'tool/result'],
async mount(ctx) {
// The tool needs `fs`; the bare provider is sufficient because policy
// changes behavior, not schema shape.
// changes behavior, not schema shape. The catalog seam marker opts into
// the attachments-conditional read_image schema without attachment I/O.
await ctx.plugin(LocalFileSystem)
await ctx.plugin(CatalogAttachmentStore)
await ctx.plugin(ToolFs)
},
note:
'The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The tool schemas above are identical with or without the policy plugin.',
'The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. `read_image` is not registered without `ctx.attachments`; its schema is route-independent, and execution refuses unless the exact routed model declares image input.',
},
{
pkg: '@deepseek-ai/dsh-tool-fs-search',
@@ -393,7 +420,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 `packages/bundle/base/cordis.patch.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 compositions load this package once per subagent backend, so the model additionally sees `subagent_fork` bound to the fork backend. Each instance\'s description and `run_in_background` parameter follow its own `backgroundMode` and `enableRunInBackground`, so the two shipped schemas are not identical: `subagent` is `continuable`, while `subagent_fork` stays `one-shot` — see `packages/bundle/base/cordis.patch.yml` and `examples/acp-agent/cordis.yml`.',
},
{
pkg: '@deepseek-ai/dsh-tool-subagent-control',
@@ -421,20 +448,22 @@ const TOOL_PACKAGES: ToolPackage[] = [
pkg: '@deepseek-ai/dsh-tool-subagent-report',
dir: 'tool-subagent-report',
source: 'packages/subagent/tool-subagent-report/src/index.ts',
requires: ['ctx.subagents', 'a live continuable in-process child Agent'],
requires: ['ctx.subagents', 'ctx.systemPrompt', 'a live continuable in-process child Agent'],
writes: ['tool/call', 'tool/result', 'a user-role message in the direct parent session'],
async mount(ctx) {
await ctx.plugin(AgentRegistry)
await ctx.plugin(SubagentService)
const { reportDelivery } = ToolSubagentReport.Config({}) as { reportDelivery: SubagentReportDelivery }
await mountCatalogChildScope(ctx, (childCtx) => {
ToolSubagentReport.installReportTool(childCtx, ctx, 'quiet')
ToolSubagentReport.installReportTool(childCtx, ctx, reportDelivery)
})
},
scope: ctx => catalogChildScopes.get(ctx) as Agent,
note:
'Registered per continuable in-process child rather than globally, so this schema is visible only '
+ 'inside such a child and survives its global `toolFilter`. The parent-facing `send_message` tool '
+ 'is installed independently.',
+ 'inside such a child and survives its global `toolFilter`. The same contribution installs the '
+ 'child-scoped `tool:report` prompt section, which this catalog does not render. The parent-facing '
+ '`send_message` tool is installed independently.',
},
{
pkg: '@deepseek-ai/dsh-tool-tasks',
@@ -447,7 +476,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
await ctx.plugin(ToolTasks)
},
note:
'The kind-agnostic background-task control surface: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the control surface that arms producers\' `ctx.tasks.start()`.',
'The kind-agnostic background-task controller: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers\' `ctx.tasks.start()`.',
},
{
pkg: '@deepseek-ai/dsh-tool-todo',
@@ -608,11 +637,11 @@ export function render(catalog: ToolCatalog): string {
'',
'# Tool Schema Catalog',
'',
'Every model-facing tool a shipped plugin contributes to `ctx.tools`: the `name`, `description`, and JSON-Schema `parameters` the model receives via the system-prompt assembly. It complements the [subsystem pages](subsystems/core.md) (the types plus each page\'s generated `cordis-surface` wiring region) — this page is the *tools* the agent is offered.',
'Every model-facing tool a shipped plugin contributes to `ctx.tools`: the `name`, `description`, and JSON-Schema `parameters` the model receives via the system-prompt assembly. It complements the [subsystem pages](subsystems/core.md) (the types plus each page\'s generated Cordis API region) — this page is the *tools* the agent is offered.',
'',
'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, 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.',
'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 expose 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

@@ -1,425 +0,0 @@
#!/bin/sh
# dsh one-line installer.
#
# curl -fsSL https://raw.githubusercontent.com/deepseek-ai/deepseek-harness-sdk/master/scripts/install.sh | sh
#
# It clones the harness under ~/.dsh/source (the master clone at
# ~/.dsh/source/master), adds a per-install staging worktree at
# ~/.dsh/source/staging-<timestamp> on branch dsh-staging/<timestamp>, checks
# host dependencies (git, Node, pnpm) and offers to install a missing pnpm, runs
# `pnpm install`, points the stable `~/.dsh/source/current` symlink
# at that staging worktree and symlinks `dsh` onto PATH at `current/bin/dsh`,
# records your API credentials in the Harness home (`~/.dsh`) dsh reads at boot,
# builds the repository artifacts, and launches the Web UI. Keeping every
# checkout under ~/.dsh/source keeps successive
# upgrades in one place instead of scattered sibling clones, and lets staging
# worktrees share the master clone's object store. The PATH symlink resolves through
# `current`, so an upgrade repoints one stable symlink instead of relinking PATH:
# the `dsh` on PATH never moves and can never dangle.
#
# When run from inside an existing checkout (e.g. `sh scripts/install.sh` rather
# than `curl ... | sh`) it never clones and never touches that working tree;
# DSH_REF is ignored. Instead it *adopts* the checkout: `git rev-parse
# --git-common-dir` resolves the repository behind it (for a linked worktree that
# is the real clone, not the worktree), and a fresh staging worktree branched
# from the checkout's HEAD lands in the source container beside `current`. The
# container owns staging worktrees and `current`; the clone is discovered, not
# owned, so an arbitrary clone (~/src/dsh) and a managed one converge on one
# layout and stay upgradable. Adoption carries committed work only: the staging
# worktree branches from HEAD, so uncommitted changes stay in the checkout.
# Setting DSH_SOURCE to a different directory opts back into the normal
# clone/worktree path.
#
# Adopting an arbitrary clone leaves the container not self-contained: its
# staging worktrees hold an absolute gitdir pointer into that clone, so deleting
# it breaks them. `git worktree list` in that clone is the record of which
# worktrees depend on it.
#
# When run through `curl | sh` the script text arrives on stdin, so every
# prompt and the final launch read the controlling terminal (/dev/tty) directly;
# with no terminal the script prints the manual next steps instead.
#
# Overridable via environment:
# DSH_REF branch or tag to clone/checkout (default: master)
# DSH_REPO clone URL (default: the GitHub repo)
# DSH_SOURCE source container directory (default: ~/.dsh/source)
# 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 profiles and user patches (default: ~/.dsh)
set -eu
DSH_REF=${DSH_REF:-master}
DSH_REPO=${DSH_REPO:-https://github.com/deepseek-ai/deepseek-harness-sdk.git}
# DSH_SOURCE is the staging-worktree container and the default home of `current`.
# DSH_MASTER names the main clone: clone mode defaults it inside DSH_SOURCE,
# while adoption discovers an existing clone anywhere on disk. Remember whether
# DSH_SOURCE was explicit so a different path selects clone mode.
if [ -n "${DSH_SOURCE:-}" ]; then DSH_SOURCE_EXPLICIT=1; else DSH_SOURCE_EXPLICIT=0; fi
DSH_SOURCE=${DSH_SOURCE:-$HOME/.dsh/source}
DSH_MASTER=${DSH_MASTER:-$DSH_SOURCE/master}
# The stable symlink the PATH launcher resolves through: PATH/dsh ->
# current/bin/dsh -> <staging>/bin/dsh. Installs and upgrades repoint `current`;
# the PATH target remains current/bin/dsh.
DSH_CURRENT=${DSH_CURRENT:-$DSH_SOURCE/current}
DSH_BIN_DIR=${DSH_BIN_DIR:-$HOME/.local/bin}
# One UTC basic timestamp names this install's staging branch and worktree.
DSH_STAMP=$(date -u +%Y%m%dT%H%M%SZ)
DSH_STAGING_BRANCH=dsh-staging/$DSH_STAMP
DSH_STAGING=$DSH_SOURCE/staging-$DSH_STAMP
# --- path helpers ---------------------------------------------------------------
# Every path comparison below runs on physical paths. Git always reports resolved
# paths, so comparing one against an unresolved path disagrees whenever a symlink
# sits anywhere above the checkout — a symlinked home directory is enough, and
# macOS reaches every mktemp path that way through /var -> private/var. The
# mismatch silently misclassifies an existing managed install as a foreign clone
# and builds a second container beside the real one.
# `git rev-parse --path-format=absolute` would do this, but it needs git 2.31+.
#
# A not-yet-created directory (the container on a fresh install) has no physical
# path. Falling back here rather than at each call site keeps every caller a
# plain assignment, so no site can compare against an empty path by forgetting
# its own fallback.
resolve_dir() { CDPATH= cd -- "$1" 2>/dev/null && pwd -P || printf '%s\n' "$1"; }
# --- in-repo detection ---------------------------------------------------------
# Under `curl ... | sh` the script text arrives on stdin, so $0 is the shell
# name and no file path resolves; running a checked-out copy (`sh
# scripts/install.sh`) makes $0 the script file. When $0 is a readable file whose
# parent is a scripts/ dir inside a real dsh checkout (bin/dsh launcher present),
# this is in-repo mode: never clone, never touch that working tree. An explicit
# DSH_SOURCE pointing elsewhere opts back into the clone/worktree path.
IN_REPO=0
DSH_CHECKOUT=''
if [ -f "$0" ]; then
_self_dir=$(resolve_dir "$(dirname -- "$0")")
if [ -n "$_self_dir" ]; then
# Physical without its own resolve_dir: dirname is textual, so trimming a
# resolved path leaves one. The comparison below depends on that.
_repo_root=$(dirname -- "$_self_dir")
if [ "$(basename -- "$_self_dir")" = scripts ] \
&& [ -x "$_repo_root/bin/dsh" ] && [ -f "$_repo_root/scripts/install.sh" ]; then
# Compare the explicit DSH_SOURCE physically: an unresolved but equivalent
# path must still count as "the caller meant this checkout".
_src_resolved=$(resolve_dir "$DSH_SOURCE")
if [ "$DSH_SOURCE_EXPLICIT" = 0 ] || [ "$_src_resolved" = "$_repo_root" ]; then
IN_REPO=1
DSH_CHECKOUT=$_repo_root
fi
fi
fi
fi
# --- terminal-aware prompting --------------------------------------------------
# stdin is the piped script, so read the controlling terminal for input.
if { true </dev/tty; } 2>/dev/null; then
HAS_TTY=1
# Restore terminal echo on exit or interrupt: ask_secret disables echo between
# its stty toggles, and dash (a common `sh`) does not run an EXIT trap when the
# shell is killed by a signal, so the fatal signals need their own handler. A
# successful run ends in exec, which replaces this process and drops the traps.
trap 'stty echo </dev/tty 2>/dev/null || true' EXIT
trap 'stty echo </dev/tty 2>/dev/null || true; exit 130' INT TERM HUP
else
HAS_TTY=0
fi
# Colour only when writing to a terminal.
if [ -t 1 ]; then
B=$(printf '\033[1m'); DIM=$(printf '\033[2m'); RED=$(printf '\033[31m')
GRN=$(printf '\033[32m'); YEL=$(printf '\033[33m'); RST=$(printf '\033[0m')
else
B=''; DIM=''; RED=''; GRN=''; YEL=''; RST=''
fi
info() { printf '%s==>%s %s\n' "$GRN" "$RST" "$1"; }
step() { printf '\n%s==>%s %s%s%s\n' "$GRN" "$RST" "$B" "$1" "$RST"; }
warn() { printf '%s warn%s %s\n' "$YEL" "$RST" "$1" >&2; }
die() { printf '%serror%s %s\n' "$RED" "$RST" "$1" >&2; exit 1; }
# ask PROMPT [DEFAULT] -> answer on stdout (plain-text line).
ask() {
[ "$HAS_TTY" = 1 ] || die "no terminal available for input; re-run in an interactive shell"
printf '%s%s%s ' "$B" "$1" "$RST" >/dev/tty
IFS= read -r _ans </dev/tty || _ans=''
[ -n "$_ans" ] || _ans=${2:-}
printf '%s' "$_ans"
}
# ask_secret PROMPT -> answer on stdout, with terminal echo suppressed.
ask_secret() {
[ "$HAS_TTY" = 1 ] || die "no terminal available for input; re-run in an interactive shell"
printf '%s%s%s ' "$B" "$1" "$RST" >/dev/tty
stty -echo </dev/tty 2>/dev/null || true
IFS= read -r _sec </dev/tty || _sec=''
stty echo </dev/tty 2>/dev/null || true
printf '\n' >/dev/tty
printf '%s' "$_sec"
}
# confirm PROMPT [Y] -> exit 0 on yes. Default is no unless second arg is "Y".
confirm() {
_def=${2:-N}
if [ "$HAS_TTY" != 1 ]; then
[ "$_def" = Y ] # non-interactive: take the default
return
fi
if [ "$_def" = Y ]; then _hint='[Y/n]'; else _hint='[y/N]'; fi
printf '%s%s%s %s ' "$B" "$1" "$RST" "$_hint" >/dev/tty
IFS= read -r _r </dev/tty || _r=''
[ -n "$_r" ] || _r=$_def
case "$_r" in [yY]|[yY][eE][sS]) return 0 ;; *) return 1 ;; esac
}
printf '%s\n' "${B}DeepSeek Harness — dsh installer${RST}"
if [ "$IN_REPO" = 1 ]; then
printf '%scheckout %s%s\n' "$DIM" "$DSH_CHECKOUT" "$RST"
else
printf '%smaster %s @ %s%s\n' "$DIM" "$DSH_MASTER" "$DSH_REF" "$RST"
printf '%sstaging %s%s\n' "$DIM" "$DSH_STAGING" "$RST"
printf '%scurrent %s%s\n' "$DIM" "$DSH_CURRENT" "$RST"
fi
# --- 1. dependency check -------------------------------------------------------
step "Checking dependencies"
command -v git >/dev/null 2>&1 || die "git is required but not found. Install git, then re-run."
info "git ... ok"
# Node ^22.19.0 || >=24.0.0 (see the root package.json "engines" field).
node_ok() {
command -v node >/dev/null 2>&1 || return 1
_v=$(node -v 2>/dev/null) || return 1
_v=${_v#v}
_major=${_v%%.*}
_rest=${_v#*.}
_minor=${_rest%%.*}
case "$_major" in ''|*[!0-9]*) return 1 ;; esac
case "$_minor" in ''|*[!0-9]*) _minor=0 ;; esac
[ "$_major" -ge 24 ] && return 0
[ "$_major" -eq 22 ] && [ "$_minor" -ge 19 ] && return 0
return 1
}
if node_ok; then
info "node $(node -v) ... ok"
else
if command -v node >/dev/null 2>&1; then
die "Node $(node -v) is unsupported. dsh needs ^22.19.0 || >=24.0.0 — upgrade Node, then re-run."
fi
die "Node is required but not found. Install Node ^22.19.0 || >=24, then re-run."
fi
# pnpm is the only dependency we offer to install for you.
if command -v pnpm >/dev/null 2>&1; then
info "pnpm $(pnpm --version) ... ok"
else
warn "pnpm is not installed."
if confirm "Install pnpm now?" Y; then
if command -v corepack >/dev/null 2>&1 && corepack enable pnpm >/dev/null 2>&1; then
info "enabled pnpm via corepack"
elif command -v npm >/dev/null 2>&1 && npm install -g pnpm >/dev/null 2>&1; then
info "installed pnpm via npm"
else
die "could not install pnpm automatically. Install it (https://pnpm.io/installation), then re-run."
fi
command -v pnpm >/dev/null 2>&1 || die "pnpm still not on PATH after install. Open a new shell, then re-run."
else
die "pnpm is required. Install it (https://pnpm.io/installation), then re-run."
fi
fi
# --- 2. resolve the repository and lay out the staging worktree ---------------
# The source container owns staging worktrees and `current`; the repository is
# *discovered*, not owned. A curl install discovers it by cloning to $DSH_MASTER;
# in-repo adoption discovers it from the checkout. Both then run one shared
# worktree/exclude/lock path, so an arbitrary clone and a managed install
# converge on the same layout.
#
# REPO_COMMON is the shared git directory every worktree of the repository
# points at; REPO_ROOT is the working tree that owns it (the master clone).
REPO_COMMON=''
REPO_ROOT=''
if [ "$IN_REPO" = 1 ]; then
step "Using existing checkout at $DSH_CHECKOUT"
info "running from inside the repo — never cloning, and DSH_REF is ignored"
# Resolve the repository behind the checkout. --git-common-dir returns the
# SHARED git dir, so a linked worktree resolves to the real clone rather than
# itself; it is relative for a plain clone, so anchor it before resolving.
# Require the resolved git dir to exist: resolve_dir echoes its argument back
# for a missing path, so test the directory rather than the returned string.
if _common=$(git -C "$DSH_CHECKOUT" rev-parse --git-common-dir 2>/dev/null) && [ -n "$_common" ]; then
case "$_common" in /*) ;; *) _common=$DSH_CHECKOUT/$_common ;; esac
[ -d "$_common" ] && REPO_COMMON=$(resolve_dir "$_common")
fi
[ -n "$REPO_COMMON" ] || die "$DSH_CHECKOUT is not a git repository — cannot adopt it."
REPO_ROOT=$(dirname -- "$REPO_COMMON")
# Reuse the container when the repository already lives inside it (the normal
# managed install re-running its own script); otherwise treat that clone as
# its own master and keep worktrees in the default container.
_src_resolved=$(resolve_dir "$DSH_SOURCE")
case "$REPO_ROOT/" in
"$_src_resolved"/*) info "repository $REPO_ROOT is already inside $DSH_SOURCE" ;;
*) info "adopting clone $REPO_ROOT as its own master" ;;
esac
DSH_MASTER=$REPO_ROOT
else
step "Fetching source into $DSH_MASTER"
if [ -d "$DSH_MASTER/.git" ]; then
info "existing master clone found — updating"
git -C "$DSH_MASTER" fetch origin "$DSH_REF"
# Reset the master checkout to the freshly fetched tip. FETCH_HEAD (not
# origin/<ref>) so this resolves for a tag as well as a branch, and -B makes
# the re-run idempotent whether or not DSH_REF changed since the last install.
git -C "$DSH_MASTER" checkout -q -B "$DSH_REF" FETCH_HEAD
else
mkdir -p "$DSH_SOURCE"
git clone --branch "$DSH_REF" "$DSH_REPO" "$DSH_MASTER"
fi
# Physical on both branches: REPO_ROOT is compared against resolved paths
# below, and REPO_COMMON stays symmetric with it so neither can be read as
# carrying a different kind of path.
REPO_COMMON=$(resolve_dir "$DSH_MASTER/.git")
REPO_ROOT=$(resolve_dir "$DSH_MASTER")
fi
step "Adding staging worktree at $DSH_STAGING"
[ -e "$DSH_STAGING" ] && die "staging path $DSH_STAGING already exists — remove it or set DSH_SOURCE elsewhere, then re-run."
mkdir -p "$DSH_SOURCE"
# The staging worktree owns the branch dsh runs from; the repository stays as
# the fetch/upgrade base and is never a launcher target. A clone install
# branches from the ref it just fetched; adoption branches from the checkout's
# HEAD so the contributor's committed work is what runs.
if [ "$IN_REPO" = 1 ]; then
git -C "$DSH_CHECKOUT" worktree add -b "$DSH_STAGING_BRANCH" "$DSH_STAGING" HEAD
else
git -C "$DSH_MASTER" worktree add -b "$DSH_STAGING_BRANCH" "$DSH_STAGING" FETCH_HEAD 2>/dev/null \
|| git -C "$DSH_MASTER" worktree add -b "$DSH_STAGING_BRANCH" "$DSH_STAGING" HEAD
fi
# Exclude the per-worktree merge lock in the shared git dir's info/exclude,
# which every linked worktree inherits.
_exclude="$REPO_COMMON/info/exclude"
if [ -f "$_exclude" ] && ! grep -qxF '.agents/merge.lock' "$_exclude" 2>/dev/null; then
printf '.agents/merge.lock\n' >>"$_exclude"
fi
mkdir -p "$DSH_STAGING/.agents"
: >"$DSH_STAGING/.agents/merge.lock"
# --- 3. install dependencies (no build; the launcher runs from source) --------
step "Installing dependencies with pnpm (this can take a while)"
( cd "$DSH_STAGING" && pnpm install )
[ -x "$DSH_STAGING/bin/dsh" ] || die "launcher $DSH_STAGING/bin/dsh missing after install — is DSH_REF a branch that ships apps/cli?"
# --- 4. put `dsh` on PATH ------------------------------------------------------
# Every install goes through a stable `current` symlink so an upgrade repoints
# one symlink (current -> new worktree) and the PATH launcher never moves:
# PATH/dsh -> current/bin/dsh -> <staging>/bin/dsh.
step "Linking dsh into $DSH_BIN_DIR"
mkdir -p "$DSH_BIN_DIR"
# The launcher must resolve to a staging worktree, never to the repository
# itself: an upgrade repoints `current`, so aliasing it onto the master clone
# would make every upgrade rewrite the fetch/upgrade base. Compare physical
# paths — a symlinked or unresolved path would slip past a string compare.
_staging_resolved=$(resolve_dir "$DSH_STAGING")
[ "$_staging_resolved" = "$REPO_ROOT" ] \
&& die "refusing to point $DSH_CURRENT at the repository $REPO_ROOT — the launcher must resolve to a staging worktree."
# Point `current` at this staging worktree with `ln -sfn`: -f replaces an
# existing `current` (re-run or upgrade) and -n stops `ln` from dereferencing
# an existing symlink-to-directory and dropping the new link *inside* the old
# worktree. `mv` is unusable here — BSD/macOS `mv` follows the existing dir
# symlink the same way. The swap is one unlink+symlink pair on a local fs; the
# installer holds no other process racing this path.
ln -sfn "$DSH_STAGING" "$DSH_CURRENT"
info "pointed $DSH_CURRENT -> $DSH_STAGING"
DSH_LAUNCH_TARGET=$DSH_CURRENT/bin/dsh
ln -sf "$DSH_LAUNCH_TARGET" "$DSH_BIN_DIR/dsh"
info "linked $DSH_BIN_DIR/dsh -> $DSH_LAUNCH_TARGET"
case ":$PATH:" in
*":$DSH_BIN_DIR:"*) ON_PATH=1 ;;
*) ON_PATH=0 ;;
esac
if [ "$ON_PATH" = 0 ]; then
warn "$DSH_BIN_DIR is not on your PATH."
_line="export PATH=\"$DSH_BIN_DIR:\$PATH\""
_rc=''
_sh=${SHELL:-} # SHELL may be unset; word-removal on an unset var trips set -u under dash.
case "${_sh##*/}" in
zsh) _rc="$HOME/.zshrc" ;;
bash) _rc="$HOME/.bashrc" ;;
esac
if [ -n "$_rc" ] && [ -f "$_rc" ] && grep -qF "$_line" "$_rc" 2>/dev/null; then
info "$_rc already exports $DSH_BIN_DIR — open a new shell to pick it up"
elif [ -n "$_rc" ] && confirm "Add it to $_rc?" Y; then
printf '\n# Added by the dsh installer\n%s\n' "$_line" >>"$_rc"
info "updated $_rc — run 'source $_rc' or open a new shell to pick it up"
else
warn "add this line to your shell profile yourself:"
printf ' %s\n' "$_line"
fi
fi
# --- 5. credentials ------------------------------------------------------------
# Mirror app-boot's resolveDshHome precedence ($DSH_HOME, else ~/.dsh) so creds land where dsh reads them.
if [ -n "${DSH_HOME:-}" ]; then
CONF="$DSH_HOME"
else
CONF="$HOME/.dsh"
fi
ENV_FILE="$CONF/.env"
step "Configuring credentials"
if [ -f "$ENV_FILE" ] && grep -q '^DEEPSEEK_API_KEY=' "$ENV_FILE" 2>/dev/null; then
info "DEEPSEEK_API_KEY already set in $ENV_FILE"
if ! confirm "Replace it?" N; then
SKIP_CREDS=1
fi
fi
if [ "${SKIP_CREDS:-0}" != 1 ]; then
if [ "$HAS_TTY" = 1 ]; then
API_KEY=$(ask_secret "DeepSeek API key (input hidden):")
if [ -z "$API_KEY" ]; then
warn "no key entered — skipping. Set DEEPSEEK_API_KEY in $ENV_FILE before using dsh."
else
BASE_URL=$(ask "DeepSeek base URL (optional, Enter to skip):")
mkdir -p "$CONF"
# The installer owns exactly the two DEEPSEEK_* lines; any other lines the
# user keeps in this .env are preserved. The rewrite happens in a subshell
# so umask 077 (which closes the create-time permission race) does not leak
# into the exec'd dsh, and lands atomically via a same-dir temp + mv.
_tmp="$ENV_FILE.dsh.$$"
(
umask 077
if [ -f "$ENV_FILE" ]; then
grep -v -e '^DEEPSEEK_API_KEY=' -e '^DEEPSEEK_BASE_URL=' "$ENV_FILE" >"$_tmp" || true
else
: >"$_tmp"
fi
printf 'DEEPSEEK_API_KEY=%s\n' "$API_KEY" >>"$_tmp"
if [ -n "$BASE_URL" ]; then printf 'DEEPSEEK_BASE_URL=%s\n' "$BASE_URL" >>"$_tmp"; fi
)
mv "$_tmp" "$ENV_FILE"
chmod 600 "$ENV_FILE" 2>/dev/null || true
info "wrote $ENV_FILE"
fi
else
warn "no terminal for credential input — set DEEPSEEK_API_KEY in $ENV_FILE before using dsh."
fi
fi
# --- 6. build and launch the Web interface -------------------------------------
step "Done"
if [ "$HAS_TTY" = 1 ]; then
step "Building DeepSeek Harness for Web UI"
( cd "$DSH_STAGING" && pnpm run build )
info "launching Web UI — run 'dsh web' anytime to start again"
exec "$DSH_BIN_DIR/dsh" web </dev/tty
else
info "install complete. Build and start the Web UI with:"
printf ' (cd %s && pnpm run build)\n' "$DSH_STAGING"
printf ' %s web\n' "$DSH_BIN_DIR/dsh"
fi

View File

@@ -1,6 +1,6 @@
/**
* Shared JSDoc parsing and completeness checks for the Cordis, persistence,
* and config catalogs and the export-surface gate.
* and config catalogs and the exported-API gate.
*/
import ts from 'typescript'
@@ -124,7 +124,7 @@ export function parseTags(raw: string): { params: Map<string, string>; returns:
* binding-pattern parameters, and reject stale tags. Exempt parameters may
* still be documented.
* @param where - the offender label violations open with, e.g. `event 'x' (file:1)`.
* @param surface - surface noun used in binding-pattern diagnostics.
* @param apiKind - API kind used in binding-pattern diagnostics.
* @param parameters - the declaration's parameter list.
* @param tags - the parsed `@param` name→description map from parseTags.
* @param sf - source file used to render binding patterns.
@@ -133,7 +133,7 @@ export function parseTags(raw: string): { params: Map<string, string>; returns:
*/
export function checkParams(
where: string,
surface: string,
apiKind: string,
parameters: readonly ts.ParameterDeclaration[],
tags: Map<string, string>,
sf: ts.SourceFile,
@@ -142,7 +142,7 @@ export function checkParams(
): void {
for (const p of parameters) {
if (!ts.isIdentifier(p.name)) {
violations.push(`${where}: parameter '${p.name.getText(sf)}' is a binding pattern; the ${surface} surface needs simple identifier parameters so @param can name them.`)
violations.push(`${where}: parameter '${p.name.getText(sf)}' is a binding pattern; the ${apiKind} API needs simple identifier parameters so @param can name them.`)
continue
}
if (isExempt(p)) continue

View File

@@ -104,7 +104,7 @@ describe('rewriteMarkdown', () => {
repositoryRef: 'abc123',
})).toBe(
'[B](./reference/b.md#part) '
+ '[source](https://github.com/deepseek-ai/deepseek-harness-sdk/blob/abc123/packages/tool.ts#L2) '
+ '[source](https://github.com/deepseek-ai/deepseek-harness/blob/abc123/packages/tool.ts#L2) '
+ '[web](https://example.com)\n',
)
})
@@ -130,7 +130,7 @@ describe('rewriteMarkdown', () => {
pages,
repoRoot: root,
repositoryRef: 'abc123',
})).toBe('![logo](https://raw.githubusercontent.com/deepseek-ai/deepseek-harness-sdk/abc123/packages/logo.svg)\n')
})).toBe('![logo](https://raw.githubusercontent.com/deepseek-ai/deepseek-harness/abc123/packages/logo.svg)\n')
})
it('hands an image to the placer and uses the URL it returns', () => {
@@ -209,7 +209,7 @@ describe('rewriteMarkdown', () => {
repositoryRef: 'abc123',
})).toBe(
'[title](./reference/b.md "b.md") '
+ '[escaped](https://github.com/deepseek-ai/deepseek-harness-sdk/blob/abc123/docs/x(y).md)\n',
+ '[escaped](https://github.com/deepseek-ai/deepseek-harness/blob/abc123/docs/x(y).md)\n',
)
})

View File

@@ -15,7 +15,7 @@ import { gfm } from 'micromark-extension-gfm'
import type { Nodes } from 'mdast'
import { docsPages, type DocsLocale, type DocsPage } from '../website/docs.ts'
const REPOSITORY_URL = 'https://github.com/deepseek-ai/deepseek-harness-sdk'
const REPOSITORY_URL = 'https://github.com/deepseek-ai/deepseek-harness'
const root = resolve(import.meta.dirname, '..')
const generatedRoot = resolve(root, 'website/.generated')
@@ -209,7 +209,7 @@ function githubTarget(
image: boolean,
): string {
const path = repoPath(absPath, repoRoot)
if (image) return `https://raw.githubusercontent.com/deepseek-ai/deepseek-harness-sdk/${repositoryRef}/${path}${suffix}`
if (image) return `https://raw.githubusercontent.com/deepseek-ai/deepseek-harness/${repositoryRef}/${path}${suffix}`
const kind = lstatSync(absPath).isDirectory() ? 'tree' : 'blob'
const lineSuffix = line === undefined ? suffix : `#L${line}`
return `${REPOSITORY_URL}/${kind}/${repositoryRef}/${path}${lineSuffix}`

View File

@@ -79,8 +79,6 @@ interface GenericSkip {
const GENERIC_SKIPS: readonly GenericSkip[] = [
// `vendorPackages` lists vendor/ directory names, joined with 'vendor' below it.
{ file: 'packages/examples/acp-demo/tests/built-bin.e2e.ts', upstream: ['cordis', 'cosmokit', 'schemastery'] },
// Mixes join(root, 'vendor', 'cordis') paths with real manifest names.
{ file: 'packages/scaffold/helper/tests/documents.spec.ts', upstream: ['cordis'] },
// `Symbol.for('schemastery')` and the `vendor:` metadata field are upstream identifiers.
{ file: 'vendor/schemastery/src/index.ts', upstream: ['schemastery'] },
// Asserts the vendored-manifest table, which gains an upstream-name column.
@@ -121,8 +119,6 @@ const POSTCONDITIONS: readonly PostCondition[] = [
{ file: 'scripts/gen-scoped-events.ts', text: '=== \'@deepseek-ai/cordis\'', count: 1 },
{ file: 'packages/typert/generator/src/analyzer.ts', text: '!== \'@deepseek-ai/cordis\'', count: 2 },
{ file: 'scripts/check-workspace-constraints.ts', text: '?.[\'@deepseek-ai/cordis\']', count: 2 },
{ file: 'packages/scaffold/helper/src/project/npm-dependency-policy.ts', text: '\'@deepseek-ai/cordis\': \'^4.0.0-rc.7\'', count: 1 },
{ file: 'packages/scaffold/helper/src/plugins/local-plugin-blueprint.ts', text: '\'@deepseek-ai/cordis\': cordisSpec', count: 2 },
{ file: 'packages/boot/app-boot/tsdown.config.ts', text: '[\'@deepseek-ai/cordis-plugin-include\']', count: 1 },
{ file: 'tsconfig.base.json', text: '"@deepseek-ai/cordis-plugin-loader": ["./vendor/loader/src"]', count: 1 },
// One insertion, once: a duplicated log entry is what a non-idempotent apply produced.
@@ -134,8 +130,6 @@ const POSTCONDITIONS: readonly PostCondition[] = [
// The preset id the shipped composition documents to its own model.
{ file: 'apps/cli/config/agent-presets/cordis/agent.cordis.yml', text: 'The `cordis` agent preset', count: 1 },
{ file: 'apps/cli/config/agent-presets/cordis/agent.cordis.yml', text: 'corrupting the `cordis` preset', count: 1 },
// The vendor-directory paths in these fixtures must survive the rename.
{ file: 'packages/scaffold/helper/tests/documents.spec.ts', text: 'join(root, \'vendor\', \'cordis\')', count: 2 },
{ file: 'packages/examples/acp-demo/tests/built-bin.e2e.ts', text: '\'cordis\', \'loader\', \'include\', \'timer\', \'hmr\', \'logger-console\',', count: 1 },
]
@@ -171,76 +165,6 @@ const EXACT_EDITS: readonly ExactEdit[] = [
errors.push(\`\${label}: @deepseek-ai/cordis peer (\${peer}) and dev (\${dev}) ranges must match\`)`,
expect: 1,
},
{
id: 'scaffold-dependency-policy',
file: 'packages/scaffold/helper/src/project/npm-dependency-policy.ts',
find: ' cordis: \'^4.0.0-rc.7\',',
replace: ' \'@deepseek-ai/cordis\': \'^4.0.0-rc.7\',',
expect: 1,
},
{
id: 'scaffold-plugin-blueprint',
file: 'packages/scaffold/helper/src/plugins/local-plugin-blueprint.ts',
find: ` cordis: cordisSpec,
},
devDependencies: {
cordis: cordisSpec,
},`,
replace: ` '@deepseek-ai/cordis': cordisSpec,
},
devDependencies: {
'@deepseek-ai/cordis': cordisSpec,
},`,
expect: 1,
},
{
id: 'scaffold-link-workspace-lookup',
file: 'packages/scaffold/create-sdk/tests/link-workspace.e2e.ts',
find: 'manifest.dependencies.cordis',
replace: 'manifest.dependencies[\'@deepseek-ai/cordis\']',
expect: 1,
},
{
id: 'documents-spec-manifest-name',
file: 'packages/scaffold/helper/tests/documents.spec.ts',
find: 'JSON.stringify({ name: \'cordis\' })',
replace: 'JSON.stringify({ name: \'@deepseek-ai/cordis\' })',
expect: 1,
},
{
id: 'documents-spec-peer-key',
file: 'packages/scaffold/helper/tests/documents.spec.ts',
find: 'peerDependencies: { cordis: \'^4\' },',
replace: 'peerDependencies: { \'@deepseek-ai/cordis\': \'^4\' },',
expect: 1,
},
{
id: 'documents-spec-closure-order',
file: 'packages/scaffold/helper/tests/documents.spec.ts',
find: ' \'@deepseek-ai/dsh-helper\', \'@deepseek-ai/dsh-scripts\', \'cordis\',',
replace: ' \'@deepseek-ai/cordis\', \'@deepseek-ai/dsh-helper\', \'@deepseek-ai/dsh-scripts\',',
expect: 1,
},
{
id: 'documents-spec-lookups',
file: 'packages/scaffold/helper/tests/documents.spec.ts',
find: ` expect(manifest.npmDependency('cordis')?.spec).toMatch(/^link:/)
expect(pnpmWorkspace.serialize()).toContain('autoInstallPeers: false')
expect(workspace.packageDirectory('cordis')).toBe(join(root, 'vendor', 'cordis'))
expect(await readFile(join(root, 'vendor', 'cordis', 'package.json'), 'utf8')).toContain('cordis')`,
replace: ` expect(manifest.npmDependency('@deepseek-ai/cordis')?.spec).toMatch(/^link:/)
expect(pnpmWorkspace.serialize()).toContain('autoInstallPeers: false')
expect(workspace.packageDirectory('@deepseek-ai/cordis')).toBe(join(root, 'vendor', 'cordis'))
expect(await readFile(join(root, 'vendor', 'cordis', 'package.json'), 'utf8')).toContain('@deepseek-ai/cordis')`,
expect: 1,
},
{
id: 'documents-spec-policy-lookup',
file: 'packages/scaffold/helper/tests/documents.spec.ts',
find: ' expect(resolveNpmDependency(\'cordis\', \'devDependencies\', \'0.0.1\')).toEqual({',
replace: ' expect(resolveNpmDependency(\'@deepseek-ai/cordis\', \'devDependencies\', \'0.0.1\')).toEqual({',
expect: 1,
},
{
// The rescoped name is already covered by the `@deepseek-ai/.+` pattern beside it.
id: 'knip-logger-console',
@@ -318,8 +242,8 @@ const EXACT_EDITS: readonly ExactEdit[] = [
{
id: 'vendor-readme-local-modification-log',
file: 'vendor/README.md',
find: '\n## Sync procedure',
replace: '17. **`@deepseek-ai` rescope**: every vendored manifest `name`, every internal dependency entry among the vendored set, and every module specifier that reaches them use the scoped names in the manifest table\'s `npm name` column. Directory names, version numbers, and dependency ranges are unchanged, and no upstream runtime identifier is renamed — `Symbol.for(\'schemastery\')` and Schemastery\'s `vendor:` metadata field keep their upstream values. Re-apply with `pnpm run rescope-vendor --apply` after a sync; the table\'s two name columns are the mapping, restated for consumers in [docs/rescope.md](../docs/rescope.md).\n\n## Sync procedure',
find: '\n18. **`cordis/package.json` publishes `src`**',
replace: '\n17. **`@deepseek-ai` rescope**: every vendored manifest `name`, every internal dependency entry among the vendored set, and every module specifier that reaches them use the scoped names in the manifest table\'s `npm name` column. Directory names, version numbers, and dependency ranges are unchanged, and no upstream runtime identifier is renamed — `Symbol.for(\'schemastery\')` and Schemastery\'s `vendor:` metadata field keep their upstream values. Re-apply with `pnpm run rescope-vendor --apply` after a sync; the table\'s two name columns are the mapping, restated for consumers in [docs/rescope.md](../docs/rescope.md).\n18. **`cordis/package.json` publishes `src`**',
expect: 1,
},
{

View File

@@ -622,7 +622,7 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate {
'apps/cli/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/scaffold/server/tests/built-scope-carrier.e2e.ts',
'packages/sdk/server/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',

File diff suppressed because one or more lines are too long

View File

@@ -281,7 +281,7 @@ export function parseTranslationPairingCliArgs(argv: string[]): TranslationPairi
}
}
/** The structural surface compared between the two sides of a pair. */
/** The structural signature compared between the two sides of a pair. */
export interface TranslationStructureSignature {
/** Heading depths in document order (h2 -> 2). */
headings: number[]

View File

@@ -440,6 +440,11 @@
"symbol": "SessionLocation",
"source": "packages/session/session-persistence/src/index.ts"
},
{
"doc": "docs/subsystems/persistence.md",
"symbol": "SessionRawArtifact",
"source": "packages/session/session-persistence/src/index.ts"
},
{
"doc": "docs/subsystems/session-query.md",
"symbol": "SessionEventSurface",
@@ -1165,6 +1170,11 @@
"symbol": "SubagentReportDelivery",
"source": "packages/subagent/subagent/src/continuation.ts"
},
{
"doc": "docs/subsystems/subagent.md",
"symbol": "SubagentSettledMessageSource",
"source": "packages/subagent/subagent/src/continuation.ts"
},
{
"doc": "docs/subsystems/subagent.md",
"symbol": "SubagentReportOptions",
@@ -1739,6 +1749,101 @@
"doc": "docs/subsystems/core.md",
"symbol": "AgentOptions",
"source": "packages/core/agent/src/runtime-types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackVersion",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackRating",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackItem",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackListRequest",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackListValue",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackPutRequest",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackDeleteRequest",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackDeleteValue",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackSessionNotFound",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackTargetNotFound",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackVersionConflict",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackNoteBlank",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackNoteTooLarge",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackFailure",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackSuccess",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackRejected",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackListResult",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackPutResult",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackDeleteResult",
"source": "packages/feedback/message-feedback/src/types.ts"
}
]
}

View File

@@ -6,7 +6,7 @@
* across domains.
*
* Layer model (lower may not import higher):
* 0 contract/ shared contract surface (types + slot declarations)
* 0 contract/ shared contract API (types + slot declarations)
* 1 <domain>/ + service domain implementations (skeleton/, chat/, ...)
* 2 apply.ts, index.ts assembly point and re-export shell
*
@@ -71,7 +71,7 @@ function checkPackage(pkgName: string, clientDir: string): Violation[] {
imported: spec,
reason: fromDomain === ''
? `top-level non-assembly file imports domain "${toDomain}" (only apply/index may assemble)`
: `domain "${fromDomain}" imports sibling domain "${toDomain}" (route shared surface through contract/)`,
: `domain "${fromDomain}" imports sibling domain "${toDomain}" (route shared API through contract/)`,
})
}
}

View File

@@ -58,7 +58,7 @@ function thisReceiver(p: ts.ParameterDeclaration): boolean {
}
/**
* Peel wrapper expressions that carry no surface of their own — parentheses,
* Peel wrapper expressions that define no API of their own — parentheses,
* `as` / `satisfies` / angle-bracket casts, non-null assertions — so a
* wrapped function expression is still classified as function-like.
* @param e - the expression to unwrap.
@@ -91,7 +91,7 @@ function callableAnnotation(type: ts.TypeNode): ts.SignatureDeclarationBase | 'r
}
/**
* Find inherited documentation for a class member without exempting newly public surface.
* Find inherited documentation for a class member without exempting a newly public API.
* @param cls - the class whose heritage to search.
* @param name - the member name to look up.
* @param staticSide - whether to search the constructor side instead of the instance side.
@@ -112,7 +112,7 @@ function heritageExemption(
const prop = type.getProperty(name)
if (prop === undefined) continue
const decls = prop.declarations ?? []
if (decls.length > 0 && decls.every(isProtected)) continue // public override of a protected base: new surface
if (decls.length > 0 && decls.every(isProtected)) continue // public override of a protected base: new API
let baseParams: Set<string> | null = null
let baseVoidReturn: boolean | null = null
for (const d of decls) {
@@ -192,7 +192,7 @@ function checkFunctionLike(
if (!raw) { w.violations.push(`${where} has no JSDoc.`); return }
if (!parseJsDoc(raw).doc) w.violations.push(`${where} has no description prose above its block tags.`)
const { params, returns } = parseTags(raw)
checkParams(where, 'export', parameters, params, w.sf, thisReceiver, w.violations)
checkParams(where, 'exported', parameters, params, w.sf, thisReceiver, w.violations)
if (!returnsWaived) checkReturns(where, returnType, returns, w.sf, w.violations)
}
@@ -205,7 +205,7 @@ function checkFunctionLike(
* statics are exempt; constructors are not checked (framework-constructed
* plugins, and the class doc owns the story).
* @param cls - the exported class declaration.
* @param name - the class's surface name (namespace-qualified).
* @param name - the class's exported name (namespace-qualified).
* @param w - the walk state violations append to.
*/
function checkClass(cls: ts.ClassDeclaration, name: string, w: Walk): void {
@@ -230,12 +230,12 @@ function checkClass(cls: ts.ClassDeclaration, name: string, w: Walk): void {
const raw = rawJsDoc(w.text, m)
// The heritage declaration owns the prose; parameters the base never
// names — including binding patterns, which no base declaration can
// name — are new surface and keep their @param duty.
// name — are new API and keep their @param duty.
const base = exemption.baseParams
const inBase = (p: ts.ParameterDeclaration): boolean =>
base !== null && ts.isIdentifier(p.name) && base.has(p.name.text.replace(/^_+/, ''))
if (base !== null && m.parameters.some(p => !thisReceiver(p) && !inBase(p))) {
checkParams(where, 'export', m.parameters, parseTags(raw).params, w.sf,
checkParams(where, 'exported', m.parameters, parseTags(raw).params, w.sf,
p => thisReceiver(p) || inBase(p), w.violations)
}
// A void base return carried no @returns duty, so an override growing a concrete result
@@ -258,7 +258,7 @@ function checkClass(cls: ts.ClassDeclaration, name: string, w: Walk): void {
} else if (ts.isSetAccessorDeclaration(m) && !documentedGetters.has(mname)) {
checkDescribed(`exported class accessor '${name}.${mname}' (${pointer(w.rel, w.sf, m)})`, rawJsDoc(w.text, m), w)
}
// index signatures / static blocks: not named surface
// index signatures / static blocks: no named API
}
}
@@ -310,7 +310,7 @@ function checkDecl(
const raw = rawJsDoc(w.text, stmt) // JSDoc sits on the statement, not the declarator
for (const d of stmt.declarationList.declarations) {
const name = ts.isIdentifier(d.name) ? d.name.text : d.name.getText(w.sf)
if (only !== null && !only.has(name)) continue // sibling declarator the export list never named: not surface
if (only !== null && !only.has(name)) continue // sibling declarator the export list never named: not exported API
if (prefix === '' && PROTOCOL_EXPORTS.has(name)) continue // cordis plugin-protocol slot
const where = `exported const '${prefix}${name}'${at(d)}`
const annotation = d.type !== undefined ? callableAnnotation(d.type) : null
@@ -321,7 +321,7 @@ function checkDecl(
// tags against — fail closed rather than silently narrow the check.
w.violations.push(`${where}: its callable type literal is not gate-classifiable; extract a named type and document it there.`)
} else if (annotation !== null) {
// An INLINE callable annotation is the surface signature itself: its
// An INLINE callable annotation is the exported signature itself: its
// parameters and result need docs right here. (A NAMED reference
// type carries its docs at the type's own declaration instead.)
checkFunctionLike(where, raw, annotation.parameters, annotation.type, false, w)
@@ -350,7 +350,7 @@ function checkDecl(
}
// In an ambient (`declare`) namespace body, members are implicitly
// exported — no `export` modifier required — so the recursion must treat
// every statement as surface.
// every statement as exported API.
const declared = ambient
|| ((ts.canHaveModifiers(stmt) ? ts.getModifiers(stmt) : undefined)?.some(m => m.kind === ts.SyntaxKind.DeclareKeyword) ?? false)
if (body !== undefined && ts.isModuleBlock(body)) checkScope(body.statements, nsPrefix, w, declared)
@@ -376,7 +376,7 @@ function checkDecl(
}
// Fail CLOSED: an exported statement kind this dispatch does not recognize
// must never pass silently — the gate's whole promise is that unchecked
// surface cannot exist. New TypeScript export forms extend the gate here.
// unchecked API cannot exist. New TypeScript export forms extend the gate here.
w.violations.push(`exported statement${at(stmt)} uses an export form verify-export-jsdoc does not handle; extend the gate.`)
}
@@ -385,7 +385,7 @@ function checkDecl(
* exported declaration, resolving `export { … }` lists (no module specifier)
* to their local declarations.
* @param statements - the scope's statements.
* @param prefix - the namespace qualification for surface names ('' at top level).
* @param prefix - the namespace qualification for exported names ('' at top level).
* @param w - the walk state violations append to.
* @param ambient - whether this scope is ambient (`declare` namespace or a declaration file), where members export implicitly.
*/
@@ -445,7 +445,7 @@ function checkScope(
}
if (ts.isExportAssignment(stmt)) {
if (stmt.isExportEquals) {
// `export =` has no ESM consumer surface in this repo and the walk
// `export =` has no ESM consumer API in this repo and the walk
// cannot classify its operand's type; refuse rather than fail open.
w.violations.push(`export-equals assignment (${pointer(w.rel, w.sf, stmt)}) is not a gate-supported export form; use ESM named exports.`)
continue
@@ -599,11 +599,11 @@ export function collectExportJsdocViolations(scanRoot: string = root): string[]
return violations
}
/** CLI entry: list every violation and exit 1, or confirm a clean surface. */
/** CLI entry: list every violation and exit 1, or confirm a documented API. */
function main(): void {
const violations = collectExportJsdocViolations()
if (violations.length === 0) {
console.log('verify-export-jsdoc: every exported name on the package surface is documented.')
console.log('verify-export-jsdoc: every exported name in each package API is documented.')
return
}
console.error(`verify-export-jsdoc: ${violations.length} JSDoc completeness violation(s) (see AGENTS.md):`)

View File

@@ -27,7 +27,6 @@ const PATTERNS = [
'AGENTS.md',
'packages/AGENTS.md',
'.agents/skills/**/*.md',
'skills/**/*.md',
]
/** A broken relative link: a missing target path or a missing anchor on it. */

View File

@@ -26,7 +26,6 @@ const PATTERNS = [
'AGENTS.md',
'packages/AGENTS.md',
'.agents/skills/**/*.md',
'skills/**/*.md',
]
interface Block {

View File

@@ -151,7 +151,7 @@ try {
cwd: root,
stdio: 'pipe',
})
console.log(`verify-node-next-types: ${packages.length} workspace package declaration surface(s) compile under NodeNext.`)
console.log(`verify-node-next-types: ${packages.length} workspace package declaration API(s) compile under NodeNext.`)
} catch (error: unknown) {
failed = true
const output = error as { stdout?: Buffer; stderr?: Buffer }

View File

@@ -68,7 +68,7 @@ function isDriftedPackageReference(ref: string): boolean {
// A missing reference is drift only when a path segment names a live package.
// A leading segment that is itself an existing group directory is explained by
// the group, not by a relocated leaf sharing its name (`client` is both the
// client-modules group and the scaffold leaf), so only later segments count.
// client-modules group and the sdk leaf), so only later segments count.
const segments = ref.split('/').slice(1)
const [group] = segments
const scanned = group !== undefined && segments.length > 1 && existsSync(resolve(root, 'packages', group))

View File

@@ -38,14 +38,14 @@ const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
/**
* Packages whose Model Experience is simple enough for one gated sentence plus
* a KV-cache field. Every other package must carry canonical context-surface
* a KV-cache field. Every other package must carry canonical model-context
* blocks. A package moves on or off this list with its context behavior.
*/
const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/attachment/attachment': { kind: 'indirect', reason: 'The storage seam delegates model request rendering to provider adapters.' },
'packages/attachment/attachment-local': { kind: 'indirect', reason: 'The local backend delegates model request rendering to provider adapters.' },
'packages/bash/bash': { kind: 'indirect', reason: 'The service interface delegates all model rendering to dsh-tool-bash.' },
'packages/bash/bash-env': { kind: 'indirect', reason: 'The env service surfaces managed DSH_* facts through the shell tools (dsh-tool-bash/dsh-tool-pwsh); it registers no prompt or schema of its own.' },
'packages/bash/bash-env': { kind: 'indirect', reason: 'The env service exposes 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.' },
@@ -57,22 +57,23 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/typert/registry': { kind: 'none', reason: 'Runtime type registry; consumers (cordis_inspect, wire faces, gates) own any model-visible projection of registry contents.' },
'packages/typert/loader': { kind: 'none', reason: 'Loader integration only registers generated artifacts; consumers own any model-visible projection.' },
'packages/e2b/e2b': { kind: 'none', reason: 'The shared remote-runtime owner registers no model context; provider adapters and consumers own rendered effects.' },
'packages/client/hmr': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/modules': { kind: 'none', reason: 'Browser-side module-loading kernel machinery; registers no model surface.' },
'packages/client/test-runtime': { kind: 'none', reason: 'Browser-side test infrastructure (jsdom bench); registers no model surface.' },
'packages/client/ui-slots': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-primitives': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'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/client/hmr': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/modules': { kind: 'none', reason: 'Browser-side module-loading kernel machinery; registers nothing model-facing.' },
'packages/client/test-runtime': { kind: 'none', reason: 'Browser-side test infrastructure (jsdom bench); registers nothing model-facing.' },
'packages/client/ui-slots': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-primitives': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/web-react': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/schema-form': { kind: 'none', reason: 'Browser-side form-rendering library; registers nothing model-facing.' },
'packages/client/connection': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'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/runtime': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-layout': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-sidebar': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-conversation': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-tool': { kind: 'none', reason: 'Browser-side Tool presentation layer; renders logged calls without changing model context.' },
'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-deliverables': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-task': { kind: 'none', reason: 'Browser-side read-only projection of ctx.tasks records; dsh-tool-tasks owns the model-facing behavior.' },
'packages/client/ui-slash': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'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 selection at the next prompt-assembly boundary and owns the model-visible effect.' },
'packages/client/ui-goal': { kind: 'indirect', reason: 'The strip verbs route goal.* mutations; the host GoalService owns the model-visible goal/change context message.' },
@@ -80,27 +81,27 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/client/ui-plugin-config': { kind: 'none', reason: 'Browser-side settings surface; registers no model surface.' },
'packages/client/ui-plan': { kind: 'indirect', reason: 'The chip dispatches /plan off; dsh-plan-mode owns the model-visible policy, exit tool, and logged state.' },
'packages/client/ui-question': { kind: 'indirect', reason: 'The package mounts dsh-tool-ask-user; that tool owns the model-visible schema and answer rendering.' },
'packages/client/ui-trajectory': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-workspace': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-theme': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-settings': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-settings-general': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-models': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/locale': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/web': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-trajectory': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-workspace': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-theme': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-settings': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-settings-general': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-models': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/locale': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/web': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/examples/agent-spine-demo': { kind: 'indirect', reason: 'The bundle only mounts model-facing child plugins.' },
'packages/fs/fs': { kind: 'indirect', reason: 'The service interface delegates model rendering to dsh-tool-fs.' },
'packages/e2b/fs-e2b': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-fs.' },
'packages/fs/fs-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-fs.' },
'packages/hooks/hook-protocol': { kind: 'indirect', reason: 'Only the hook bridge plugins render decoded hook output to a model.' },
'packages/host/apiproxy': { kind: 'none', reason: 'The wire contract and fetch carriers move already-composed messages and register no model surface.' },
'packages/host/directory-picker': { kind: 'none', reason: 'The GUI-host picking seam registers no model surface.' },
'packages/host/directory-picker-auto': { kind: 'none', reason: 'The GUI-host picking chooser only mounts a backend row; registers no model surface.' },
'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/host/apiproxy': { kind: 'none', reason: 'The wire contract and fetch carriers move already-composed messages and register nothing model-facing.' },
'packages/host/directory-picker': { kind: 'none', reason: 'The GUI-host picking seam registers nothing model-facing.' },
'packages/host/directory-picker-auto': { kind: 'none', reason: 'The GUI-host picking chooser only mounts a backend row; it registers nothing model-facing.' },
'packages/host/directory-picker-browse': { kind: 'none', reason: 'The GUI-host picking backend registers nothing model-facing.' },
'packages/host/directory-picker-native': { kind: 'none', reason: 'The GUI-host picking backend registers nothing model-facing.' },
'packages/host/webserver': { kind: 'none', reason: 'The HTTP carrier bridges browser and API handler and registers nothing model-facing.' },
'packages/host/frontend-static': { kind: 'none', reason: 'The SPA dist server answers browser asset requests and registers nothing model-facing.' },
'packages/bundle/base': { kind: 'indirect', reason: 'The bundle is a patch-list carrier; each inserted row\'s package owns its model-facing behavior.' },
'packages/bundle/headless': { kind: 'none', reason: 'The one-shot runner submits the task as an ordinary user message; prompts and tools belong to the composed base and headless bundles.' },
'packages/llm/llm': { kind: 'none', reason: 'The adapter registry forwards already-assembled requests unchanged.' },
'packages/llm/token-meter': { kind: 'indirect', reason: 'The measurement service leaves model-visible changes to its consumers.' },
@@ -111,24 +112,20 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/subprocess/subprocess-local': { kind: 'indirect', reason: 'The spawn backend delegates model rendering to consumer seams such as the bash executor family.' },
'packages/sandbox/sandbox-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-bash-sandbox and dsh-tool-bash.' },
'packages/sandbox/sandbox-windows-acl': { kind: 'indirect', reason: 'The provider backend delegates model rendering to the bash/pwsh sandbox executors and their tools.' },
'packages/scaffold/create-sdk': { kind: 'indirect', reason: 'The initializer only writes project files; selected runtime plugins provide the generated project model surface.' },
'packages/scaffold/helper': { kind: 'none', reason: 'The project domain edits files and registers no live agent or model surface.' },
'packages/scaffold/scripts': { kind: 'indirect', reason: 'The launcher delegates model context to the loaded project plugin tree.' },
'packages/scaffold/client': { kind: 'none', reason: 'Client-process library; the model surface lives in the spawned runtime\'s composed plugins.' },
'packages/scaffold/protocol': { kind: 'none', reason: 'Client-facing wire library; the runtime plugins behind the serving entry own the model surface.' },
'packages/scaffold/telemetry': { kind: 'none', reason: 'The launcher-side reporter sends developer-cycle telemetry and registers no live agent or model surface.' },
'packages/session/session-projection': { kind: 'none', reason: 'The projection registry serves client-facing read models of already-logged session state and registers no model surface.' },
'packages/session/session-projection-cache': { kind: 'none', reason: 'The persisted cache accelerates host-side cold reads of projection state and registers no model surface.' },
'packages/session-query/session-query': { kind: 'none', reason: 'The trusted query service exposes cloned records only to callers and registers no model surface.' },
'packages/session-query/session-query-sqlite': { kind: 'none', reason: 'The search backend returns hits only to callers and registers no model surface.' },
'packages/settings/settings': { kind: 'indirect', reason: 'The seam stores and resolves user settings; consumer plugins own any model surface a value feeds.' },
'packages/settings/settings-local': { kind: 'indirect', reason: 'The file provider stores and publishes namespace sections; consumers of ctx.settings own any model surface.' },
'packages/credentials/credentials': { kind: 'indirect', reason: 'The seam resolves credential references; the consuming adapter owns every model surface a value authorizes.' },
'packages/credentials/credentials-local': { kind: 'indirect', reason: 'The file/environment provider stores credential values; consumers of ctx.credentials own any model surface.' },
'packages/util/atomic-write': { kind: 'none', reason: 'Pure filesystem write primitive; registers no model surface.' },
'packages/session/session-telemetry': { kind: 'none', reason: 'The seam observes the session stream and hands redacted copies outward; it registers no model surface.' },
'packages/session/session-telemetry-otel': { kind: 'none', reason: 'The backend forwards seam records into the OTel SDK pipeline and registers no model surface.' },
'packages/session/user-id': { kind: 'none', reason: 'The shared identifier appears only in telemetry metadata and a direct human command response; it registers no model surface.' },
'packages/sdk/client': { kind: 'none', reason: 'Client-process library; model-facing behavior lives in the spawned runtime\'s composed plugins.' },
'packages/sdk/protocol': { kind: 'none', reason: 'Client-facing wire library; the runtime plugins behind the serving entry own model-facing behavior.' },
'packages/session/session-projection': { kind: 'none', reason: 'The projection registry serves client-facing read models of already-logged session state and registers nothing model-facing.' },
'packages/session/session-projection-cache': { kind: 'none', reason: 'The persisted cache accelerates host-side cold reads of projection state and registers nothing model-facing.' },
'packages/session-query/session-query': { kind: 'none', reason: 'The trusted query service exposes cloned records only to callers and registers nothing model-facing.' },
'packages/session-query/session-query-sqlite': { kind: 'none', reason: 'The search backend returns hits only to callers and registers nothing model-facing.' },
'packages/settings/settings': { kind: 'indirect', reason: 'The seam stores and resolves user settings; consumer plugins own any model-facing content fed by a value.' },
'packages/settings/settings-local': { kind: 'indirect', reason: 'The file provider stores and publishes namespace sections; consumers of ctx.settings own any model-facing behavior.' },
'packages/credentials/credentials': { kind: 'indirect', reason: 'The seam resolves credential references; the consuming adapter owns every model-facing use a value authorizes.' },
'packages/credentials/credentials-local': { kind: 'indirect', reason: 'The file/environment provider stores credential values; consumers of ctx.credentials own any model-facing behavior.' },
'packages/util/atomic-write': { kind: 'none', reason: 'Pure filesystem write primitive; registers nothing model-facing.' },
'packages/session/session-telemetry': { kind: 'none', reason: 'The seam observes the session stream and hands redacted copies outward; it registers nothing model-facing.' },
'packages/session/session-telemetry-otel': { kind: 'none', reason: 'The backend forwards seam records into the OTel SDK pipeline and registers nothing model-facing.' },
'packages/session/user-id': { kind: 'none', reason: 'The shared identifier appears only in telemetry metadata and a direct human command response; it registers nothing model-facing.' },
'packages/skill/skill': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-skill.' },
'packages/skill/skill-badge': { kind: 'indirect', reason: 'The bundled provider delegates model rendering to dsh-tool-skill.' },
'packages/skill/skill-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-skill.' },
@@ -141,9 +138,9 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'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/type-meta': { kind: 'none', reason: 'Compiler-independent Remote protocol declarations; registers nothing model-facing.' },
'packages/typert/generator': { kind: 'none', reason: 'The build-time generator runs outside any agent runtime and touches no model request.' },
'packages/tasks/tasks': { kind: 'indirect', reason: 'Producer and control-surface plugins own all model rendering over the task registry.' },
'packages/tasks/tasks': { kind: 'indirect', reason: 'Producer and controller 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.' },
'packages/examples/acp-demo': { kind: 'indirect', reason: 'The app bundle delegates request composition to dsh-agent-spine-demo and dsh-acp.' },
'packages/boot/app-boot': { kind: 'indirect', reason: 'Only the loaded plugin tree contributes model context.' },
@@ -153,7 +150,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/interaction/user-interaction': { kind: 'indirect', reason: 'Model-facing consumers render provider answers and seam errors.' },
'packages/util/timeout': { kind: 'indirect', reason: 'Only timeout consumers render timeout outcomes.' },
'packages/util/retention': { kind: 'indirect', reason: 'Only retention consumers render retained content and omission metadata.' },
'packages/util/native-command': { kind: 'none', reason: 'The host-side subprocess runner registers no model surface.' },
'packages/util/native-command': { kind: 'none', reason: 'The host-side subprocess runner registers nothing model-facing.' },
'packages/web/web': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-web.' },
'packages/web/web-fetch-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-web.' },
'packages/web/web-search-exa': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-web.' },
@@ -167,7 +164,7 @@ interface Failure {
type Line = MarkdownProseLine
interface ContextSurface {
interface ModelExperienceEntry {
heading: Line
modelView: Line
tokenEffect: Line
@@ -199,7 +196,7 @@ function validateNestedVerbatim(raw: readonly string[], fragments: Set<string>):
const fragment = headingFragment(title)
if (fragment.length === 0) return { blocks, error: 'verbatim H5 title must be non-empty' }
if (fragments.has(fragment)) {
return { blocks, error: `verbatim H5 title ${JSON.stringify(title)} is duplicated within its context surface` }
return { blocks, error: `verbatim H5 title ${JSON.stringify(title)} is duplicated within its model-context entry` }
}
fragments.add(fragment)
cursor += 1
@@ -224,7 +221,7 @@ function headingFragment(title: string): string {
}
/** A direct stable system-prompt contribution, as named by the README rules. */
function isDirectSystemPromptSurface(title: string): boolean {
function isDirectSystemPromptEntry(title: string): boolean {
return /\bsystem prompt\b/i.test(title)
}
@@ -244,13 +241,13 @@ const failures: Failure[] = []
const packageJsons = globSync('packages/*/*/package.json', { cwd: root }).map(path => path.split(sep).join('/')).sort()
const scannedPackages = new Set(packageJsons.map(path => path.slice(0, -'/package.json'.length)))
let structuredCount = 0
let contextSurfaceCount = 0
let modelContextEntryCount = 0
let omittedSectionCount = 0
let explainedNoneCount = 0
let indirectCount = 0
let verbatimBlockCount = 0
let systemPromptSurfaceCount = 0
let toolSchemaSurfaceCount = 0
let systemPromptEntryCount = 0
let toolSchemaEntryCount = 0
let kvCacheEffectCount = 0
for (const [pkg, reason] of Object.entries(NO_MODEL_EXPERIENCE_SECTION)) {
@@ -270,7 +267,7 @@ for (const [pkg, contract] of Object.entries(SENTENCE_MODEL_EXPERIENCE)) {
failures.push({ path: `${pkg}/README.md`, message: 'sentence allowlist entry does not name a scanned package' })
}
if (contract.reason.trim().length === 0) {
failures.push({ path: `${pkg}/README.md`, message: 'sentence allowlist entry must justify why structured context surfaces are unnecessary' })
failures.push({ path: `${pkg}/README.md`, message: 'sentence allowlist entry must justify why structured model-context entries are unnecessary' })
}
}
@@ -379,47 +376,47 @@ for (const packageJson of packageJsons) {
continue
}
const surfaceStarts = content
const entryStarts = content
.map((line, index) => ({ line, index }))
.filter(entry => /^### \S/.test(entry.line.raw))
if (surfaceStarts.length === 0 || surfaceStarts[0]?.index !== 0) {
failures.push({ path: readme, message: 'must contain one or more complete context-surface blocks' })
if (entryStarts.length === 0 || entryStarts[0]?.index !== 0) {
failures.push({ path: readme, message: 'must contain one or more complete model-context entries' })
continue
}
const surfaces: ContextSurface[] = []
const surfaceFragments = new Set<string>()
let surfaceError = false
for (let surfaceIndex = 0; surfaceIndex < surfaceStarts.length; surfaceIndex += 1) {
const start = surfaceStarts[surfaceIndex] as { line: Line; index: number }
const end = surfaceStarts[surfaceIndex + 1]?.index ?? content.length
const modelContextEntries: ModelExperienceEntry[] = []
const entryFragments = new Set<string>()
let entryError = false
for (let entryIndex = 0; entryIndex < entryStarts.length; entryIndex += 1) {
const start = entryStarts[entryIndex] as { line: Line; index: number }
const end = entryStarts[entryIndex + 1]?.index ?? content.length
const entries = content.slice(start.index, end)
const heading = entries[0] as Line
const title = heading.raw.slice('### '.length)
const fragment = headingFragment(title)
if (fragment.length === 0) {
failures.push({ path: readme, message: `line ${heading.index}: each context surface requires a non-empty H3 heading` })
surfaceError = true
failures.push({ path: readme, message: `line ${heading.index}: each model-context entry requires a non-empty H3 heading` })
entryError = true
break
}
if (surfaceFragments.has(fragment)) {
failures.push({ path: readme, message: `line ${heading.index}: duplicate context-surface link fragment ${JSON.stringify(fragment)}` })
surfaceError = true
if (entryFragments.has(fragment)) {
failures.push({ path: readme, message: `line ${heading.index}: duplicate model-context entry link fragment ${JSON.stringify(fragment)}` })
entryError = true
break
}
const fieldStarts = entries
.map((line, index) => ({ line, index }))
.filter(entry => /^#### \S/.test(entry.line.raw))
if (fieldStarts.length !== FIELD_HEADINGS.length || fieldStarts[0]?.index !== 1) {
failures.push({ path: readme, message: `line ${heading.index}: context surface requires exactly three ordered H4 fields: ${FIELD_HEADINGS.join(', ')}` })
surfaceError = true
failures.push({ path: readme, message: `line ${heading.index}: model-context entry requires exactly three ordered H4 fields: ${FIELD_HEADINGS.join(', ')}` })
entryError = true
break
}
if ((surfaceIndex === 0 && heading.index !== modelHeading.index + 2)
if ((entryIndex === 0 && heading.index !== modelHeading.index + 2)
|| rawLines[heading.index - 2]?.trim().length !== 0
|| fieldStarts[0].line.index !== heading.index + 2) {
failures.push({ path: readme, message: `line ${heading.index}: context-surface heading and first field require one blank line between them` })
surfaceError = true
failures.push({ path: readme, message: `line ${heading.index}: model-context entry heading and first field require one blank line between them` })
entryError = true
break
}
const parsedFields: ParsedField[] = []
@@ -429,7 +426,7 @@ for (const packageJson of packageJsons) {
const expectedHeading = FIELD_HEADINGS[fieldIndex] as string
if (fieldStart.line.raw !== expectedHeading) {
failures.push({ path: readme, message: `line ${fieldStart.line.index}: expected exact field heading ${JSON.stringify(expectedHeading)}, found ${JSON.stringify(fieldStart.line.raw)}` })
surfaceError = true
entryError = true
break
}
const fieldEnd = fieldStarts[fieldIndex + 1]?.index ?? entries.length
@@ -437,42 +434,42 @@ for (const packageJson of packageJsons) {
const value = fieldEntries[1]
if (value === undefined || /^#{1,6} /.test(value.raw) || value.raw.trim().length === 0) {
failures.push({ path: readme, message: `line ${fieldStart.line.index}: ${expectedHeading} requires one non-empty paragraph` })
surfaceError = true
entryError = true
break
}
if (value.index !== fieldStart.line.index + 2) {
failures.push({ path: readme, message: `line ${fieldStart.line.index}: ${expectedHeading} and its paragraph require one blank line between them` })
surfaceError = true
entryError = true
break
}
const unexpected = fieldEntries.slice(2).find(line => !/^##### \S/.test(line.raw))
if (unexpected !== undefined) {
failures.push({ path: readme, message: `line ${unexpected.index}: content after ${expectedHeading} paragraph must be a titled H5 plus \`markdown\` fence owned by that field` })
surfaceError = true
entryError = true
break
}
const nextHeadingLine = fieldStarts[fieldIndex + 1]?.line.index
?? surfaceStarts[surfaceIndex + 1]?.line.index
?? entryStarts[entryIndex + 1]?.line.index
?? nextH2Line
if (rawLines[nextHeadingLine - 2]?.trim().length !== 0) {
failures.push({ path: readme, message: `line ${nextHeadingLine}: Model Experience headings require a preceding blank line` })
surfaceError = true
entryError = true
break
}
const verbatim = validateNestedVerbatim(rawLines.slice(value.index, nextHeadingLine - 1), verbatimFragments)
if (verbatim.error !== undefined) {
failures.push({ path: readme, message: `line ${value.index}: ${verbatim.error}` })
surfaceError = true
entryError = true
break
}
if (fieldEntries.length - 2 !== verbatim.blocks) {
failures.push({ path: readme, message: `line ${value.index}: every nested H5 must own exactly one \`markdown\` fence` })
surfaceError = true
entryError = true
break
}
parsedFields.push({ value, verbatimBlocks: verbatim.blocks })
}
if (surfaceError) break
if (entryError) break
const modelViewField = parsedFields[0] as ParsedField
const tokenEffectField = parsedFields[1] as ParsedField
const kvCacheEffectField = parsedFields[2] as ParsedField
@@ -481,11 +478,11 @@ for (const packageJson of packageJsons) {
const kvCacheEffect = kvCacheEffectField.value
if (/\]\(#[^)]+\)/.test(modelView.raw) || /\]\(#[^)]+\)/.test(tokenEffect.raw) || /\]\(#[^)]+\)/.test(kvCacheEffect.raw)) {
failures.push({ path: readme, message: `line ${heading.index}: Model Experience fields must not link between local subsections; nest the H5 in its owning H4 field` })
surfaceError = true
entryError = true
break
}
surfaceFragments.add(fragment)
surfaces.push({
entryFragments.add(fragment)
modelContextEntries.push({
heading,
modelView,
tokenEffect,
@@ -495,49 +492,49 @@ for (const packageJson of packageJsons) {
verbatimBlocks: parsedFields.reduce((total, field) => total + field.verbatimBlocks, 0),
})
}
if (surfaceError) continue
if (entryError) continue
const promptWithoutVerbatim = surfaces.find(surface => isDirectSystemPromptSurface(surface.title)
&& surface.modelViewVerbatimBlocks === 0)
const promptWithoutVerbatim = modelContextEntries.find(entry => isDirectSystemPromptEntry(entry.title)
&& entry.modelViewVerbatimBlocks === 0)
if (promptWithoutVerbatim !== undefined) {
failures.push({ path: readme, message: `line ${promptWithoutVerbatim.heading.index}: system-prompt surface must contain a titled H5 plus verbatim \`markdown\` block under ${MODEL_VIEW_HEADING}` })
failures.push({ path: readme, message: `line ${promptWithoutVerbatim.heading.index}: system-prompt entry must contain a titled H5 plus verbatim \`markdown\` block under ${MODEL_VIEW_HEADING}` })
continue
}
const hasConcreteLiteral = surfaces.some(surface => surface.verbatimBlocks > 0
|| surface.modelView.raw.includes('`')
|| surface.tokenEffect.raw.includes('`')
|| toolCatalogLinkFragments(surface.modelView.raw).length > 0)
const hasConcreteLiteral = modelContextEntries.some(entry => entry.verbatimBlocks > 0
|| entry.modelView.raw.includes('`')
|| entry.tokenEffect.raw.includes('`')
|| toolCatalogLinkFragments(entry.modelView.raw).length > 0)
if (!hasConcreteLiteral) {
failures.push({ path: readme, message: 'structured Model Experience must ground at least one surface with inline code, a nested `markdown` block, or an anchored tool-catalog link' })
failures.push({ path: readme, message: 'structured Model Experience must ground at least one entry with inline code, a nested `markdown` block, or an anchored tool-catalog link' })
continue
}
let catalogError = false
for (const surface of surfaces) {
if (!/\bschemas?\b/i.test(surface.title)) continue
const fragments = toolCatalogLinkFragments(surface.modelView.raw)
for (const entry of modelContextEntries) {
if (!/\bschemas?\b/i.test(entry.title)) continue
const fragments = toolCatalogLinkFragments(entry.modelView.raw)
if (fragments.length === 0) {
failures.push({ path: readme, message: `line ${surface.heading.index}: tool-schema surface must link an anchored section of ../../../docs/tool-catalog.md` })
failures.push({ path: readme, message: `line ${entry.heading.index}: tool-schema entry must link an anchored section of ../../../docs/tool-catalog.md` })
catalogError = true
break
}
const invalid = fragments.find(fragment => !toolCatalogFragments.has(fragment))
if (invalid !== undefined) {
failures.push({ path: readme, message: `line ${surface.modelView.index}: tool-catalog link fragment ${JSON.stringify(invalid)} does not name an H2 section` })
failures.push({ path: readme, message: `line ${entry.modelView.index}: tool-catalog link fragment ${JSON.stringify(invalid)} does not name an H2 section` })
catalogError = true
break
}
}
if (catalogError) continue
verbatimBlockCount += surfaces.reduce((total, surface) => total + surface.verbatimBlocks, 0)
contextSurfaceCount += surfaces.length
systemPromptSurfaceCount += surfaces.filter(surface => isDirectSystemPromptSurface(surface.title)).length
toolSchemaSurfaceCount += surfaces.filter(surface => /\bschemas?\b/i.test(surface.title)).length
kvCacheEffectCount += surfaces.length
verbatimBlockCount += modelContextEntries.reduce((total, entry) => total + entry.verbatimBlocks, 0)
modelContextEntryCount += modelContextEntries.length
systemPromptEntryCount += modelContextEntries.filter(entry => isDirectSystemPromptEntry(entry.title)).length
toolSchemaEntryCount += modelContextEntries.filter(entry => /\bschemas?\b/i.test(entry.title)).length
kvCacheEffectCount += modelContextEntries.length
structuredCount += 1
}
if (failures.length === 0) {
console.log(`verify-package-readme-model-experience: ${packageJsons.length} README(s) checked (${omittedSectionCount} audited omissions, ${structuredCount} structured, ${contextSurfaceCount} context surfaces, ${kvCacheEffectCount} KV-cache fields, ${systemPromptSurfaceCount} fenced system-prompt surfaces, ${toolSchemaSurfaceCount} catalog-linked tool-schema surfaces, ${explainedNoneCount} explained none, ${indirectCount} indirect, ${verbatimBlockCount} verbatim markdown blocks), all conform.`)
console.log(`verify-package-readme-model-experience: ${packageJsons.length} README(s) checked (${omittedSectionCount} audited omissions, ${structuredCount} structured, ${modelContextEntryCount} model-context entries, ${kvCacheEffectCount} KV-cache fields, ${systemPromptEntryCount} fenced system-prompt entries, ${toolSchemaEntryCount} catalog-linked tool-schema entries, ${explainedNoneCount} explained none, ${indirectCount} indirect, ${verbatimBlockCount} verbatim markdown blocks), all conform.`)
process.exit(0)
}

View File

@@ -1,60 +1,45 @@
import { describe, expect, it } from 'vitest'
import { findInternalRepositoryReferences } from './verify-public-repository-links.ts'
import { findUnavailableRepositoryReferences } from './verify-public-repository-links.ts'
describe('public repository link policy', () => {
it('rejects encoded and case-varied internal identities without blocking public repositories', () => {
const internalOwner = ['deepseek', 'harness'].join('-')
const internalRepository = [internalOwner, internalOwner].join('/')
const encodedRepository = internalRepository.replaceAll('-', '%2D').replace('/', '%2F')
const htmlEncodedRepository = internalRepository.replace('/', '&#x2f;')
const jsonEscapedRepository = internalRepository.replace('/', '\\/')
const unicodeEscapedRepository = internalRepository.replace('/', String.raw`\u002f`)
describe('repository link policy', () => {
it('rejects encoded and case-varied references to the unavailable repository', () => {
const unavailableOwner = ['deepseek', 'ai'].join('-')
const unavailableName = ['deepseek', 'harness', 'sdk'].join('-')
const unavailableRepository = `${unavailableOwner}/${unavailableName}`
const encodedRepository = unavailableRepository.replaceAll('-', '%2D').replace('/', '%2F')
const htmlEncodedRepository = unavailableRepository.replace('/', '&#x2f;')
const jsonEscapedRepository = unavailableRepository.replace('/', '\\/')
const unicodeEscapedRepository = unavailableRepository.replace('/', String.raw`\u002f`)
const source = [
'https://github.com/deepseek-ai/deepseek-harness-sdk',
`https://github.com/${internalOwner}/cordis`,
`https://github.com/${internalRepository.toUpperCase()}/issues/1`,
'https://github.com/deepseek-ai/deepseek-harness',
`https://github.com/${unavailableRepository.toUpperCase()}/issues/1`,
`https://github.com/${encodedRepository}/issues/2`,
`https://github.com/${htmlEncodedRepository}/issues/3`,
`"https:\\/\\/github.com\\/${jsonEscapedRepository}\\/issues\\/4"`,
`"https:\\/\\/github.com\\/${unicodeEscapedRepository}\\/issues\\/5"`,
`${internalOwner.toUpperCase()}#6`,
`https://github.com/${unavailableOwner}/cordis`,
`https://github.com/example/${unavailableName}`,
].join('\n')
expect(findInternalRepositoryReferences('subject.md', source)).toEqual([
expect(findUnavailableRepositoryReferences('subject.md', source)).toEqual([
{ file: 'subject.md', line: 2 },
{ file: 'subject.md', line: 3 },
{ file: 'subject.md', line: 4 },
{ file: 'subject.md', line: 5 },
{ file: 'subject.md', line: 6 },
{ file: 'subject.md', line: 7 },
{ file: 'subject.md', line: 8 },
])
})
it('allows only the exact audited trusted-publishing repository declarations', () => {
const internalOwner = ['deepseek', 'harness'].join('-')
const internalRepository = [internalOwner, internalOwner].join('/')
const repositoryUrl = `git+https://github.com/${internalRepository}.git`
const manifestLine = ` "url": "${repositoryUrl}",`
const constraintLine = `const repositoryUrl = '${repositoryUrl}'`
const allowedDeclarations = [
['native/landlock-run/packages/entry/package.json', manifestLine],
['native/landlock-run/packages/linux-arm64/package.json', manifestLine],
['native/landlock-run/packages/linux-x64/package.json', manifestLine],
['scripts/check-workspace-constraints.ts', constraintLine],
] as const
it('preserves frozen archived Agent Notes', () => {
const unavailableRepository = ['deepseek-ai', 'deepseek-harness-sdk'].join('/')
for (const [file, source] of allowedDeclarations) {
expect(findInternalRepositoryReferences(file, source)).toEqual([])
}
const wrongFile = 'native/landlock-run/package.json'
expect(findInternalRepositoryReferences(wrongFile, manifestLine)).toEqual([{ file: wrongFile, line: 1 }])
const manifestFile = 'native/landlock-run/packages/entry/package.json'
const wrongField = ` "homepage": "${repositoryUrl}",`
expect(findInternalRepositoryReferences(manifestFile, wrongField)).toEqual([{ file: manifestFile, line: 1 }])
const encodedLine = manifestLine.replace('github.com/', 'github.com\\/')
expect(findInternalRepositoryReferences(manifestFile, encodedLine)).toEqual([{ file: manifestFile, line: 1 }])
expect(findUnavailableRepositoryReferences(
'.agents/notes/archived/process/historical-record.md',
`https://github.com/${unavailableRepository}`,
)).toEqual([])
expect(findUnavailableRepositoryReferences(
'.agents/notes/implemented/process/active-record.md',
`https://github.com/${unavailableRepository}`,
)).toEqual([{ file: '.agents/notes/implemented/process/active-record.md', line: 1 }])
})
})

View File

@@ -1,4 +1,4 @@
/** Reject tracked files that expose the internal repository identity outside audited publishing declarations. */
/** Reject tracked files that reference an unavailable legacy repository. */
import { execFileSync } from 'node:child_process'
import { existsSync, lstatSync, readFileSync, readlinkSync } from 'node:fs'
@@ -6,22 +6,13 @@ import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
const root = resolve(import.meta.dirname, '..')
const internalOwner = ['deepseek', 'harness'].join('-')
const internalRepository = [internalOwner, internalOwner].join('/')
const internalIssueShorthand = `${internalOwner}#`
const trustedPublishingRepositoryUrl = `git+https://github.com/${internalRepository}.git`
/** Exact declarations that intentionally expose the source repository for trusted publishing. */
const allowedInternalRepositoryLineByFile: Readonly<Record<string, string>> = {
'native/landlock-run/packages/entry/package.json': `"url": "${trustedPublishingRepositoryUrl}",`,
'native/landlock-run/packages/linux-arm64/package.json': `"url": "${trustedPublishingRepositoryUrl}",`,
'native/landlock-run/packages/linux-x64/package.json': `"url": "${trustedPublishingRepositoryUrl}",`,
'scripts/check-workspace-constraints.ts': `const repositoryUrl = '${trustedPublishingRepositoryUrl}'`,
}
const unavailableOwner = ['deepseek', 'ai'].join('-')
const unavailableRepositoryName = ['deepseek', 'harness', 'sdk'].join('-')
const unavailableRepository = `${unavailableOwner}/${unavailableRepositoryName}`
const archivedAgentNotePrefix = '.agents/notes/archived/'
const namedReferenceCharacters: Readonly<Record<string, string>> = {
hyphen: '-',
num: '#',
sol: '/',
}
@@ -40,8 +31,8 @@ function canonicalReferenceText(source: string): string {
.toLowerCase()
}
/** One tracked reference to the internal repository. */
export interface InternalRepositoryReference {
/** One tracked reference to the unavailable repository. */
export interface UnavailableRepositoryReference {
/** Repository-relative file path. */
file: string
/** One-based source line. */
@@ -49,20 +40,18 @@ export interface InternalRepositoryReference {
}
/**
* Locate unaudited internal-repository references in one text file.
* Locate unavailable-repository references in one active text file.
* @param file - Repository-relative path used in diagnostics.
* @param source - Text to inspect.
* @returns every matching source line.
* @returns every matching source line, excluding frozen archived Agent Notes.
*/
export function findInternalRepositoryReferences(file: string, source: string): InternalRepositoryReference[] {
const references: InternalRepositoryReference[] = []
export function findUnavailableRepositoryReferences(file: string, source: string): UnavailableRepositoryReference[] {
if (file.startsWith(archivedAgentNotePrefix)) return []
const references: UnavailableRepositoryReference[] = []
for (const [index, line] of source.split('\n').entries()) {
const canonicalLine = canonicalReferenceText(line)
const isAllowedPublishingDeclaration = line.trim() === allowedInternalRepositoryLineByFile[file]
if (!isAllowedPublishingDeclaration
&& (canonicalLine.includes(internalRepository) || canonicalLine.includes(internalIssueShorthand))) {
references.push({ file, line: index + 1 })
}
if (canonicalLine.includes(unavailableRepository)) references.push({ file, line: index + 1 })
}
return references
}
@@ -73,8 +62,8 @@ function trackedFiles(repoRoot: string): string[] {
.filter(file => file !== '')
}
function scanRepository(repoRoot: string): InternalRepositoryReference[] {
const references: InternalRepositoryReference[] = []
function scanRepository(repoRoot: string): UnavailableRepositoryReference[] {
const references: UnavailableRepositoryReference[] = []
for (const file of trackedFiles(repoRoot)) {
const path = resolve(repoRoot, file)
if (!existsSync(path)) continue
@@ -82,7 +71,7 @@ function scanRepository(repoRoot: string): InternalRepositoryReference[] {
if (!stat.isFile() && !stat.isSymbolicLink()) continue
const source = stat.isSymbolicLink() ? readlinkSync(path) : readFileSync(path, 'utf8')
if (source.includes('\0')) continue
references.push(...findInternalRepositoryReferences(file, source))
references.push(...findUnavailableRepositoryReferences(file, source))
}
return references
}
@@ -92,9 +81,9 @@ const isMain = invokedPath !== undefined && import.meta.url === pathToFileURL(re
if (isMain) {
const references = scanRepository(root)
if (references.length === 0) {
console.log('verify-public-repository-links: tracked files expose no unexpected internal repository identity.')
console.log('verify-public-repository-links: tracked files reference no unavailable repository.')
} else {
console.error('verify-public-repository-links: unexpected internal repository references found:')
console.error('verify-public-repository-links: unavailable repository references found:')
for (const reference of references) console.error(` ${reference.file}:${String(reference.line)}`)
process.exitCode = 1
}