dsh-agent-plugins-market 0.7.0 → 0.7.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 (114) hide show
  1. package/README.md +31 -26
  2. package/README.zh.md +31 -26
  3. package/client/client.js +29164 -1136
  4. package/client/client.js.map +1 -1
  5. package/lib/application/catalog-context.js +8 -1
  6. package/lib/application/catalog.js +27 -2
  7. package/lib/application/details.js +13 -2
  8. package/lib/application/install-store.js +1 -1
  9. package/lib/application/mcp-service.js +191 -19
  10. package/lib/application/panel-resources.js +16 -4
  11. package/lib/application/project-agent-roles.js +7 -1
  12. package/lib/catalog/archive.js +16 -1
  13. package/lib/catalog/manifests.js +124 -13
  14. package/lib/catalog/paths.js +8 -0
  15. package/lib/catalog/plugin-variables.js +38 -0
  16. package/lib/catalog/project-hooks.js +14 -1
  17. package/lib/catalog/scan-resolvers.js +117 -12
  18. package/lib/catalog/skills-parse.js +47 -0
  19. package/lib/catalog/source-catalog.js +8 -3
  20. package/lib/catalog/suite-components.js +2 -2
  21. package/lib/catalog/surfaces.js +73 -27
  22. package/lib/catalog/validate.js +205 -54
  23. package/lib/contracts/market.js +3 -0
  24. package/lib/contracts/settings.js +63 -0
  25. package/lib/index.js +32 -13
  26. package/lib/model/layouts.js +44 -1
  27. package/lib/model/preset-source.js +8 -0
  28. package/lib/routes.js +43 -2
  29. package/lib/runtime/agent-role-router.js +203 -56
  30. package/lib/runtime/commands-mounts.js +50 -9
  31. package/lib/runtime/deadline.js +32 -6
  32. package/lib/runtime/dynamic-context.js +140 -0
  33. package/lib/runtime/feedback-tool.js +7 -3
  34. package/lib/runtime/host-locale.js +45 -15
  35. package/lib/runtime/lsp-mounts.js +23 -2
  36. package/lib/runtime/mcp-backend.js +14 -15
  37. package/lib/runtime/mcp-client/config.js +8 -0
  38. package/lib/runtime/mcp-client/connection.js +9 -8
  39. package/lib/runtime/mcp-client/host-seams.js +8 -88
  40. package/lib/runtime/mcp-client/oauth.js +82 -14
  41. package/lib/runtime/mcp-client/tools.js +2 -1
  42. package/lib/runtime/mcp-config.js +182 -35
  43. package/lib/runtime/mcp-direct-config.js +49 -6
  44. package/lib/runtime/mcp-mounts.js +15 -3
  45. package/lib/runtime/mcp-overrides.js +131 -1
  46. package/lib/runtime/mcp-redaction.js +8 -0
  47. package/lib/runtime/mcp-status.js +21 -5
  48. package/lib/runtime/project-runtime.js +23 -9
  49. package/lib/runtime/reconcile-scheduler.js +6 -8
  50. package/lib/runtime/reconciler.js +2 -1
  51. package/lib/runtime/regions.js +0 -19
  52. package/lib/runtime/server-config.js +49 -8
  53. package/lib/runtime/settings-namespace.js +21 -13
  54. package/lib/runtime/skills-provider.js +33 -11
  55. package/lib/runtime/source-auto-update.js +10 -14
  56. package/lib/runtime/subagent-catalog.js +62 -22
  57. package/lib/runtime/timer-seat.js +80 -0
  58. package/lib/runtime/tool-registry-observer.js +24 -20
  59. package/lib/runtime/user-commands.js +5 -1
  60. package/lib/runtime/user-panels.js +110 -41
  61. package/lib/runtime/user-store.js +114 -58
  62. package/lib/types/application/catalog-context.d.ts +6 -0
  63. package/lib/types/application/catalog.d.ts +19 -1
  64. package/lib/types/application/install-store.d.ts +1 -1
  65. package/lib/types/application/mcp-service.d.ts +38 -3
  66. package/lib/types/application/ports.d.ts +1 -1
  67. package/lib/types/application/queries.d.ts +8 -1
  68. package/lib/types/catalog/manifests.d.ts +6 -2
  69. package/lib/types/catalog/paths.d.ts +6 -0
  70. package/lib/types/catalog/plugin-variables.d.ts +16 -0
  71. package/lib/types/catalog/skills-parse.d.ts +13 -0
  72. package/lib/types/catalog/source-catalog.d.ts +7 -2
  73. package/lib/types/catalog/surfaces.d.ts +14 -4
  74. package/lib/types/catalog/validate.d.ts +72 -11
  75. package/lib/types/contracts/market.d.ts +77 -0
  76. package/lib/types/contracts/mcp-status.d.ts +11 -0
  77. package/lib/types/contracts/settings.d.ts +59 -0
  78. package/lib/types/model/layouts.d.ts +30 -0
  79. package/lib/types/model/preset-source.d.ts +18 -0
  80. package/lib/types/model/types.d.ts +37 -1
  81. package/lib/types/runtime/agent-role-router.d.ts +96 -18
  82. package/lib/types/runtime/commands-mounts.d.ts +15 -7
  83. package/lib/types/runtime/deadline.d.ts +10 -11
  84. package/lib/types/runtime/dynamic-context.d.ts +81 -0
  85. package/lib/types/runtime/host-locale.d.ts +20 -2
  86. package/lib/types/runtime/lsp-mounts.d.ts +7 -0
  87. package/lib/types/runtime/mcp-backend.d.ts +8 -15
  88. package/lib/types/runtime/mcp-client/config.d.ts +8 -0
  89. package/lib/types/runtime/mcp-client/host-seams.d.ts +20 -64
  90. package/lib/types/runtime/mcp-client/oauth.d.ts +4 -0
  91. package/lib/types/runtime/mcp-config.d.ts +75 -13
  92. package/lib/types/runtime/mcp-direct-config.d.ts +22 -0
  93. package/lib/types/runtime/mcp-overrides.d.ts +44 -0
  94. package/lib/types/runtime/mcp-redaction.d.ts +6 -0
  95. package/lib/types/runtime/project-runtime.d.ts +10 -3
  96. package/lib/types/runtime/reconcile-scheduler.d.ts +1 -1
  97. package/lib/types/runtime/regions.d.ts +1 -4
  98. package/lib/types/runtime/server-config.d.ts +26 -1
  99. package/lib/types/runtime/settings-namespace.d.ts +10 -10
  100. package/lib/types/runtime/skills-provider.d.ts +11 -2
  101. package/lib/types/runtime/source-auto-update.d.ts +3 -4
  102. package/lib/types/runtime/subagent-catalog.d.ts +14 -2
  103. package/lib/types/runtime/timer-seat.d.ts +51 -0
  104. package/lib/types/runtime/tool-registry-observer.d.ts +14 -6
  105. package/lib/types/runtime/user-panels.d.ts +65 -12
  106. package/lib/types/runtime/user-store.d.ts +65 -16
  107. package/package.json +23 -6
  108. package/schemas/1.0.0/mcp.schema.json +0 -22
  109. package/schemas/1.1.0/mcp.schema.json +120 -0
  110. package/schemas/1.1.0/plugin.schema.json +65 -0
  111. package/schemas/README.md +25 -19
  112. package/schemas/agent-plugins/spec.md +23 -9
  113. package/schemas/com.deepseek.harness/namespace.schema.json +42 -0
  114. package/schemas/com.deepseek.harness/spec.md +52 -0
