dsh-agent-plugins-market 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +2 -2
  2. package/README.zh.md +2 -2
  3. package/client/client.js +1591 -1726
  4. package/client/client.js.map +1 -1
  5. package/lib/application/agent-roles.js +53 -0
  6. package/lib/application/catalog-context.js +2 -2
  7. package/lib/application/catalog.js +4 -8
  8. package/lib/{runtime → application}/deadline.js +1 -1
  9. package/lib/application/details.js +3 -3
  10. package/lib/application/json-file.js +43 -0
  11. package/lib/{runtime → application/lsp}/lsp-direct-config.js +9 -7
  12. package/lib/application/lsp/lsp-server-state.js +17 -0
  13. package/lib/{runtime → application/lsp}/lsp-status.js +22 -7
  14. package/lib/{runtime → application/lsp}/profile-seam.js +1 -1
  15. package/lib/application/lsp-service.js +5 -5
  16. package/lib/{runtime → application/mcp}/mcp-backend.js +3 -19
  17. package/lib/{runtime → application/mcp}/mcp-config.js +4 -4
  18. package/lib/{runtime → application/mcp}/mcp-direct-config.js +8 -49
  19. package/lib/{runtime → application/mcp}/mcp-overrides.js +3 -5
  20. package/lib/{runtime → application/mcp}/mcp-status.js +13 -7
  21. package/lib/application/mcp-service.js +11 -35
  22. package/lib/application/panel-resources.js +48 -11
  23. package/lib/{runtime → application/panels}/user-hooks.js +2 -2
  24. package/lib/{runtime → application/panels}/user-store.js +2 -2
  25. package/lib/application/ports.js +1 -0
  26. package/lib/application/project-agent-roles.js +1 -1
  27. package/lib/{runtime → application}/server-config.js +7 -17
  28. package/lib/application/snapshot-cache.js +1 -1
  29. package/lib/application/source-store.js +3 -4
  30. package/lib/{runtime → application/state}/state-store.js +5 -7
  31. package/lib/{runtime → application/state}/storage-migration.js +4 -4
  32. package/lib/catalog/source-catalog.js +0 -4
  33. package/lib/contracts/lsp.js +1 -0
  34. package/lib/contracts/market.js +0 -1
  35. package/lib/contracts/mcp.js +1 -0
  36. package/lib/index.js +42 -33
  37. package/lib/routes.js +2 -9
  38. package/lib/runtime/{agent-role-router.js → agents/agent-role-router.js} +7 -53
  39. package/lib/runtime/{subagent-catalog.js → agents/subagent-catalog.js} +24 -16
  40. package/lib/runtime/core/context.js +31 -0
  41. package/lib/runtime/{reconciler.js → core/reconciler.js} +5 -5
  42. package/lib/runtime/host/failure-detail.js +58 -0
  43. package/lib/runtime/{feedback-tool.js → host/feedback-tool.js} +1 -1
  44. package/lib/runtime/host/host-locale.js +83 -0
  45. package/lib/runtime/{settings-namespace.js → host/settings-namespace.js} +1 -1
  46. package/lib/runtime/host/shell-path.js +196 -0
  47. package/lib/runtime/{tool-registry-observer.js → host/tool-registry-observer.js} +2 -1
  48. package/lib/runtime/{lsp-mounts.js → lsp/lsp-mounts.js} +69 -20
  49. package/lib/runtime/{mcp-client → mcp/bridge}/bridge.js +52 -22
  50. package/lib/runtime/{mcp-client → mcp/bridge}/connection.js +2 -2
  51. package/lib/runtime/{mcp-client → mcp/bridge}/oauth.js +1 -1
  52. package/lib/runtime/{mcp-client → mcp/bridge}/tools.js +2 -2
  53. package/lib/runtime/{mcp-auth-record.js → mcp/mcp-auth-record.js} +1 -1
  54. package/lib/runtime/{mcp-mounts.js → mcp/mcp-mounts.js} +14 -7
  55. package/lib/runtime/{user-commands.js → panels/user-commands.js} +3 -3
  56. package/lib/runtime/{user-panels.js → panels/user-panels.js} +2 -2
  57. package/lib/runtime/{commands-mounts.js → surfaces/commands-mounts.js} +7 -7
  58. package/lib/runtime/{hooks-mounts.js → surfaces/hooks-mounts.js} +2 -2
  59. package/lib/runtime/{project-runtime.js → surfaces/project-runtime.js} +5 -5
  60. package/lib/runtime/{skills-provider.js → surfaces/skills-provider.js} +4 -4
  61. package/lib/types/application/agent-roles.d.ts +30 -0
  62. package/lib/types/application/catalog.d.ts +4 -7
  63. package/lib/types/application/details.d.ts +1 -1
  64. package/lib/types/application/json-file.d.ts +12 -0
  65. package/lib/types/{runtime → application/lsp}/lsp-direct-config.d.ts +1 -1
  66. package/lib/types/{runtime → application/lsp}/lsp-status.d.ts +3 -3
  67. package/lib/types/{runtime → application/mcp}/mcp-backend.d.ts +4 -9
  68. package/lib/types/{runtime/mcp-client/config.d.ts → application/mcp/mcp-bridge-config.d.ts} +11 -1
  69. package/lib/types/{runtime → application/mcp}/mcp-config.d.ts +6 -7
  70. package/lib/types/application/mcp/mcp-direct-config.d.ts +17 -0
  71. package/lib/types/{runtime → application/mcp}/mcp-overrides.d.ts +1 -1
  72. package/lib/types/{runtime → application/mcp}/mcp-status.d.ts +7 -7
  73. package/lib/types/application/mcp-service.d.ts +4 -11
  74. package/lib/types/application/panel-resources.d.ts +9 -2
  75. package/lib/types/{runtime → application/panels}/user-hooks.d.ts +1 -1
  76. package/lib/types/{runtime → application/panels}/user-store.d.ts +1 -1
  77. package/lib/types/application/ports.d.ts +17 -3
  78. package/lib/types/application/project-agent-roles.d.ts +1 -1
  79. package/lib/types/application/queries.d.ts +3 -6
  80. package/lib/types/{runtime → application}/regions.d.ts +1 -1
  81. package/lib/types/{runtime → application/state}/state-store.d.ts +2 -2
  82. package/lib/types/catalog/source-catalog.d.ts +0 -2
  83. package/lib/types/contracts/lsp-status.d.ts +6 -0
  84. package/lib/types/contracts/lsp.d.ts +16 -0
  85. package/lib/types/contracts/market.d.ts +1 -4
  86. package/lib/types/contracts/mcp-status.d.ts +21 -2
  87. package/lib/types/contracts/mcp.d.ts +20 -0
  88. package/lib/types/model/types.d.ts +0 -14
  89. package/lib/types/runtime/{agent-role-router.d.ts → agents/agent-role-router.d.ts} +3 -28
  90. package/lib/types/runtime/{subagent-catalog.d.ts → agents/subagent-catalog.d.ts} +6 -6
  91. package/lib/types/runtime/core/context.d.ts +14 -0
  92. package/lib/types/runtime/{reconcile-scheduler.d.ts → core/reconcile-scheduler.d.ts} +2 -2
  93. package/lib/types/runtime/{reconciler.d.ts → core/reconciler.d.ts} +8 -8
  94. package/lib/types/runtime/host/failure-detail.d.ts +6 -0
  95. package/lib/types/runtime/host/host-locale.d.ts +66 -0
  96. package/lib/types/runtime/{model-catalog.d.ts → host/model-catalog.d.ts} +1 -1
  97. package/lib/types/runtime/{settings-namespace.d.ts → host/settings-namespace.d.ts} +2 -2
  98. package/lib/types/runtime/host/shell-path.d.ts +70 -0
  99. package/lib/types/runtime/{lsp-mounts.d.ts → lsp/lsp-mounts.d.ts} +17 -11
  100. package/lib/types/runtime/{mcp-client → mcp/bridge}/bridge.d.ts +1 -1
  101. package/lib/types/runtime/{mcp-client → mcp/bridge}/connection.d.ts +3 -11
  102. package/lib/types/runtime/{mcp-client → mcp/bridge}/host-contract.d.ts +1 -1
  103. package/lib/types/runtime/{mcp-client → mcp/bridge}/oauth.d.ts +1 -1
  104. package/lib/types/runtime/{mcp-client → mcp/bridge}/transport.d.ts +1 -1
  105. package/lib/types/runtime/{mcp-auth-record.d.ts → mcp/mcp-auth-record.d.ts} +1 -1
  106. package/lib/types/runtime/{mcp-credentials.d.ts → mcp/mcp-credentials.d.ts} +1 -1
  107. package/lib/types/runtime/{mcp-mounts.d.ts → mcp/mcp-mounts.d.ts} +5 -11
  108. package/lib/types/runtime/{user-commands.d.ts → panels/user-commands.d.ts} +1 -1
  109. package/lib/types/runtime/{user-panels.d.ts → panels/user-panels.d.ts} +1 -1
  110. package/lib/types/runtime/{commands-mounts.d.ts → surfaces/commands-mounts.d.ts} +2 -2
  111. package/lib/types/runtime/{hooks-mounts.d.ts → surfaces/hooks-mounts.d.ts} +1 -1
  112. package/lib/types/runtime/{project-runtime.d.ts → surfaces/project-runtime.d.ts} +3 -3
  113. package/lib/types/runtime/{skills-provider.d.ts → surfaces/skills-provider.d.ts} +1 -1
  114. package/package.json +35 -32
  115. package/lib/runtime/host-locale.js +0 -89
  116. package/lib/runtime/legacy-root-migration.js +0 -12
  117. package/lib/runtime/lsp-server-state.js +0 -19
  118. package/lib/runtime/mcp-client/json-schema-subset.js +0 -181
  119. package/lib/types/runtime/host-locale.d.ts +0 -31
  120. package/lib/types/runtime/legacy-root-migration.d.ts +0 -2
  121. package/lib/types/runtime/mcp-client/json-schema-subset.d.ts +0 -58
  122. package/lib/types/runtime/mcp-direct-config.d.ts +0 -39
  123. /package/lib/{runtime/mcp-client/config.js → application/mcp/mcp-bridge-config.js} +0 -0
  124. /package/lib/{runtime → application/mcp}/mcp-redaction.js +0 -0
  125. /package/lib/{runtime → application}/regions.js +0 -0
  126. /package/lib/runtime/{agent-role-names.js → agents/agent-role-names.js} +0 -0
  127. /package/lib/runtime/{mount-lifecycle.js → core/mount-lifecycle.js} +0 -0
  128. /package/lib/runtime/{reconcile-scheduler.js → core/reconcile-scheduler.js} +0 -0
  129. /package/lib/runtime/{source-auto-update.js → core/source-auto-update.js} +0 -0
  130. /package/lib/runtime/{timer-seat.js → core/timer-seat.js} +0 -0
  131. /package/lib/runtime/{model-catalog.js → host/model-catalog.js} +0 -0
  132. /package/lib/runtime/{plugin-message-source.js → host/plugin-message-source.js} +0 -0
  133. /package/lib/runtime/{mcp-client → mcp/bridge}/host-contract.js +0 -0
  134. /package/lib/runtime/{mcp-client → mcp/bridge}/host-seams.js +0 -0
  135. /package/lib/runtime/{mcp-client → mcp/bridge}/plugin-identity.js +0 -0
  136. /package/lib/runtime/{mcp-client → mcp/bridge}/projection.js +0 -0
  137. /package/lib/runtime/{mcp-client → mcp/bridge}/transport.js +0 -0
  138. /package/lib/runtime/{mcp-credentials.js → mcp/mcp-credentials.js} +0 -0
  139. /package/lib/runtime/{dynamic-context.js → surfaces/dynamic-context.js} +0 -0
  140. /package/lib/types/{runtime → application}/deadline.d.ts +0 -0
  141. /package/lib/types/{runtime → application/lsp}/lsp-server-state.d.ts +0 -0
  142. /package/lib/types/{runtime → application/lsp}/profile-seam.d.ts +0 -0
  143. /package/lib/types/{runtime → application/mcp}/mcp-redaction.d.ts +0 -0
  144. /package/lib/types/{runtime → application}/server-config.d.ts +0 -0
  145. /package/lib/types/{runtime → application/state}/storage-migration.d.ts +0 -0
  146. /package/lib/types/runtime/{agent-role-names.d.ts → agents/agent-role-names.d.ts} +0 -0
  147. /package/lib/types/runtime/{mount-lifecycle.d.ts → core/mount-lifecycle.d.ts} +0 -0
  148. /package/lib/types/runtime/{source-auto-update.d.ts → core/source-auto-update.d.ts} +0 -0
  149. /package/lib/types/runtime/{timer-seat.d.ts → core/timer-seat.d.ts} +0 -0
  150. /package/lib/types/runtime/{feedback-tool.d.ts → host/feedback-tool.d.ts} +0 -0
  151. /package/lib/types/runtime/{plugin-message-source.d.ts → host/plugin-message-source.d.ts} +0 -0
  152. /package/lib/types/runtime/{tool-registry-observer.d.ts → host/tool-registry-observer.d.ts} +0 -0
  153. /package/lib/types/runtime/{mcp-client → mcp/bridge}/host-seams.d.ts +0 -0
  154. /package/lib/types/runtime/{mcp-client → mcp/bridge}/plugin-identity.d.ts +0 -0
  155. /package/lib/types/runtime/{mcp-client → mcp/bridge}/projection.d.ts +0 -0
  156. /package/lib/types/runtime/{mcp-client → mcp/bridge}/tools.d.ts +0 -0
  157. /package/lib/types/runtime/{dynamic-context.d.ts → surfaces/dynamic-context.d.ts} +0 -0
