external-subagent 0.1.2 → 0.1.3

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.
package/README.md CHANGED
@@ -15,32 +15,43 @@ independent axes:
15
15
 
16
16
  ## Status
17
17
 
18
- Current work happens on the `audit/external-subagent-20260917` branch; the
19
- productization closeout below is the historical record of that stage. DSH and
20
- ZCode installed-artifact lifecycles, active-task upgrade, and standalone
21
- initialization are implemented and reviewed. `init` installs the standalone
22
- daemon service only — no host is bound implicitly; hosts are bound through the
23
- explicit `install-plugin` (Codex or ZCode) / `install-mcp` (Codex-only TOML
24
- binding) commands. Installing from a packed artifact and explicitly
25
- initializing to a launchd-resident service (idempotent repeat init/start) is
26
- live-verified on a real GUI session. Both productization
27
- candidates passed the four-cell consumer matrix — DSH/ZCode × public CLI /
28
- real Codex CLI via the managed plugin over MCP, each against its own tarball:
29
- the pre-fix candidate C1 (`0.1.1`, historical) and the post-installer-fix
30
- final candidate C2 (`0.1.2`), whose fresh consumer verification passed
31
- through the public install surface — all four cells COMPLETED with the
32
- plugin cache verified against the staged binding (`cache_verified: true`;
33
- see [docs/acceptance/productization.md](docs/acceptance/productization.md)).
34
- The managed plugin manifest is versioned per released candidate because its
35
- version is codex's plugin-cache identity, and codex resolves the reserved
36
- `personal` marketplace name machine-globally to the real user root
37
- (regardless of `CODEX_HOME`), so each candidate's consumer runs pin that
38
- identity's bytes there and any later candidate bumps again. The installer
39
- re-verified on 0.154.0; DSH admission currently supports `build` and strict
40
- `plan`; its model selection is explicit spawn model, configured default, then
41
- the native default. The package is published to the public npm registry as
18
+ The package is published to the public npm registry as
42
19
  `external-subagent@0.1.0` (macOS arm64 only; `os`/`cpu` in `package.json`
43
- reject installation on other platforms at npm install time).
20
+ reject installation on other platforms at npm install time). `init` installs
21
+ the standalone daemon service only — no host is bound implicitly; hosts are
22
+ bound through the explicit `install-plugin` (Codex or ZCode) /
23
+ `install-mcp` (Codex-only TOML binding) commands. Installing from a packed
24
+ artifact and explicitly initializing to a launchd-resident service
25
+ (idempotent repeat init/start) is live-verified on a real GUI session;
26
+ active-task upgrade and standalone initialization are implemented and
27
+ reviewed.
28
+
29
+ All three subagents are **disabled by default** and are enabled per name with
30
+ `external-subagent agents enable <zcode|dsh|codex>`: a successful local probe
31
+ writes the configuration (no write on failure), and `dsh`/`codex` need a
32
+ daemon restart to take effect. ZCode admits all four permission modes
33
+ (build/edit/plan/yolo) and rejects an explicit spawn `model`; DSH admits
34
+ `build` and strict `plan`, with model selection as explicit spawn model,
35
+ configured default, then the native default, and its `build` mode admits a
36
+ non-empty caller `write_manifest` through the guarded manifest-build
37
+ composition (workspace-write sandbox, `tool-fs`-only writes, out-of-manifest
38
+ paths rejected as `FS_WRITE_MANIFEST_DENIED`; an explicit `["."]` keeps the
39
+ legacy build composition and `plan` still requires an empty manifest); Codex
40
+ admits all four modes
41
+ (build/edit → workspace-write, plan → read-only, yolo → danger-full-access,
42
+ all pinning `approvalPolicy=never`) and rejects a non-empty `write_manifest`
43
+ with `codex_write_manifest_unsupported`. `observe` works on all three: ZCode
44
+ and DSH expose the public reasoning tail (at most 200 characters) and Codex
45
+ reports `reasoning: null`.
46
+
47
+ The daemon socket environment variable is now the neutral
48
+ `EXTERNAL_SUBAGENT_SOCKET`, with no fallback to the retired name. An existing
49
+ installation upgrading to this version must run the public
50
+ `external-subagent update` (or `reconcile`) once to rewrite the host
51
+ bindings, then restart the service; see
52
+ [docs/operations.md](docs/operations.md). The historical productization
53
+ acceptance matrix is recorded in
54
+ [docs/acceptance/productization.md](docs/acceptance/productization.md).
44
55
 
45
56
  ## Versioning
46
57
 