package/lib/index.js CHANGED
@@ -4,7 +4,7 @@ import { RuntimeReconciler } from './runtime/reconciler.js';
4
4
  import { ReconcileScheduler } from './runtime/reconcile-scheduler.js';
5
5
  import { MarketSettingsNamespace } from './runtime/settings-namespace.js';
6
6
  import { deleteMcpAuthGrant } from './runtime/mcp-auth-record.js';
7
- import { inspectToolRegistry } from './runtime/tool-registry-observer.js';
7
+ import { inspectToolRegistry, toolsServiceOf } from './runtime/tool-registry-observer.js';
8
8
  import { migratePluginStorage } from './runtime/storage-migration.js';
9
9
  import { mountAgentRoleTool } from './runtime/agent-role-router.js';
10
10
  import { projectAgentRoles } from './application/project-agent-roles.js';
@@ -13,13 +13,15 @@ import { createPanelResources } from './application/panel-resources.js';
13
13
  import { resolveAgentsRoot, resolveDataRoot, resolveUserRoot } from './catalog/paths.js';
14
14
  import { mountSuiteRoutes } from './routes.js';
15
15
  import { SuiteSkillProvider } from './runtime/skills-provider.js';
16
+ import { shellSeamOf } from './runtime/dynamic-context.js';
16
17
  import { loadLspServers } from './runtime/lsp-direct-config.js';
17
18
  import { loadDisabledLspServers } from './runtime/lsp-server-state.js';
18
- import { bindHostLocale, loadHostLocale } from './runtime/host-locale.js';
19
+ import { bindHostLocale, loadHostLocale, readHostLocalePreference, setHostLocaleSource } from './runtime/host-locale.js';
19
20
  import { createUserPanelStores } from './runtime/user-panels.js';
20
21
  import { SourceAutoUpdater } from './runtime/source-auto-update.js';
21
22
  import { UserPanelSkillProvider } from './runtime/user-panels.js';
22
23
  import { UserCommandMountRegistry } from './runtime/user-commands.js';
24
+ import { presetSourceRef } from './model/preset-source.js';
23
25
  export const name = 'dsh-agent-plugins-market';
24
26
  export const inject = ['skills', 'commands'];
