Преглед на файлове

fix(subagent): simplify optional provider delivery

pku-xht преди 1 месец
родител
ревизия
208f37157a

+ 2 - 2
.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
-2026-08-12-production-dsh-excludes-product-subagent-providers.md: 94cfe82d0aa42076f3c0723ed99a83a1e53e3724
-2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: a9dbdcff748ea46dc84c1480e6e50a945219a5c7
+2026-08-12-production-dsh-excludes-product-subagent-providers.md: 53551ad06ce7b735620d605669ddb5ca2d20aef5
+2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: a5ff017e2717dee326e8aaf7987ab8360452902f

+ 1 - 1
.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md

@@ -16,7 +16,7 @@ The two Bundles remain independent. The Codex Bundle owns its `@deepseek-ai/dsh-
 
 ## Verification
 
-Package tests pin each Bundle manifest, exported patch, exact self-provider row, and product-specific runtime dependency. Workspace validation discovers Bundle manifests by declaration rather than directory. Production-closure tests prove the default, Codex-only, and Claude-only dependency boundaries, while real Bundle-patch and Agent-Preset composition covers all four installed sets against all four tool sets and proves composition starts no product process. The base bundle test continues to reject both provider dependencies and configuration rows.
+Package tests pin each Bundle manifest, published patch, exact self-provider row, and product-specific runtime dependency. Workspace validation derives each published patch from its Bundle declaration rather than a package catalog. Production-closure tests prove the default, Codex-only, and Claude-only dependency boundaries, while real Bundle-patch and Agent-Preset composition covers all four installed sets, the full tool-grant matrix on a Host with both providers, representative missing-provider cases, and zero product processes. The base bundle test continues to reject both provider dependencies and configuration rows.
 
 ## Alternatives considered
 

+ 1 - 1
.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md

@@ -16,7 +16,7 @@ Status: implemented
 
 ## 验证
 
-包测试会固定每个 Bundle 的 manifest、导出的 patch、准确的自身提供方行以及产品专属运行时依赖。工作区验证会按 Bundle 声明发现 manifest,而非按目录发现。生产闭包测试证明默认、仅 Codex 与仅 Claude 三种依赖边界;真实 Bundle patch 与 Agent Preset 的组装会覆盖四种安装集合与四种工具集合的完整矩阵,并证明组装不会启动产品进程。base 组合包测试仍会拒绝这两个提供方依赖与配置行。
+包测试会固定每个 Bundle 的 manifest、发布 patch、准确的自身提供方行以及产品专属运行时依赖。工作区验证会从 Bundle 声明派生每个发布 patch,而非维护包目录。生产闭包测试证明默认、仅 Codex 与仅 Claude 三种依赖边界;真实 Bundle patch 与 Agent Preset 的组装会覆盖四种安装集合、同时安装两个提供方时的完整工具授权矩阵、缺失提供方的代表场景以及零产品进程。base 组合包测试仍会拒绝这两个提供方依赖与配置行。
 
 ## 考虑过的替代方案
 

+ 1 - 1
apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md

@@ -154,7 +154,7 @@ Copy these disabled templates from a shipped full preset and remove `disabled` o
     maxDepth: provider-managed
 ```
 
-The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only an installed matching provider, and enabling both exposes the installed intersection. The host must already provide `codex` or `claude` on `PATH`; neither the Bundle nor the preset installs, authenticates, selects a model for, starts, or probes either product.
+The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only an installed matching provider, and enabling both exposes the installed intersection. The host must already provide `codex` or `claude` on `PATH`; neither the Bundle nor the preset starts, authenticates, selects a model for, probes, or manages a host-level installation of either product. The Claude Code Bundle's Agent SDK dependency still carries its platform CLI payload, which production ignores in favor of the host's `claude`.
 
 ## What not to move into a preset
 

+ 2 - 2
apps/cli/reference/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/cli/reference/README.md
-README.md: c220fd68cb0d79d2060e46a33d8af54c249a9c68
-README.zh.md: 15f026c79665ae2978bdfd65c321b05c10cc06e4
+README.md: dcbe28fab05031b2f176e5a20df77967d2d97885
+README.zh.md: 0cd7216615a58b206b441096292e1a19a73a01b3

+ 1 - 1
apps/cli/reference/README.md

