Ver Fonte

fix(cli): restore shipped surface capabilities

Turtle há 2 meses atrás
pai
commit
857a4941be

+ 15 - 4
apps/cli/base.cordis.yml

@@ -8,8 +8,8 @@
 # A patch replaces the targeted row's whole `config` rather than merging into
 # it, so a row whose value differs per surface does NOT live here: it belongs to
 # each overlay, keeping any single row down to one overlay layer plus the user's.
-# That is why `agent-loop`, `system-prompt`, `tools`, `fs-local`, and
-# `llm-deepseek` thinking defaults are absent below.
+# Rows with surface-specific values appear below only with shared plugin identity
+# and neutral defaults; each overlay restates the complete surface configuration.
 #
 # Row order carries no load semantics (activation is service-availability
 # driven); the grouping is for readers.
@@ -51,9 +51,20 @@
 - id: llm-retry
   name: '@deepseek-ai/dsh-llm-retry'
 
+- id: llm-pi-ai
+  name: '@deepseek-ai/dsh-llm-pi-ai'
+  config:
+    providers:
+      - provider: openai
+        apiKey: !!js process.env.OPENAI_API_KEY
+        baseURL: !!js process.env.OPENAI_BASE_URL
+      - provider: anthropic
+        apiKey: !!js process.env.ANTHROPIC_API_KEY
+        baseURL: !!js process.env.ANTHROPIC_BASE_URL
+
 # The session store root is the launcher's policy, not a plugin's: `dsh` shares
-# one store under the Harness home across every cwd, so `/resume` and
-# `/resume` spans workspaces. Without a launcher the project-local fallback keeps
+# one store under the Harness home across every cwd, so `/resume` spans
+# workspaces. Without a launcher the project-local fallback keeps
 # an embedder's sessions beside its project.
 - id: session-persistence-jsonl
   name: '@deepseek-ai/dsh-session-persistence-jsonl'

+ 9 - 3
apps/cli/composition.md

@@ -7,7 +7,7 @@ The TUI agent combines the real DeepSeek adapter, coding tools, compaction, suba
 
 ```mermaid
 flowchart LR
-  cfg["apps/cli (dsh)<br/>cordis.yml"]
+  cfg["apps/cli (dsh)<br/>base.cordis.yml + tui.cordis.yml"]
   plugin_tui_timer["timer<br/>@cordisjs/plugin-timer"]
   cfg --> plugin_tui_timer
   plugin_tui_llm["llm<br/>@deepseek-ai/dsh-llm"]
@@ -26,6 +26,8 @@ flowchart LR
   cfg --> plugin_tui_tasks
   plugin_tui_llm_retry["llm-retry<br/>@deepseek-ai/dsh-llm-retry"]
   cfg --> plugin_tui_llm_retry
+  plugin_tui_llm_pi_ai["llm-pi-ai<br/>@deepseek-ai/dsh-llm-pi-ai"]
+  cfg --> plugin_tui_llm_pi_ai
   plugin_tui_session_persistence_jsonl["session-persistence-jsonl<br/>@deepseek-ai/dsh-session-persistence-jsonl"]
   cfg --> plugin_tui_session_persistence_jsonl
   plugin_tui_subprocess["subprocess<br/>@deepseek-ai/dsh-subprocess-local"]
@@ -94,6 +96,8 @@ flowchart LR
   cfg --> plugin_tui_fs_local
   plugin_tui_llm_deepseek["llm-deepseek<br/>@deepseek-ai/dsh-llm-deepseek"]
   cfg --> plugin_tui_llm_deepseek
+  plugin_tui_tools["tools<br/>@deepseek-ai/dsh-tool-ask-user"]
+  cfg --> plugin_tui_tools
 ```
 
 | Plugin id | Package / module |
@@ -107,6 +111,7 @@ flowchart LR
 | `agent` | `@deepseek-ai/dsh-agent` |
 | `tasks` | `@deepseek-ai/dsh-tasks-local` |
 | `llm-retry` | `@deepseek-ai/dsh-llm-retry` |
+| `llm-pi-ai` | `@deepseek-ai/dsh-llm-pi-ai` |
 | `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` |
 | `subprocess` | `@deepseek-ai/dsh-subprocess-local` |
 | `bash-local` | `@deepseek-ai/dsh-bash-local` |