@@ -1,11 +1,12 @@
1
1
  /** Installed suite and user resources share one inventory; paths never come from HTTP callers. */
2
- import { realpath, stat, unlink } from 'node:fs/promises';
2
+ import { realpath, stat } from 'node:fs/promises';
3
+ import { isDeepStrictEqual as deepEqual } from 'node:util';
3
4
  import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
4
5
  import { defaultMarkdownResources, resourceText } from '../catalog/component-files.js';
5
6
  import { pluginRootOf } from '../catalog/plugin-variables.js';
6
7
  import { isWithin, suiteDataDir } from '../catalog/paths.js';
7
8
  import { stripFrontmatter } from '../catalog/skills-parse.js';
8
- import { parseFrontmatterRecord } from '../runtime/user-store.js';
9
+ import { parseFrontmatterRecord } from './panels/user-store.js';
9
10
  /**
10
11
  * Id of one panel entry backed by a file inside an installed suite's checkout.
11
12
  * The leading `[` is what distinguishes a plugin entry from a user-authored
@@ -18,6 +19,16 @@ export function pluginResourceId(sourceId, suiteId, kind, name) {
18
19
  export function isPluginResourceId(id) {
19
20
  return id.startsWith('[');
20
21
  }
22
+ /**
23
+ * Frontmatter keys beyond the enable switch that a panel control may diff on a
24
+ * plugin document, per kind: the harness invocation pair on skills, and the
25
+ * model-routing keys the persona form edits on agents.
26
+ */
27
+ const FLIPPABLE_KEYS = {
28
+ skills: ['disable-model-invocation', 'user-invocable'],
29
+ commands: [],
30
+ agents: ['model', 'provider', 'reasoning_effort', 'reasoningEffort']
31
+ };
21
32
  export function createPanelResources(catalog, users) {
22
33
  return {
23
34
  skills: new PanelResources(catalog, users.skills, 'skills'),
@@ -96,11 +107,12 @@ class PanelResources {
96
107
  * A plugin resource is writable only when it sits inside the user dimension
97
108
  * root; suite checkouts elsewhere on disk stay read-only. The user-panel
98
109
  * store lives in the shared Agent layout root instead, so containment is
99
- * measured against the catalog's root, not the panel directory.
110
+ * measured against the catalog's root, not the panel directory. Callers pass
111
+ * the entry they already fetched: a full inventory scan per call would read
112
+ * every suite document twice per write.
100
113
  */
101
- async pluginPath(id) {
102
- const entry = await this.get(id);
103
- if (entry?.origin !== 'plugin')
114
+ async pluginPath(entry) {
115
+ if (entry.origin !== 'plugin')
104
116
  throw new Error('Unknown installed plugin resource');
105
117
  if (entry.path.endsWith('.json'))
106
118
  throw new Error('Inline manifest resources are read-only; edit their source manifest');
@@ -113,12 +125,37 @@ class PanelResources {
113
125
  async update(id, text) {
114
126
  if (!isPluginResourceId(id))
115
127
  return this.users.update(id, text);
116
- parseFrontmatterRecord(text);
117
- await writeFileAtomic(await this.pluginPath(id), text, { mode: 0o644 });
128
+ const previous = await this.get(id);
129
+ if (previous === undefined)
130
+ throw new Error('Unknown installed plugin resource');
131
+ this.assertStateFlipOnly(previous.rawText, text);
132
+ await writeFileAtomic(await this.pluginPath(previous), text, { mode: 0o644 });
133
+ }
134
+ /**
135
+ * A suite owns its files' content: the panel may only flip state frontmatter.
136
+ * The document body must stay byte-identical, and the frontmatter diff is
137
+ * confined to `disabled` everywhere, plus {@link FLIPPABLE_KEYS}'s per-kind
138
+ * control keys — the harness invocation pair on skills, model routing on
139
+ * agents.
140
+ */
141
+ assertStateFlipOnly(previousText, text) {
142
+ if (stripFrontmatter(previousText) !== stripFrontmatter(text))
143
+ throw new Error('plugin resources are read-only; the suite owns their content');
144
+ const before = parseFrontmatterRecord(previousText);
145
+ const after = parseFrontmatterRecord(text);
146
+ const flippable = new Set(['disabled', ...FLIPPABLE_KEYS[this.kind]]);
147
+ for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
148
+ if (flippable.has(key))
149
+ continue;
150
+ if (!deepEqual(before[key], after[key])) {
151
+ throw new Error('plugin resources are read-only; only the enable state can be changed');
152
+ }
153
+ }
118
154
  }
119
155
  async remove(id) {
120
- if (!isPluginResourceId(id))
121
- return this.users.remove(id);
122
- await unlink(await this.pluginPath(id));
156
+ // Plugin files belong to their suite checkout; install/uninstall manages them.
157
+ if (isPluginResourceId(id))
158
+ throw new Error('plugin resources are managed by their suite; uninstall the suite instead');
159
+ return this.users.remove(id);
123
160
  }
124
161
  }
@@ -11,8 +11,8 @@
11
11
  */
12
12
  import { readFile } from 'node:fs/promises';
13
13
  import { join } from 'node:path';
14
- import { normalizeHookDocuments, standaloneHookDocument } from '../catalog/project-hooks.js';
15
- import { effectiveSurfaces } from '../model/types.js';
14
+ import { normalizeHookDocuments, standaloneHookDocument } from '../../catalog/project-hooks.js';
15
+ import { effectiveSurfaces } from '../../model/types.js';
16
16
  export const USER_HOOKS_SOURCE = '@user-hooks';
17
17
  export const USER_HOOKS_SUITE = 'user-hooks';
18
18
  /**
@@ -11,13 +11,13 @@
11
11
  * to that directory. Skills also accept the cross-tool directory spelling
12
12
  * (`<name>/SKILL.md`) that other Agent tools and the harness's own reader of
13
13
  * `~/.agents/skills` author, and read it from the top level only.
14
- * @module runtime/user-store
14
+ * @module application/user-store
15
15
  */
16
16
  import { readdir, readFile, rmdir, rm, stat } from 'node:fs/promises';
17
17
  import { dirname, join } from 'node:path';
18
18
  import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
19
19
  import { parseDocument, stringify } from 'yaml';
20
- import { parseSkillFrontmatter, stripFrontmatter } from '../catalog/skills-parse.js';
20
+ import { parseSkillFrontmatter, stripFrontmatter } from '../../catalog/skills-parse.js';
21
21
  /** Where user panel entries persist: `<agentsRoot>/<kind>/`. */
22
22
  export function userEntryDir(agentsRoot, kind) {
23
23
  return join(agentsRoot, kind);
@@ -10,6 +10,7 @@ export const defaultCatalogPorts = {
10
10
  mcpServerOwner: () => undefined,
11
11
  lspStatusSource: { diagnosticsSnapshot: () => new Map(), hasLiveMounts: () => false },
12
12
  mcpBackend: async () => 'builtin',
13
+ localePreference: () => 'zh',
13
14
  setMcpBackend: async () => {
14
15
  throw new Error('the settings service is not mounted');
15
16
  },
@@ -2,7 +2,7 @@
2
2
  import { defaultMarkdownResources, resourceText } from '../catalog/component-files.js';
3
3
  import { pluginRootOf } from '../catalog/plugin-variables.js';
4
4
  import { suiteDataDir } from '../catalog/paths.js';
5
- import { parseAgentRole } from '../runtime/agent-role-router.js';
5
+ import { parseAgentRole } from './agent-roles.js';
6
6
  export async function projectAgentRoles(catalog, parent) {
7
7
  const cwd = parent?.session?.header?.cwd;
8
8
  if (typeof cwd !== 'string' || cwd === '')
@@ -1,10 +1,9 @@
1
- import { readFile } from 'node:fs/promises';
2
1
  import { join } from 'node:path';
3
- import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
2
+ import { readJsonFile, writeJsonDocument } from './json-file.js';
4
3
  import { MCP_SCHEMA_ID, validateAgainstSchema, validateMcpJson } from '../catalog/validate.js';
5
4
  import { parseLspServers } from '../catalog/lsp-spec.js';
6
5
  import { qualifiedSuiteId } from '../catalog/paths.js';
7
- import { redactMcpConfig } from './mcp-redaction.js';
6
+ import { redactMcpConfig } from './mcp/mcp-redaction.js';
8
7
  /**
9
8
  * The errors the closed-set package shape produces. A user-owned service does
10
9
  * not obey that shape, and the per-transport checks still require what it needs.
@@ -85,24 +84,15 @@ export function lspConfig(spec) {
85
84
  return Object.fromEntries(Object.entries(spec).filter(([key]) => key !== 'key'));
86
85
  }
87
86
  export async function loadLspOverrides(root) {
88
- try {
89
- const raw = JSON.parse(await readFile(join(root, 'lsp-overrides.json'), 'utf8'));
90
- return Object.fromEntries(Object.entries(raw).map(([id, config]) => [id, validateServerLsp(id.slice(id.lastIndexOf('/') + 1), config)]));
91
- }
92
- catch (error) {
93
- if (error.code === 'ENOENT')
94
- return {};
95
- throw error;
96
- }
87
+ const raw = await readJsonFile(join(root, 'lsp-overrides.json'));
88
+ if (typeof raw !== 'object' || raw === null)
89
+ return {};
90
+ return Object.fromEntries(Object.entries(raw).map(([id, config]) => [id, validateServerLsp(id.slice(id.lastIndexOf('/') + 1), config)]));
97
91
  }
98
92
  export async function saveLspOverride(root, id, config) {
99
93
  const overrides = await loadLspOverrides(root);
100
94
  overrides[id] = config;
101
- const path = join(root, 'lsp-overrides.json');
102
- await writeFileAtomic(path, JSON.stringify(Object.fromEntries(Object.entries(overrides).map(([key, value]) => [key, lspConfig(value)])), null, 2), {
103
- mode: 0o600,
104
- dirMode: 0o700
105
- });
95
+ await writeJsonDocument(join(root, 'lsp-overrides.json'), Object.fromEntries(Object.entries(overrides).map(([key, value]) => [key, lspConfig(value)])));
106
96
  }
107
97
  export async function applyLspOverrides(root, suites) {
108
98
  const overrides = await loadLspOverrides(root);
@@ -12,7 +12,7 @@
12
12
  import { join, resolve } from 'node:path';
13
13
  import { resolveProjectRoot, STATE_FILE_NAME } from '../catalog/paths.js';
14
14
  import { discoverSourceListWithNotes } from '../catalog/source-catalog.js';
15
- import { loadState } from '../runtime/state-store.js';
15
+ import { loadState } from './state/state-store.js';
16
16
  export const SCAN_CACHE_TTL_MS = 30_000;
17
17
  const SCAN_CACHE_MAX_ENTRIES = 8;
18
18
  export class SnapshotCache {
@@ -15,8 +15,7 @@ import { repoName } from '../catalog/manifests.js';
15
15
  import { deriveSourceIdCandidates, expandHome, sanitizeId, sourceCheckoutDir, sourcesDir } from '../catalog/paths.js';
16
16
  import { isDirectory, pathExists } from '../catalog/fs-probes.js';
17
17
  import { canonicalGitUrl } from '../catalog/scan-resolvers.js';
18
- import { readLocalePreference } from '../runtime/host-locale.js';
19
- import { githubCloneUrl, resolveRegion } from '../runtime/regions.js';
18
+ import { githubCloneUrl, resolveRegion } from './regions.js';
20
19
  import { resolveSourceKind } from '../model/types.js';
21
20
  export class SourceStore {
22
21
  context;
@@ -280,7 +279,7 @@ export class SourceStore {
280
279
  // source.url) are re-pointed — a foreign remote is left untouched.
281
280
  if (source.kind === 'git' || (source.kind === undefined && source.local !== true)) {
282
281
  try {
283
- const region = resolveRegion(await this.ports.downloadRegion(), await readLocalePreference());
282
+ const region = resolveRegion(await this.ports.downloadRegion(), this.ports.localePreference());
284
283
  const routed = githubCloneUrl(region, source.url);
285
284
  const origin = await gitRemoteUrl(checkout);
286
285
  if (origin !== routed && (origin === source.url || origin === githubCloneUrl('china', source.url))) {
@@ -401,7 +400,7 @@ export class SourceStore {
401
400
  try {
402
401
  // The region routes github.com clones through the China mirror prefix;
403
402
  // the proxied URL becomes `origin`, so refreshes follow the same route.
404
- const region = resolveRegion(await this.ports.downloadRegion(), await readLocalePreference());
403
+ const region = resolveRegion(await this.ports.downloadRegion(), this.ports.localePreference());
405
404
  await gitClone(githubCloneUrl(region, source.url), source.branch, checkout, this.context.git);
406
405
  return undefined;
407
406
  }
@@ -7,15 +7,13 @@
7
7
  * like the market profile state this pattern follows. The record shapes live
8
8
  * in `src/model/types.ts`; only the reading and writing live here.
9
9
  */
10
- import { readFile } from 'node:fs/promises';
11
- import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
12
- import { SUITE_SURFACE_KEYS } from '../model/types.js';
10
+ import { readJsonFile, writeJsonDocument } from '../json-file.js';
11
+ import { SUITE_SURFACE_KEYS } from '../../model/types.js';
13
12
  export const EMPTY_STATE = { version: 1, sources: [], installed: {} };
14
13
  /** Parse persisted state; unreadable or wrong-version files yield a contained empty state. */
15
14
  export async function loadState(statePath) {
16
15
  try {
17
- const text = await readFile(statePath, 'utf8');
18
- const parsed = JSON.parse(text);
16
+ const parsed = await readJsonFile(statePath);
19
17
  if (typeof parsed !== 'object' || parsed === null)
20
18
  return EMPTY_STATE;
21
19
  const record = parsed;
@@ -87,7 +85,7 @@ function parseSurfaceOverrides(raw) {
87
85
  }
88
86
  return hasAny ? { surfaces: overrides } : {};
89
87
  }
90
- /** Persist state atomically through the harness's atomic-write helper. */
88
+ /** Persist state atomically through the shared JSON document writer. */
91
89
  export async function saveState(statePath, state) {
92
- await writeFileAtomic(statePath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600, dirMode: 0o700 });
90
+ await writeJsonDocument(statePath, state);
93
91
  }
@@ -2,8 +2,8 @@
2
2
  import { constants } from 'node:fs';
3
3
  import { copyFile, lstat, mkdir, readdir, readFile, rmdir, unlink } from 'node:fs/promises';
4
4
  import { dirname, join, relative, resolve } from 'node:path';
5
- import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
6
- import { expandHome, isWithin, resolveAgentsRoot, resolveDataRoot, resolveDshHome, resolveUserRoot } from '../catalog/paths.js';
5
+ import { readJsonFile, writeJsonDocument } from '../json-file.js';
6
+ import { expandHome, isWithin, resolveAgentsRoot, resolveDataRoot, resolveDshHome, resolveUserRoot } from '../../catalog/paths.js';
7
7
  async function info(path) {
8
8
  try {
9
9
  return await lstat(path);
@@ -156,7 +156,7 @@ async function relocateManagedSourceUrls(userRoot, legacyUserRoot) {
156
156
  const path = join(userRoot, 'state.json');
157
157
  if ((await info(path)) === undefined)
158
158
  return;
159
- const state = JSON.parse(await readFile(path, 'utf8'));
159
+ const state = await readJsonFile(path);
160
160
  if (typeof state !== 'object' || state === null || !('sources' in state) || !Array.isArray(state.sources))
161
161
  return;
162
162
  let changed = false;
@@ -170,7 +170,7 @@ async function relocateManagedSourceUrls(userRoot, legacyUserRoot) {
170
170
  changed = true;
171
171
  }
172
172
  if (changed) {
173
- await writeFileAtomic(path, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600, dirMode: 0o700 });
173
+ await writeJsonDocument(path, state);
174
174
  }
175
175
  }
176
176
  async function collectUnsupportedEntries(path, conflicts) {
@@ -13,10 +13,6 @@ import { expandHome, sourcesDir } from './paths.js';
13
13
  import { isDirectory } from './fs-probes.js';
14
14
  import { scanSource as scanSourceWithNotes } from './suite-scanner.js';
15
15
  import { discoverNativeProjectSuites } from './native-project.js';
16
- /** Discover suites from the selected configured or project checkouts. */
17
- export async function discoverSourceList(sources, dimension, dimensionRoot) {
18
- return (await discoverSourceListWithNotes(sources, dimension, dimensionRoot, true)).suites;
19
- }
20
16
  /**
21
17
  * Discover suites plus per-source scan diagnostics for one dimension.
22
18
  * @param scanProjectLayouts - whether the project's own native Agent directories
@@ -0,0 +1 @@
1
+ export {};
@@ -6,7 +6,6 @@ export const MARKET_ROUTES = {
6
6
  overview: `${MARKET_API_PREFIX}overview`,
7
7
  mcpStatus: `${MARKET_API_PREFIX}mcp-status`,
8
8
  addMcpServer: `${MARKET_API_PREFIX}mcp-servers/add`,
9
- importMcpServers: `${MARKET_API_PREFIX}mcp-servers/import`,
10
9
  lspStatus: `${MARKET_API_PREFIX}lsp-status`,
11
10
  lspServers: `${MARKET_API_PREFIX}lsp-servers`,
12
11
  addLspServer: `${MARKET_API_PREFIX}lsp-servers/add`,
@@ -0,0 +1 @@
1
+ export {};
package/lib/index.js CHANGED
@@ -1,28 +1,28 @@
1
1
  import z from '@deepseek-ai/schemastery';
2
- import { MarketSettingsFields } from './runtime/mcp-backend.js';
2
+ import { MarketSettingsFields } from './application/mcp/mcp-backend.js';
3
3
  import { Catalog } from './application/catalog.js';
4
- import { settlesWithin } from './runtime/deadline.js';
5
- import { RuntimeReconciler } from './runtime/reconciler.js';
6
- import { ReconcileScheduler } from './runtime/reconcile-scheduler.js';
7
- import { MarketSettingsNamespace } from './runtime/settings-namespace.js';
8
- import { deleteMcpAuthGrant } from './runtime/mcp-auth-record.js';
9
- import { inspectToolRegistry, toolsServiceOf } from './runtime/tool-registry-observer.js';
10
- import { migratePluginStorage } from './runtime/storage-migration.js';
11
- import { mountAgentRoleTool } from './runtime/agent-role-router.js';
4
+ import { settlesWithin } from './application/deadline.js';
5
+ import { RuntimeReconciler } from './runtime/core/reconciler.js';
6
+ import { ReconcileScheduler } from './runtime/core/reconcile-scheduler.js';
7
+ import { MarketSettingsNamespace } from './runtime/host/settings-namespace.js';
8
+ import { deleteMcpAuthGrant } from './runtime/mcp/mcp-auth-record.js';
9
+ import { inspectToolRegistry, toolsServiceOf } from './runtime/host/tool-registry-observer.js';
10
+ import { migratePluginStorage } from './application/state/storage-migration.js';
11
+ import { mountAgentRoleTool } from './runtime/agents/agent-role-router.js';
12
12
  import { projectAgentRoles } from './application/project-agent-roles.js';
13
- import { mountProjectCommands, mountProjectMcp, mountProjectHooks, mountSuiteInstructions } from './runtime/project-runtime.js';
13
+ import { mountProjectCommands, mountProjectMcp, mountProjectHooks, mountSuiteInstructions } from './runtime/surfaces/project-runtime.js';
14
14
  import { createPanelResources } from './application/panel-resources.js';
15
15
  import { resolveAgentsRoot, resolveDataRoot, resolveUserRoot } from './catalog/paths.js';
16
16
  import { mountSuiteRoutes } from './routes.js';
17
- import { SuiteSkillProvider } from './runtime/skills-provider.js';
18
- import { shellSeamOf } from './runtime/dynamic-context.js';
19
- import { loadLspServers } from './runtime/lsp-direct-config.js';
20
- import { loadDisabledLspServers } from './runtime/lsp-server-state.js';
21
- import { bindHostLocale, loadHostLocale, readHostLocalePreference, setHostLocaleSource } from './runtime/host-locale.js';
22
- import { createUserPanelStores } from './runtime/user-panels.js';
23
- import { SourceAutoUpdater } from './runtime/source-auto-update.js';
24
- import { UserPanelSkillProvider } from './runtime/user-panels.js';
25
- import { UserCommandMountRegistry } from './runtime/user-commands.js';
17
+ import { SuiteSkillProvider } from './runtime/surfaces/skills-provider.js';
18
+ import { shellSeamOf } from './runtime/surfaces/dynamic-context.js';
19
+ import { loadLspServers } from './application/lsp/lsp-direct-config.js';
20
+ import { loadDisabledLspServers } from './application/lsp/lsp-server-state.js';
21
+ import { bindHostLocale, LOCALE_SETTINGS_ENTRY, readHostLocalePreference, readLocalePreference, setHostLocaleSource } from './runtime/host/host-locale.js';
22
+ import { createUserPanelStores } from './runtime/panels/user-panels.js';
23
+ import { SourceAutoUpdater } from './runtime/core/source-auto-update.js';
24
+ import { UserPanelSkillProvider } from './runtime/panels/user-panels.js';
25
+ import { UserCommandMountRegistry } from './runtime/panels/user-commands.js';
26
26
  import { presetSourceRef } from './model/preset-source.js';
27
27
  export const name = 'dsh-agent-plugins-market';
28
28
  export const inject = ['skills', 'commands'];
@@ -54,24 +54,32 @@ export async function apply(ctx, config = { mcpEnhanced: undefinedRef, scanProje
54
54
  throw new Error(`Plugin storage migration conflicts (original files retained): ${migration.conflicts.join(', ')}`);
55
55
  let providerControl;
56
56
  let userPanelControl;
57
- // Host runtime copy resolves from the harness `locale.preference` setting;
58
- // the async settings read lands before the first session starts in practice.
57
+ // Host runtime copy resolves from the harness `locale.preference` setting.
59
58
  const hostLocale = { t: bindHostLocale(undefined) };
60
59
  const refreshHostLocale = () => {
61
- void loadHostLocale().then(locale => {
62
- hostLocale.t = locale.t;
63
- providerControl?.invalidate();
64
- userPanelControl?.invalidate();
65
- });
60
+ hostLocale.t = bindHostLocale(readLocalePreference());
61
+ providerControl?.invalidate();
62
+ userPanelControl?.invalidate();
66
63
  };
67
- void refreshHostLocale();
68
- // The locale preference the GUI writes is owned by the host settings service:
69
- // read it there rather than re-parsing the document, and re-bind once the
70
- // service lands (the locale namespace may register after this plugin).
71
- ctx.inject(['settings'], settingsCtx => {
72
- setHostLocaleSource(() => readHostLocalePreference(settingsCtx));
64
+ // The preference is the `locale` entry's live configuration, projected by the
65
+ // host settings service. Resolve that service through the store from this
66
+ // entry's own context, and register the wiring as an effect so it lives
67
+ // exactly as long as this fiber: a context captured from an injection
68
+ // callback throws `cannot get required service "settings" in inactive
69
+ // context` once a profile reload disposes that fiber, and the unhandled
70
+ // rejection ends the host. Re-read once the service lands, because it may
71
+ // mount after this plugin.
72
+ ctx.effect(() => setHostLocaleSource(() => readHostLocalePreference(ctx.get('settings'))), 'dsh-agent-plugins-market: host locale source');
73
+ refreshHostLocale();
74
+ ctx.inject(['settings'], () => {
73
75
  refreshHostLocale();
74
76
  });
77
+ // A language switch writes the `locale` entry's form; re-read so the copy the
78
+ // market renders follows it without a reload.
79
+ ctx.effect(() => ctx.on('settings/document-updated', (ns) => {
80
+ if (ns === LOCALE_SETTINGS_ENTRY)
81
+ refreshHostLocale();
82
+ }), 'dsh-agent-plugins-market: host locale refresh');
75
83
  const runtime = new RuntimeReconciler(ctx, dataRoot, key => hostLocale.t(key));
76
84
  // User panel stores (skills / commands / agent personas) and their runtime
77
85
  // contributions: one extra skill provider plus one command mount registry.
@@ -162,7 +170,8 @@ export async function apply(ctx, config = { mcpEnhanced: undefinedRef, scanProje
162
170
  lspStatusSource: runtime.lsp,
163
171
  mcpBackend: () => settings.backend(),
164
172
  setMcpBackend: backend => settings.setBackend(backend),
165
- downloadRegion: () => settings.downloadRegion()
173
+ downloadRegion: () => settings.downloadRegion(),
174
+ localePreference: () => readLocalePreference() ?? 'zh'
166
175
  };
167
176
  const catalog = new Catalog({ userRoot, dataRoot, agentsRoot, onChanged, ports, ...(config.git === undefined ? {} : { git: config.git }) });
168
177
  await catalog.load();
package/lib/routes.js CHANGED
@@ -9,8 +9,8 @@
9
9
  import { isAbsolute } from 'node:path';
10
10
  import { MARKET_ROUTES, userPanelRoute } from './contracts/market.js';
11
11
  import { expandHome } from './catalog/paths.js';
12
- import { sanitizeOverridePatch } from './runtime/mcp-overrides.js';
13
- import { readModelCatalog } from './runtime/model-catalog.js';
12
+ import { sanitizeOverridePatch } from './application/mcp/mcp-overrides.js';
13
+ import { readModelCatalog } from './runtime/host/model-catalog.js';
14
14
  const MAX_BODY_BYTES = 64 * 1024;
15
15
  /** Mount every route; returns the disposer releasing them all. */
16
16
  export function mountSuiteRoutes(hostCtx, manager, panels) {
@@ -93,13 +93,6 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
93
93
  await manager.addMcpServer(body.name, body.config);
94
94
  return {};
95
95
  });
96
- // Paste import: many services in one write. Each entry reports its own
97
- // outcome, so one rejected definition never fails the whole paste.
98
- post(MARKET_ROUTES.importMcpServers, async (body) => {
99
- const overwrite = body['overwrite'] === true;
100
- const result = await manager.importMcpServers(body['servers'], overwrite);
101
- return { imported: result.imported, skipped: result.skipped };
102
- });
103
96
  get(MARKET_ROUTES.lspStatus, async (_request, response) => {
104
97
  sendJson(response, 200, await manager.lspStatus());
105
98
  });
@@ -1,62 +1,17 @@
1
1
  /** Execute user and installed-suite agent cards through the host subagent service. */
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { defineTool } from '@deepseek-ai/dsh-tools';
4
- import { parse as parseYaml } from 'yaml';
5
4
  import { mountSubagentCatalog } from './subagent-catalog.js';
6
5
  import { namedAgentRoles } from './agent-role-names.js';
7
- import { expandPluginPaths } from '../catalog/plugin-variables.js';
6
+ import { expandPluginPaths } from '../../catalog/plugin-variables.js';
7
+ import { parseAgentRole } from '../../application/agent-roles.js';
8
+ export { parseAgentRole };
8
9
  export const AGENT_ROLE_TOOL_NAME = 'subagent_role';
9
10
  /**
10
11
  * Absolute delegation-depth cap applied to every role child. The host default is
11
12
  * the same value; keeping it explicit preserves the shipped recursion budget.
12
13
  */
13
14
  const MAX_AGENT_ROLE_DEPTH = 3;
14
- function optionalText(value, key) {
15
- if (value === undefined)
16
- return undefined;
17
- if (typeof value !== 'string' || value.trim() === '')
18
- throw new Error(`agent metadata ${key} must be a non-empty string`);
19
- return value.trim();
20
- }
21
- /** Strict YAML parsing prevents malformed routing metadata from silently inheriting wider privileges. */
22
- export function parseAgentRole(text) {
23
- const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
24
- if (match === null) {
25
- if (/^(?:\uFEFF)?---(?:\r?\n|$)/.test(text))
26
- throw new Error('agent frontmatter is not closed');
27
- return { content: text, disabled: false };
28
- }
29
- // A matched block always carries its body; the check keeps the type honest.
30
- const frontmatter = match[1];
31
- if (frontmatter === undefined)
32
- throw new Error('agent frontmatter is not closed');
33
- const parsed = parseYaml(frontmatter);
34
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
35
- throw new Error('agent frontmatter must be a YAML object');
36
- const metadata = parsed;
37
- if (metadata.disabled !== undefined && typeof metadata.disabled !== 'boolean')
38
- throw new Error('agent metadata disabled must be a boolean');
39
- const model = optionalText(metadata.model, 'model');
40
- const provider = optionalText(metadata.provider, 'provider');
41
- const effort = optionalText(metadata.reasoning_effort, 'reasoning_effort');
42
- const camelEffort = optionalText(metadata.reasoningEffort, 'reasoningEffort');
43
- if (effort !== undefined && camelEffort !== undefined && effort !== camelEffort)
44
- throw new Error('agent metadata reasoning_effort and reasoningEffort conflict');
45
- const reasoningEffort = effort ?? camelEffort;
46
- const title = optionalText(metadata.name, 'name');
47
- const description = optionalText(metadata.description, 'description');
48
- if (model === 'inherit' && provider !== undefined)
49
- throw new Error('agent model inherit cannot specify a provider');
50
- return {
51
- content: text.slice(match[0].length),
52
- disabled: metadata.disabled === true,
53
- ...(model === undefined || model === 'inherit' ? {} : { model }),
54
- ...(provider === undefined ? {} : { provider }),
55
- ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
56
- ...(title === undefined ? {} : { title }),
57
- ...(description === undefined ? {} : { description })
58
- };
59
- }
60
15
  function parentRouteOf(parent) {
61
16
  const agent = parent;
62
17
  return agent?.session?.requestHeader?.()?.config ?? agent?.options ?? {};
@@ -287,11 +242,10 @@ export function mountAgentRoleTool(ctx, listRoles) {
287
242
  const host = ctx;
288
243
  const tool = defineTool({
289
244
  name: AGENT_ROLE_TOOL_NAME,
290
- description: "Delegate a task to a role child defined by an installed plugin: the named role's own instructions and its configured model apply, and the child starts without this conversation. This tool runs in " +
291
- 'the background by default, immediately returns a durable subagent id, and keeps the child available for later turns; when that run settles, the runtime sends the parent a notice carrying its outcome ' +
292
- 'and final reply. Set `run_in_background: false` only when your next action depends on receiving the result. A child usually runs for minutes, so while it works on the background path, continue with ' +
293
- 'independent work instead of waiting. `prompt` must stand alone, since the child cannot see this conversation and a question it asks while it runs goes unanswered. When the notice arrives, verify its ' +
294
- "assertions against the files and relay the result to the user; the child's output is not visible to them. The current catalog lists the available roles and states the usage guidance; when no listed " +
245
+ description: "Delegate a task to a named role child from the session's role catalog: the role's own instructions and its configured model apply, and the child starts without this conversation. Use it proactively for self-contained work — codebase exploration whose raw reads would fill this context, a scoped implementation, a review, or an analysis you can brief in one prompt — and start independent children together in one message. The child runs in " +
246
+ 'the background by default and returns a durable subagent id at once, keeping the child available for later turns; when the run settles, the runtime sends the parent a notice carrying its outcome and ' +
247
+ 'final reply. A child usually runs for minutes, so continue with independent work instead of waiting. `prompt` must stand alone, since the child cannot see this conversation and a question it asks while it runs goes unanswered. When the notice arrives, verify its ' +
248
+ "assertions against the files and relay the result to the user; the child's output is not visible to them. The current catalog lists the available roles and the full usage guidance; when no listed " +
295
249
  "role matches, use the host's own `subagent` tools instead.",
296
250
  parameters: {
297
251
  agent: { type: 'string', required: true, description: 'Exact callable role name from the current subagent catalog.' },
@@ -61,26 +61,34 @@ const CATALOG_UPDATED = 'The available subagent roles changed. This complete lis
61
61
  const CATALOG_PROMPT = 'Writing the prompt: brief the child like a colleague who just walked into the room — it has not seen this conversation, does not know what you tried, and does not know why the task matters. Explain what you are trying to accomplish, describe what you learned and ruled out, and give enough context for the child to make judgment calls. The child cannot see this conversation, so prompt must be self-contained: the task itself, the relevant files and known findings, the output you expect, and the boundaries of the task (research only, or may edit files). Terse command-style prompts produce shallow, generic work.';
62
62
  const CATALOG_USAGE = [
63
63
  'Usage notes:',
64
- '- Call subagent_role with the exact catalog name as agent. A child has its own context: it starts without this conversation, so it suits self-contained work that one briefing can state in full.',
64
+ "- Call subagent_role with the exact catalog name as agent. A child has its own context: it starts without this conversation, so it suits self-contained work that one briefing can state in full. If no listed role matches, do not substitute a similarly named one — delegate through one of the host's general delegation channels instead, and do not load these roles through skill or slash commands.",
65
65
  '- By default the call returns a durable subagent id immediately and runs in the background without blocking you. A child usually runs for minutes: spend that time advancing independent work that does not depend on it, rather than idling — and do not poll it or re-check its progress. While it runs, send_message adds an instruction or more material and list_agents reports its status.',
66
- '- run_in_background true runs the same child as a tracked background job and returns a job id instead: collect it with job_output and stop it with job_kill. Use it when the result should be collected later through the job tools; leave the parameter out when you want the child reachable with send_message and list_agents.',
67
- "- run_in_background false runs one foreground child and returns its report as this call's result instead of a settlement notice. Use it only when your next action depends on the result.",
66
+ "- run_in_background true runs the same child as a tracked background job and returns a job id instead: collect it with job_output and stop it with job_kill. run_in_background false runs one foreground child and returns its report as this call's result — use it when your next action depends on the result and no independent work remains.",
68
67
  '- A question a child asks while it runs goes unanswered: nothing will reply on your behalf. So state the prompt in full the first time, and a child should decide for itself, keep going, and list in its final reply which choices it made alone and what information it still lacked.',
69
- "- A background child's output is not visible to the user. When it finishes, summarize the result to the user yourself.",
70
- "- The child's settlement notice arrives as a user message, not as a tool result. Until it arrives, do not assume or predict its findings, and do not deliver anything that depends on them; act on them once it lands, by verifying or delegating further as needed.",
68
+ "- The settlement notice arrives as a user message, not as a tool result. Until it arrives, do not assume or predict its findings, and do not deliver anything that depends on them. A background child's output is not visible to the user: when it finishes, summarize the result to the user yourself.",
71
69
  "- The child's final reply is its own report: check its assertions against the files themselves, and treat its statements about its own configuration the same way — a child cannot see how its role instructions were installed.",
72
- "- The route parameters override the route the role card declares; omit them to use the card's route, or the parent route when the card declares none.",
73
- '- Do not duplicate work a child is already doing.',
74
- '- When several children run at once, give each its own git worktree and name the files it owns in prompt.'
70
+ '- Do not duplicate work a child is already doing. When several children run at once, give each its own git worktree and name the files it owns in prompt.'
75
71
  ].join('\n');
76
- const CATALOG_WHEN_NOT_TO_USE = "When NOT to use a role child: no listed role matches; your next action needs the result; or another child is already doing this work. When no role matches, do not substitute a similarly named one — delegate through one of the host's general delegation channels instead. Do not load these roles through skill or slash commands.";
77
72
  /**
78
- * Model-facing catalog text: the role list followed by the same guidance on
79
- * first publication and on every update, so a replacement never arrives without
80
- * the rules that govern it. The guidance covers how to brief a child, how to
81
- * work with one while it runs, and when a role child is the wrong answer; the
82
- * per-channel arbitration between the host's delegation tools stays in the tool
83
- * descriptions the harness owns.
73
+ * The positive half of the delegation decision, published with every catalog
74
+ * ahead of the role list so the model reads the triggers before the names.
75
+ * Delegation had only negative framing before this block: every other paragraph
76
+ * states what must not happen, and none states when delegating is the default.
77
+ */
78
+ const CATALOG_WHEN_TO_DELEGATE = [
79
+ 'Delegate proactively: hand self-contained work to a role child by default instead of doing it inline. Delegate when any of these holds:',
80
+ '- The task would burn many tool calls or file reads whose raw output would flood this conversation — codebase exploration, tracing behavior across files, digesting long logs or reports. The child reads everything and returns only the distilled result.',
81
+ '- The task is a complete unit one briefing can state: a scoped implementation, a review, an analysis, a document.',
82
+ '- Two or more such tasks are independent: start all the children in one message and let them run in parallel.',
83
+ 'Decide by the briefing test: if you can state the task in full and it needs no further input from the user, delegate it and keep working.'
84
+ ].join('\n');
85
+ /**
86
+ * Model-facing catalog text: the delegation triggers, then the role list, then
87
+ * the same guidance on first publication and on every update, so a replacement
88
+ * never arrives without the rules that govern it. The guidance covers when a
89
+ * role child is the default, how to brief one, and how to work with one while
90
+ * it runs; the per-channel arbitration between the host's delegation tools
91
+ * stays in the tool descriptions the harness owns.
84
92
  *
85
93
  * Role lines carry a name and a description only. The configured route stays in
86
94
  * the durable entries for execution and change detection; publishing it would
@@ -91,12 +99,12 @@ export function renderCatalogText(entries, update) {
91
99
  return [
92
100
  '<system-reminder>',
93
101
  update ? CATALOG_UPDATED : CATALOG_INTRO,
102
+ CATALOG_WHEN_TO_DELEGATE,
94
103
  '<available_subagents>',
95
104
  ...lines,
96
105
  '</available_subagents>',
97
106
  CATALOG_PROMPT,
98
107
  CATALOG_USAGE,
99
- CATALOG_WHEN_NOT_TO_USE,
100
108
  '</system-reminder>'
101
109
  ].join('\n');
102
110
  }