@@ -52,7 +52,7 @@ dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-codex
 dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-claude-code
 ```
 
-The successful pnpm operation changes the Profile manifest and Bundle list on disk; a running Profile keeps the Bundle set from its current start. Restart that Profile after adding, removing, or updating a Bundle. This startup boundary applies to Bundle membership, while ordinary edits to the Profile or home `cordis.patch.yml` retain their existing hot-reload behavior. On the next start, each installed product Bundle registers only its dormant Host provider and does not start, install, authenticate, or configure the native product. Full Agent Presets keep both product tool rows disabled, so a copied Preset must separately enable the matching row before a new Agent can see that tool. Installing one provider never installs the other product package; the default dsh dependency closure includes neither provider nor the Claude Agent SDK.
+The successful pnpm operation changes the Profile manifest and Bundle list on disk; a running Profile keeps the Bundle set from its current start. Restart that Profile after adding, removing, or updating a Bundle. This startup boundary applies to Bundle membership, while ordinary edits to the Profile or home `cordis.patch.yml` retain their existing hot-reload behavior. On the next start, each installed product Bundle registers only its dormant Host provider and does not start, authenticate, configure, or manage a host-level installation of the native product. The Claude Code Bundle's Agent SDK dependency still carries its platform CLI payload, which production ignores in favor of the host's `claude`. Full Agent Presets keep both product tool rows disabled, so a copied Preset must separately enable the matching row before a new Agent can see that tool. Installing one provider never installs the other product package; the default dsh dependency closure includes neither provider nor the Claude Agent SDK.
 
 ```sh
 dsh plugin --profile tui add github:deepseek-harness/turtle-ui

+ 1 - 1
apps/cli/reference/README.zh.md

@@ -52,7 +52,7 @@ dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-codex
 dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-claude-code
 ```
 
-pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑仍保留既有热重载行为。下一次启动时,每个已安装的产品 Bundle 只注册自己的休眠 Host provider,不会启动、安装、认证或配置原生产品。完整 Agent Preset 中的两个产品工具行仍默认禁用,因此还须在复制出的 Preset 中单独启用对应行,新 Agent 才能看到该工具。只安装一个 provider 不会安装另一个产品包;默认 dsh 依赖闭包不包含任一 provider,也不包含 Claude Agent SDK。
+pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑仍保留既有热重载行为。下一次启动时,每个已安装的产品 Bundle 只注册自己的休眠 Host provider,不会启动、认证、配置原生产品,也不会管理宿主级产品安装。Claude Code Bundle 的 Agent SDK 依赖仍携带平台 CLI 载荷,但生产环境会忽略该载荷并使用宿主提供的 `claude`。完整 Agent Preset 中的两个产品工具行仍默认禁用,因此还须在复制出的 Preset 中单独启用对应行,新 Agent 才能看到该工具。只安装一个 provider 不会安装另一个产品包;默认 dsh 依赖闭包不包含任一 provider,也不包含 Claude Agent SDK。
 
 ```sh
 dsh plugin --profile tui add github:deepseek-harness/turtle-ui

+ 15 - 13
apps/cli/tests/web-agent-presets.e2e.ts