@@ -141,7 +146,8 @@ flowchart LR
 | `agent-loop` | `@deepseek-ai/dsh-agent-loop` |
 | `fs-local` | `@deepseek-ai/dsh-fs-local` |
 | `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` |
+| `tools` | `@deepseek-ai/dsh-tool-ask-user` |
 
-Source config: [`apps/cli/base.cordis.yml`](base.cordis.yml).
+Source configs: [`apps/cli/base.cordis.yml`](base.cordis.yml), [`apps/cli/tui.cordis.yml`](tui.cordis.yml).
 
-Maintenance mode: hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source.
+Maintenance mode: hybrid: the leaf plugin list is parsed from its shipped config files; app package expansion is curated from package source.

+ 8 - 1
apps/cli/package.json

@@ -22,6 +22,7 @@
     "@deepseek-ai/dsh-agent-loop": "workspace:^",
     "@deepseek-ai/dsh-app-boot": "workspace:^",
     "@deepseek-ai/dsh-bash-local": "workspace:^",
+    "@deepseek-ai/dsh-bash-sandbox": "workspace:^",
     "@deepseek-ai/dsh-client-connection": "workspace:^",
     "@deepseek-ai/dsh-client-hmr": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
@@ -33,6 +34,7 @@
     "@deepseek-ai/dsh-client-ui-layout": "workspace:^",
     "@deepseek-ai/dsh-client-ui-model": "workspace:^",
     "@deepseek-ai/dsh-client-ui-models": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-permission": "workspace:^",
     "@deepseek-ai/dsh-client-ui-plan": "workspace:^",
     "@deepseek-ai/dsh-client-ui-question": "workspace:^",
     "@deepseek-ai/dsh-client-ui-settings": "workspace:^",
@@ -52,10 +54,11 @@
     "@deepseek-ai/dsh-frontend": "workspace:^",
     "@deepseek-ai/dsh-fs-local": "workspace:^",
     "@deepseek-ai/dsh-fs-policy": "workspace:^",
+    "@deepseek-ai/dsh-fs-sandbox": "workspace:^",
     "@deepseek-ai/dsh-goal": "workspace:^",
     "@deepseek-ai/dsh-goal-session": "workspace:^",
-    "@deepseek-ai/dsh-helper": "workspace:^",
     "@deepseek-ai/dsh-host-apiproxy": "workspace:^",
+    "@deepseek-ai/dsh-host-directory-picker-browse": "workspace:^",
     "@deepseek-ai/dsh-host-webserver": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-llm": "workspace:^",
@@ -63,7 +66,10 @@
     "@deepseek-ai/dsh-llm-pi-ai": "workspace:^",
     "@deepseek-ai/dsh-llm-retry": "workspace:^",
     "@deepseek-ai/dsh-paths": "workspace:^",
+    "@deepseek-ai/dsh-permission": "workspace:^",
     "@deepseek-ai/dsh-plan-mode": "workspace:^",
+    "@deepseek-ai/dsh-sandbox-local": "workspace:^",
+    "@deepseek-ai/dsh-sandbox-policy": "workspace:^",
     "@deepseek-ai/dsh-scope": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-session-checkpoint-policy": "workspace:^",
@@ -102,6 +108,7 @@
     "@deepseek-ai/dsh-tool-workflow": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/dsh-tui": "workspace:^",
+    "@deepseek-ai/dsh-user-approval": "workspace:^",
     "@deepseek-ai/dsh-user-interaction": "workspace:^",
     "@deepseek-ai/dsh-workflow-workerthread": "workspace:^",
     "@deepseek-ai/dsh-workspace": "workspace:^",

+ 2 - 17
apps/cli/src/args.ts

@@ -213,27 +213,12 @@ Examples:
       resolved = resolveWeb(options)
     })
 
-  // `--resume` is NOT redeclared here: an option a subcommand shares with its
-  // parent parses into `program.opts()` and leaves the subcommand's own options
-  // empty, so redeclaring it would silently drop the id. Commander therefore
-  // omits it from this subcommand's option list, hence the trailing help text.
   program
     .command('meta')
     .description('work on the dsh source that runs this command, from any directory')
