@heretyc/subagent-mcp 2.12.13 → 2.12.15
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 +170 -170
- package/dist/advanced-ruleset.py +67 -67
- package/dist/concurrency.js +131 -17
- package/dist/config-scaffold.js +1 -1
- package/dist/drivers.js +13 -6
- package/dist/effort.js +5 -3
- package/dist/global-concurrency.jsonc +43 -43
- package/dist/global-subagent-mcp-config.jsonc +43 -43
- package/dist/hooks/orchestration-codex.js +2 -2
- package/dist/index.js +6 -6
- package/dist/launch-prompt.js +3 -2
- package/dist/orchestration/hook-core.js +10 -8
- package/dist/orchestration/liveness.js +24 -3
- package/dist/orchestration/marker.js +9 -46
- package/dist/orchestration/model-mode.js +1 -1
- package/dist/orchestration/reminder.js +66 -14
- package/dist/output-helpers.js +39 -30
- package/dist/pending-permissions.js +13 -3
- package/dist/platform.js +5 -2
- package/dist/routing-table.json +2133 -2133
- package/dist/ruleset-scaffold.js +1 -1
- package/package.json +70 -70
- package/scripts/postinstall.mjs +102 -102
package/dist/config-scaffold.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// GENERATED by scripts/gen-ruleset-scaffold.mjs from src/global-subagent-mcp-config.jsonc — DO NOT EDIT.
|
|
2
|
-
export const CONCURRENCY_SCAFFOLD = "// subagent-mcp - Global Subagent MCP Config\
|
|
2
|
+
export const CONCURRENCY_SCAFFOLD = "// subagent-mcp - Global Subagent MCP Config\n// ------------------------------------------------------------------\n// SOLE source of truth for machine-wide subagent-mcp defaults that must be\n// available beside the compiled server.\n//\n// globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE\n// across EVERY session, process, and user on this machine. There is NO\n// environment-variable override for the cap.\n//\n// RE-READ on every launch_agent call - edits take effect immediately, no\n// server restart required.\n//\n// Cap value rules:\n// - missing / unset / non-integer / 0 / negative -> reset to default 20\n// - 1 through 9 -> forced UP to minimum 10\n// - 10 or greater -> used as-is\n//\n// checkForUpdates controls the silent npmjs update check started when the MCP\n// server connects. Default true. Set to false to skip the registry fetch and\n// suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.\n//\n// permissionsCeiling controls launch-time permissions posture:\n// - auto (default): shared engine gates unsafe/residue actions\n// - manual: human approval for residue while still auto-denying DANGER\n// - yolo: preserve the historical bypass/danger-full-access path, except\n// for non-bypassable config-file self-protection\n//\n// escalation applies only in auto mode. irreversible-only routes irreversible\n// NEUTRAL residue to a human; off leaves residue to orchestrator judgment.\n//\n// strictReadParity controls logging only. Unparseable Codex approval payloads\n// always fail closed to ask; warn logs the construct, off silences that log.\n//\n// sandboxNetwork controls Codex workspace-write network access. Default false.\n// Set true only when sub-agents need network after permission approval.\n{\n \"globalConcurrentSubagents\": 20,\n \"checkForUpdates\": true,\n \"permissionsCeiling\": \"auto\",\n \"escalation\": \"irreversible-only\",\n \"strictReadParity\": \"warn\",\n \"sandboxNetwork\": false\n}\n";
|
package/dist/drivers.js
CHANGED
|
@@ -18,6 +18,7 @@ export const CLAUDE_SESSION_LIMIT = /^\s*you['’]ve hit your session limit\s*·
|
|
|
18
18
|
export function isClaudeSessionLimit(text) {
|
|
19
19
|
return CLAUDE_SESSION_LIMIT.test(text);
|
|
20
20
|
}
|
|
21
|
+
const TRANSIENT_FAILURE_RE = /\b429\b|\b(?:http(?:\/\d(?:\.\d)?)?|status|statuscode|status_code|code|error)\b[\s:=#-]*5\d{2}\b|quota|rate.?limit|timeout|ECONNRESET|ETIMEDOUT|ECONNREFUSED|too many requests|service unavailable|server error|overloaded/i;
|
|
21
22
|
export function claudeMessageText(message) {
|
|
22
23
|
if (message?.type === "assistant") {
|
|
23
24
|
const content = message.message?.content;
|
|
@@ -167,7 +168,9 @@ function isElicitationMethod(method) {
|
|
|
167
168
|
// Shape the `result` payload for an elicitation reply. When the RPC offered a
|
|
168
169
|
// discrete option set, select only an exact case-insensitive label match and
|
|
169
170
|
// echo its identifier; otherwise pass the answer through as text.
|
|
170
|
-
function buildElicitationResult(options, answer) {
|
|
171
|
+
function buildElicitationResult(options, answer, action = "answer") {
|
|
172
|
+
if (action === "decline")
|
|
173
|
+
return { action: "decline" };
|
|
171
174
|
if (options && options.length > 0) {
|
|
172
175
|
const norm = answer.trim().toLowerCase();
|
|
173
176
|
const chosen = options.find((opt) => {
|
|
@@ -751,7 +754,8 @@ export class CodexAppServerDriver {
|
|
|
751
754
|
else if (isJsonRpcId(message.id) &&
|
|
752
755
|
typeof message.method === "string" &&
|
|
753
756
|
isElicitationMethod(message.method)) {
|
|
754
|
-
|
|
757
|
+
console.error(`[codex app-server driver] declining unhandled elicitation method: ${message.method}`);
|
|
758
|
+
void this.replyJsonRpc(message.id, buildElicitationResult(undefined, "", "decline"));
|
|
755
759
|
}
|
|
756
760
|
if (message.method === "turn/started" && message.params && typeof message.params === "object") {
|
|
757
761
|
const turn = (message.params.turn ?? {});
|
|
@@ -819,7 +823,7 @@ export class CodexAppServerDriver {
|
|
|
819
823
|
}
|
|
820
824
|
fail(error) {
|
|
821
825
|
const msg = error.message;
|
|
822
|
-
const transient =
|
|
826
|
+
const transient = TRANSIENT_FAILURE_RE.test(msg);
|
|
823
827
|
this._definitelyStartedReject(transient ? new ProviderTransientError(msg) : error);
|
|
824
828
|
this.process.fail(error);
|
|
825
829
|
this.rejectPending(error);
|
|
@@ -1028,7 +1032,7 @@ export class ClaudeSdkDriver {
|
|
|
1028
1032
|
catch (error) {
|
|
1029
1033
|
if (!this.process.killed) {
|
|
1030
1034
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1031
|
-
const transient =
|
|
1035
|
+
const transient = TRANSIENT_FAILURE_RE.test(msg);
|
|
1032
1036
|
this._definitelyStartedReject(transient ? new ProviderTransientError(msg) : new Error(msg));
|
|
1033
1037
|
this.closedFlag = true;
|
|
1034
1038
|
this.process.stderr.write(msg);
|
|
@@ -1038,8 +1042,11 @@ export class ClaudeSdkDriver {
|
|
|
1038
1042
|
}
|
|
1039
1043
|
}
|
|
1040
1044
|
export async function createProviderDriver(options) {
|
|
1041
|
-
|
|
1042
|
-
|
|
1045
|
+
const testSeamsEnabled = process.env.NODE_ENV === "test" || process.env.SUBAGENT_MCP_ENABLE_TEST_SEAMS === "1";
|
|
1046
|
+
if (testSeamsEnabled &&
|
|
1047
|
+
((options.provider === "claude" && process.env.SUBAGENT_MOCK_CLAUDE_DRIVER === "jsonl") ||
|
|
1048
|
+
(options.provider === "codex" && process.env.SUBAGENT_MOCK_CODEX_DRIVER === "jsonl"))) {
|
|
1049
|
+
// Test seam: never honor mock-driver script env vars in production by default.
|
|
1043
1050
|
const mockScript = process.env.SUBAGENT_MOCK_DRIVER_SCRIPT;
|
|
1044
1051
|
const child = mockScript
|
|
1045
1052
|
? spawn(process.execPath, [mockScript, options.provider], {
|
package/dist/effort.js
CHANGED
|
@@ -6,9 +6,13 @@ export function mapModel(provider, model) {
|
|
|
6
6
|
if (provider === "claude") {
|
|
7
7
|
if (model === "opus" || model === "opus-4-8")
|
|
8
8
|
return "claude-opus-4-8";
|
|
9
|
+
if (model === "sonnet")
|
|
10
|
+
return "claude-sonnet-4-6";
|
|
11
|
+
if (model === "haiku")
|
|
12
|
+
return "claude-haiku-4-5";
|
|
9
13
|
if (model === "fable")
|
|
10
14
|
return "claude-fable-5";
|
|
11
|
-
return model;
|
|
15
|
+
return model;
|
|
12
16
|
}
|
|
13
17
|
else {
|
|
14
18
|
return model; // gpt-5.5
|
|
@@ -60,10 +64,8 @@ export function buildCommand(provider, model, effort, cwd, agentId) {
|
|
|
60
64
|
const ucSettingsPath = join(ucSettingsDir, "settings.json");
|
|
61
65
|
writeFileSync(ucSettingsPath, '{"ultracode":true}', { mode: 0o600 });
|
|
62
66
|
args.push("--settings", ucSettingsPath);
|
|
63
|
-
args.push("--permission-mode", "bypassPermissions", "--tools", "default", "--max-turns", "50");
|
|
64
67
|
return { args, ucSettingsPath, ucSettingsDir };
|
|
65
68
|
}
|
|
66
|
-
args.push("--permission-mode", "bypassPermissions", "--tools", "default", "--max-turns", "50");
|
|
67
69
|
return { args };
|
|
68
70
|
}
|
|
69
71
|
else {
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
// subagent-mcp - Global Subagent MCP Config
|
|
2
|
-
// ------------------------------------------------------------------
|
|
3
|
-
// SOLE source of truth for machine-wide subagent-mcp defaults that must be
|
|
4
|
-
// available beside the compiled server.
|
|
5
|
-
//
|
|
6
|
-
// globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE
|
|
7
|
-
// across EVERY session, process, and user on this machine. There is NO
|
|
8
|
-
// environment-variable override for the cap.
|
|
9
|
-
//
|
|
10
|
-
// RE-READ on every launch_agent call - edits take effect immediately, no
|
|
11
|
-
// server restart required.
|
|
12
|
-
//
|
|
13
|
-
// Cap value rules:
|
|
14
|
-
// - missing / unset / non-integer / 0 / negative -> reset to default 20
|
|
15
|
-
// - 1 through 9 -> forced UP to minimum 10
|
|
16
|
-
// - 10 or greater -> used as-is
|
|
17
|
-
//
|
|
18
|
-
// checkForUpdates controls the silent npmjs update check started when the MCP
|
|
19
|
-
// server connects. Default true. Set to false to skip the registry fetch and
|
|
20
|
-
// suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.
|
|
21
|
-
//
|
|
22
|
-
// permissionsCeiling controls launch-time permissions posture:
|
|
23
|
-
// - auto (default): shared engine gates unsafe/residue actions
|
|
24
|
-
// - manual: human approval for residue while still auto-denying DANGER
|
|
25
|
-
// - yolo: preserve the historical bypass/danger-full-access path, except
|
|
26
|
-
// for non-bypassable config-file self-protection
|
|
27
|
-
//
|
|
28
|
-
// escalation applies only in auto mode. irreversible-only routes irreversible
|
|
29
|
-
// NEUTRAL residue to a human; off leaves residue to orchestrator judgment.
|
|
30
|
-
//
|
|
31
|
-
// strictReadParity controls logging only. Unparseable Codex approval payloads
|
|
32
|
-
// always fail closed to ask; warn logs the construct, off silences that log.
|
|
33
|
-
//
|
|
34
|
-
// sandboxNetwork controls Codex workspace-write network access. Default false.
|
|
35
|
-
// Set true only when sub-agents need network after permission approval.
|
|
36
|
-
{
|
|
37
|
-
"globalConcurrentSubagents": 20,
|
|
38
|
-
"checkForUpdates": true,
|
|
39
|
-
"permissionsCeiling": "auto",
|
|
40
|
-
"escalation": "irreversible-only",
|
|
41
|
-
"strictReadParity": "warn",
|
|
42
|
-
"sandboxNetwork": false
|
|
43
|
-
}
|
|
1
|
+
// subagent-mcp - Global Subagent MCP Config
|
|
2
|
+
// ------------------------------------------------------------------
|
|
3
|
+
// SOLE source of truth for machine-wide subagent-mcp defaults that must be
|
|
4
|
+
// available beside the compiled server.
|
|
5
|
+
//
|
|
6
|
+
// globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE
|
|
7
|
+
// across EVERY session, process, and user on this machine. There is NO
|
|
8
|
+
// environment-variable override for the cap.
|
|
9
|
+
//
|
|
10
|
+
// RE-READ on every launch_agent call - edits take effect immediately, no
|
|
11
|
+
// server restart required.
|
|
12
|
+
//
|
|
13
|
+
// Cap value rules:
|
|
14
|
+
// - missing / unset / non-integer / 0 / negative -> reset to default 20
|
|
15
|
+
// - 1 through 9 -> forced UP to minimum 10
|
|
16
|
+
// - 10 or greater -> used as-is
|
|
17
|
+
//
|
|
18
|
+
// checkForUpdates controls the silent npmjs update check started when the MCP
|
|
19
|
+
// server connects. Default true. Set to false to skip the registry fetch and
|
|
20
|
+
// suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.
|
|
21
|
+
//
|
|
22
|
+
// permissionsCeiling controls launch-time permissions posture:
|
|
23
|
+
// - auto (default): shared engine gates unsafe/residue actions
|
|
24
|
+
// - manual: human approval for residue while still auto-denying DANGER
|
|
25
|
+
// - yolo: preserve the historical bypass/danger-full-access path, except
|
|
26
|
+
// for non-bypassable config-file self-protection
|
|
27
|
+
//
|
|
28
|
+
// escalation applies only in auto mode. irreversible-only routes irreversible
|
|
29
|
+
// NEUTRAL residue to a human; off leaves residue to orchestrator judgment.
|
|
30
|
+
//
|
|
31
|
+
// strictReadParity controls logging only. Unparseable Codex approval payloads
|
|
32
|
+
// always fail closed to ask; warn logs the construct, off silences that log.
|
|
33
|
+
//
|
|
34
|
+
// sandboxNetwork controls Codex workspace-write network access. Default false.
|
|
35
|
+
// Set true only when sub-agents need network after permission approval.
|
|
36
|
+
{
|
|
37
|
+
"globalConcurrentSubagents": 20,
|
|
38
|
+
"checkForUpdates": true,
|
|
39
|
+
"permissionsCeiling": "auto",
|
|
40
|
+
"escalation": "irreversible-only",
|
|
41
|
+
"strictReadParity": "warn",
|
|
42
|
+
"sandboxNetwork": false
|
|
43
|
+
}
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
// subagent-mcp - Global Subagent MCP Config
|
|
2
|
-
// ------------------------------------------------------------------
|
|
3
|
-
// SOLE source of truth for machine-wide subagent-mcp defaults that must be
|
|
4
|
-
// available beside the compiled server.
|
|
5
|
-
//
|
|
6
|
-
// globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE
|
|
7
|
-
// across EVERY session, process, and user on this machine. There is NO
|
|
8
|
-
// environment-variable override for the cap.
|
|
9
|
-
//
|
|
10
|
-
// RE-READ on every launch_agent call - edits take effect immediately, no
|
|
11
|
-
// server restart required.
|
|
12
|
-
//
|
|
13
|
-
// Cap value rules:
|
|
14
|
-
// - missing / unset / non-integer / 0 / negative -> reset to default 20
|
|
15
|
-
// - 1 through 9 -> forced UP to minimum 10
|
|
16
|
-
// - 10 or greater -> used as-is
|
|
17
|
-
//
|
|
18
|
-
// checkForUpdates controls the silent npmjs update check started when the MCP
|
|
19
|
-
// server connects. Default true. Set to false to skip the registry fetch and
|
|
20
|
-
// suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.
|
|
21
|
-
//
|
|
22
|
-
// permissionsCeiling controls launch-time permissions posture:
|
|
23
|
-
// - auto (default): shared engine gates unsafe/residue actions
|
|
24
|
-
// - manual: human approval for residue while still auto-denying DANGER
|
|
25
|
-
// - yolo: preserve the historical bypass/danger-full-access path, except
|
|
26
|
-
// for non-bypassable config-file self-protection
|
|
27
|
-
//
|
|
28
|
-
// escalation applies only in auto mode. irreversible-only routes irreversible
|
|
29
|
-
// NEUTRAL residue to a human; off leaves residue to orchestrator judgment.
|
|
30
|
-
//
|
|
31
|
-
// strictReadParity controls logging only. Unparseable Codex approval payloads
|
|
32
|
-
// always fail closed to ask; warn logs the construct, off silences that log.
|
|
33
|
-
//
|
|
34
|
-
// sandboxNetwork controls Codex workspace-write network access. Default false.
|
|
35
|
-
// Set true only when sub-agents need network after permission approval.
|
|
36
|
-
{
|
|
37
|
-
"globalConcurrentSubagents": 20,
|
|
38
|
-
"checkForUpdates": true,
|
|
39
|
-
"permissionsCeiling": "auto",
|
|
40
|
-
"escalation": "irreversible-only",
|
|
41
|
-
"strictReadParity": "warn",
|
|
42
|
-
"sandboxNetwork": false
|
|
43
|
-
}
|
|
1
|
+
// subagent-mcp - Global Subagent MCP Config
|
|
2
|
+
// ------------------------------------------------------------------
|
|
3
|
+
// SOLE source of truth for machine-wide subagent-mcp defaults that must be
|
|
4
|
+
// available beside the compiled server.
|
|
5
|
+
//
|
|
6
|
+
// globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE
|
|
7
|
+
// across EVERY session, process, and user on this machine. There is NO
|
|
8
|
+
// environment-variable override for the cap.
|
|
9
|
+
//
|
|
10
|
+
// RE-READ on every launch_agent call - edits take effect immediately, no
|
|
11
|
+
// server restart required.
|
|
12
|
+
//
|
|
13
|
+
// Cap value rules:
|
|
14
|
+
// - missing / unset / non-integer / 0 / negative -> reset to default 20
|
|
15
|
+
// - 1 through 9 -> forced UP to minimum 10
|
|
16
|
+
// - 10 or greater -> used as-is
|
|
17
|
+
//
|
|
18
|
+
// checkForUpdates controls the silent npmjs update check started when the MCP
|
|
19
|
+
// server connects. Default true. Set to false to skip the registry fetch and
|
|
20
|
+
// suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.
|
|
21
|
+
//
|
|
22
|
+
// permissionsCeiling controls launch-time permissions posture:
|
|
23
|
+
// - auto (default): shared engine gates unsafe/residue actions
|
|
24
|
+
// - manual: human approval for residue while still auto-denying DANGER
|
|
25
|
+
// - yolo: preserve the historical bypass/danger-full-access path, except
|
|
26
|
+
// for non-bypassable config-file self-protection
|
|
27
|
+
//
|
|
28
|
+
// escalation applies only in auto mode. irreversible-only routes irreversible
|
|
29
|
+
// NEUTRAL residue to a human; off leaves residue to orchestrator judgment.
|
|
30
|
+
//
|
|
31
|
+
// strictReadParity controls logging only. Unparseable Codex approval payloads
|
|
32
|
+
// always fail closed to ask; warn logs the construct, off silences that log.
|
|
33
|
+
//
|
|
34
|
+
// sandboxNetwork controls Codex workspace-write network access. Default false.
|
|
35
|
+
// Set true only when sub-agents need network after permission approval.
|
|
36
|
+
{
|
|
37
|
+
"globalConcurrentSubagents": 20,
|
|
38
|
+
"checkForUpdates": true,
|
|
39
|
+
"permissionsCeiling": "auto",
|
|
40
|
+
"escalation": "irreversible-only",
|
|
41
|
+
"strictReadParity": "warn",
|
|
42
|
+
"sandboxNetwork": false
|
|
43
|
+
}
|
|
@@ -3,7 +3,7 @@ import { pathToFileURL } from "node:url";
|
|
|
3
3
|
import { claimAndEmit, classifyOwnerClaim, computeEffectiveActive, countJsonlType, cullHookZombies, ownerKey, runHook, TRANSCRIPT_READ_CAP, } from "../orchestration/hook-core.js";
|
|
4
4
|
import * as marker from "../orchestration/marker.js";
|
|
5
5
|
import { resolveContextWindow, } from "../orchestration/metering.js";
|
|
6
|
-
import {
|
|
6
|
+
import { isParentProcessMarkerFirstLine } from "../launch-prompt.js";
|
|
7
7
|
/**
|
|
8
8
|
* Codex CLI hook entry. Branches on payload.hook_event_name:
|
|
9
9
|
* - 'SessionStart' -> if active and not a subagent, emit FULL + the ON
|
|
@@ -174,7 +174,7 @@ export const codexAdapter = {
|
|
|
174
174
|
if (typeof source === "string" && SUBAGENT_SOURCE_STRINGS.has(source)) {
|
|
175
175
|
return true;
|
|
176
176
|
}
|
|
177
|
-
return
|
|
177
|
+
return isParentProcessMarkerFirstLine(payload.prompt);
|
|
178
178
|
},
|
|
179
179
|
// Count JSONL lines whose parsed object.type === 'turn_context'. Delegates to
|
|
180
180
|
// the bounded counter (reads at most the trailing window so a huge/
|
package/dist/index.js
CHANGED
|
@@ -393,10 +393,11 @@ function withMaintenance(handler, options = {}) {
|
|
|
393
393
|
}
|
|
394
394
|
export function classifyFailureReason(reason, stderr) {
|
|
395
395
|
const text = `${reason}\n${stderr}`;
|
|
396
|
-
return
|
|
396
|
+
return TRANSIENT_FAILURE_RE.test(text)
|
|
397
397
|
? "transient_provider"
|
|
398
398
|
: "permanent";
|
|
399
399
|
}
|
|
400
|
+
const TRANSIENT_FAILURE_RE = /\b429\b|\b(?:http(?:\/\d(?:\.\d)?)?|status|statuscode|status_code|code|error)\b[\s:=#-]*5\d{2}\b|quota|usage.?cap|rate.?limit|timeout|connection.?reset|ECONNRESET|ETIMEDOUT|ECONNREFUSED|too many requests|service unavailable|server error|overloaded/i;
|
|
400
401
|
function buildFailoverNote(skipped, winner) {
|
|
401
402
|
const top = skipped[0];
|
|
402
403
|
const topLabel = `${top.model}@${top.effort} (${top.provider})`;
|
|
@@ -438,10 +439,9 @@ export function isClaudeBackgroundWakeLine(line) {
|
|
|
438
439
|
return true;
|
|
439
440
|
if (marker.includes("taskoutput") && marker.includes("complete"))
|
|
440
441
|
return true;
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
|
|
444
|
-
return true;
|
|
442
|
+
// Only concrete Claude bg-task-complete markers should wake the parked SDK
|
|
443
|
+
// loop; unrelated post-turn JSONL activity is not a resume signal.
|
|
444
|
+
return false;
|
|
445
445
|
}
|
|
446
446
|
catch {
|
|
447
447
|
return false;
|
|
@@ -572,7 +572,7 @@ const ORCHESTRATION_INSTRUCTIONS = "subagent-mcp - CANONICAL OPERATING MODEL (fu
|
|
|
572
572
|
const SUBAGENT_INSTRUCTIONS = "SUB-AGENT SESSION: you are a child process launched by subagent-mcp. Follow the parent prompt. Do not treat yourself as the orchestrator, do not re-trigger orchestration carryover, and do not launch further sub-agents unless the parent prompt explicitly assigns that. launch_agent is code-capped at 2 spawn levels below the main orchestrator: depth 1 may launch depth 2 workers; depth 2 workers cannot spawn further.\n\nMODEL SELECTION MODE (parallel to orchestration-mode, set via the model-selection-mode tool). DEFAULT is \"smart\" and is used whenever unset: in smart, launch_agent REJECTS any call supplying provider/model/effort selectors and the server auto-picks the best model. \"user-approved-overrides\" opens a 30-MINUTE window where selectors are HONORED, enforced LAZILY (the mode reverts to smart on the next launch_agent call after 30 minutes) and re-enabling does NOT extend an active window. HONOR-BASED: you MUST NOT set \"user-approved-overrides\" without explicit interactive USER authorization via the structured-question tool (AskUserQuestion on Claude / request-user-input on Codex); never enable it on your own initiative.";
|
|
573
573
|
const server = new McpServer({
|
|
574
574
|
name: "subagent-mcp",
|
|
575
|
-
version: "2.12.
|
|
575
|
+
version: "2.12.15",
|
|
576
576
|
description: "Launches always-interactive local Claude and Codex sub-agent sessions and is the orchestrator's sole launch channel. Claude runs via the Claude Agent SDK over the local Claude Code executable; Codex via `codex app-server` over stdio. The server never calls Anthropic or OpenAI HTTP APIs directly.",
|
|
577
577
|
}, {
|
|
578
578
|
instructions: process.env.SUBAGENT_MCP_SUBAGENT === "1"
|
package/dist/launch-prompt.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// fail-safe-ON default from recursively orchestrating child sessions (fork-bomb
|
|
8
8
|
// prevention). Idempotent, silent, never mutates the prompt body.
|
|
9
9
|
export const MARKER = "<this is a request from a parent process>";
|
|
10
|
-
export function
|
|
10
|
+
export function isParentProcessMarkerFirstLine(prompt) {
|
|
11
11
|
if (typeof prompt !== "string")
|
|
12
12
|
return false;
|
|
13
13
|
const head = prompt.slice(0, 4096);
|
|
@@ -19,6 +19,7 @@ export function hasParentMarker(prompt) {
|
|
|
19
19
|
firstLine = firstLine.slice(1);
|
|
20
20
|
return firstLine.startsWith(MARKER);
|
|
21
21
|
}
|
|
22
|
+
export const hasParentMarker = isParentProcessMarkerFirstLine;
|
|
22
23
|
// Return `prompt` with MARKER guaranteed as its literal first line.
|
|
23
24
|
// - First line = position 0 up to the first "\n"; a trailing "\r" (CRLF) is
|
|
24
25
|
// stripped before comparison only.
|
|
@@ -29,7 +30,7 @@ export function hasParentMarker(prompt) {
|
|
|
29
30
|
// - Present (after BOM-strip) -> returned unchanged (no duplicate).
|
|
30
31
|
// - Absent -> MARKER + "\n" + prompt.
|
|
31
32
|
export function ensureParentMarker(prompt) {
|
|
32
|
-
if (
|
|
33
|
+
if (isParentProcessMarkerFirstLine(prompt))
|
|
33
34
|
return prompt;
|
|
34
35
|
return MARKER + "\n" + prompt;
|
|
35
36
|
}
|
|
@@ -65,8 +65,8 @@ export function resolveDirectivesDir(env) {
|
|
|
65
65
|
}
|
|
66
66
|
/** Read a directive asset by filename. On ANY failure return '' (fail-safe). */
|
|
67
67
|
export function readDirective(env, fileName) {
|
|
68
|
-
const directivesDir = resolveDirectivesDir(env);
|
|
69
68
|
try {
|
|
69
|
+
const directivesDir = resolveDirectivesDir(env);
|
|
70
70
|
return readFileSync(join(directivesDir, fileName), "utf8");
|
|
71
71
|
}
|
|
72
72
|
catch {
|
|
@@ -356,13 +356,6 @@ export function computeEffectiveActive(cwd, current, now, meteringUndetectableFa
|
|
|
356
356
|
*/
|
|
357
357
|
export function claimAndEmit(cwd, current, turn, m, kind, env, adapter, effectiveActive = true, phase = "normal", usedPercentage = null, updateNoticeSessionId, fullBodyFile) {
|
|
358
358
|
const firstCarryover = kind === "carryover" && !m.carryover_ack;
|
|
359
|
-
claimOwner(m, current, turn, Date.now());
|
|
360
|
-
if (kind === "carryover") {
|
|
361
|
-
m.provenance = "carried-over";
|
|
362
|
-
m.carryover_ack = true;
|
|
363
|
-
}
|
|
364
|
-
marker.writeMarker(cwd, m);
|
|
365
|
-
reminder.rebase(cwd, current, 0);
|
|
366
359
|
const full = fullBodyFile
|
|
367
360
|
? bodyFromDirective(readDirective(env, fullBodyFile))
|
|
368
361
|
: bodyFromDirective(readDirective(env, adapter.fullDirectiveFile)) +
|
|
@@ -385,6 +378,15 @@ export function claimAndEmit(cwd, current, turn, m, kind, env, adapter, effectiv
|
|
|
385
378
|
kind: firstCarryover ? "carryover" : "directive",
|
|
386
379
|
isLong: true,
|
|
387
380
|
};
|
|
381
|
+
if (emission.body.trim() === "")
|
|
382
|
+
return "";
|
|
383
|
+
claimOwner(m, current, turn, Date.now());
|
|
384
|
+
if (kind === "carryover") {
|
|
385
|
+
m.provenance = "carried-over";
|
|
386
|
+
m.carryover_ack = true;
|
|
387
|
+
}
|
|
388
|
+
marker.writeMarker(cwd, m);
|
|
389
|
+
reminder.rebase(cwd, current, 0);
|
|
388
390
|
return composeHookUpdateNotice(env, updateNoticeSessionId, emission, effectiveActive, phase, usedPercentage);
|
|
389
391
|
}
|
|
390
392
|
function hookCullDeps(env = process.env) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, statSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { atomicWriteFile } from "./atomic-write.js";
|
|
4
4
|
import { stateDir } from "./marker.js";
|
|
@@ -10,16 +10,37 @@ export function alivePath() {
|
|
|
10
10
|
export function touchAlive(now = Date.now()) {
|
|
11
11
|
try {
|
|
12
12
|
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
13
|
-
atomicWriteFile(alivePath(), `${now}\n`, {
|
|
13
|
+
atomicWriteFile(alivePath(), `${now}\npid=${process.pid}\n`, {
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
mode: 0o600,
|
|
16
|
+
});
|
|
14
17
|
}
|
|
15
18
|
catch {
|
|
16
19
|
// Hooks fail open when liveness cannot be observed.
|
|
17
20
|
}
|
|
18
21
|
}
|
|
22
|
+
function pidAlive(pid) {
|
|
23
|
+
try {
|
|
24
|
+
process.kill(pid, 0);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
19
31
|
export function serverAlive(now = Date.now()) {
|
|
20
32
|
try {
|
|
21
33
|
const s = statSync(alivePath());
|
|
22
|
-
|
|
34
|
+
if (now - s.mtimeMs > LIVENESS_TTL_MS)
|
|
35
|
+
return false;
|
|
36
|
+
const raw = readFileSync(alivePath(), "utf8");
|
|
37
|
+
const pid = raw
|
|
38
|
+
.split(/\r?\n/)
|
|
39
|
+
.map((line) => /^pid=(\d+)$/.exec(line)?.[1])
|
|
40
|
+
.find((value) => value !== undefined);
|
|
41
|
+
if (!pid)
|
|
42
|
+
return false;
|
|
43
|
+
return pidAlive(Number(pid));
|
|
23
44
|
}
|
|
24
45
|
catch {
|
|
25
46
|
return false;
|
|
@@ -14,16 +14,17 @@ export const ANON_PREFIX = "anon-";
|
|
|
14
14
|
export const stateDir = markerDir;
|
|
15
15
|
/**
|
|
16
16
|
* Canonicalize a working directory so two spellings of the same path hash
|
|
17
|
-
* identically. Strip
|
|
18
|
-
*
|
|
19
|
-
* resolve
|
|
20
|
-
*
|
|
21
|
-
* forward slashes; lowercase on win32 (the FS is case-insensitive there); drop
|
|
22
|
-
* a trailing slash.
|
|
17
|
+
* identically. Strip Windows extended-length prefixes first on the raw input.
|
|
18
|
+
* Handle \\?\UNC\ before the generic \\?\ form so UNC paths keep their network
|
|
19
|
+
* root. Then resolve() to an absolute path; use forward slashes; lowercase on
|
|
20
|
+
* win32 (the FS is case-insensitive there); drop a trailing slash.
|
|
23
21
|
*/
|
|
24
22
|
export function normalizeCwd(cwd) {
|
|
25
23
|
let raw = cwd;
|
|
26
|
-
if (raw.startsWith("\\\\?\\")) {
|
|
24
|
+
if (raw.startsWith("\\\\?\\UNC\\")) {
|
|
25
|
+
raw = "\\\\" + raw.slice(8);
|
|
26
|
+
}
|
|
27
|
+
else if (raw.startsWith("\\\\?\\")) {
|
|
27
28
|
raw = raw.slice(4);
|
|
28
29
|
}
|
|
29
30
|
let p = resolve(raw);
|
|
@@ -60,50 +61,12 @@ export function disablePath(sessionKey) {
|
|
|
60
61
|
export function enablePath(sessionKey) {
|
|
61
62
|
return join(stateDir, `orch-enable-${hashKey(sessionKey)}.json`);
|
|
62
63
|
}
|
|
63
|
-
function cwdDisablePath(cwd) {
|
|
64
|
-
return join(stateDir, `orch-disable-${cwdHash(cwd)}.json`);
|
|
65
|
-
}
|
|
66
64
|
export function sessionPointerPath(cwd) {
|
|
67
65
|
return join(stateDir, `orch-session-${cwdHash(cwd)}.json`);
|
|
68
66
|
}
|
|
69
67
|
export function serverSessionPointerPath(cwd, serverKey = process.ppid) {
|
|
70
68
|
return join(stateDir, `orch-session-${cwdHash(cwd)}-${hashKey(String(serverKey))}.json`);
|
|
71
69
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Enable orchestration for cwd. ALWAYS overwrites — re-enabling re-baselines by
|
|
74
|
-
* clearing owner_session/baseline_turn back to null so the next hook turn
|
|
75
|
-
* re-claims and re-baselines.
|
|
76
|
-
*/
|
|
77
|
-
export function enable(cwd) {
|
|
78
|
-
try {
|
|
79
|
-
// Restrictive POSIX perms: the marker dir/file live in the shared,
|
|
80
|
-
// world-readable /tmp on Linux/macOS and persist a session_id. mode 0o700/
|
|
81
|
-
// 0o600 keeps them owner-only so other local users cannot read the
|
|
82
|
-
// session_id or enumerate which projects have orchestration enabled. mode is
|
|
83
|
-
// ignored on Windows (harmless; tmpdir is already per-user there).
|
|
84
|
-
mkdirSync(markerDir, { recursive: true, mode: 0o700 });
|
|
85
|
-
const state = {
|
|
86
|
-
owner_session: null,
|
|
87
|
-
baseline_turn: null,
|
|
88
|
-
claimed_at: null,
|
|
89
|
-
owners: {},
|
|
90
|
-
provenance: "user-enabled",
|
|
91
|
-
carryover_ack: false,
|
|
92
|
-
};
|
|
93
|
-
atomicWriteJson(markerPath(cwd), state, { encoding: "utf8", mode: 0o600 });
|
|
94
|
-
try {
|
|
95
|
-
unlinkSync(cwdDisablePath(cwd));
|
|
96
|
-
}
|
|
97
|
-
catch (e) {
|
|
98
|
-
if (e?.code !== "ENOENT") {
|
|
99
|
-
// Fail-safe: enable still succeeds if stale disable cleanup fails.
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
// Fail-safe: never throw to the caller.
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
70
|
export function writeDisable(sessionKey) {
|
|
108
71
|
if (!isSessionScopedKey(sessionKey))
|
|
109
72
|
return;
|
|
@@ -300,7 +263,7 @@ export function readMarker(cwd) {
|
|
|
300
263
|
}
|
|
301
264
|
export function writeMarker(cwd, obj) {
|
|
302
265
|
try {
|
|
303
|
-
// Owner-only perms
|
|
266
|
+
// Owner-only perms: the marker persists owner_session.
|
|
304
267
|
mkdirSync(markerDir, { recursive: true, mode: 0o700 });
|
|
305
268
|
atomicWriteJson(markerPath(cwd), obj, { encoding: "utf8", mode: 0o600 });
|
|
306
269
|
}
|
|
@@ -94,7 +94,7 @@ export function setMode(cwd, mode, now = Date.now()) {
|
|
|
94
94
|
}
|
|
95
95
|
export function gateLaunch(cwd, selectors, now = Date.now()) {
|
|
96
96
|
const r = resolveMode(cwd, now);
|
|
97
|
-
const supplied =
|
|
97
|
+
const supplied = [selectors.provider, selectors.model, selectors.effort].some((value) => value !== undefined);
|
|
98
98
|
if (r.mode === "user-approved-overrides") {
|
|
99
99
|
return { allowed: true, mode: r.mode, reverted: r.reverted };
|
|
100
100
|
}
|