@@ -437,6 +437,7 @@ describe('the shipped Web composition', () => {
 describe('product subagent Bundle and user-preset intersection', () => {
   const presetIds = ['products-none', 'products-codex', 'products-claude', 'products-both'] as const
   type Product = 'codex' | 'claude-code'
+  type PresetId = typeof presetIds[number]
 
   async function bootProducts(installed: readonly Product[]): Promise<Context> {
     const root = await mkdtemp(join(tmpdir(), 'dsh-product-presets-'))
@@ -477,20 +478,20 @@ describe('product subagent Bundle and user-preset intersection', () => {
   }
 
   it('composes the intersection of installed Bundles and enabled preset rows', async () => {
-    const enabledByPreset = new Map<string, Product[]>([
-      ['products-none', []],
-      ['products-codex', ['codex']],
-      ['products-claude', ['claude-code']],
-      ['products-both', ['codex', 'claude-code']],
-    ])
-    const installations: Product[][] = [
-      [],
-      ['codex'],
-      ['claude-code'],
-      ['codex', 'claude-code'],
+    const enabledByPreset: Record<PresetId, Product[]> = {
+      'products-none': [],
+      'products-codex': ['codex'],
+      'products-claude': ['claude-code'],
+      'products-both': ['codex', 'claude-code'],
+    }
+    const scenarios: Array<{ installed: Product[]; presets: readonly PresetId[] }> = [
+      { installed: [], presets: ['products-both'] },
+      { installed: ['codex'], presets: ['products-both'] },
+      { installed: ['claude-code'], presets: ['products-both'] },
+      { installed: ['codex', 'claude-code'], presets: presetIds },
     ]
 
-    for (const installed of installations) {
+    for (const { installed, presets } of scenarios) {
       const productCtx = await bootProducts(installed)
       const spawn = vi.spyOn(productCtx.subprocess, 'spawn')
       try {
@@ -498,7 +499,8 @@ describe('product subagent Bundle and user-preset intersection', () => {
           .filter(name => name === 'codex' || name === 'claude-code')
           .sort())
           .toEqual([...installed].sort())
-        for (const [id, enabled] of enabledByPreset) {
+        for (const id of presets) {
+          const enabled = enabledByPreset[id]
           const handle = await productCtx.agents.create({
             sessionId: SessionId(`preset-${id}-${installed.join('-') || 'none'}-${randomUUID()}`),
             setup: agentCtx => productCtx.agentPresets.mount(agentCtx, id).then(() => undefined),

+ 12 - 1
examples/acp-agent/tests/acp.snapshot.ts

@@ -1,7 +1,7 @@
 import { fileURLToPath } from 'node:url'
 import { readFileSync } from 'node:fs'
 import { spawnSync } from 'node:child_process'
-import { mkdir, utimes, writeFile } from 'node:fs/promises'
+import { copyFile, mkdir, utimes, writeFile } from 'node:fs/promises'
 import { dirname, join } from 'node:path'
 import { homedir } from 'node:os'
 import { expect, it } from 'vitest'
@@ -28,6 +28,10 @@ const AGENT = {
   configPath: fileURLToPath(new URL('../cordis.yml', import.meta.url)),
   tsconfigPath: fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)),
 }
+const EDITING_CORDIS_SKILL = fileURLToPath(new URL(
+  '../../../apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md',
+  import.meta.url,
+))
 
 // The Code Mode overlay configs (include-patched variants of cordis.yml; the
 // replay swap resolves each one's sibling `*cordis.snapshot.yml`).
@@ -69,6 +73,12 @@ const FS_DIFF_BOUND_CONFIG = fileURLToPath(new URL('./fs-diff-bound.cordis.yml',
 const SNAPSHOTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
 const PACKED_CHUNKS_SOURCE = 'hook-cc-pretool-deny'
 
+async function prepareEditingCordisSkillWorkspace(cwd: string): Promise<void> {
+  const target = join(cwd, '.dsh', 'skills', 'editing-cordis-compositions', 'SKILL.md')
+  await mkdir(dirname(target), { recursive: true })
+  await copyFile(EDITING_CORDIS_SKILL, target)
+}
+
 async function prepareDelimiterPathWorkspace(cwd: string): Promise<void> {
   const dir = join(cwd, 'scope</system-reminder>')
   await mkdir(dir, { recursive: true })
@@ -280,6 +290,7 @@ const SCENARIOS: Scenario[] = [
     headerClass: 'skill',
     systemPromptSource: 'text-turn',
     toolSchemasSource: 'text-turn',
+    prepareWorkspace: prepareEditingCordisSkillWorkspace,
   },
   { name: 'lsp-definition', hasModelTurn: true, recorded: false, pinsHeader: true, headerClass: 'lsp', configPath: LSP_CONFIG },
   // web_fetch markdown rendering end to end: the overlay's loopback fixture

+ 1 - 1
examples/acp-agent/tests/snapshots/skill-load/input.json

@@ -2,6 +2,6 @@
   "steps": [
     { "op": "initialize" },
     { "op": "newSession" },
-    { "op": "prompt", "text": "Load the snapshot-skill skill with the skill tool, then reply DONE." }
+    { "op": "prompt", "text": "Load the editing-cordis-compositions skill with the skill tool, then reply DONE." }
   ]
 }

Файловите разлики са ограничени, защото са твърде много
+ 2 - 4
examples/acp-agent/tests/snapshots/skill-load/session.jsonl


+ 0 - 1
packages/subagent/subagent-claude-code/package.json

@@ -22,7 +22,6 @@
       "types": "./lib/types/invariant.d.ts",
       "default": "./lib/invariant.js"
     },
-    "./cordis.patch.yml": "./cordis.patch.yml",
     "./src/*": "./src/*",
     "./package.json": "./package.json"
   },

+ 0 - 2
packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts

@@ -290,12 +290,10 @@ describe('task admission and package contracts', () => {
     const root = fileURLToPath(new URL('..', import.meta.url))
     const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as {
       dependencies?: Record<string, string>
-      exports?: Record<string, unknown>
       files?: string[]
       dsh?: { bundle?: { patch?: string } }
     }
     expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml')
-    expect(manifest.exports?.['./cordis.patch.yml']).toBe('./cordis.patch.yml')
     expect(manifest.files).toContain('cordis.patch.yml')
     expect(manifest.dependencies).toHaveProperty('@anthropic-ai/claude-agent-sdk')
     expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subagent-codex')

+ 0 - 1
packages/subagent/subagent-codex/package.json

@@ -22,7 +22,6 @@
       "types": "./lib/types/invariant.d.ts",
       "default": "./lib/invariant.js"
     },
-    "./cordis.patch.yml": "./cordis.patch.yml",
     "./src/*": "./src/*",
     "./package.json": "./package.json"
   },

+ 0 - 2
packages/subagent/subagent-codex/tests/subagent-codex.spec.ts

@@ -269,12 +269,10 @@ describe('task admission and package contracts', () => {
     const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as {
       dependencies?: Record<string, string>
       peerDependencies?: Record<string, string>
-      exports?: Record<string, unknown>
       files?: string[]
       dsh?: { bundle?: { patch?: string } }
     }
     expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml')
-    expect(manifest.exports?.['./cordis.patch.yml']).toBe('./cordis.patch.yml')
     expect(manifest.files).toContain('cordis.patch.yml')
     expect(manifest.dependencies).toHaveProperty('@deepseek-ai/dsh-sdk-protocol')
     expect(manifest.peerDependencies).not.toHaveProperty('@deepseek-ai/dsh-sdk-protocol')

+ 11 - 7
scripts/check-workspace-constraints.ts

@@ -84,6 +84,11 @@ interface PackageManifest {
   devDependencies?: Record<string, string>
   dependencies?: Record<string, string>
   optionalDependencies?: Record<string, string>
+  dsh?: {
+    bundle?: {
+      patch?: string
+    }
+  }
 }
 
 /** One workspace manifest and its repo-relative path. */
@@ -131,12 +136,6 @@ function workspaceManifests(): WorkspaceManifest[] {
 }
 
 const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
-  // Profile bundles publish their dsh.bundle.patch layer beside the lib.
-  '@deepseek-ai/dsh-base': ['cordis.patch.yml'],
-  '@deepseek-ai/dsh-web-app': ['cordis.patch.yml'],
-  '@deepseek-ai/dsh-headless': ['cordis.patch.yml'],
-  '@deepseek-ai/dsh-subagent-codex': ['cordis.patch.yml'],
-  '@deepseek-ai/dsh-subagent-claude-code': ['cordis.patch.yml'],
   '@deepseek-ai/dsh-client-ui-theme': ['lib/styles'],
   // The Python runtime uses a distinct closed-resolution bin; the public CLI
   // keeps config-owned bare-package resolution through lib/bin.js.
@@ -154,7 +153,12 @@ function sameStringList(actual: readonly string[] | undefined, expected: readonl
 }
 
 function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
-  const extras = manifest.name ? packageFileExtras[manifest.name] ?? [] : []
+  const declaredPatch = manifest.dsh?.bundle?.patch
+  const bundleFiles = declaredPatch === undefined ? [] : [declaredPatch.replace(/^\.\//, '')]
+  const extras = [
+    ...bundleFiles,
+    ...(manifest.name ? packageFileExtras[manifest.name] ?? [] : []),
+  ]
   return [
     'lib/index.js',
     // Every package publishes its invariant ownership companion as a separate

Някои файлове не бяха показани, защото твърде много файлове са промени