25
27
  export async function apply(ctx, config = {}) {
@@ -34,10 +36,20 @@ export async function apply(ctx, config = {}) {
34
36
  // Host runtime copy resolves from the harness `locale.preference` setting;
35
37
  // the async settings read lands before the first session starts in practice.
36
38
  const hostLocale = { t: bindHostLocale(undefined) };
37
- void loadHostLocale().then(locale => {
38
- hostLocale.t = locale.t;
39
- providerControl?.invalidate();
40
- userPanelControl?.invalidate();
39
+ const refreshHostLocale = () => {
40
+ void loadHostLocale().then(locale => {
41
+ hostLocale.t = locale.t;
42
+ providerControl?.invalidate();
43
+ userPanelControl?.invalidate();
44
+ });
45
+ };
46
+ void refreshHostLocale();
47
+ // The locale preference the GUI writes is owned by the host settings service:
48
+ // read it there rather than re-parsing the document, and re-bind once the
49
+ // service lands (the locale namespace may register after this plugin).
50
+ ctx.inject(['settings'], settingsCtx => {
51
+ setHostLocaleSource(() => readHostLocalePreference(settingsCtx));
52
+ refreshHostLocale();
41
53
  });
42
54
  const runtime = new RuntimeReconciler(ctx, dataRoot, key => hostLocale.t(key));
43
55
  // User panel stores (skills / commands / agent personas) and their runtime
@@ -110,9 +122,10 @@ export async function apply(ctx, config = {}) {
110
122
  // port below is read at call time: this plugin's apply may run before the
111
123
  // credentials, tools and settings services provision, and a snapshot taken
112
124
  // here would be permanently undefined even after the service is live.
113
- let toolsRegistry;
125
+ // Read at call time: the tools service provisions after apply() returns.
126
+ let toolsRegistry = toolsServiceOf(ctx);
114
127
  const ports = {
115
- mcpToolSnapshot: () => (toolsRegistry === undefined ? [] : inspectToolRegistry(toolsRegistry)),
128
+ mcpToolSnapshot: () => inspectToolRegistry(toolsRegistry),
116
129
  credentialsStore: {
117
130
  deleteGrantRecord: async (serverName) => {
118
131
  const store = ctx.get?.('credentials');
@@ -132,7 +145,10 @@ export async function apply(ctx, config = {}) {
132
145
  };
133
146
  const catalog = new Catalog({ userRoot, dataRoot, agentsRoot, onChanged, ports, ...(config.git === undefined ? {} : { git: config.git }) });
134
147
  await catalog.load();
135
- await catalog.mergeSources(config.sources ?? []);
148
+ // Configured seeds first, then the record this plugin presets: the market
149
+ // lists the first-party collection on the first open, and the ordinary
150
+ // refresh path clones it. Registration performs no network access.
151
+ await catalog.mergeSources([...(config.sources ?? []), presetSourceRef()]);
136
152
  const resources = createPanelResources(catalog, panels);
137
153
  runtime.setMcpOverridesProvider(async () => catalog.allMcpOverrides(await catalog.enabledUserSuites()));
138
154
  runtime.lsp.setDirectProvider(async () => (await loadLspServers(agentsRoot)).servers);
@@ -155,9 +171,12 @@ export async function apply(ctx, config = {}) {
155
171
  await scheduler.request();
156
172
  })
157
173
  .catch(() => { });
174
+ // The shell seam resolves lazily: the service may land after this plugin, and
175
+ // a profile without one keeps dynamic-context placeholders literal.
176
+ const shellSeam = () => shellSeamOf(ctx);
158
177
  ctx.skills.registerProvider(control => {
159
178
  providerControl = control;
160
- return new SuiteSkillProvider(catalog);
179
+ return new SuiteSkillProvider(catalog, { dataRoot, shell: shellSeam });
161
180
  });
162
181
  // User panel skills ride a second provider so a panel
163
182
  // edit invalidates only its own catalog contribution.
@@ -169,14 +188,14 @@ export async function apply(ctx, config = {}) {
169
188
  hostCtx.effect(() => mountAgentRoleTool(hostCtx, async (parent) => [
170
189
  ...(await resources.agents.list(true)).map(entry => ({ ...entry, title: entry.name, name: entry.id ?? entry.name })),
171
190
  ...(await projectAgentRoles(catalog, parent))
172
- ], (key, params) => hostLocale.t(key, params)), 'dsh-agent-plugins-market: agent role routing');
191
+ ]), 'dsh-agent-plugins-market: agent role routing');
173
192
  });
174
193
  ctx.inject(['agents'], hostCtx => {
175
194
  hostCtx.effect(() => {
176
- const mounted = mountProjectCommands(hostCtx, catalog, key => hostLocale.t(key));
195
+ const mounted = mountProjectCommands(hostCtx, catalog, key => hostLocale.t(key), dataRoot);
177
196
  const mcp = mountProjectMcp(hostCtx, catalog, dataRoot);
178
197
  const hooks = mountProjectHooks(hostCtx, catalog);
179
- const prompts = mountSuiteInstructions(hostCtx, catalog);
198
+ const prompts = mountSuiteInstructions(hostCtx, catalog, dataRoot);
180
199
  projectCommands = mounted;
181
200
  projectMcp = mcp;
182
201
  projectHooks = hooks;
@@ -12,9 +12,29 @@ export const PLUGIN_LAYOUTS = [
12
12
  ];
13
13
  /** Kimi Code's primary spelling precedes its compatibility spelling within that dialect. */
14
14
  export const MANIFEST_ALIASES = { kimi: ['kimi.plugin.json'] };
15
+ /**
16
+ * This client's Agent Plugins extension namespace (§8): manifest data rides
17
+ * `plugin.json` `extensions` under this key, and file-based extensions live
18
+ * in the top-level directory named exactly this. The namespace contract
19
+ * version is declared inside the manifest data.
20
+ */
21
+ export const EXTENSION_NAMESPACE = 'com.deepseek.harness';
22
+ /** Namespace contract versions this manager implements. */
23
+ export const NAMESPACE_SCHEMA_VERSIONS = new Set(['1.0.0']);
24
+ /** Namespace directory locations, relative to the plugin root. */
25
+ export const NAMESPACE_DIR = EXTENSION_NAMESPACE;
26
+ export const NAMESPACE_HOOKS_FILE = joinNamespace(NAMESPACE_DIR, 'hooks', 'hooks.json');
27
+ export const NAMESPACE_LSP_FILE = joinNamespace(NAMESPACE_DIR, 'lsp.json');
28
+ function joinNamespace(...parts) {
29
+ return parts.join('/');
30
+ }
15
31
  /** Equivalent plugin-owned path variables; these never resolve as credentials. */
16
32
  export const PLUGIN_ROOT_VARIABLES = new Set(['PLUGIN_ROOT', 'CLAUDE_PLUGIN_ROOT', 'CODEX_PLUGIN_ROOT', 'ZCODE_PLUGIN_ROOT', 'QODER_PLUGIN_ROOT']);
17
33
  export const PLUGIN_DATA_VARIABLES = new Set(['PLUGIN_DATA', 'CLAUDE_PLUGIN_DATA', 'ZCODE_PLUGIN_DATA', 'QODER_PLUGIN_DATA']);
34
+ /** Equivalent session project-directory variables; only a calling session supplies the directory. */
35
+ export const PROJECT_DIR_VARIABLES = new Set(['CLAUDE_PROJECT_DIR', 'ZCODE_PROJECT_DIR', 'QODER_PROJECT_DIR']);
36
+ /** Claude Code names the directory holding the skill's own file; no dialect alias exists. */
37
+ export const SKILL_DIR_VARIABLE = 'CLAUDE_SKILL_DIR';
18
38
  export const PROJECT_LAYOUTS = [
19
39
  {
20
40
  dirName: '.claude',
@@ -23,7 +43,12 @@ export const PROJECT_LAYOUTS = [
23
43
  mcpFiles: ['.mcp.json'],
24
44
  hookFiles: ['.claude/settings.json', '.claude/settings.local.json']
25
45
  },
26
- { dirName: '.agents', label: 'agents', subdirs: ['skills', 'agents', 'commands'] },
46
+ {
47
+ dirName: '.agents',
48
+ label: 'agents',
49
+ subdirs: ['skills', 'agents', 'commands'],
50
+ hookFiles: ['.agents/hooks/hooks.json', '.agents/hooks.json']
51
+ },
27
52
  { dirName: '.codex', label: 'Codex', subdirs: ['skills'], mcpFiles: ['.codex/config.toml'], mcpFormat: 'codex' },
28
53
  { dirName: '.cursor', label: 'Cursor', subdirs: ['skills', 'agents', 'commands'], mcpFiles: ['.cursor/mcp.json'] },
29
54
  { dirName: '.kimi', label: 'Kimi', subdirs: ['skills'] },
@@ -47,3 +72,21 @@ export const PROJECT_LAYOUTS = [
47
72
  ];
48
73
  /** Marketplace catalogs follow manifest layout order; the shared root catalog is the fallback. */
49
74
  export const MARKETPLACE_PATHS = [...PLUGIN_LAYOUTS.flatMap(layout => layout.marketplaces), 'marketplace.json'];
75
+ /**
76
+ * Inline top-level manifest keys this manager reads for the portable v1
77
+ * dialect. The specification's closed manifest schema carries none of them
78
+ * (§5.2, §6.1), so under `agent-plugin-v1` they are reported as unknown
79
+ * top-level fields and ignored; every other dialect keeps reading them.
80
+ */
81
+ export const V1_IGNORED_MANIFEST_KEYS = [
82
+ 'skills',
83
+ 'commands',
84
+ 'agents',
85
+ 'hooks',
86
+ 'mcpServers',
87
+ 'lspServers',
88
+ 'skillInstructions',
89
+ 'systemPrompt',
90
+ 'systemPromptPath',
91
+ 'sessionStart'
92
+ ];
@@ -0,0 +1,8 @@
1
+ /** Source id the preset record registers under. */
2
+ export const PRESET_SOURCE_ID = 'dsh-agent-plugins';
3
+ /** Repository the preset record points at. */
4
+ export const PRESET_SOURCE_URL = 'https://github.com/Sivan757/dsh-agent-plugins.git';
5
+ /** The preset registration, as the market stores it. */
6
+ export function presetSourceRef() {
7
+ return { id: PRESET_SOURCE_ID, url: PRESET_SOURCE_URL, kind: 'git' };
8
+ }
package/lib/routes.js CHANGED
@@ -39,7 +39,14 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
39
39
  sendJson(response, 200, { ok: true, ...value });
40
40
  }
41
41
  catch (error) {
42
- sendJson(response, 400, { ok: false, error: error instanceof Error ? error.message : String(error) });
42
+ // A rejection that names its fields travels with them, so the editor
43
+ // can place each reason beside the input it belongs to.
44
+ const fields = error.fields;
45
+ sendJson(response, 400, {
46
+ ok: false,
47
+ error: error instanceof Error ? error.message : String(error),
48
+ ...(Array.isArray(fields) ? { fields } : {})
49
+ });
43
50
  }
44
51
  })();
45
52
  }
@@ -77,7 +84,7 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
77
84
  throw new Error('invalid service kind');
78
85
  if (typeof body.id !== 'string' || body.id === '')
79
86
  throw new Error('missing service id');
80
- await manager.saveServerConfig(body.kind, body.id, body.config);
87
+ await manager.saveServerConfig(body.kind, body.id, body.config, body.policy);
81
88
  return {};
82
89
  });
83
90
  post(MARKET_ROUTES.addMcpServer, async (body) => {
@@ -86,6 +93,13 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
86
93
  await manager.addMcpServer(body.name, body.config);
87
94
  return {};
88
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
+ });
89
103
  get(MARKET_ROUTES.lspStatus, async (_request, response) => {
90
104
  sendJson(response, 200, await manager.lspStatus());
91
105
  });
@@ -257,6 +271,33 @@ export function mountSuiteRoutes(hostCtx, manager, panels) {
257
271
  await manager.setMcpOverride(sourceId, suiteId, serverKey, sanitized ?? null);
258
272
  return {};
259
273
  });
274
+ // Enable or disable one declared MCP server. Addressed by the same
275
+ // source-qualified suite id the status rows carry, so the card can toggle a
276
+ // server without holding the source and suite ids apart.
277
+ post(MARKET_ROUTES.setMcpServerEnabled, async (body) => {
278
+ const suiteKey = textField(body['suiteId'] ?? '', 'MCP suite id');
279
+ const serverKey = textField(body['serverKey'] ?? '', 'MCP server key');
280
+ const enabled = body['enabled'];
281
+ if (typeof enabled !== 'boolean')
282
+ throw new Error('missing boolean enabled');
283
+ await manager.setMcpServerEnabled(suiteKey, serverKey, enabled);
284
+ return {};
285
+ });
286
+ // Allow or deny one tool of a declared MCP server. Addressed by the same
287
+ // source-qualified suite id the status rows carry; the denial lands in the
288
+ // override record, so the suite's own mcp.json stays source-owned.
289
+ post(MARKET_ROUTES.setMcpServerTool, async (body) => {
290
+ const suiteKey = textField(body['suiteId'] ?? '', 'MCP suite id');
291
+ const serverKey = textField(body['serverKey'] ?? '', 'MCP server key');
292
+ const tool = textField(body['tool'] ?? '', 'MCP tool name');
293
+ const enabled = body['enabled'];
294
+ if (tool === '')
295
+ throw new Error('missing MCP tool name');
296
+ if (typeof enabled !== 'boolean')
297
+ throw new Error('missing boolean enabled');
298
+ await manager.setMcpServerToolEnabled(suiteKey, serverKey, tool, enabled);
299
+ return {};
300
+ });
260
301
  // Manual MCP reconcile: retries failed mounts and clears residual tools
261
302
  // without touching any catalog state.
262
303
  post(MARKET_ROUTES.mcpRetry, async () => {
@@ -3,9 +3,9 @@ import { readFile } from 'node:fs/promises';
3
3
  import { defineTool } from '@deepseek-ai/dsh-tools';
4
4
  import { parse as parseYaml } from 'yaml';
5
5
  import { mountSubagentCatalog } from './subagent-catalog.js';
6
- import { bindHostLocale } from './host-locale.js';
7
6
  import { namedAgentRoles } from './agent-role-names.js';
8
- export const AGENT_ROLE_TOOL_NAME = 'subagent_run';
7
+ import { expandPluginPaths } from '../catalog/plugin-variables.js';
8
+ export const AGENT_ROLE_TOOL_NAME = 'subagent_role';
9
9
  /**
10
10
  * Absolute delegation-depth cap applied to every role child. The host default is
11
11
  * the same value; keeping it explicit preserves the shipped recursion budget.
@@ -62,13 +62,17 @@ function parentRouteOf(parent) {
62
62
  return agent?.session?.requestHeader?.()?.config ?? agent?.options ?? {};
63
63
  }
64
64
  /**
65
- * Resolve the child LLM options one card may contribute.
65
+ * Resolve the child LLM options for one role call.
66
66
  *
67
- * A card route is advisory. Only an exact `provider` plus `model` pair is
68
- * applied, and only when the live LLM runtime accepts it; every other
69
- * declaration — a bare id, a Claude alias, a lone `provider`, an unsupported
70
- * effort — degrades to inheriting the parent route and reports a diagnostic.
71
- * @param policy - the card's parsed routing metadata.
67
+ * The call's own `provider` + `model` pair wins, and the role card supplies the
68
+ * default when the call names neither. Only an exact pair is applied, and only
69
+ * when the live LLM runtime accepts it; a bare id, a Claude alias, a lone
70
+ * `provider` or an unsupported effort degrades to inheriting the parent route
71
+ * and reports a diagnostic. An explicit route with no explicit effort uses the
72
+ * selected model's default, so a card's effort never leaks onto a route it was
73
+ * not written for.
74
+ * @param policy - the card's parsed routing metadata, used as the default route.
75
+ * @param requested - the route the call itself asked for.
72
76
  * @param parent - the calling Agent whose route an unusable declaration falls back to.
73
77
  * @param llm - live LLM runtime owning provider, model and effort validation.
74
78
  * @param signal - tool-call cancellation signal.
@@ -76,27 +80,34 @@ function parentRouteOf(parent) {
76
80
  * @param diagnose - diagnostic sink; degradation is reported, never silent.
77
81
  * @returns the options to send, or undefined for pure inheritance.
78
82
  */
79
- export async function resolveAgentOptions(policy, parent, llm, signal, subject, diagnose) {
80
- // One value, so a half-specified route can never be forwarded downstream.
81
- const requested = policy.provider === undefined || policy.model === undefined ? undefined : { provider: policy.provider, model: policy.model };
82
- if (requested === undefined && (policy.provider !== undefined || policy.model !== undefined)) {
83
+ export async function resolveAgentOptions(policy, requested, parent, llm, signal, subject, diagnose) {
84
+ const cardRoute = policy.provider !== undefined && policy.model !== undefined ? { provider: policy.provider, model: policy.model } : undefined;
85
+ if (cardRoute === undefined && (policy.provider !== undefined || policy.model !== undefined)) {
83
86
  const declared = [policy.provider, policy.model].filter(value => value !== undefined).join('/');
84
87
  diagnose(`agent "${subject}": route "${declared}" needs an exact provider and model pair; it is ignored and the child inherits the parent route`);
85
88
  }
86
- const effort = policy.reasoningEffort;
87
- if (effort === undefined && requested === undefined)
89
+ const callRoute = requested.provider !== undefined && requested.model !== undefined ? { provider: requested.provider, model: requested.model } : undefined;
90
+ if (callRoute === undefined && (requested.provider !== undefined || requested.model !== undefined)) {
91
+ const declared = [requested.provider, requested.model].filter(value => value !== undefined).join('/');
92
+ diagnose(`agent "${subject}": the call's route "${declared}" needs an exact provider and model pair; it is ignored`);
93
+ }
94
+ // One value, so a half-specified route can never be forwarded downstream.
95
+ const route = callRoute ?? cardRoute;
96
+ const routeEffort = callRoute === undefined ? policy.reasoningEffort : undefined;
97
+ const effort = requested.reasoningEffort ?? routeEffort;
98
+ if (effort === undefined && route === undefined)
88
99
  return undefined;
89
100
  const inherited = parentRouteOf(parent);
90
- const provider = requested?.provider ?? inherited.provider;
91
- const model = requested?.model ?? inherited.model;
101
+ const provider = route?.provider ?? inherited.provider;
102
+ const model = route?.model ?? inherited.model;
92
103
  if (provider === undefined || model === undefined) {
93
104
  /* v8 ignore next 3 -- an effective route is always known once a parent Agent ran a request. */
94
105
  if (effort !== undefined)
95
106
  diagnose(`agent "${subject}": reasoning effort "${effort}" has no effective route to validate against; it is ignored`);
96
107
  return undefined;
97
108
  }
98
- const routeChanged = provider !== inherited.provider || model !== inherited.model;
99
- const effectiveEffort = effort ?? (routeChanged ? undefined : inherited.reasoningEffort);
109
+ // An explicit route choice starts from the selected model's default effort.
110
+ const effectiveEffort = effort ?? (route === undefined ? inherited.reasoningEffort : undefined);
100
111
  try {
101
112
  await llm.resolveCallConfig({ provider, model, ...(effectiveEffort === undefined ? {} : { reasoningEffort: effectiveEffort }) }, signal);
102
113
  }
@@ -108,12 +119,12 @@ export async function resolveAgentOptions(policy, parent, llm, signal, subject,
108
119
  // Cancellation raised during the adapter lookup must not create a child.
109
120
  signal.throwIfAborted();
110
121
  return {
111
- ...(requested ?? {}),
122
+ ...(route ?? {}),
112
123
  ...(effort === undefined ? {} : { reasoningEffort: effort })
113
124
  };
114
125
  }
115
126
  /** Stable summaries; invalid declarations are excluded and transient read failures abort publication. */
116
- export async function agentRoleCatalog(entries, signal, diagnose = () => { }) {
127
+ export async function agentRoleCatalog(entries, signal, diagnose = () => { }, projectDir) {
117
128
  const summaries = [];
118
129
  const counts = new Map();
119
130
  for (const entry of entries)
@@ -137,7 +148,7 @@ export async function agentRoleCatalog(entries, signal, diagnose = () => { }) {
137
148
  }
138
149
  let policy;
139
150
  try {
140
- policy = parseAgentRole(text);
151
+ policy = expandPolicyPaths(parseAgentRole(text), entry, projectDir);
141
152
  }
142
153
  catch (error) {
143
154
  diagnose(`${entry.path}: ${String(error)}`);
@@ -160,74 +171,210 @@ export async function agentRoleCatalog(entries, signal, diagnose = () => { }) {
160
171
  signal.throwIfAborted();
161
172
  return summaries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
162
173
  }
174
+ /** A suite card carries the same path variables as every other surface its author wrote for. */
175
+ function expandPolicyPaths(policy, entry, projectDir) {
176
+ if (entry.suiteRoot === undefined && entry.suiteData === undefined && projectDir === undefined)
177
+ return policy;
178
+ const context = {
179
+ ...(entry.suiteRoot === undefined ? {} : { root: entry.suiteRoot }),
180
+ ...(entry.suiteData === undefined ? {} : { data: entry.suiteData }),
181
+ ...(projectDir === undefined ? {} : { projectDir })
182
+ };
183
+ return {
184
+ ...policy,
185
+ content: expandPluginPaths(policy.content, context),
186
+ ...(policy.title === undefined ? {} : { title: expandPluginPaths(policy.title, context) }),
187
+ ...(policy.description === undefined ? {} : { description: expandPluginPaths(policy.description, context) })
188
+ };
189
+ }
163
190
  /** Read the same declaration for catalog summaries and execution, including inline resources. */
164
- export async function readAgentRole(entry) {
165
- return parseAgentRole(entry.rawText ?? (await readFile(entry.path, 'utf8')));
191
+ export async function readAgentRole(entry, projectDir) {
192
+ return expandPolicyPaths(parseAgentRole(entry.rawText ?? (await readFile(entry.path, 'utf8'))), entry, projectDir);
193
+ }
194
+ /** The calling session's directory, which is what a card's `${CLAUDE_PROJECT_DIR}` names. */
195
+ function sessionCwd(agent) {
196
+ const cwd = agent?.session?.header?.cwd;
197
+ return typeof cwd === 'string' && cwd !== '' ? cwd : undefined;
166
198
  }
167
199
  /**
168
- * Start one role child and return its durable id without waiting for the result.
169
- * Resume state, steering, and the settlement notice belong to the host's
170
- * continuation manager; this executor owns only role resolution.
200
+ * Run one role child through the requested channel.
201
+ * @param host - the exact live seams this executor owns: tools, llm and subagents.
202
+ * @param mode - which channel the call asked for.
203
+ * @param jobs - the host jobs registry; only the `background` channel needs it.
171
204
  */
172
- export async function executeAgentRole(host, listRoles, agentName, prompt, parent, signal, diagnose = () => { }) {
205
+ export async function executeAgentRole(host, listRoles, agentName, prompt, parent, requestedRoute, mode, signal, diagnose = () => { }, jobs) {
173
206
  if (parent === undefined)
174
- throw new Error('subagent_run requires a calling agent');
207
+ throw new Error(`${AGENT_ROLE_TOOL_NAME} requires a calling agent`);
175
208
  if (prompt.trim() === '')
176
- throw new Error('subagent_run requires a non-empty prompt');
209
+ throw new Error(`${AGENT_ROLE_TOOL_NAME} requires a non-empty prompt`);
177
210
  signal.throwIfAborted();
178
211
  const [entry, ...duplicates] = namedAgentRoles(await listRoles(parent)).filter(candidate => candidate.callName === agentName);
179
212
  if (entry === undefined || duplicates.length > 0)
180
213
  throw new Error(`agent "${agentName}" is unavailable or ambiguous`);
181
214
  if (entry.disabled)
182
215
  throw new Error(`agent "${agentName}" is disabled`);
183
- const policy = await readAgentRole(entry);
216
+ const policy = await readAgentRole(entry, sessionCwd(parent));
184
217
  if (policy.disabled)
185
218
  throw new Error(`agent "${agentName}" is disabled`);
186
- const agentOptions = await resolveAgentOptions(policy, parent, host.llm, signal, agentName, diagnose);
219
+ const agentOptions = await resolveAgentOptions(policy, requestedRoute, parent, host.llm, signal, agentName, diagnose);
187
220
  signal.throwIfAborted();
188
- const started = await host.subagents.startContinuable({
189
- provider: 'spawn',
190
- label: agentName,
191
- request: {
192
- prompt: [{ type: 'text', text: prompt }],
193
- parent,
194
- maxDepth: MAX_AGENT_ROLE_DEPTH,
195
- persona: policy.content,
196
- ...(agentOptions === undefined ? {} : { agentOptions })
197
- },
198
- signal
199
- });
200
- return { subagentId: started.childId };
221
+ const request = {
222
+ prompt: [{ type: 'text', text: prompt }],
223
+ parent,
224
+ maxDepth: MAX_AGENT_ROLE_DEPTH,
225
+ persona: policy.content,
226
+ ...(agentOptions === undefined ? {} : { agentOptions })
227
+ };
228
+ if (mode === 'continuable') {
229
+ const started = await host.subagents.startContinuable({ provider: 'spawn', label: agentName, request, signal });
230
+ return { kind: 'continuable', subagentId: started.childId };
231
+ }
232
+ if (mode === 'background') {
233
+ if (jobs === undefined) {
234
+ throw new Error(`background role jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs, or call ${AGENT_ROLE_TOOL_NAME} with run_in_background false`);
235
+ }
236
+ // The run's own controller outlives `jobs.start`, which returns at
237
+ // registration: cancellation and disposal both go through the job's hooks.
238
+ return {
239
+ kind: 'background',
240
+ jobId: jobs.start({
241
+ kind: 'subagent',
242
+ label: agentName,
243
+ owner: parent,
244
+ run: () => {
245
+ const controller = new AbortController();
246
+ return {
247
+ cancel: (reason) => controller.abort(reason ?? 'background role job killed'),
248
+ done: (async () => {
249
+ const run = await host.subagents.start('spawn', { ...request, signal: controller.signal });
250
+ try {
251
+ const result = await run.result;
252
+ return result.stopReason === 'completed' ? { status: 'completed' } : { status: 'failed', detail: result.diagnostic ?? result.stopReason };
253
+ }
254
+ finally {
255
+ await run.dispose();
256
+ }
257
+ })()
258
+ };
259
+ }
260
+ })
261
+ };
262
+ }
263
+ const run = await host.subagents.start('spawn', { ...request, signal });
264
+ try {
265
+ const result = await run.result;
266
+ if (result.stopReason !== 'completed') {
267
+ const detail = result.diagnostic === undefined ? '' : `: ${result.diagnostic}`;
268
+ throw new Error(`agent "${agentName}" did not complete (${result.stopReason})${detail}`);
269
+ }
270
+ // The registry performs the authoritative lossless snapshot of the child's
271
+ // output blocks on the way out, exactly as the host's own delegation tool does.
272
+ return { kind: 'foreground', runId: run.id, output: result.output };
273
+ }
274
+ finally {
275
+ await run.dispose();
276
+ }
201
277
  }
202
- /** Mount once after tools, llm and subagents are available; the returned disposer belongs to the plugin lifecycle. */
203
- export function mountAgentRoleTool(ctx, listRoles, t = bindHostLocale(undefined)) {
278
+ /**
279
+ * Mount once after tools, llm and subagents are available; the returned disposer belongs to the plugin lifecycle.
280
+ *
281
+ * The delegation contract lives here rather than in the injected catalog: a tool
282
+ * description is present in every request, while the catalog message is
283
+ * re-published whenever roles change, so guidance kept out of the catalog is
284
+ * sent once and stays readable through compaction.
285
+ */
286
+ export function mountAgentRoleTool(ctx, listRoles) {
204
287
  const host = ctx;
205
288
  const tool = defineTool({
206
289
  name: AGENT_ROLE_TOOL_NAME,
207
- description: 'Delegate a self-contained task to a role from the current subagent catalog. The child starts without the parent conversation and runs ' +
208
- 'in the background: this returns a durable subagent id immediately. When the run settles the runtime sends you a notice carrying its ' +
209
- "outcome and closing message, and send_message steers the child while it runs. The role's saved persona and model settings are applied automatically.",
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 " +
295
+ "role matches, use the host's own `subagent` tools instead.",
210
296
  parameters: {
211
297
  agent: { type: 'string', required: true, description: 'Exact callable role name from the current subagent catalog.' },
212
- prompt: { type: 'string', required: true, description: 'Complete task and context for the isolated child.' }
298
+ prompt: {
299
+ type: 'string',
300
+ required: true,
301
+ description: "The complete, self-contained task for the role child. It does not share this conversation's context, so include everything it needs: what the task is and why, the relevant files and known findings, the output you expect, and the boundaries of the task."
302
+ },
303
+ provider: {
304
+ type: 'string',
305
+ description: "LLM provider route for the child. Supply together with model to override the route the role card declares; omit both to use the card's route or inherit the parent route."
306
+ },
307
+ model: {
308
+ type: 'string',
309
+ description: "Model id interpreted by provider. Supply together with provider to override the route the role card declares; omit both to use the card's route or inherit the parent route."
310
+ },
311
+ reasoning_effort: {
312
+ type: 'string',
313
+ description: "Adapter-owned reasoning effort for the effective child route. Omit to use the card's effort on the card's route, or the selected model's default after an explicit route change."
314
+ },
315
+ run_in_background: {
316
+ type: 'boolean',
317
+ description: "Which channel runs the child. Omit for the default: a continuable child whose durable subagent id comes back at once, with steering and a settlement notice. Set true for a tracked background job that returns a job id collected with `job_output` and stopped with `job_kill`. Set false for one foreground child whose report comes back as this call's result."
318
+ }
213
319
  },
214
320
  output: {
215
321
  schema: {
216
- type: 'object',
217
- additionalProperties: false,
218
- properties: { subagentId: { type: 'string', required: true } }
322
+ oneOf: [
323
+ {
324
+ type: 'object',
325
+ additionalProperties: false,
326
+ properties: {
327
+ kind: { type: 'string', required: true, const: 'continuable' },
328
+ subagentId: { type: 'string', required: true }
329
+ }
330
+ },
331
+ {
332
+ type: 'object',
333
+ additionalProperties: false,
334
+ properties: {
335
+ kind: { type: 'string', required: true, const: 'background' },
336
+ jobId: { type: 'string', required: true }
337
+ }
338
+ },
339
+ {
340
+ type: 'object',
341
+ additionalProperties: false,
342
+ properties: {
343
+ kind: { type: 'string', required: true, const: 'foreground' },
344
+ runId: { type: 'string', required: true },
345
+ output: { type: 'array', required: true, items: { type: 'json' } }
346
+ }
347
+ }
348
+ ]
219
349
  },
220
- render: (_args, value) => [{ type: 'text', text: `started subagent ${value.subagentId}` }]
350
+ render: (_args, value) => [
351
+ {
352
+ type: 'text',
353
+ text: value.kind === 'continuable'
354
+ ? `started subagent ${value.subagentId}`
355
+ : value.kind === 'background'
356
+ ? `started background subagent job ${value.jobId}`
357
+ : `subagent ${value.runId} finished`
358
+ }
359
+ ]
221
360
  },
222
361
  isConcurrencySafe: () => true,
223
362
  async execute(args, exec) {
224
- return executeAgentRole(host, listRoles, args.agent, args.prompt, exec.agent, exec.signal, message => ctx.logger?.warn(message));
363
+ // The parameter keeps the host tool's name and boolean shape while this
364
+ // tool owns three channels: omitting it keeps the durable child, `true`
365
+ // runs a tracked job, and `false` returns the report from one foreground run.
366
+ const mode = args.run_in_background === undefined ? 'continuable' : args.run_in_background ? 'background' : 'foreground';
367
+ return executeAgentRole(host, listRoles, args.agent, args.prompt, exec.agent, {
368
+ ...(args.provider === undefined ? {} : { provider: args.provider }),
369
+ ...(args.model === undefined ? {} : { model: args.model }),
370
+ ...(args.reasoning_effort === undefined ? {} : { reasoningEffort: args.reasoning_effort })
371
+ }, mode, exec.signal, message => ctx.logger?.warn(message), ctx.get('jobs'));
225
372
  }
226
373
  });
227
374
  const disposeTool = host.tools.register(tool);
228
375
  let disposeCatalog;
229
376
  try {
230
- disposeCatalog = mountSubagentCatalog(ctx, tool, async (agent, signal) => agentRoleCatalog(await listRoles(agent), signal, message => ctx.logger?.warn(message)), t);
377
+ disposeCatalog = mountSubagentCatalog(ctx, tool, async (agent, signal) => agentRoleCatalog(await listRoles(agent), signal, message => ctx.logger?.warn(message), sessionCwd(agent)));
231
378
  }
232
379
  catch (error) {
233
380
  disposeTool();