-    .addHelpText('after', '\nAccepts --resume <id> to resume a persisted session from this checkout.\n')
     .action(() => {
-      // Commander parses the parent (default-surface) options on either side of
-      // the subcommand into `program.opts()`. `meta` accepts only `--resume`, so
-      // a leaked `--config`/`-p` is a mistyped invocation that must fail loud
-      // rather than silently be dropped.
-      const parent = program.opts<{ config?: string; configReplace?: string; prompt?: string; resume?: string }>()
-      if (parent.config !== undefined || parent.configReplace !== undefined || parent.prompt !== undefined) {
-        program.error('error: meta takes none of --config, --config-replace, or -p/--prompt')
-      }
-      // Same reason as the default surface: an empty id would start a fresh
-      // session downstream instead of failing the mistyped resume.
-      if (parent.resume === '') program.error('error: --resume needs a session id')
-      resolved = { mode: 'meta', ...parent.resume !== undefined && { resume: parent.resume } }
+      rejectParentOptions('meta')
+      resolved = { mode: 'meta' }
     })
 
   try {

+ 7 - 5
apps/cli/src/tui.ts

@@ -18,7 +18,7 @@
  */
 
 import { randomUUID } from 'node:crypto'
-import { join } from 'node:path'
+import { join, resolve } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import {
   addHarnessSourceSection,
@@ -155,6 +155,8 @@ export async function runTui(
   // selected session may belong to another workspace, so the handoff also enters
   // that directory. The host is offered only when Node exposes `process.execve`
   // and knows its own entry.
+  const resolvedConfig = config === undefined ? undefined : resolve(config)
+  const resolvedConfigReplace = configReplace === undefined ? undefined : resolve(configReplace)
   const entry = process.argv[1]
   const execve = process.execve?.bind(process)
   const app: { current?: Context } = {}
@@ -172,8 +174,8 @@ export async function runTui(
         `--resume=${sessionId}`,
         // Both config flags must survive the handoff: resuming into a different
         // tree than the session was created in would silently change the agent.
-        ...config !== undefined ? ['--config', config] : [],
-        ...configReplace !== undefined ? ['--config-replace', configReplace] : [],
+        ...resolvedConfig !== undefined ? ['--config', resolvedConfig] : [],
+        ...resolvedConfigReplace !== undefined ? ['--config-replace', resolvedConfigReplace] : [],
       ]
   // Mint the fresh id here rather than in the app bundle: the exit line names
   // the session to resume, so the launcher must know it before the tree boots.
@@ -223,11 +225,11 @@ export async function runTui(
     ...loadOverlayPatches(NAME, TUI_OVERLAY),
     ...config === undefined
       ? loadPersonalPatches(NAME) ?? []
-      : loadOverlayPatches(NAME, resolveConfigPath(config, undefined)),
+      : loadOverlayPatches(NAME, resolveConfigPath(resolve(config), undefined)),
   ]
   const ctx = await boot(
     NAME,
-    replaceTree ? resolveConfigPath(configReplace, undefined) : BASE_CONFIG,
+    replaceTree ? resolveConfigPath(resolve(configReplace), undefined) : BASE_CONFIG,
     patches,
     (hostCtx) => {
       // The launcher owns session identity and the exit line: a config-mounted

+ 3 - 8
apps/cli/tests/args.spec.ts

@@ -30,12 +30,7 @@ describe('parseDshArgs', () => {
     expect(parse(['--config-replace', 'tree.yml'])).toEqual({ mode: 'tui', configReplace: 'tree.yml' })
     expect(parse(['--resume', 'sess', '--config', 'app.yml'])).toEqual({ mode: 'tui', config: 'app.yml', resume: 'sess' })
     expect(parse(['-p', 'do the thing'])).toEqual({ mode: 'headless', prompt: 'do the thing' })
-    // `meta` accepts `--resume` but does not redeclare it: a shared option parses
-    // into program.opts() on either side of the subcommand, and redeclaring it
-    // would leave the subcommand's own options empty and drop the id.
     expect(parse(['meta'])).toEqual({ mode: 'meta' })
-    expect(parse(['meta', '--resume', 'sess'])).toEqual({ mode: 'meta', resume: 'sess' })
-    expect(parse(['--resume', 'sess', 'meta'])).toEqual({ mode: 'meta', resume: 'sess' })
     // Credential setup is option-free: it writes the Harness-home .env, so
     // there is nothing for a flag to select.
     // Bare `web` carries no host/port: the shipped cordis.yml owns the default.
@@ -72,9 +67,9 @@ describe('parseDshArgs', () => {
     expect(exitCode(['--config-replace', 'tree.yml', 'web'])).toBe(1)
     // Same rule for credential setup: it shares no option with the default
     // surface, so a leaked flag is a typo, not something to ignore.
-    // `meta` fixes its own config tree and is interactive, so --config/-p are
-    // rejected; an empty id is swallowed downstream exactly as above.
-    expect(exitCode(['meta', '--resume='])).toBe(1)
+    // `meta` fixes its own config tree and always starts fresh, so every
+    // default-surface option is rejected.
+    expect(exitCode(['meta', '--resume', 's'])).toBe(1)
     expect(exitCode(['meta', '--config', 'c.yml'])).toBe(1)
     expect(exitCode(['meta', '--config-replace', 'tree.yml'])).toBe(1)
     expect(exitCode(['meta', '-p', 'task'])).toBe(1)

+ 2 - 3
apps/cli/tui.cordis.yml

@@ -94,9 +94,8 @@
     - id: session-reference
       name: '@deepseek-ai/dsh-session-reference'
 
-    # Refuses write/edit inside the dsh checkout this launcher runs from, on that
-    # checkout's own branch, until the session loads dsh-customize. Inert
-    # everywhere else, so an ordinary project sees no change.
+    # Compacts oversized tool results before the broader conversation compactor
+    # runs, preserving the model-visible result within the configured budget.
     - id: tool-result-prune
       name: '@deepseek-ai/dsh-compact-tool-result-prune'
 

+ 48 - 0
apps/cli/web.cordis.yml

@@ -31,7 +31,49 @@
     apiKey: !!js process.env.DEEPSEEK_API_KEY
     baseURL: !!js process.env.DEEPSEEK_BASE_URL
 
+# The web surface replaces the unrestricted local executors with the shared
+# sandbox policy. Its default preserves the previous unrestricted behavior;
+# DSH_PERMISSION_MODE and the browser permission picker can confine a session.
+- insert:
+    - id: sandbox
+      name: '@deepseek-ai/dsh-sandbox-local'
+
+    - id: sandbox-policy
+      name: '@deepseek-ai/dsh-sandbox-policy'
+      config:
+        mode: !!js process.env.DSH_PERMISSION_MODE ?? 'danger-full-access'
+        workspaceRoot: !!js process.cwd()
+
+    - id: bash-sandbox
+      name: '@deepseek-ai/dsh-bash-sandbox'
+
+    - id: approval
+      name: '@deepseek-ai/dsh-user-approval'
+      config:
+        policy: !!js "(process.env.DSH_PERMISSION_MODE ?? 'danger-full-access') === 'danger-full-access' ? 'never' : 'ask'"
+
+    - id: permission
+      name: '@deepseek-ai/dsh-permission'
+      config:
+        presets:
+          read-only:
+            sandbox: read-only
+            approval: ask
+          workspace-write:
+            sandbox: workspace-write
+            approval: ask
+          danger-full-access:
+            sandbox: danger-full-access
+            approval: never
+
+    - id: fs-sandbox
+      name: '@deepseek-ai/dsh-fs-sandbox'
+
+- id: bash-local
+  remove: true
+
 - id: fs-local
+  remove: true
 
 # ── web-only host rows, the transport layer, and the browser roster ─────────
 
@@ -72,6 +114,9 @@
     # The API gateway: the transport-agnostic dispatch face every client shape
     # shares. provider/model are the host default routing — the profile json's
     # mapping target (user config overrides these engineering defaults).
+    - id: directory-picker
+      name: '@deepseek-ai/dsh-host-directory-picker-browse'
+
     - id: api-gateway
       name: '@deepseek-ai/dsh-host-apiproxy'
       config:
@@ -156,6 +201,9 @@
     - id: ui-model
       name: '@deepseek-ai/dsh-client-ui-model'
 
+    - id: ui-permission
+      name: '@deepseek-ai/dsh-client-ui-permission'
+
     # Plan control: the composer plan seat over the plan projection + /plan channel.
     - id: ui-plan
       name: '@deepseek-ai/dsh-client-ui-plan'

+ 1 - 1
examples/acp-agent/composition.md

@@ -94,4 +94,4 @@ flowchart LR
 
 Source config: [`examples/acp-agent/cordis.yml`](cordis.yml).
 
-Maintenance mode: hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source.
+Maintenance mode: hybrid: the leaf plugin list is parsed from its shipped config files; app package expansion is curated from package source.

+ 1 - 1
examples/headless-agent/composition.md

@@ -76,4 +76,4 @@ flowchart LR
 
 Source config: [`examples/headless-agent/cordis.yml`](cordis.yml).
 
-Maintenance mode: hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source.
+Maintenance mode: hybrid: the leaf plugin list is parsed from its shipped config files; app package expansion is curated from package source.

+ 24 - 3
pnpm-lock.yaml

@@ -134,6 +134,9 @@ importers:
       '@deepseek-ai/dsh-bash-local':
         specifier: workspace:^
         version: link:../../packages/bash/bash-local
+      '@deepseek-ai/dsh-bash-sandbox':
+        specifier: workspace:^
+        version: link:../../packages/bash/bash-sandbox
       '@deepseek-ai/dsh-client-connection':
         specifier: workspace:^
         version: link:../../packages/client/connection
@@ -167,6 +170,9 @@ importers:
       '@deepseek-ai/dsh-client-ui-models':
         specifier: workspace:^
         version: link:../../packages/client/ui-models
+      '@deepseek-ai/dsh-client-ui-permission':
+        specifier: workspace:^
+        version: link:../../packages/client/ui-permission
       '@deepseek-ai/dsh-client-ui-plan':
         specifier: workspace:^
         version: link:../../packages/client/ui-plan
@@ -224,18 +230,21 @@ importers:
       '@deepseek-ai/dsh-fs-policy':
         specifier: workspace:^
         version: link:../../packages/fs/fs-policy
+      '@deepseek-ai/dsh-fs-sandbox':
+        specifier: workspace:^
+        version: link:../../packages/fs/fs-sandbox
       '@deepseek-ai/dsh-goal':
         specifier: workspace:^
         version: link:../../packages/goal/goal
       '@deepseek-ai/dsh-goal-session':
         specifier: workspace:^
         version: link:../../packages/goal/goal-session
-      '@deepseek-ai/dsh-helper':
-        specifier: workspace:^
-        version: link:../../packages/sdk/helper
       '@deepseek-ai/dsh-host-apiproxy':
         specifier: workspace:^
         version: link:../../packages/host/apiproxy
+      '@deepseek-ai/dsh-host-directory-picker-browse':
+        specifier: workspace:^
+        version: link:../../packages/host/directory-picker-browse
       '@deepseek-ai/dsh-host-webserver':
         specifier: workspace:^
         version: link:../../packages/host/webserver
@@ -257,9 +266,18 @@ importers:
       '@deepseek-ai/dsh-paths':
         specifier: workspace:^
         version: link:../../packages/util/paths
+      '@deepseek-ai/dsh-permission':
+        specifier: workspace:^
+        version: link:../../packages/ui/permission
       '@deepseek-ai/dsh-plan-mode':
         specifier: workspace:^
         version: link:../../packages/plan/plan-mode
+      '@deepseek-ai/dsh-sandbox-local':
+        specifier: workspace:^
+        version: link:../../packages/sandbox/sandbox-local
+      '@deepseek-ai/dsh-sandbox-policy':
+        specifier: workspace:^
+        version: link:../../packages/sandbox/sandbox-policy
       '@deepseek-ai/dsh-scope':
         specifier: workspace:^
         version: link:../../packages/core/scope
@@ -374,6 +392,9 @@ importers:
       '@deepseek-ai/dsh-tui':
         specifier: workspace:^
         version: link:../../packages/ui/tui
+      '@deepseek-ai/dsh-user-approval':
+        specifier: workspace:^
+        version: link:../../packages/ui/user-approval
       '@deepseek-ai/dsh-user-interaction':
         specifier: workspace:^
         version: link:../../packages/ui/user-interaction

+ 7 - 7
scripts/gen-doc-graphs.ts

@@ -597,7 +597,7 @@ const APP_EXAMPLES = [
     rel: 'apps/cli/composition.md',
     title: 'dsh TUI Composition',
     label: 'apps/cli (dsh)',
-    config: 'apps/cli/base.cordis.yml',
+    configs: ['apps/cli/base.cordis.yml', 'apps/cli/tui.cordis.yml'],
     summary: 'The TUI agent combines the real DeepSeek adapter, coding tools, compaction, subagents, and workflows with the full-screen terminal app package.',
   },
   {
@@ -605,7 +605,7 @@ const APP_EXAMPLES = [
     rel: 'examples/headless-agent/composition.md',
     title: 'Headless Agent App Composition',
     label: 'examples/headless-agent',
-    config: 'examples/headless-agent/cordis.yml',
+    configs: ['examples/headless-agent/cordis.yml'],
     summary: 'The headless demo combines the real DeepSeek adapter and coding capabilities with the one-shot app package, format-pure stdout, and one fresh persisted top-level session.',
   },
   {
@@ -613,7 +613,7 @@ const APP_EXAMPLES = [
     rel: 'examples/acp-agent/composition.md',
     title: 'ACP Automation App Composition',
     label: 'examples/acp-agent',
-    config: 'examples/acp-agent/cordis.yml',
+    configs: ['examples/acp-agent/cordis.yml'],
     summary: 'The ACP demo exposes fresh baseline-prompt agent sessions to programmatic clients over JSON-RPC stdio, with no stdout logger, human UI, or pre-created agent.',
   },
 ]
@@ -639,15 +639,15 @@ function renderAppExpansion(lines: string[], appNode: string, pluginName: string
 }
 
 function renderAppComposition(example: AppExample): string {
-  const plugins = parseExampleCordis(example.config)
-  const maintenance = 'hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source'
+  const plugins = example.configs.flatMap(parseExampleCordis)
+  const maintenance = 'hybrid: the leaf plugin list is parsed from its shipped config files; app package expansion is curated from package source'
   const lines = generatedHeader(example.title)
   lines.push(
     example.summary,
     '',
     '```mermaid',
     'flowchart LR',
-    `  cfg["${escLabel(example.label)}<br/>cordis.yml"]`,
+    `  cfg["${escLabel(example.label)}<br/>${escLabel(example.configs.map(config => config.split('/').at(-1)).join(' + '))}"]`,
   )
   for (const plugin of plugins) {
     const pluginNode = nodeId(`plugin_${example.id}`, plugin.id)
@@ -664,7 +664,7 @@ function renderAppComposition(example: AppExample): string {
     '| --- | --- |',
     ...plugins.map(plugin => `| \`${plugin.id}\` | \`${plugin.name}\` |`),
     '',
-    `Source config: [\`${example.config}\`](${linkFromDoc(example.rel, example.config)}).`,
+    `Source config${example.configs.length === 1 ? '' : 's'}: ${example.configs.map(config => `[\`${config}\`](${linkFromDoc(example.rel, config)})`).join(', ')}.`,
   )
   lines.push('', ...maintenanceFooter(maintenance))
   return lines.join('\n')

+ 6 - 1
scripts/verify-cordis-config.ts

@@ -78,6 +78,11 @@ function validateEntry(value: unknown, file: string, path: string): void {
       validateEntry(value.config[index], file, `${path}.config[${index}]`)
     }
   }
+  if (isUnknownArray(value.insert)) {
+    for (let index = 0; index < value.insert.length; index++) {
+      validateEntry(value.insert[index], file, `${path}.insert[${index}]`)
+    }
+  }
   if (value.name !== '@cordisjs/plugin-include') return
   const config = value.config
   if (!isRecord(config) || !isUnknownArray(config.patches)) return
@@ -124,7 +129,7 @@ function validateExampleResolution(): string[] {
 
 function validateAppResolution(): string[] {
   const dependencies = readManifest('apps/cli/package.json').dependencies ?? {}
-  const references = pluginReferences.filter(reference => reference.file === 'apps/cli/base.cordis.yml')
+  const references = pluginReferences.filter(reference => reference.file.startsWith('apps/cli/'))
   return missingPluginDependencies(references, dependencies, 'apps/cli/package.json')
 }