@@ -122,3 +133,10 @@ cargo test --workspace
122
133
  node --test tests/install/*.test.mjs tests/platform/*.test.mjs tests/cli/*.test.mjs tests/contract/*.test.mjs
123
134
  node --test tests/integration/*.test.mjs tools/probes/dsh-acp/probe.test.mjs
124
135
  ```
136
+
137
+ The JavaScript install/upgrade suites build the native facade through
138
+ `scripts/release/build-native-payload.mjs`; set
139
+ `EXTERNAL_SUBAGENT_CARGO_PROFILE=debug` to make that entry build from
140
+ `target/debug` (no `--release`) and record `profile: debug` in the staged
141
+ manifest. The default stays a release build for `prepack`/publishing, and
142
+ `npm run check` is the JavaScript syntax gate.
@@ -1,4 +1,7 @@
1
1
  import { readConfig } from '../config/read.mjs';
2
+ import { updateConfig } from '../config/write.mjs';
3
+ import { SUBAGENT_IDS } from '../config/schema.mjs';
4
+ import path from 'node:path';
2
5
  import { CliError } from '../errors.mjs';
3
6
 
4
7
  const INPUT_FIELDS = new Set(['operation', 'subagent', 'through', 'workspace', 'home']);
@@ -60,7 +63,11 @@ function parseModelsArgs(rest) {
60
63
  export function parseSubagentsArgs(args) {
61
64
  if (args.length === 0) return { operation: 'list' };
62
65
  const [operation, ...rest] = args;
63
- if (!['list', 'status', 'probe', 'models'].includes(operation)) throw new CliError('INVALID_ARGUMENT', `unsupported subagents operation: ${operation}`, 2);
66
+ if (!['list', 'status', 'probe', 'models', 'enable'].includes(operation)) throw new CliError('INVALID_ARGUMENT', `unsupported subagents operation: ${operation}`, 2);
67
+ if (operation === 'enable') {
68
+ if (rest.length !== 1 || !SUBAGENT_IDS.includes(rest[0])) throw new CliError('INVALID_ARGUMENT', 'usage: agents enable <zcode|dsh|codex>', 2);
69
+ return { operation, subagent: rest[0] };
70
+ }
64
71
  if (operation === 'probe') return parseProbeArgs(rest);
65
72
  if (operation === 'models') return parseModelsArgs(rest);
66
73
  if (operation === 'list' && rest.length !== 0) throw new CliError('INVALID_ARGUMENT', 'usage: subagents list', 2);
@@ -72,6 +79,44 @@ export async function subagentsCommand(paths, input = {}, options = {}) {
72
79
  validateInput(input);
73
80
  const operation = input.operation ?? 'list';
74
81
  const config = readConfig(paths.config);
82
+ if (operation === 'enable') {
83
+ const agent = input.subagent;
84
+ if (!SUBAGENT_IDS.includes(agent) || ['through', 'workspace', 'home'].some((key) => input[key] !== undefined)) {
85
+ throw new CliError('INVALID_ARGUMENT', 'usage: agents enable <zcode|dsh|codex>', 2);
86
+ }
87
+ if (typeof options.callDaemon !== 'function' || typeof options.socket !== 'string') throw new CliError('INTERNAL_ERROR', 'agents enable requires a daemon connection');
88
+ const entry = config.subagents[agent];
89
+ const scope = Object.fromEntries(['home', 'profile'].filter((key) => entry[key] != null).map((key) => [key, entry[key]]));
90
+ const result = await options.callDaemon(options.socket, 'agent-probe', { subagent: agent, through: 'local', scope });
91
+ const local = result.evidence?.local;
92
+ if (local?.state !== 'READY') throw new CliError('agent_probe_failed', `Cannot enable ${agent}: ${local?.reason ?? 'local runtime probe did not succeed'}`, 2);
93
+ const requiredVersion = result.status?.required_version;
94
+ if (agent === 'dsh' && (typeof requiredVersion !== 'string' || !requiredVersion.trim())) {
95
+ throw new CliError('agent_probe_failed', 'Cannot enable dsh: daemon did not report required_version; update and restart the daemon, then retry', 2);
96
+ }
97
+ if (requiredVersion != null && local.version !== requiredVersion) {
98
+ throw new CliError('agent_probe_failed', `Cannot enable ${agent}: expected version ${requiredVersion}, observed ${local.version ?? 'unknown'}; install the required runtime version and retry`, 2);
99
+ }
100
+ const patch = { enabled: true, spawn_supported: true };
101
+ if (agent !== 'zcode') {
102
+ if (typeof local.runtime_path !== 'string' || !path.isAbsolute(local.runtime_path) || typeof local.version !== 'string' || !local.version.trim()) {
103
+ throw new CliError('agent_probe_failed', 'Probe did not return an absolute runtime_path and observed version', 2);
104
+ }
105
+ const home = local.scope?.home;
106
+ if (typeof home !== 'string' || !path.isAbsolute(home)) throw new CliError('agent_probe_failed', `Cannot enable ${agent}: configure subagents.${agent}.home or export ${agent.toUpperCase()}_HOME before starting the daemon`, 2);
107
+ Object.assign(patch, { runtime_path: local.runtime_path, home, profile: agent === 'dsh' ? 'acp' : (entry.profile ?? null), version: local.version });
108
+ }
109
+ const updated = updateConfig(paths.config, (latest) => {
110
+ if (Object.entries(patch).every(([key, value]) => latest.subagents[agent][key] === value)) return latest;
111
+ return { ...latest, subagents: { ...latest.subagents, [agent]: { ...latest.subagents[agent], ...patch } } };
112
+ });
113
+ const restart = agent !== 'zcode';
114
+ return {
115
+ subagent: agent, enabled: true, config_revision: updated.revision,
116
+ config: updated, evidence: subagentView(result.evidence), restart_required: restart,
117
+ message: restart ? '配置已保存,重启 daemon 后生效。' : '配置已保存,新的 zcode 任务即时生效,无需重启。',
118
+ };
119
+ }
75
120
  if (operation === 'list') {
76
121
  return { default_subagent: config.default_subagent, config_revision: config.revision, subagents: Object.entries(config.subagents).map(([id, value]) => ({ subagent: id, ...value })) };
77
122
  }
@@ -99,7 +144,10 @@ export async function subagentsCommand(paths, input = {}, options = {}) {
99
144
  const entries = status.subagents ?? status.agents;
100
145
  if (!Array.isArray(entries)) throw new CliError('PROTOCOL_ERROR', 'daemon status did not include subagent status');
101
146
  const subagents = entries.filter((entry) => input.subagent === undefined || (entry.subagent ?? entry.agent) === input.subagent).map(subagentView);
102
- if (input.subagent !== undefined && subagents.length === 0) throw new CliError('subagent_unknown', `daemon did not report subagent: ${input.subagent}`, 2);
147
+ if (input.subagent !== undefined && subagents.length === 0) {
148
+ const roster = entries.map((entry) => entry.subagent ?? entry.agent).filter(Boolean).map((name) => `"${name}"`).join(', ');
149
+ throw new CliError('subagent_unknown', `daemon did not report subagent: ${input.subagent}, available subagents are [${roster}]`, 2);
150
+ }
103
151
  return { service_generation: status.service_generation ?? null, subagents };
104
152
  }
105
153
 
@@ -36,8 +36,8 @@ function unsetPatch(key) {
36
36
  if (key === 'default_subagent') return { default_subagent: null };
37
37
  const [, agent, field] = key.split('.');
38
38
  const defaults = {
39
- enabled: agent === 'zcode',
40
- spawn_supported: agent === 'zcode',
39
+ enabled: false,
40
+ spawn_supported: false,
41
41
  default_model: null, runtime_path: null, home: null, profile: null, version: null,
42
42
  };
43
43
  return { subagents: { [agent]: { [field]: defaults[field] } } };
@@ -1,4 +1,4 @@
1
- import { backupData, cleanupLegacy, purge, restoreData, uninstall as removeService } from '../maintenance.mjs';
1
+ import { backupData, purge, restoreData, uninstall as removeService } from '../maintenance.mjs';
2
2
  import { unregisterAllCodexHomes } from '../install/reconcile.mjs';
3
3
  import { uninstallZcodePlugin } from '../install/zcode.mjs';
4
4
 
@@ -24,4 +24,4 @@ export function uninstall(paths, options = {}) {
24
24
  return { ...removed, codex_homes_unregistered: released.unregistered, zcode_binding: zcode };
25
25
  }
26
26
 
27
- export { backupData, cleanupLegacy, purge, restoreData };
27
+ export { backupData, purge, restoreData };
@@ -40,7 +40,7 @@ export async function updateCommand(paths, args = [], daemon = {}) {
40
40
  const registryPath = paths.data ? path.join(paths.data, 'codex-homes.json') : null;
41
41
  const priorRegistryBytes = (() => { try { return fs.readFileSync(registryPath); } catch { return null; } })();
42
42
  const requestedVersion = args.find((arg) => arg.startsWith('--version='))?.slice(10) || 'current';
43
- const socket = daemon.socket || process.env.ZCODE_AGENTD_SOCKET || paths.socket;
43
+ const socket = daemon.socket || process.env.EXTERNAL_SUBAGENT_SOCKET || paths.socket;
44
44
  const rpc = daemon.callDaemon || callDaemon;
45
45
  const drainDeadline = Number.isFinite(daemon.drainTimeoutMs) && daemon.drainTimeoutMs > 0
46
46
  ? Date.now() + daemon.drainTimeoutMs : null;
@@ -21,7 +21,7 @@ export function defaultConfig() {
21
21
  revision: 0,
22
22
  default_subagent: null,
23
23
  subagents: {
24
- zcode: { enabled: true, spawn_supported: true, default_model: null },
24
+ zcode: { enabled: false, spawn_supported: false, default_model: null },
25
25
  dsh: { enabled: false, spawn_supported: false, default_model: null, runtime_path: null, home: null, profile: null, version: null },
26
26
  codex: { enabled: false, spawn_supported: false, default_model: null, runtime_path: null, home: null, profile: null, version: null },
27
27
  },
@@ -31,7 +31,7 @@ export function updateConfig(file, updater) {
31
31
  try {
32
32
  const current = readConfig(file);
33
33
  const next = updater(current);
34
- return writeConfig(file, next);
34
+ return next === current ? current : writeConfig(file, next);
35
35
  } finally {
36
36
  try { process.kill(-helper.pid, 'SIGTERM'); } catch {}
37
37
  try { fs.unlinkSync(ready); } catch {}
package/cli/constants.mjs CHANGED
@@ -8,7 +8,7 @@ const IS_DEBUG_VARIANT = process.env.EXTERNAL_SUBAGENT_VARIANT === 'debug';
8
8
 
9
9
  export const PRODUCT_NAME = IS_DEBUG_VARIANT ? 'external-subagent-debug' : 'external-subagent';
10
10
  export const PRODUCT_ID = IS_DEBUG_VARIANT ? 'external_subagent_debug' : 'external_subagent';
11
- export const VERSION = '0.1.2';
11
+ export const VERSION = '0.1.3';
12
12
  export const LAUNCH_AGENT_LABEL = IS_DEBUG_VARIANT ? 'com.external-subagent-debug.daemon' : 'com.external-subagent.daemon';
13
13
  export const DAEMON_BIN_NAME = IS_DEBUG_VARIANT ? 'external-subagent-debugd' : 'external-subagentd';
14
14
  export const MCP_BIN_NAME = IS_DEBUG_VARIANT ? 'external-subagent-debug-mcp' : 'external-subagent-mcp';
@@ -18,7 +18,7 @@ export const ZCODE_RUNTIME = '/Applications/ZCode.app/Contents/Resources/glm/zco
18
18
 
19
19
  export const BUSINESS_COMMANDS = new Set([
20
20
  'init', 'hooks', 'status', 'diagnose', 'backup', 'restore', 'start', 'stop',
21
- 'uninstall', 'purge', 'cleanup-legacy', 'install-plugin', 'install-mcp', 'create', 'spawn', 'wait', 'list', 'send',
21
+ 'uninstall', 'purge', 'install-plugin', 'install-mcp', 'create', 'spawn', 'wait', 'list', 'send',
22
22
  'respond', 'cancel', 'result', 'close', 'config', 'agents', 'subagents',
23
23
  'observe', 'update', 'reconcile',
24
24
  ]);
@@ -97,10 +97,10 @@ function updateMarketplace(file, staging) {
97
97
  function stagedManagedBinding(staging) {
98
98
  const manifest = JSON.parse(fs.readFileSync(path.join(staging, '.codex-plugin', 'plugin.json'), 'utf8'));
99
99
  const server = JSON.parse(fs.readFileSync(path.join(staging, '.mcp.json'), 'utf8')).mcpServers?.external_subagent;
100
- if (!server || typeof server.command !== 'string' || typeof server.env?.ZCODE_AGENTD_SOCKET !== 'string') {
100
+ if (!server || typeof server.command !== 'string' || typeof server.env?.EXTERNAL_SUBAGENT_SOCKET !== 'string') {
101
101
  throw new CliError('INVALID_PLUGIN_SOURCE', 'staged plugin MCP binding is incomplete');
102
102
  }
103
- return { manifest, command: server.command, socket: server.env.ZCODE_AGENTD_SOCKET };
103
+ return { manifest, command: server.command, socket: server.env.EXTERNAL_SUBAGENT_SOCKET };
104
104
  }
105
105
 
106
106
  function readCacheJson(file, cache, identity) {
@@ -152,7 +152,7 @@ function verifyCodexCache(add, { codexHome, staging, marketplaceName }) {
152
152
  }
153
153
  }
154
154
  const server = readCacheJson(path.join(cache, '.mcp.json'), cache, identity).mcpServers?.external_subagent;
155
- const socket = server?.env?.ZCODE_AGENTD_SOCKET;
155
+ const socket = server?.env?.EXTERNAL_SUBAGENT_SOCKET;
156
156
  if (!server || server.command !== expected.command || socket !== expected.socket) {
157
157
  throw new CliError('CODEX_CACHE_BINDING_MISMATCH', `codex plugin cache for ${identity} carries a different managed binding (command=${server?.command}, socket=${socket}); expected this install's staged binding (command=${expected.command}, socket=${expected.socket}). The cache resolved to another installation's bytes (${cache}); either the machine-global content store reused them for the same identity (release a distinct plugin version instead of accepting them), or the reserved marketplace name personal resolved machine-globally to the real user root regardless of CODEX_HOME (refresh or re-register that root, or use a non-reserved marketplace name in isolated homes)`);
158
158
  }
@@ -309,7 +309,7 @@ export function uninstallPlugin(paths, options = {}) {
309
309
  function codexMcpConfig(paths) {
310
310
  const command = nativeBinary(MCP_BIN_NAME);
311
311
  const tools = CODEX_TOOLS.map((tool) => ` "${tool}",`).join('\n');
312
- return `[${CODEX_MCP_SECTION}]\ncommand = ${JSON.stringify(command)}\nenabled = true\nrequired = true\nstartup_timeout_sec = 10\ntool_timeout_sec = 304\nenabled_tools = [\n${tools}\n]\ndefault_tools_approval_mode = "prompt"\n\n[${CODEX_MCP_SECTION}.env]\nZCODE_AGENTD_SOCKET = ${JSON.stringify(paths.socket)}\n\n[${CODEX_MCP_SECTION}.tools.external_subagent_status]\napproval_mode = "auto"\n\n[${CODEX_MCP_SECTION}.tools.external_subagent_list]\napproval_mode = "auto"\n\n[${CODEX_MCP_SECTION}.tools.external_subagent_wait]\napproval_mode = "auto"\n\n[${CODEX_MCP_SECTION}.tools.external_subagent_result]\napproval_mode = "auto"\n`;
312
+ return `[${CODEX_MCP_SECTION}]\ncommand = ${JSON.stringify(command)}\nenabled = true\nrequired = true\nstartup_timeout_sec = 10\ntool_timeout_sec = 304\nenabled_tools = [\n${tools}\n]\ndefault_tools_approval_mode = "prompt"\n\n[${CODEX_MCP_SECTION}.env]\nEXTERNAL_SUBAGENT_SOCKET = ${JSON.stringify(paths.socket)}\n\n[${CODEX_MCP_SECTION}.tools.external_subagent_status]\napproval_mode = "auto"\n\n[${CODEX_MCP_SECTION}.tools.external_subagent_list]\napproval_mode = "auto"\n\n[${CODEX_MCP_SECTION}.tools.external_subagent_wait]\napproval_mode = "auto"\n\n[${CODEX_MCP_SECTION}.tools.external_subagent_result]\napproval_mode = "auto"\n`;
313
313
  }
314
314
 
315
315
  function removeTomlSection(text, section) {
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { spawnSync } from 'node:child_process';
4
- import { LAUNCH_AGENT_LABEL, ZCODE_RUNTIME } from '../constants.mjs';
4
+ import { LAUNCH_AGENT_LABEL } from '../constants.mjs';
5
5
  import { CliError } from '../errors.mjs';
6
6
  import { parseConfig } from '../config/read.mjs';
7
7
  import { writeConfig } from '../config/write.mjs';
@@ -10,7 +10,7 @@ import { pathReport } from './path.mjs';
10
10
  import { packageRoot, payloadManifestPath } from './layout.mjs';
11
11
  import { updateInstallation } from './update.mjs';
12
12
  import { loadInstallState, markInstallStep, removeCreatedDirectories, rollbackFiles, snapshotFile } from './recovery.mjs';
13
- import { bootstrapService, bootoutService, installLaunchAgent } from './service-macos.mjs';
13
+ import { bootstrapService, bootoutService, installLaunchAgent, runtimeObservations } from './service-macos.mjs';
14
14
 
15
15
  // Standalone-install coordination (S05, AUD-005 decision D1). A plain npm
16
16
  // install only stages the package and payload; an explicit `init` installs
@@ -197,7 +197,7 @@ export function runInit(options = {}) {
197
197
  // Honest runtime report instead of a hard dependency: the pinned ZCode
198
198
  // runtime is an adapter capability, observed and forwarded by the service
199
199
  // template only when present.
200
- runtime: { path: ZCODE_RUNTIME, present: fs.existsSync(ZCODE_RUNTIME) },
200
+ runtimes: runtimeObservations(paths),
201
201
  payload,
202
202
  baseline,
203
203
  path_report: pathFindings,
@@ -75,6 +75,16 @@ export function treeDigest(root) {
75
75
  // The publish protocol keeps two private sibling trees beside the target,
76
76
  // named `.<target>.candidate.<pid>.<uuid>` and `.<target>.prior.<pid>.<uuid>`.
77
77
 
78
+ // The socket environment key every managed binding pins. Runtime consumers
79
+ // (the stdio bridge, the native MCP facade, the CLI, and the daemon) read
80
+ // exactly this name, with no fallback to any retired spelling.
81
+ export const SOCKET_ENV = 'EXTERNAL_SUBAGENT_SOCKET';
82
+ // The retired key earlier installers pinned into staged `.mcp.json` files.
83
+ // It is recognized ONLY here, to refresh a managed staging tree in place, and
84
+ // is never a runtime fallback. The literal is assembled from fragments so the
85
+ // retired name never appears as a searchable token (the rg gate stays clean).
86
+ export const LEGACY_SOCKET_ENV = ['ZCODE', 'AGENTD', 'SOCKET'].join('_');
87
+
78
88
  const siblingSuffix = () => `${process.pid}.${crypto.randomUUID()}`;
79
89
 
80
90
  function discardTree(dir) {
@@ -97,7 +107,10 @@ function bindCandidate(candidate, paths, command, args, timeoutMs) {
97
107
  server.command = command;
98
108
  if (args !== null) server.args = args;
99
109
  if (timeoutMs !== null) server.timeoutMs = timeoutMs;
100
- server.env = { ...(server.env || {}), ZCODE_AGENTD_SOCKET: paths.socket };
110
+ server.env = { ...(server.env || {}), [SOCKET_ENV]: paths.socket };
111
+ // Rewrite, never accumulate: a managed prior tree bound under the retired
112
+ // key is refreshed to carry the new key and nothing else.
113
+ delete server.env[LEGACY_SOCKET_ENV];
101
114
  fs.writeFileSync(mcpPath, `${JSON.stringify(mcp, null, 2)}\n`, { mode: 0o600 });
102
115
  }
103
116
 
@@ -138,7 +151,14 @@ export function preparePluginStage(source, staging, paths, binding = {}) {
138
151
  // the staged bridge); a prior native-form binding carried none, and the
139
152
  // rewrite below installs the requested form regardless.
140
153
  const argsMatch = args === null || priorCommand !== command || JSON.stringify(priorArgs) === JSON.stringify(args);
141
- if (!priorServer || (!commandMatches && !argsOwned) || !argsMatch || priorServer.env?.ZCODE_AGENTD_SOCKET !== paths.socket) {
154
+ // Ownership of an existing tree is proved by the requested binding, the
155
+ // native facade, or a staged script. Its socket may be pinned under the
156
+ // current key OR the retired one this installer used before the rename:
157
+ // either one pointing at paths.socket is a managed product binding that
158
+ // gets rewritten to the current key. Any other binding stays foreign.
159
+ const priorEnv = priorServer?.env || {};
160
+ const priorSocketManaged = priorEnv[SOCKET_ENV] === paths.socket || priorEnv[LEGACY_SOCKET_ENV] === paths.socket;
161
+ if (!priorServer || (!commandMatches && !argsOwned) || !argsMatch || !priorSocketManaged) {
142
162
  throw new CliError('PLUGIN_STAGING_CONFLICT', 'staging MCP binding differs from the managed product endpoint');
143
163
  }
144
164
  }
@@ -18,6 +18,15 @@ function escapeXml(value) {
18
18
  return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
19
19
  }
20
20
 
21
+ // Presence is diagnostic only; enabling is an explicit probe + config operation.
22
+ export function runtimeObservations(paths, options = {}) {
23
+ const config = readConfig(paths.config);
24
+ return Object.fromEntries(['zcode', 'dsh', 'codex'].map((agent) => {
25
+ const runtime = agent === 'zcode' ? (options.zcodeRuntime ?? ZCODE_RUNTIME) : config.subagents[agent].runtime_path;
26
+ return [agent, { path: runtime, present: Boolean(runtime && fs.existsSync(runtime)), enabled: config.subagents[agent].enabled }];
27
+ }));
28
+ }
29
+
21
30
  export function launchAgentPlist(paths, options = {}) {
22
31
  const daemon = nativeBinary(DAEMON_BIN_NAME);
23
32
  const config = readConfig(paths.config);
@@ -105,7 +105,7 @@ function verifyZcodeBinding({ config, staging, dir }) {
105
105
  const binding = zcodeMcpBinding(staging);
106
106
  if (!server || server.command !== binding.command || JSON.stringify(server.args || []) !== JSON.stringify(binding.args)
107
107
  || server.timeoutMs !== binding.timeoutMs
108
- || server.env?.ZCODE_AGENTD_SOCKET === undefined) {
108
+ || server.env?.EXTERNAL_SUBAGENT_SOCKET === undefined) {
109
109
  throw new CliError('ZCODE_BINDING_UNVERIFIABLE', `the staged plugin MCP binding is not the managed endpoint (${staging})`);
110
110
  }
111
111
  return server;
@@ -148,7 +148,7 @@ export function installZcodePlugin(paths, options = {}) {
148
148
  plugin_id: ZCODE_PLUGIN_ID,
149
149
  digest: treeDigest(staging),
150
150
  config_verified: true,
151
- binding: { command: server.command, socket: server.env.ZCODE_AGENTD_SOCKET },
151
+ binding: { command: server.command, socket: server.env.EXTERNAL_SUBAGENT_SOCKET },
152
152
  };
153
153
  if (prior.doc.plugins?.enabled === false) {
154
154
  result.warning = 'plugins.enabled is false in the ZCode config; the binding will not load until plugins are enabled';
package/cli/main.mjs CHANGED
@@ -5,7 +5,7 @@ import { BUSINESS_COMMANDS, PRODUCT_NAME, VERSION, ZCODE_RUNTIME } from './const
5
5
  import { CliError } from './errors.mjs';
6
6
  import { installHooks, installPlan, runInit } from './install/init.mjs';
7
7
  import { nativeBinary } from './install/layout.mjs';
8
- import { cleanupLegacy, purge, restoreData, backupData, uninstall } from './commands/maintenance.mjs';
8
+ import { purge, restoreData, backupData, uninstall } from './commands/maintenance.mjs';
9
9
  import { updateCommand } from './commands/update.mjs';
10
10
  import { localInstallStatus, startDaemon, stopDaemon } from './commands/daemon.mjs';
11
11
  import { serviceRegistrationStatus } from './install/service-macos.mjs';
@@ -17,7 +17,7 @@ import { parseConfigArgs } from './commands/config.mjs';
17
17
  import { subagentsCommand, parseSubagentsArgs } from './commands/agents.mjs';
18
18
  import { parseSpawnArgs, prepareSpawnInput } from './commands/tasks.mjs';
19
19
 
20
- const HELP = `external-subagent ${VERSION}\n\nUsage: external-subagent <command> [options]\n\nCommands:\n help, version Show basic product information\n init [--dry-run] [--resume] [--install-hooks] [--skip-service-start]\n Install the standalone daemon service only; bind a\n host afterwards with install-plugin or install-mcp\n hooks install [--dry-run] Install ZCode policy hooks explicitly\n install-plugin [codex|zcode] [--dry-run|--uninstall] [--codex-home <path>]\n Install or remove the managed host plugin (MCP + skill);\n the host defaults to codex, --codex-home is codex-only\n install-mcp [--dry-run|--uninstall] [--codex-home <path>]\n Install or remove the direct Codex MCP TOML binding\n status, diagnose Inspect local service and runtime state\n start, stop Bootstrap or boot out the daemon LaunchAgent\n backup --output <dir> Back up retained product data\n restore --input <dir> Verify and restore product data\n uninstall Release Codex/ZCode bindings; remove service registration; retain data\n purge --yes Explicitly delete new product data\n cleanup-legacy --yes Delete old unpublished installation (no migration)\n`;
20
+ const HELP = `external-subagent ${VERSION}\n\nUsage: external-subagent <command> [options]\n\nCommands:\n help, version Show basic product information\n init [--dry-run] [--resume] [--install-hooks] [--skip-service-start]\n Install the standalone daemon service only; bind a\n host afterwards with install-plugin or install-mcp\n hooks install [--dry-run] Install ZCode policy hooks explicitly\n install-plugin [codex|zcode] [--dry-run|--uninstall] [--codex-home <path>]\n Install or remove the managed host plugin (MCP + skill);\n the host defaults to codex, --codex-home is codex-only\n install-mcp [--dry-run|--uninstall] [--codex-home <path>]\n Install or remove the direct Codex MCP TOML binding\n status, diagnose Inspect local service and runtime state\n start, stop Bootstrap or boot out the daemon LaunchAgent\n backup --output <dir> Back up retained product data\n restore --input <dir> Verify and restore product data\n uninstall Release Codex/ZCode bindings; remove service registration; retain data\n purge --yes Explicitly delete new product data\n`;
21
21
  const DAEMON_HELP = ` config get [key] | config set <key> <value>\n subagents list | subagents status [subagent] | subagents probe/models [subagent]\n create/spawn, wait, list, send, respond, cancel, result, close, observe\n Daemon calls accept --json '<object>' or JSON stdin\n list JSON requires repository (workspace is an alias)\n observe JSON requires only agent_id\n`;
22
22
 
23
23
  function structuredInput(args, parser) {
@@ -117,7 +117,7 @@ function boundedFailureRecord(record) {
117
117
  function diagnosticTail(text) {
118
118
  let incomplete = false;
119
119
  const decoded = text.split('\n').map((line) => {
120
- const match = line.match(/^(\[zcode-agentd\] failure agent=[^:\r\n]+: )(.+)$/u);
120
+ const match = line.match(/^(\[external-subagentd\] failure agent=[^:\r\n]+: )(.+)$/u);
121
121
  if (match && match[2].startsWith('{')) {
122
122
  try {
123
123
  const record = JSON.parse(match[2]);
@@ -166,7 +166,7 @@ function diagnosticLogs(logDirectory) {
166
166
  let decodeStart = Math.max(0, start - 256) - readStart;
167
167
  if (decodeStart > 0) {
168
168
  const lineStart = buffer.lastIndexOf(0x0a, decodeStart - 1) + 1;
169
- if (buffer.subarray(lineStart, read).toString('utf8').startsWith('[zcode-agentd] failure agent=')) decodeStart = lineStart;
169
+ if (buffer.subarray(lineStart, read).toString('utf8').startsWith('[external-subagentd] failure agent=')) decodeStart = lineStart;
170
170
  }
171
171
  // Legacy text keeps its original lookbehind; known records are decoded
172
172
  // (or marked incomplete) before any display clipping can hide the prefix.
@@ -211,7 +211,7 @@ function agentDiagnosticLogs(logDirectory, agentId) {
211
211
  if (lines.pop()) report.incomplete.push(`record_incomplete:${name}`); // The writer may still be appending.
212
212
  for (const line of lines.reverse()) {
213
213
  if (Buffer.byteLength(line) > DIAGNOSTIC_RECORD_BYTES) { report.incomplete.push(`record_truncated:${name}`); continue; }
214
- const prefix = `[zcode-agentd] failure agent=${agentId}: `;
214
+ const prefix = `[external-subagentd] failure agent=${agentId}: `;
215
215
  if (!line.startsWith(prefix)) continue;
216
216
  const raw = line.slice(prefix.length);
217
217
  // JSON records repeat the identifier; reject misleading prefix matches.
@@ -256,7 +256,7 @@ function diagnoseInput(args) {
256
256
 
257
257
  async function diagnose(paths, args) {
258
258
  const { agent, outputDirectory } = diagnoseInput(args);
259
- const socket = process.env.ZCODE_AGENTD_SOCKET || paths.socket;
259
+ const socket = process.env.EXTERNAL_SUBAGENT_SOCKET || paths.socket;
260
260
  const report = {
261
261
  schema_version: 1,
262
262
  scope: agent ? { agent_id: agent } : { kind: 'global' },
@@ -381,7 +381,7 @@ export async function main(args) {
381
381
  // keep it out of the ordinary status projection.
382
382
  const { pid: _pid, ...service } = serviceRaw;
383
383
  try {
384
- output({ ...local, service, daemon_status: publicDaemonStatus(await callDaemon(process.env.ZCODE_AGENTD_SOCKET || paths.socket, 'status', {}), { verbose }) });
384
+ output({ ...local, service, daemon_status: publicDaemonStatus(await callDaemon(process.env.EXTERNAL_SUBAGENT_SOCKET || paths.socket, 'status', {}), { verbose }) });
385
385
  } catch (error) {
386
386
  output({ ...local, service, daemon_status: null, daemon_error: { code: error.code || 'DAEMON_ERROR', message: error.message } });
387
387
  }
@@ -391,7 +391,7 @@ export async function main(args) {
391
391
  const input = structuredInput(args.slice(1), command === 'config' ? parseConfigArgs : parseSubagentsArgs);
392
392
  output(command === 'config'
393
393
  ? configCommand(paths, input)
394
- : await subagentsCommand(paths, input, { callDaemon, socket: process.env.ZCODE_AGENTD_SOCKET || paths.socket }));
394
+ : await subagentsCommand(paths, input, { callDaemon, socket: process.env.EXTERNAL_SUBAGENT_SOCKET || paths.socket }));
395
395
  return;
396
396
  }
397
397
  if (command === 'diagnose') {
@@ -405,16 +405,12 @@ export async function main(args) {
405
405
  if (command === 'restore') { output(restoreData(value(args, '--input'), paths)); return; }
406
406
  if (command === 'start') { output(startDaemon(paths)); return; }
407
407
  if (command === 'stop') { output(stopDaemon(paths)); return; }
408
- if (command === 'update' || command === 'reconcile') { output(await updateCommand(paths, command === 'reconcile' ? ['reconcile', ...args.slice(1)] : args.slice(1), { socket: process.env.ZCODE_AGENTD_SOCKET || paths.socket })); return; }
408
+ if (command === 'update' || command === 'reconcile') { output(await updateCommand(paths, command === 'reconcile' ? ['reconcile', ...args.slice(1)] : args.slice(1), { socket: process.env.EXTERNAL_SUBAGENT_SOCKET || paths.socket })); return; }
409
409
  if (command === 'uninstall') { output(uninstall(paths)); return; }
410
410
  if (command === 'purge') {
411
411
  if (!args.includes('--yes')) throw new CliError('CONFIRMATION_REQUIRED', 'purge requires --yes');
412
412
  output(purge(paths)); return;
413
413
  }
414
- if (command === 'cleanup-legacy') {
415
- if (!args.includes('--yes')) throw new CliError('CONFIRMATION_REQUIRED', 'cleanup-legacy requires --yes');
416
- output(cleanupLegacy(paths.home)); return;
417
- }
418
414
  let input;
419
415
  if (command === 'create' || command === 'spawn') {
420
416
  const spawnArgs = args.slice(1);
@@ -422,7 +418,7 @@ export async function main(args) {
422
418
  ? prepareSpawnInput(parseDaemonInput(spawnArgs))
423
419
  : parseSpawnArgs(spawnArgs);
424
420
  } else input = parseDaemonInput(args.slice(1));
425
- const result = await callDaemon(process.env.ZCODE_AGENTD_SOCKET || paths.socket, command, input);
421
+ const result = await callDaemon(process.env.EXTERNAL_SUBAGENT_SOCKET || paths.socket, command, input);
426
422
  output({ command, result });
427
423
  }
428
424
 
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { CliError } from './errors.mjs';
4
4
  import { jsonBytes, sha256 } from './fs-atomic.mjs';
5
- import { legacyPaths, productPaths } from './paths.mjs';
5
+ import { productPaths } from './paths.mjs';
6
6
  import { bootoutService } from './install/service-macos.mjs';
7
7
 
8
8
  function copyTree(source, destination, records, root = source) {
@@ -97,8 +97,3 @@ export function uninstall(paths = productPaths(), options = {}) {
97
97
  export function purge(paths = productPaths()) {
98
98
  return { purged: removeOne(paths.data), logs_purged: removeOne(paths.logs) };
99
99
  }
100
-
101
- export function cleanupLegacy(home) {
102
- const removed = legacyPaths(home).filter(removeOne);
103
- return { migration: false, aliases_created: [], removed };
104
- }
package/cli/paths.mjs CHANGED
@@ -4,7 +4,7 @@ import { LAUNCH_AGENT_LABEL, PRODUCT_NAME } from './constants.mjs';
4
4
  import { PLUGIN_NAME } from './install/layout.mjs';
5
5
 
6
6
  export function platform() {
7
- return process.env.ZCODE_AS_SUBAGENT_TEST_PLATFORM || process.platform;
7
+ return process.env.EXTERNAL_SUBAGENT_TEST_PLATFORM || process.platform;
8
8
  }
9
9
 
10
10
  // State paths derive from PRODUCT_NAME, so the debug variant gets a fully
@@ -26,15 +26,3 @@ export function productPaths(home = os.homedir()) {
26
26
  zcodePlugin: path.join(data, 'zcode-plugin', PLUGIN_NAME),
27
27
  };
28
28
  }
29
-
30
- export function legacyPaths(home = os.homedir()) {
31
- return [
32
- path.join(home, 'Library', 'LaunchAgents', 'com.zcode-reviewd.plist'),
33
- path.join(home, 'Library', 'LaunchAgents', 'com.zcode-review-mcp.plist'),
34
- path.join(home, 'Library', 'Logs', 'zcode-reviewd'),
35
- path.join(home, 'Library', 'Application Support', 'zcode-review-mcp'),
36
- path.join(home, 'Library', 'Application Support', 'zcode-reviewd'),
37
- path.join(home, '.local', 'bin', 'zcode-reviewd'),
38
- path.join(home, '.local', 'bin', 'zcode-review-mcp'),
39
- ];
40
- }
package/cli/rpc.mjs CHANGED
@@ -184,6 +184,7 @@ function publicResult(result) {
184
184
  total_bytes: result.total_bytes,
185
185
  next_offset: result.next_offset ?? null,
186
186
  complete: result.complete,
187
+ reason_code: result.reason_code ?? null,
187
188
  };
188
189
  }
189
190
 
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "product": "external-subagent",
4
- "version": "0.1.2",
4
+ "version": "0.1.3",
5
5
  "platform": "darwin-arm64",
6
6
  "files": [
7
7
  {
8
8
  "name": "external-subagentd",
9
- "bytes": 9269360,
9
+ "bytes": 9331504,
10
10
  "mode": "755",
11
- "sha256": "ba5ea77bd4a47d41bd759a312fb6678cae2a9512eeaeaf09690de9cb502dba75"
11
+ "sha256": "8f8625ccf600f6a4c252318ab7b67cc6f111d0e19282edd759f02a3267fb7a14"
12
12
  },
13
13
  {
14
14
  "name": "external-subagent-mcp",
15
15
  "bytes": 937840,
16
16
  "mode": "755",
17
- "sha256": "aa6f61545de3a0480de16028fa5b17683a9c02fe97a7a03c41ff623f4e25d600"
17
+ "sha256": "5d933f567572c570b76733cb8eccae6eb08d3a4e84c239cc06b334bd760f4008"
18
18
  }
19
19
  ]
20
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "external-subagent",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Manage bounded ZCode, DeepSeek Harness (DSH), and Codex subagent tasks through a local MCP service for any local host (Codex, ZCode, or a custom MCP client)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "external-subagent",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Manage bounded external agent tasks through the external-subagent MCP service.",
5
5
  "author": { "name": "external-subagent maintainers" },
6
6
  "skills": "./skills/",
@@ -3,7 +3,7 @@
3
3
  "external_subagent": {
4
4
  "command": "__EXTERNAL_SUBAGENT_MCP_EXECUTABLE__",
5
5
  "args": [],
6
- "env": { "ZCODE_AGENTD_SOCKET": "__EXTERNAL_SUBAGENT_SOCKET__" }
6
+ "env": { "EXTERNAL_SUBAGENT_SOCKET": "__EXTERNAL_SUBAGENT_SOCKET__" }
7
7
  }
8
8
  }
9
9
  }
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // Mirrors crates/external-mcp (the native facade) byte for byte in
5
5
  // behavior: resolve the daemon MCP stream endpoint from
6
- // ZCODE_AGENTD_SOCKET, connect, and pipe client stdio <-> socket until
6
+ // EXTERNAL_SUBAGENT_SOCKET, connect, and pipe client stdio <-> socket until
7
7
  // either side reaches EOF. Hosts that spawn MCP servers inside
8
8
  // restricted execution contexts (ZCode kills ad-hoc-signed native
9
9
  // binaries there, while node processes and unix sockets are allowed)
@@ -12,9 +12,9 @@
12
12
  import net from 'node:net';
13
13
  import path from 'node:path';
14
14
 
15
- const socket = process.env.ZCODE_AGENTD_SOCKET;
15
+ const socket = process.env.EXTERNAL_SUBAGENT_SOCKET;
16
16
  if (!socket || !path.isAbsolute(socket)) {
17
- console.error('mcp-stdio-bridge: ZCODE_AGENTD_SOCKET must be an absolute path');
17
+ console.error('mcp-stdio-bridge: EXTERNAL_SUBAGENT_SOCKET must be an absolute path');
18
18
  process.exit(1);
19
19
  }
20
20
  // The stable daemon RPC socket keeps its name; MCP is exposed by the
@@ -25,7 +25,7 @@ Spawn accepts an optional `effort` token that steers the task's reasoning
25
25
  effort. Omit it to keep each agent's current default (codex keeps its existing
26
26
  wire default; zcode and dsh send no effort field or call at all). `codex`
27
27
  admits only the closed set
28
- `low | medium | high | xhigh` — `minimal` and `max` are rejected;
28
+ `low | medium | high | xhigh | max` — `minimal` and `ultra` are rejected;
29
29
  `zcode` and `dsh` accept any bounded token (1..24 bytes of `[a-z0-9_]`) and
30
30
  pass it through to the runtime, because the supported set is only known at
31
31
  runtime. A malformed or unsupported token is rejected before dispatch, so no
@@ -40,16 +40,23 @@ Codex home instances, and offers plugin or direct MCP bindings; built-in
40
40
  dirs with a single per-user instance. This is separate
41
41
  from `subagent.codex`. Protocol implementations are adapters.
42
42
 
43
+ All three subagents (`zcode`, `dsh`, `codex`) are disabled by default; enable one
44
+ with `external-subagent agents enable <name>`, which runs a local probe and writes
45
+ the configuration only on success (a daemon restart is required for `dsh`/`codex`).
43
46
  Actual per-subagent limitations (from the accepted code):
44
47
 
45
- - `zcode` — enabled and spawn-supported by default; all four permission modes
48
+ - `zcode` — disabled until explicitly enabled; all four permission modes
46
49
  (build/edit/plan/yolo); an explicit spawn `model` is rejected
47
50
  (`model_selection_unsupported`).
48
51
  - `dsh` — disabled until explicitly enabled and configured
49
52
  (`runtime_path`/`home`/`profile`/`version`); admits only `build` and strict
50
53
  `plan`.
51
- - `codex` — disabled by default; `plan` (sandbox=read-only) and `yolo`
52
- (sandbox=danger-full-access), both pinning approvalPolicy=never;
53
- `build`/`edit` are refused before the prompt.
54
- - `observe` — verified public reasoning exists only for zcode tasks; observe
55
- on a non-zcode task truthfully errors `unavailable`.
54
+ - `codex` — disabled until explicitly enabled; all four permission modes:
55
+ `build`/`edit` map to sandbox=workspace-write, `plan` to sandbox=read-only,
56
+ and `yolo` to sandbox=danger-full-access, all pinning approvalPolicy=never.
57
+ A non-empty spawn `write_manifest` is rejected before task creation with
58
+ `codex_write_manifest_unsupported`.
59
+ - `observe` — available for all three subagents; `zcode` and `dsh` return the
60
+ public reasoning tail (at most 200 characters), while `codex` does not collect
61
+ reasoning and returns `reasoning: null`. Tool history and coverage are reported
62
+ truthfully per adapter capability.
@@ -62,15 +62,19 @@
62
62
  "type": "object",
63
63
  "required": ["repository", "prompt"],
64
64
  "properties": {
65
- "subagent": { "type": "string", "minLength": 1, "description": "Subagent route key; omission uses configured default_subagent or returns subagent_required. Null is invalid; dsh production spawn remains gated." },
65
+ "subagent": { "type": "string", "minLength": 1, "description": "Subagent route key; omission uses configured default_subagent or returns subagent_required. Null is invalid; dsh production spawn requires its enabled + spawn_supported + pinned-runtime configuration." },
66
66
  "repository": { "type": "string", "minLength": 1 },
67
- "permission_mode": { "$ref": "#/properties/permission_mode" },
67
+ "permission_mode": { "$ref": "#/properties/permission_mode", "description": "codex supports build/edit (workspace-write), plan (read-only), and yolo (danger-full-access), always with approvalPolicy=never." },
68
68
  "prompt": { "type": "string", "minLength": 1 },
69
69
  "model": { "type": "string", "minLength": 1, "description": "Task model. dsh requires provider:model, split at the first colon (the model side may contain further colons; a missing colon or an empty side is rejected before any task). codex uses its own model id; zcode rejects model selection." },
70
70
  "effort": { "type": "string", "minLength": 1, "maxLength": 24, "pattern": "^[a-z0-9_]+$", "description": "Reasoning-effort token: 1..24 bytes of [a-z0-9_]. codex admits only low, medium, high or xhigh; zcode and dsh pass the bounded token through. Omission keeps the runtime default." },
71
- "write_manifest": { "type": "array", "items": { "type": "string", "minLength": 1 } }
71
+ "write_manifest": { "type": "array", "items": { "type": "string", "minLength": 1 }, "description": "codex requires an empty or omitted write_manifest; non-empty input returns codex_write_manifest_unsupported before task creation. dsh admits a non-empty manifest in build through the guarded manifest-build composition (workspace-write sandbox, only tool-fs writable, out-of-manifest paths rejected as FS_WRITE_MANIFEST_DENIED), at most 256 entries; an explicit [\".\"] keeps the existing build composition. plan requires an empty manifest for every subagent." }
72
72
  },
73
73
  "allOf": [
74
+ { "if": { "required": ["subagent"], "properties": { "subagent": { "const": "codex" } } },
75
+ "then": { "properties": { "write_manifest": { "maxItems": 0 } } } },
76
+ { "if": { "required": ["subagent"], "properties": { "subagent": { "const": "dsh" } } },
77
+ "then": { "properties": { "write_manifest": { "maxItems": 256 } } } },
74
78
  { "if": { "required": ["permission_mode"], "properties": { "permission_mode": { "const": "plan" } } },
75
79
  "then": { "properties": { "write_manifest": { "maxItems": 0 } } } }
76
80
  ],
@@ -101,6 +105,7 @@
101
105
  "additionalProperties": false
102
106
  },
103
107
  "observe": {
108
+ "description": "Read captured task observations for zcode, dsh or codex. zcode and dsh expose at most 200 Unicode characters of public reasoning; codex reasoning is null. Coverage reports capture gaps and unsupported history truthfully.",
104
109
  "type": "object",
105
110
  "required": ["agent_id"],
106
111
  "properties": {
@@ -121,7 +126,7 @@
121
126
  },
122
127
  "result_projection": {
123
128
  "type": "object",
124
- "required": ["outcome", "final_text", "partial", "offset", "total_bytes", "next_offset", "complete"],
129
+ "required": ["outcome", "final_text", "partial", "offset", "total_bytes", "next_offset", "complete", "reason_code"],
125
130
  "properties": {
126
131
  "outcome": { "enum": ["COMPLETED", "FAILED", "CANCELLED", "TIMED_OUT", "RUNTIME_LOST", "RESULT_INVALID"] },
127
132
  "final_text": { "type": "string" },
@@ -129,7 +134,8 @@
129
134
  "offset": { "type": "integer", "minimum": 0 },
130
135
  "total_bytes": { "type": "integer", "minimum": 0 },
131
136
  "next_offset": { "type": ["integer", "null"], "minimum": 0 },
132
- "complete": { "type": "boolean" }
137
+ "complete": { "type": "boolean" },
138
+ "reason_code": { "type": ["string", "null"] }
133
139
  },
134
140
  "additionalProperties": false
135
141
  },
@@ -36,7 +36,7 @@
36
36
  }
37
37
  },
38
38
  "reasoning": {
39
- "type": "object",
39
+ "type": ["object", "null"],
40
40
  "additionalProperties": false,
41
41
  "required": ["text", "truncated"],
42
42
  "properties": {
@@ -0,0 +1,36 @@
1
+ {
2
+ "schema": "external-subagent-public-reasoning-source/1",
3
+ "subagents": {
4
+ "zcode": {
5
+ "public": true,
6
+ "status": "VERIFIED_RUNTIME_PUBLIC",
7
+ "runtime_path": "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs",
8
+ "runtime_version": "3.11.2",
9
+ "runtime_sha256": "e9f1868c0fdb863537ed910ee3828b9be96b8c2fd805473f63b439e1113266b8",
10
+ "wire_method": "session/event",
11
+ "event_type_pointer": "/params/type",
12
+ "event_type": "model.streaming",
13
+ "event_discriminator_pointer": "/params/payload/kind",
14
+ "event_discriminator_value": "reasoning_delta",
15
+ "delta_pointer": "/params/payload/delta",
16
+ "explicitly_excluded_key": "encrypted_content"
17
+ },
18
+ "dsh": {
19
+ "public": true,
20
+ "status": "VERIFIED_PROTOCOL_PUBLIC",
21
+ "runtime_version": "acp/1",
22
+ "wire_method": "session/update",
23
+ "event_type": "agent_thought_chunk",
24
+ "delta_pointer": "/params/update/content/text",
25
+ "canonical_event_type": "model.streaming",
26
+ "canonical_delta_pointer": "/params/payload/delta",
27
+ "explicitly_excluded_key": "encrypted_content"
28
+ },
29
+ "codex": {
30
+ "public": false,
31
+ "status": "HIDDEN",
32
+ "reasoning": null,
33
+ "collected": false
34
+ }
35
+ }
36
+ }
@@ -29,6 +29,14 @@ if (variant !== '' && variant !== 'debug') {
29
29
  process.exit(2);
30
30
  }
31
31
  const isDebug = variant === 'debug';
32
+ // The cargo profile is release by default so prepack/prepublish keep shipping
33
+ // release bytes; EXTERNAL_SUBAGENT_CARGO_PROFILE=debug selects the development
34
+ // profile without changing the payload/product naming.
35
+ const cargoProfile = process.env.EXTERNAL_SUBAGENT_CARGO_PROFILE || 'release';
36
+ if (cargoProfile !== 'release' && cargoProfile !== 'debug') {
37
+ process.stderr.write(`unknown cargo profile: ${cargoProfile} (supported: release, debug)\n`);
38
+ process.exit(2);
39
+ }
32
40
  const nativeDirName = isDebug ? 'native-debug' : 'native';
33
41
  const productName = isDebug ? 'external-subagent-debug' : 'external-subagent';
34
42
  const binaries = isDebug
@@ -66,6 +74,7 @@ function stagedIsCurrent(version) {
66
74
  let manifest;
67
75
  try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch { return false; }
68
76
  if (manifest.version !== version || manifest.platform !== 'darwin-arm64' || manifest.product !== productName) return false;
77
+ if ((manifest.profile ?? 'release') !== cargoProfile) return false;
69
78
  const inputs = newestInputMs();
70
79
  for (const file of manifest.files) {
71
80
  const target = path.join(platformDir, file.name);
@@ -82,24 +91,27 @@ const version = packageVersion();
82
91
  // diagnostics go to stderr there so pack output stays machine-parseable.
83
92
  const note = (text) => (process.env.npm_lifecycle_event ? process.stderr : process.stdout).write(text);
84
93
  if (process.argv.includes('--if-stale') && stagedIsCurrent(version)) {
85
- note(`native payload already current at ${version}${isDebug ? ' (debug)' : ''}\n`);
94
+ note(`native payload already current at ${version}${isDebug ? ' (debug)' : ''}${cargoProfile === 'release' ? '' : ` [${cargoProfile}]`}\n`);
86
95
  process.exit(0);
87
96
  }
88
97
 
89
- const build = spawnSync('cargo', ['build', '--release', '-p', 'external-daemon', '-p', 'external-mcp'], {
98
+ const cargoArguments = ['build'];
99
+ if (cargoProfile === 'release') cargoArguments.push('--release');
100
+ cargoArguments.push('-p', 'external-daemon', '-p', 'external-mcp');
101
+ const build = spawnSync('cargo', cargoArguments, {
90
102
  cwd: packageRoot, stdio: 'inherit',
91
103
  });
92
104
  if (build.status !== 0) {
93
- process.stderr.write('cargo release build failed\n');
105
+ process.stderr.write(`cargo ${cargoProfile} build failed\n`);
94
106
  process.exit(build.status ?? 1);
95
107
  }
96
108
 
97
109
  fs.mkdirSync(platformDir, { recursive: true });
98
110
  const files = [];
99
111
  for (const [source, name] of binaries) {
100
- const sourcePath = path.join(packageRoot, 'target', 'release', source);
112
+ const sourcePath = path.join(packageRoot, 'target', cargoProfile, source);
101
113
  if (!fs.existsSync(sourcePath)) {
102
- process.stderr.write(`release binary missing: ${sourcePath}\n`);
114
+ process.stderr.write(`${cargoProfile} binary missing: ${sourcePath}\n`);
103
115
  process.exit(1);
104
116
  }
105
117
  const target = path.join(platformDir, name);
@@ -109,11 +121,15 @@ for (const [source, name] of binaries) {
109
121
  files.push({ name, bytes: bytes.length, mode: '755', sha256: sha256(bytes) });
110
122
  }
111
123
 
112
- const manifest = { schema_version: 1, product: productName, version, platform: 'darwin-arm64', files };
124
+ const manifest = { schema_version: 1, product: productName, version, platform: 'darwin-arm64' };
125
+ // The debug profile is recorded so --if-stale can tell a debug-staged payload
126
+ // from a release one; the release manifest stays byte-identical to before.
127
+ if (cargoProfile !== 'release') manifest.profile = cargoProfile;
128
+ manifest.files = files;
113
129
  fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o644 });
114
130
  if (isDebug) {
115
131
  const stagedPlugin = stageDebugPlugin(packageRoot);
116
132
  note(`native payload ${version} (debug) staged: ${files.map((file) => file.name).join(', ')}\nplugin source staged: ${stagedPlugin}\n`);
117
133
  } else {
118
- note(`native payload ${version} staged: ${files.map((file) => file.name).join(', ')}\n`);
134
+ note(`native payload ${version} staged: ${files.map((file) => file.name).join(', ')}${cargoProfile === 'release' ? '' : ` [${cargoProfile}]`}\n`);
119
135
  }
@@ -1,14 +0,0 @@
1
- {
2
- "schema": "zas-public-reasoning-source/1",
3
- "status": "VERIFIED_RUNTIME_PUBLIC",
4
- "runtime_path": "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs",
5
- "runtime_version": "3.11.2",
6
- "runtime_sha256": "e9f1868c0fdb863537ed910ee3828b9be96b8c2fd805473f63b439e1113266b8",
7
- "wire_method": "session/event",
8
- "event_type_pointer": "/params/type",
9
- "event_type": "model.streaming",
10
- "event_discriminator_pointer": "/params/payload/kind",
11
- "event_discriminator_value": "reasoning_delta",
12
- "delta_pointer": "/params/payload/delta",
13
- "explicitly_excluded_key": "encrypted_content"
14
- }