@heretyc/subagent-mcp 2.12.1 → 2.12.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.
- package/README.md +5 -0
- package/dist/advanced-ruleset.py +2 -2
- package/dist/concurrency.js +20 -0
- package/dist/config-scaffold.js +1 -1
- package/dist/effort.js +3 -1
- package/dist/global-concurrency.jsonc +7 -2
- package/dist/hooks/orchestration-codex.js +5 -5
- package/dist/index.js +8 -6
- package/dist/orchestration/hook-core.js +13 -13
- package/dist/orchestration/pretool.js +8 -8
- package/dist/orchestration/update-check.js +174 -0
- package/dist/routing.js +6 -3
- package/dist/ruleset-scaffold.js +1 -1
- package/dist/ruleset.js +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -124,6 +124,11 @@ queue. You change the number in the `global-concurrency.jsonc` file in the
|
|
|
124
124
|
install folder (minimum 10); the file is re-read on every launch, so no restart
|
|
125
125
|
is needed.
|
|
126
126
|
|
|
127
|
+
The same settings file includes `checkForUpdates` (default `true`). When a newer
|
|
128
|
+
npm version exists, the per-turn hook can show a throttled notice to run
|
|
129
|
+
`subagent-mcp update` and then `subagent-mcp setup`. Set `checkForUpdates` to
|
|
130
|
+
`false`, or run with `SUBAGENT_UPDATE_CHECK=0` / `false`, to disable that check.
|
|
131
|
+
|
|
127
132
|
## Basic debugging
|
|
128
133
|
|
|
129
134
|
- **"An agent looks stuck."** A quiet agent is usually **still alive**, not
|
package/dist/advanced-ruleset.py
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
{"provider": "claude", "model": "sonnet", "effort": "high", "rank": 1},
|
|
13
13
|
{"provider": "codex", "model": "gpt-5.5", "effort": "xhigh", "rank": 2}
|
|
14
14
|
]
|
|
15
|
-
Valid providers: claude, codex. Valid models: haiku, sonnet, opus, opus-4-8 (claude);
|
|
15
|
+
Valid providers: claude, codex. Valid models: haiku, sonnet, opus, opus-4-8, fable (claude);
|
|
16
16
|
gpt-5.5 (codex). Valid efforts: haiku -> "none" only; sonnet -> medium|high|xhigh|max;
|
|
17
|
-
opus/opus-4-8 -> those plus ultracode; gpt-5.5 -> medium|high|xhigh.
|
|
17
|
+
fable -> medium|high|xhigh|max; opus/opus-4-8 -> those plus ultracode; gpt-5.5 -> medium|high|xhigh.
|
|
18
18
|
"rank" on output is ignored. An EMPTY array vetoes the launch. Anything else
|
|
19
19
|
invalid fails the launch hard — the server validates strictly.
|
|
20
20
|
|
package/dist/concurrency.js
CHANGED
|
@@ -6,6 +6,7 @@ import { CONCURRENCY_SCAFFOLD } from "./config-scaffold.js";
|
|
|
6
6
|
import { cullStaleSlots, ZOMBIE_FORCE_GRACE_MS, ZOMBIE_LIVE_IDLE_MS, ZOMBIE_TERMINAL_IDLE_MS, buildProcessTreeKillCommands, drainZombieIntents, drainZombieReports, parseSlotMetadata, readSlotMetadata, slotPathForAgent, writeSlotMetadata, } from "./zombie.js";
|
|
7
7
|
export const DEFAULT_CAP = 20;
|
|
8
8
|
export const MIN_CAP = 10;
|
|
9
|
+
export const DEFAULT_CHECK_FOR_UPDATES = true;
|
|
9
10
|
export const CONFIG_FILENAME = "global-concurrency.jsonc";
|
|
10
11
|
export { cullStaleSlots, ZOMBIE_FORCE_GRACE_MS, ZOMBIE_LIVE_IDLE_MS, ZOMBIE_TERMINAL_IDLE_MS, buildProcessTreeKillCommands, drainZombieIntents, drainZombieReports, parseSlotMetadata, readSlotMetadata, slotPathForAgent, writeSlotMetadata, };
|
|
11
12
|
export function clampCap(raw) {
|
|
@@ -31,6 +32,16 @@ export function parseConcurrencyConfig(text) {
|
|
|
31
32
|
}
|
|
32
33
|
return clampCap(raw);
|
|
33
34
|
}
|
|
35
|
+
export function parseCheckForUpdatesConfig(text) {
|
|
36
|
+
try {
|
|
37
|
+
const raw = JSON.parse(stripJsoncComments(text))
|
|
38
|
+
?.checkForUpdates;
|
|
39
|
+
return typeof raw === "boolean" ? raw : DEFAULT_CHECK_FOR_UPDATES;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return DEFAULT_CHECK_FOR_UPDATES;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
34
45
|
export function defaultConfigPath() {
|
|
35
46
|
return fileURLToPath(new URL("./" + CONFIG_FILENAME, import.meta.url));
|
|
36
47
|
}
|
|
@@ -51,6 +62,15 @@ export function readGlobalCap(path = defaultConfigPath()) {
|
|
|
51
62
|
return DEFAULT_CAP;
|
|
52
63
|
}
|
|
53
64
|
}
|
|
65
|
+
export function readCheckForUpdates(path = defaultConfigPath()) {
|
|
66
|
+
try {
|
|
67
|
+
ensureConcurrencyConfig(path);
|
|
68
|
+
return parseCheckForUpdatesConfig(readFileSync(path, "utf8"));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return DEFAULT_CHECK_FOR_UPDATES;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
54
74
|
export function slotDir() {
|
|
55
75
|
if (process.env.SUBAGENT_SLOT_DIR)
|
|
56
76
|
return process.env.SUBAGENT_SLOT_DIR;
|
package/dist/config-scaffold.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// GENERATED by scripts/gen-ruleset-scaffold.mjs from src/global-concurrency.jsonc — DO NOT EDIT.
|
|
2
|
-
export const CONCURRENCY_SCAFFOLD = "// subagent-mcp — Global Concurrent Subagent Cap\n// ------------------------------------------------------------------\n// SOLE source of truth for the machine-wide limit on how many subagents\n// may be ALIVE AT ONCE across EVERY session, process, and user on this\n// machine. There is NO environment-variable override.\n//\n// The whole recursive descendant tree counts toward this ONE number: a\n// subagent that itself launches subagents adds to the same machine-wide\n// total, and OTHER active agentic sessions count too.\n//\n// RE-READ on every launch_agent call — edits take effect immediately, no\n// server restart required.\n//\n// Value rules (forcibly applied to the number below):\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// Zombie culling is always enabled. There is no config knob. Before cap\n// rejection, launch/tool/hook paths refresh live owned slots, preserve stale\n// slots whose owner server is still alive, and cull stale slots whose owner is\n// gone. Managed stale slots terminate the child process tree, then force-kill\n// after 20s when needed; unmanaged stale slots are only unlinked.\n//\n// When the cap is reached after culling, launch_agent is REJECTED (never\n// queued). Free a slot with list_agents + kill_agent, then retry.\n{\n \"globalConcurrentSubagents\": 20\n}\n";
|
|
2
|
+
export const CONCURRENCY_SCAFFOLD = "// subagent-mcp — Global Concurrent Subagent Cap\n// ------------------------------------------------------------------\n// SOLE source of truth for the machine-wide limit on how many subagents\n// may be ALIVE AT ONCE across EVERY session, process, and user on this\n// machine. There is NO environment-variable override for the cap.\n//\n// The whole recursive descendant tree counts toward this ONE number: a\n// subagent that itself launches subagents adds to the same machine-wide\n// total, and OTHER active agentic sessions count too.\n//\n// RE-READ on every launch_agent call — edits take effect immediately, no\n// server restart required.\n//\n// Value rules (forcibly applied to the number below):\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// Zombie culling is always enabled. There is no config knob. Before cap\n// rejection, launch/tool/hook paths refresh live owned slots, preserve stale\n// slots whose owner server is still alive, and cull stale slots whose owner is\n// gone. Managed stale slots terminate the child process tree, then force-kill\n// after 20s when needed; unmanaged stale slots are only unlinked.\n//\n// When the cap is reached after culling, launch_agent is REJECTED (never\n// queued). Free a slot with list_agents + kill_agent, then retry.\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 \"globalConcurrentSubagents\": 20,\n \"checkForUpdates\": true\n}\n";
|
package/dist/effort.js
CHANGED
|
@@ -6,6 +6,8 @@ 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 === "fable")
|
|
10
|
+
return "claude-fable-5";
|
|
9
11
|
return model; // haiku, sonnet as-is
|
|
10
12
|
}
|
|
11
13
|
else {
|
|
@@ -26,7 +28,7 @@ export function resolveEffort(provider, model, effort) {
|
|
|
26
28
|
if (provider === "claude" && model === "haiku") {
|
|
27
29
|
return { kind: "none" };
|
|
28
30
|
}
|
|
29
|
-
if (provider === "claude" && ["sonnet", "opus", "opus-4-8"].includes(model)) {
|
|
31
|
+
if (provider === "claude" && ["sonnet", "opus", "opus-4-8", "fable"].includes(model)) {
|
|
30
32
|
if (["medium", "high", "xhigh", "max"].includes(effort)) {
|
|
31
33
|
return { kind: "flag", value: effort };
|
|
32
34
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// ------------------------------------------------------------------
|
|
3
3
|
// SOLE source of truth for the machine-wide limit on how many subagents
|
|
4
4
|
// may be ALIVE AT ONCE across EVERY session, process, and user on this
|
|
5
|
-
// machine. There is NO environment-variable override.
|
|
5
|
+
// machine. There is NO environment-variable override for the cap.
|
|
6
6
|
//
|
|
7
7
|
// The whole recursive descendant tree counts toward this ONE number: a
|
|
8
8
|
// subagent that itself launches subagents adds to the same machine-wide
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
//
|
|
25
25
|
// When the cap is reached after culling, launch_agent is REJECTED (never
|
|
26
26
|
// queued). Free a slot with list_agents + kill_agent, then retry.
|
|
27
|
+
//
|
|
28
|
+
// checkForUpdates controls the silent npmjs update check started when the MCP
|
|
29
|
+
// server connects. Default true. Set to false to skip the registry fetch and
|
|
30
|
+
// suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.
|
|
27
31
|
{
|
|
28
|
-
"globalConcurrentSubagents": 20
|
|
32
|
+
"globalConcurrentSubagents": 20,
|
|
33
|
+
"checkForUpdates": true
|
|
29
34
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { realpathSync } from "node:fs";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
|
-
import { claimAndEmit,
|
|
3
|
+
import { claimAndEmit, classifyClaim, countJsonlType, cullHookZombies, runHook, sessionKey, } from "../orchestration/hook-core.js";
|
|
4
4
|
import * as marker from "../orchestration/marker.js";
|
|
5
5
|
/**
|
|
6
6
|
* Codex CLI hook entry. Branches on payload.hook_event_name:
|
|
@@ -80,13 +80,13 @@ export const codexAdapter = {
|
|
|
80
80
|
export function runCodexHook(payload, env, adapter = codexAdapter) {
|
|
81
81
|
try {
|
|
82
82
|
if (payload.hook_event_name === "SessionStart") {
|
|
83
|
-
|
|
83
|
+
cullHookZombies();
|
|
84
84
|
if (adapter.isSubagent(payload, env)) {
|
|
85
|
-
return
|
|
85
|
+
return "";
|
|
86
86
|
}
|
|
87
87
|
const cwd = payload.cwd || process.cwd();
|
|
88
88
|
if (!marker.isActive(cwd)) {
|
|
89
|
-
return
|
|
89
|
+
return "";
|
|
90
90
|
}
|
|
91
91
|
const current = sessionKey(payload);
|
|
92
92
|
const turn = adapter.currentTurn(payload.transcript_path);
|
|
@@ -96,7 +96,7 @@ export function runCodexHook(payload, env, adapter = codexAdapter) {
|
|
|
96
96
|
// semantics — FULL + ON reminder, ack-latched CARRYOVER prepend, counter
|
|
97
97
|
// re-baseline). SessionStart claims even on SAME-SESSION (resume) so
|
|
98
98
|
// turn 0 is always covered.
|
|
99
|
-
return
|
|
99
|
+
return claimAndEmit(cwd, current, turn, m, kind, env, adapter);
|
|
100
100
|
}
|
|
101
101
|
// UserPromptSubmit (and any other event) -> normal cadence.
|
|
102
102
|
return runHook(payload, env, adapter);
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { CONFIG_FILENAME, NONBLOCKING_CULL_DEPS, ZOMBIE_FORCE_GRACE_MS, ZOMBIE_L
|
|
|
22
22
|
import * as orchestrationMarker from "./orchestration/marker.js";
|
|
23
23
|
import * as modelMode from "./orchestration/model-mode.js";
|
|
24
24
|
import { startLivenessHeartbeat } from "./orchestration/liveness.js";
|
|
25
|
+
import { checkForNpmUpdate } from "./orchestration/update-check.js";
|
|
25
26
|
import { ensureParentMarker } from "./launch-prompt.js";
|
|
26
27
|
const agents = new Map();
|
|
27
28
|
const deadlockWindow = createDeadlockWindow();
|
|
@@ -263,7 +264,7 @@ function withMaintenance(handler, options = {}) {
|
|
|
263
264
|
return async (params) => {
|
|
264
265
|
const zombieRecords = runToolMaintenance();
|
|
265
266
|
const result = await handler(params, zombieRecords);
|
|
266
|
-
if (options.
|
|
267
|
+
if (!options.includeZombieReport)
|
|
267
268
|
return result;
|
|
268
269
|
if (result && typeof result === "object" && "content" in result) {
|
|
269
270
|
return withZombieReport(result, zombieRecords);
|
|
@@ -441,7 +442,7 @@ const ORCHESTRATION_INSTRUCTIONS = "subagent-mcp - CANONICAL OPERATING MODEL (fu
|
|
|
441
442
|
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.\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.";
|
|
442
443
|
const server = new McpServer({
|
|
443
444
|
name: "subagent-mcp",
|
|
444
|
-
version: "2.12.
|
|
445
|
+
version: "2.12.2",
|
|
445
446
|
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.",
|
|
446
447
|
}, {
|
|
447
448
|
instructions: process.env.SUBAGENT_MCP_SUBAGENT === "1"
|
|
@@ -800,7 +801,7 @@ server.tool("launch_agent", "Spawn a sub-agent session. CONTRACT: every `prompt`
|
|
|
800
801
|
task_category: z.enum(TASK_CATEGORIES).describe(TASK_CATEGORY_GLOSS),
|
|
801
802
|
prompt: z.string().min(1),
|
|
802
803
|
provider: z.enum(["claude", "codex"]).optional(),
|
|
803
|
-
model: z.enum(["haiku", "sonnet", "opus", "opus-4-8", "gpt-5.5"]).optional(),
|
|
804
|
+
model: z.enum(["haiku", "sonnet", "opus", "opus-4-8", "fable", "gpt-5.5"]).optional(),
|
|
804
805
|
effort: z.enum(["medium", "high", "xhigh", "max", "ultracode"]).optional(),
|
|
805
806
|
cwd: z.string().optional(),
|
|
806
807
|
deadlock: z.boolean().optional().describe("MANDATE: ALWAYS set deadlock=true when, and ONLY when, 2 launch attempts for the SAME atomic task have already failed or been unsatisfactory — the 3rd attempt onward. Re-wording the prompt does NOT make it a different task; splitting a failed task does NOT reset attempts for its unchanged parts; re-launching for the same deliverable means the prior attempt COUNTS as failed/unsatisfactory ('partial progress' is not an exemption). NEVER set it on a 1st or 2nd attempt, NEVER for a different task, NEVER speculatively. Auto mode only: cannot be combined with provider/model/effort — from the 3rd attempt deadlock outranks any capability override, so drop those params. Passing false is identical to omitting it."),
|
|
@@ -1008,7 +1009,7 @@ server.tool("launch_agent", "Spawn a sub-agent session. CONTRACT: every `prompt`
|
|
|
1008
1009
|
.map((s, i) => ` ${i + 1}. ${s.model}@${s.effort} (${s.provider}) [${s.failure_type}]: ${s.reason}`)
|
|
1009
1010
|
.join("\n");
|
|
1010
1011
|
return errorResult(`Error: all ${skipped.length} candidate launches failed for task_category ${task_category}:\n${lines}\n${SPLIT_HINT}\n${AUTO_HINT}`);
|
|
1011
|
-
}
|
|
1012
|
+
}) // ponytail: launch_agent still reaps, but callers do not receive zombie_report.
|
|
1012
1013
|
);
|
|
1013
1014
|
// Tool 2: poll_agent
|
|
1014
1015
|
server.tool("poll_agent", "Get an agent's current status and output. `processing` = ALIVE with visible provider activity in the last 10 min; `stalled` = ALIVE but no visible provider stream item for 10 min (thinking, or awaiting a temp-file handoff) — NOT dead, so prefer `wait`/re-poll over killing. Always returns `alive` + `idle_seconds`, plus `recent_stream` (last 3 timestamped visible stream items) and a `hint` while stalled. `verbose: true` also returns `final_output`, the agent's final assistant turn from its captured stdout.", {
|
|
@@ -1080,7 +1081,7 @@ server.tool("poll_agent", "Get an agent's current status and output. `processing
|
|
|
1080
1081
|
},
|
|
1081
1082
|
],
|
|
1082
1083
|
};
|
|
1083
|
-
}));
|
|
1084
|
+
}, { includeZombieReport: true }));
|
|
1084
1085
|
// Tool 3: kill_agent
|
|
1085
1086
|
server.tool("kill_agent", "Terminate a live agent/session (status `processing`, `stalled`, or turn-finished but still interactive) by immediately force-killing its managed driver. No-op for already-terminal closed agents.", {
|
|
1086
1087
|
agent_id: z.string(),
|
|
@@ -1257,7 +1258,7 @@ server.tool("list_agents", "List all agents with token-efficient core metrics (s
|
|
|
1257
1258
|
},
|
|
1258
1259
|
],
|
|
1259
1260
|
};
|
|
1260
|
-
}));
|
|
1261
|
+
}, { includeZombieReport: true }));
|
|
1261
1262
|
// Tool 6: wait
|
|
1262
1263
|
server.tool("wait", "Blocks until one or more sub-agents reach a reportable state (turn-finished, errored, stopped, or zombie_killed), returning exit code when known + local-time timestamp; or returns the live-job list after a 15-minute timeout. This is how you learn an agent finished — do NOT poll-loop. A `finished` agent with null exit_code is still alive and accepts `send_message`; a `stalled` agent is still ALIVE and does NOT end the wait. `verbose: true` adds each finished agent's `final_output`.", {
|
|
1263
1264
|
verbose: z.boolean().optional().default(false),
|
|
@@ -1598,6 +1599,7 @@ if (isMain) {
|
|
|
1598
1599
|
// (the tool's enabled:false writes a disable record via writeDisable /
|
|
1599
1600
|
// writeDisableCwd; it does not call disable().)
|
|
1600
1601
|
getNpmPrefix();
|
|
1602
|
+
void checkForNpmUpdate().catch(() => { });
|
|
1601
1603
|
startLivenessHeartbeat();
|
|
1602
1604
|
const transport = new StdioServerTransport();
|
|
1603
1605
|
await server.connect(transport);
|
|
@@ -5,6 +5,7 @@ import { dirname, isAbsolute, join } from "node:path";
|
|
|
5
5
|
import * as marker from "./marker.js";
|
|
6
6
|
import * as reminder from "./reminder.js";
|
|
7
7
|
import { cullStaleSlots, slotDir, ZOMBIE_FORCE_GRACE_MS, } from "../concurrency.js";
|
|
8
|
+
import { appendUpdateNotice, readInstalledPackageInfo, } from "./update-check.js";
|
|
8
9
|
/**
|
|
9
10
|
* Provider-agnostic core of the UserPromptSubmit / SessionStart hook.
|
|
10
11
|
*
|
|
@@ -240,15 +241,13 @@ export function cullHookZombies(deps = hookCullDeps()) {
|
|
|
240
241
|
return [];
|
|
241
242
|
}
|
|
242
243
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const report = hookZombieReportText(records);
|
|
249
|
-
if (!report)
|
|
244
|
+
function appendHookUpdateNotice(out, current, env) {
|
|
245
|
+
try {
|
|
246
|
+
return appendUpdateNotice(out, readInstalledPackageInfo().version, current, env);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
250
249
|
return out;
|
|
251
|
-
|
|
250
|
+
}
|
|
252
251
|
}
|
|
253
252
|
/**
|
|
254
253
|
* Core hook logic. Returns the string to inject, or '' to inject nothing.
|
|
@@ -269,28 +268,29 @@ export function appendHookZombieReport(out, records) {
|
|
|
269
268
|
*/
|
|
270
269
|
export function runHook(payload, env, adapter) {
|
|
271
270
|
try {
|
|
272
|
-
|
|
271
|
+
cullHookZombies();
|
|
273
272
|
if (adapter.isSubagent(payload, env)) {
|
|
274
|
-
return
|
|
273
|
+
return "";
|
|
275
274
|
}
|
|
276
275
|
const cwd = payload.cwd || process.cwd();
|
|
277
276
|
const current = sessionKey(payload);
|
|
277
|
+
const updateNoticeSessionId = typeof payload.session_id === "string" ? payload.session_id : undefined;
|
|
278
278
|
if (current)
|
|
279
279
|
marker.writeCurrentSession(cwd, current);
|
|
280
280
|
if (!marker.isActive(cwd, current)) {
|
|
281
281
|
// OFF: no claim machinery — just the per-prompt reminder cadence.
|
|
282
282
|
const r = reminder.advance(cwd, current);
|
|
283
|
-
return
|
|
283
|
+
return appendHookUpdateNotice(cadenceEmit(env, adapter, adapter.reminderOffFile, adapter.shortOffFile, r.count, r.persisted), updateNoticeSessionId, env);
|
|
284
284
|
}
|
|
285
285
|
const m = marker.readMarker(cwd);
|
|
286
286
|
const kind = classifyClaim(m.owner_session, m.baseline_turn, current);
|
|
287
287
|
if (kind === "fresh" || kind === "carryover") {
|
|
288
288
|
const turn = adapter.currentTurn(payload.transcript_path);
|
|
289
|
-
return
|
|
289
|
+
return appendHookUpdateNotice(claimAndEmit(cwd, current, turn, m, kind, env, adapter), updateNoticeSessionId, env);
|
|
290
290
|
}
|
|
291
291
|
// SAME-SESSION: per-prompt reminder cadence, ON variant.
|
|
292
292
|
const r = reminder.advance(cwd, current);
|
|
293
|
-
return
|
|
293
|
+
return appendHookUpdateNotice(cadenceEmit(env, adapter, adapter.reminderOnFile, adapter.shortOnFile, r.count, r.persisted), updateNoticeSessionId, env);
|
|
294
294
|
}
|
|
295
295
|
catch {
|
|
296
296
|
// Any failure -> inject nothing. Never crash or stall the host turn.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { serverAlive } from "./liveness.js";
|
|
2
|
-
import { cullHookZombies
|
|
2
|
+
import { cullHookZombies } from "./hook-core.js";
|
|
3
3
|
const NATIVE_SUBAGENT_TOOLS = new Set(["Task", "Agent", "Explore"]);
|
|
4
4
|
function decision(permissionDecision, permissionDecisionReason, additionalContext) {
|
|
5
5
|
return {
|
|
@@ -22,21 +22,21 @@ function decision(permissionDecision, permissionDecisionReason, additionalContex
|
|
|
22
22
|
*/
|
|
23
23
|
export function runClaudePreTool(payload, env, now = Date.now()) {
|
|
24
24
|
try {
|
|
25
|
-
const
|
|
25
|
+
const zombieRecords = cullHookZombies();
|
|
26
26
|
if (env.SUBAGENT_MCP_SUBAGENT === "1")
|
|
27
27
|
return null;
|
|
28
|
-
const
|
|
29
|
-
? decision("allow", "
|
|
28
|
+
const maintenanceAllowedDecision = zombieRecords.length > 0
|
|
29
|
+
? decision("allow", "maintenance completed; allowing requested tool.")
|
|
30
30
|
: null;
|
|
31
31
|
if (!serverAlive(now))
|
|
32
|
-
return
|
|
32
|
+
return maintenanceAllowedDecision;
|
|
33
33
|
const tool = typeof payload.tool_name === "string" ? payload.tool_name : "";
|
|
34
34
|
if (!tool)
|
|
35
|
-
return
|
|
35
|
+
return maintenanceAllowedDecision;
|
|
36
36
|
if (NATIVE_SUBAGENT_TOOLS.has(tool)) {
|
|
37
|
-
return decision("deny", "subagent-mcp is alive; harness-native Task/Agent/Explore is not the sanctioned sub-agent channel. Use the subagent-mcp launch_agent MCP tool with the parent-process sentinel as prompt line 1."
|
|
37
|
+
return decision("deny", "subagent-mcp is alive; harness-native Task/Agent/Explore is not the sanctioned sub-agent channel. Use the subagent-mcp launch_agent MCP tool with the parent-process sentinel as prompt line 1.");
|
|
38
38
|
}
|
|
39
|
-
return
|
|
39
|
+
return maintenanceAllowedDecision;
|
|
40
40
|
}
|
|
41
41
|
catch {
|
|
42
42
|
return null;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readCheckForUpdates } from "../concurrency.js";
|
|
4
|
+
import { stateDir } from "./marker.js";
|
|
5
|
+
export const UPDATE_NOTICE_TEXT = "Notice: An improved version of subagent-mcp is available via the CLI command `subagent-mcp update` and can then be fully installed with `subagent-mcp setup`. This will fix security issues and improve user experience.";
|
|
6
|
+
export const UPDATE_CHECK_TIMEOUT_MS = 2500;
|
|
7
|
+
export const UPDATE_NOTICE_INTERVAL_MS = 12 * 60 * 60 * 1000;
|
|
8
|
+
function pendingNoticePath() {
|
|
9
|
+
return join(stateDir, "update-notice.json");
|
|
10
|
+
}
|
|
11
|
+
function emitRecordPath() {
|
|
12
|
+
return join(stateDir, "update-notice-emitted.json");
|
|
13
|
+
}
|
|
14
|
+
function atomicWriteJson(path, value) {
|
|
15
|
+
try {
|
|
16
|
+
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
17
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
18
|
+
writeFileSync(tmp, JSON.stringify(value), { encoding: "utf8", mode: 0o600 });
|
|
19
|
+
renameSync(tmp, path);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Fail-safe: update notices must never affect the host turn or MCP channel.
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function readJson(path) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function removeFile(path) {
|
|
34
|
+
try {
|
|
35
|
+
rmSync(path, { force: true });
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Fail-safe.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function clearUpdateNoticeState() {
|
|
42
|
+
removeFile(pendingNoticePath());
|
|
43
|
+
removeFile(emitRecordPath());
|
|
44
|
+
}
|
|
45
|
+
export function writePendingUpdateNotice(latestVersion, now = Date.now()) {
|
|
46
|
+
atomicWriteJson(pendingNoticePath(), {
|
|
47
|
+
latest_version: latestVersion,
|
|
48
|
+
checked_at: new Date(now).toISOString(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export function readPendingUpdateNotice() {
|
|
52
|
+
const parsed = readJson(pendingNoticePath());
|
|
53
|
+
if (parsed &&
|
|
54
|
+
typeof parsed.latest_version === "string" &&
|
|
55
|
+
typeof parsed.checked_at === "string") {
|
|
56
|
+
return { latest_version: parsed.latest_version, checked_at: parsed.checked_at };
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
export function readUpdateNoticeEmitRecord() {
|
|
61
|
+
const parsed = readJson(emitRecordPath());
|
|
62
|
+
if (!parsed || typeof parsed.notified_at !== "number")
|
|
63
|
+
return undefined;
|
|
64
|
+
return {
|
|
65
|
+
notified_at: parsed.notified_at,
|
|
66
|
+
...(typeof parsed.session_id === "string" ? { session_id: parsed.session_id } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function writeUpdateNoticeEmitRecord(record) {
|
|
70
|
+
atomicWriteJson(emitRecordPath(), record);
|
|
71
|
+
}
|
|
72
|
+
export function updateCheckEnvDisabled(env = process.env) {
|
|
73
|
+
const raw = env.SUBAGENT_UPDATE_CHECK;
|
|
74
|
+
return typeof raw === "string" && /^(?:0|false)$/i.test(raw.trim());
|
|
75
|
+
}
|
|
76
|
+
export function shouldCheckForUpdates(env = process.env, configPath) {
|
|
77
|
+
if (updateCheckEnvDisabled(env))
|
|
78
|
+
return false;
|
|
79
|
+
return readCheckForUpdates(configPath);
|
|
80
|
+
}
|
|
81
|
+
export function compareNumericVersions(a, b) {
|
|
82
|
+
const parse = (value) => {
|
|
83
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:$|[^\d])/.exec(value.trim());
|
|
84
|
+
return match ? match.slice(1, 4).map((part) => Number.parseInt(part, 10)) : undefined;
|
|
85
|
+
};
|
|
86
|
+
const left = parse(a);
|
|
87
|
+
const right = parse(b);
|
|
88
|
+
if (!left || !right)
|
|
89
|
+
return 0;
|
|
90
|
+
for (let i = 0; i < 3; i++) {
|
|
91
|
+
if (left[i] < right[i])
|
|
92
|
+
return -1;
|
|
93
|
+
if (left[i] > right[i])
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
export function isVersionNewer(latest, installed) {
|
|
99
|
+
return compareNumericVersions(installed, latest) < 0;
|
|
100
|
+
}
|
|
101
|
+
export function readInstalledPackageInfo() {
|
|
102
|
+
return JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
103
|
+
}
|
|
104
|
+
function registryPackageUrl(packageName, registryBaseUrl) {
|
|
105
|
+
const base = registryBaseUrl.replace(/\/+$/, "");
|
|
106
|
+
return `${base}/${packageName.replace("/", "%2F")}`;
|
|
107
|
+
}
|
|
108
|
+
async function fetchLatestVersion(packageName, deps) {
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
const timeout = setTimeout(() => controller.abort(), deps.timeoutMs);
|
|
111
|
+
try {
|
|
112
|
+
const response = await deps.fetch(registryPackageUrl(packageName, deps.registryBaseUrl), {
|
|
113
|
+
headers: { accept: "application/json" },
|
|
114
|
+
signal: controller.signal,
|
|
115
|
+
});
|
|
116
|
+
if (!response.ok)
|
|
117
|
+
return undefined;
|
|
118
|
+
const metadata = (await response.json());
|
|
119
|
+
return typeof metadata?.["dist-tags"]?.latest === "string"
|
|
120
|
+
? metadata["dist-tags"].latest
|
|
121
|
+
: undefined;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
clearTimeout(timeout);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export async function checkForNpmUpdate(deps = {}) {
|
|
131
|
+
try {
|
|
132
|
+
const env = deps.env ?? process.env;
|
|
133
|
+
if (!shouldCheckForUpdates(env, deps.configPath))
|
|
134
|
+
return;
|
|
135
|
+
const pkg = (deps.packageInfo ?? readInstalledPackageInfo)();
|
|
136
|
+
const latest = await fetchLatestVersion(pkg.name, {
|
|
137
|
+
fetch: deps.fetch ?? fetch,
|
|
138
|
+
registryBaseUrl: deps.registryBaseUrl ?? "https://registry.npmjs.org",
|
|
139
|
+
timeoutMs: deps.timeoutMs ?? UPDATE_CHECK_TIMEOUT_MS,
|
|
140
|
+
});
|
|
141
|
+
if (latest && isVersionNewer(latest, pkg.version)) {
|
|
142
|
+
writePendingUpdateNotice(latest, deps.now?.() ?? Date.now());
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Silent skip: never throw and never log to MCP stdio.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export function appendUpdateNotice(out, installedVersion, sessionId, env = process.env, now = Date.now(), configPath) {
|
|
150
|
+
try {
|
|
151
|
+
if (!out || !shouldCheckForUpdates(env, configPath))
|
|
152
|
+
return out;
|
|
153
|
+
const pending = readPendingUpdateNotice();
|
|
154
|
+
if (!pending)
|
|
155
|
+
return out;
|
|
156
|
+
if (!isVersionNewer(pending.latest_version, installedVersion)) {
|
|
157
|
+
removeFile(pendingNoticePath());
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
const emitted = readUpdateNoticeEmitRecord();
|
|
161
|
+
if (emitted?.session_id && sessionId && emitted.session_id === sessionId)
|
|
162
|
+
return out;
|
|
163
|
+
if (emitted && now - emitted.notified_at < UPDATE_NOTICE_INTERVAL_MS)
|
|
164
|
+
return out;
|
|
165
|
+
writeUpdateNoticeEmitRecord({
|
|
166
|
+
notified_at: now,
|
|
167
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
168
|
+
});
|
|
169
|
+
return `${out}\n${UPDATE_NOTICE_TEXT}`;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
}
|
package/dist/routing.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { readFileSync } from "node:fs";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
/** Launch model enum accepted by buildCommand. */
|
|
15
|
-
export const LAUNCH_MODELS = ["haiku", "sonnet", "opus", "opus-4-8", "gpt-5.5"];
|
|
15
|
+
export const LAUNCH_MODELS = ["haiku", "sonnet", "opus", "opus-4-8", "fable", "gpt-5.5"];
|
|
16
16
|
/** Launch effort enum accepted by buildCommand/resolveEffort. */
|
|
17
17
|
export const LAUNCH_EFFORTS = ["medium", "high", "xhigh", "max", "ultracode"];
|
|
18
18
|
/** Sentinel reported (and passed-through harmlessly) for haiku, whose effort is ignored. */
|
|
@@ -28,12 +28,14 @@ const FULL_TO_SHORT = {
|
|
|
28
28
|
"claude-opus-4-8": new Set(["opus", "opus-4-8"]),
|
|
29
29
|
"claude-sonnet-4-6": "sonnet",
|
|
30
30
|
"claude-haiku-4-5": "haiku",
|
|
31
|
+
"claude-fable-5": "fable",
|
|
31
32
|
"gpt-5.5": "gpt-5.5",
|
|
32
33
|
// Short ids may already appear in a hand-authored table; map them through.
|
|
33
34
|
haiku: "haiku",
|
|
34
35
|
sonnet: "sonnet",
|
|
35
36
|
opus: "opus",
|
|
36
37
|
"opus-4-8": "opus-4-8",
|
|
38
|
+
fable: "fable",
|
|
37
39
|
};
|
|
38
40
|
export const DEFAULT_BRANCH = "cost_efficiency";
|
|
39
41
|
/** Resolve dist/routing-table.json relative to this module at runtime. */
|
|
@@ -76,6 +78,7 @@ export function mapModelToProvider(model) {
|
|
|
76
78
|
model === "sonnet" ||
|
|
77
79
|
model === "opus" ||
|
|
78
80
|
model === "opus-4-8" ||
|
|
81
|
+
model === "fable" ||
|
|
79
82
|
model.startsWith("claude-")) {
|
|
80
83
|
return "claude";
|
|
81
84
|
}
|
|
@@ -252,8 +255,8 @@ export function validatePresence(p) {
|
|
|
252
255
|
}
|
|
253
256
|
// provider+model must satisfy the existing match rule.
|
|
254
257
|
if (provider && model) {
|
|
255
|
-
if (provider === "claude" && !["haiku", "sonnet", "opus", "opus-4-8"].includes(model)) {
|
|
256
|
-
return `Error: Claude provider only supports haiku, sonnet, opus,
|
|
258
|
+
if (provider === "claude" && !["haiku", "sonnet", "opus", "opus-4-8", "fable"].includes(model)) {
|
|
259
|
+
return `Error: Claude provider only supports haiku, sonnet, opus, opus-4-8, or fable. Got: ${model}`;
|
|
257
260
|
}
|
|
258
261
|
if (provider === "codex" && model !== "gpt-5.5") {
|
|
259
262
|
return `Error: Codex provider only supports gpt-5.5. Got: ${model}`;
|
package/dist/ruleset-scaffold.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// GENERATED by scripts/gen-ruleset-scaffold.mjs from src/advanced-ruleset.py — DO NOT EDIT.
|
|
2
|
-
export const RULESET_SCAFFOLD = "#!/usr/bin/env python3\n\"\"\"advanced-ruleset.py — final-authority model-routing override hook for subagent-mcp.\n\n(a) PERFORMANCE WARNING: this script runs synchronously inside EVERY launch_agent\n call. Slow rules slow every agent launch. Keep rules lean and low-latency —\n no network calls, no heavy imports at module top. This is YOUR responsibility;\n you have been warned.\n\n(b) OUTPUT CONTRACT (routing mode): print to stdout ONE JSON array — the modified\n candidate list (reorder / filter / replace allowed). Template:\n [\n {\"provider\": \"claude\", \"model\": \"sonnet\", \"effort\": \"high\", \"rank\": 1},\n {\"provider\": \"codex\", \"model\": \"gpt-5.5\", \"effort\": \"xhigh\", \"rank\": 2}\n ]\n Valid providers: claude, codex. Valid models: haiku, sonnet, opus, opus-4-8 (claude);\n gpt-5.5 (codex). Valid efforts: haiku -> \"none\" only; sonnet -> medium|high|xhigh|max;\n opus/opus-4-8 -> those plus ultracode; gpt-5.5 -> medium|high|xhigh.\n \"rank\" on output is ignored. An EMPTY array vetoes the launch. Anything else\n invalid fails the launch hard — the server validates strictly.\n\n(c) INPUT CONTRACT (routing mode, invoked as: <python> advanced-ruleset.py route):\n stdin receives one JSON object:\n { \"candidates\": [ {\"provider\",\"model\",\"effort\",\"rank\"} ... ], # rank 1..N best->worst\n \"context\": { \"task_category\": str, \"cwd\": str,\n \"selection_mode\": \"auto\"|\"provider\"|\"provider_model\"|\"explicit\",\n \"provider\": str|None, \"model\": str|None, \"effort\": str|None } }\n OS environment variables are visible natively (os.environ).\n\nENV-CHECK MODE (no arguments): prints {\"ready\": true|false, \"load-rules\": true|false}.\nRuns once per MCP server process. load-rules false => ruleset silently disabled\nfor the rest of the process. Set LOAD_RULES = True below to activate.\n\"\"\"\nimport json\nimport sys\n\nLOAD_RULES = False\n\n# --- Requirements stub (scaffold itself is stdlib-only) ----------------------\n# List third-party distributions your rules import, e.g.:\n# REQUIREMENTS = [\"requests\", \"pyyaml\"]\n# Install with: <python> -m pip install <name> ...\nREQUIREMENTS = []\n\ndef missing_requirements():\n \"\"\"pip-check helper: returns the REQUIREMENTS entries not importable here.\"\"\"\n import importlib.util\n return [r for r in REQUIREMENTS\n if importlib.util.find_spec(r.replace(\"-\", \"_\")) is None]\n\ndef env_check():\n missing = missing_requirements()\n json.dump({\"ready\": not missing, \"load-rules\": bool(LOAD_RULES)}, sys.stdout)\n\ndef apply_rules(candidates, context):\n \"\"\"YOUR RULES HERE. Default: passthrough (returns the list unchanged).\"\"\"\n return candidates\n\ndef route():\n payload = json.load(sys.stdin)\n out = apply_rules(payload.get(\"candidates\", []), payload.get(\"context\", {}))\n json.dump(out, sys.stdout)\n\nif __name__ == \"__main__\":\n if len(sys.argv) > 1 and sys.argv[1] == \"route\":\n route()\n else:\n env_check()\n";
|
|
2
|
+
export const RULESET_SCAFFOLD = "#!/usr/bin/env python3\n\"\"\"advanced-ruleset.py — final-authority model-routing override hook for subagent-mcp.\n\n(a) PERFORMANCE WARNING: this script runs synchronously inside EVERY launch_agent\n call. Slow rules slow every agent launch. Keep rules lean and low-latency —\n no network calls, no heavy imports at module top. This is YOUR responsibility;\n you have been warned.\n\n(b) OUTPUT CONTRACT (routing mode): print to stdout ONE JSON array — the modified\n candidate list (reorder / filter / replace allowed). Template:\n [\n {\"provider\": \"claude\", \"model\": \"sonnet\", \"effort\": \"high\", \"rank\": 1},\n {\"provider\": \"codex\", \"model\": \"gpt-5.5\", \"effort\": \"xhigh\", \"rank\": 2}\n ]\n Valid providers: claude, codex. Valid models: haiku, sonnet, opus, opus-4-8, fable (claude);\n gpt-5.5 (codex). Valid efforts: haiku -> \"none\" only; sonnet -> medium|high|xhigh|max;\n fable -> medium|high|xhigh|max; opus/opus-4-8 -> those plus ultracode; gpt-5.5 -> medium|high|xhigh.\n \"rank\" on output is ignored. An EMPTY array vetoes the launch. Anything else\n invalid fails the launch hard — the server validates strictly.\n\n(c) INPUT CONTRACT (routing mode, invoked as: <python> advanced-ruleset.py route):\n stdin receives one JSON object:\n { \"candidates\": [ {\"provider\",\"model\",\"effort\",\"rank\"} ... ], # rank 1..N best->worst\n \"context\": { \"task_category\": str, \"cwd\": str,\n \"selection_mode\": \"auto\"|\"provider\"|\"provider_model\"|\"explicit\",\n \"provider\": str|None, \"model\": str|None, \"effort\": str|None } }\n OS environment variables are visible natively (os.environ).\n\nENV-CHECK MODE (no arguments): prints {\"ready\": true|false, \"load-rules\": true|false}.\nRuns once per MCP server process. load-rules false => ruleset silently disabled\nfor the rest of the process. Set LOAD_RULES = True below to activate.\n\"\"\"\nimport json\nimport sys\n\nLOAD_RULES = False\n\n# --- Requirements stub (scaffold itself is stdlib-only) ----------------------\n# List third-party distributions your rules import, e.g.:\n# REQUIREMENTS = [\"requests\", \"pyyaml\"]\n# Install with: <python> -m pip install <name> ...\nREQUIREMENTS = []\n\ndef missing_requirements():\n \"\"\"pip-check helper: returns the REQUIREMENTS entries not importable here.\"\"\"\n import importlib.util\n return [r for r in REQUIREMENTS\n if importlib.util.find_spec(r.replace(\"-\", \"_\")) is None]\n\ndef env_check():\n missing = missing_requirements()\n json.dump({\"ready\": not missing, \"load-rules\": bool(LOAD_RULES)}, sys.stdout)\n\ndef apply_rules(candidates, context):\n \"\"\"YOUR RULES HERE. Default: passthrough (returns the list unchanged).\"\"\"\n return candidates\n\ndef route():\n payload = json.load(sys.stdin)\n out = apply_rules(payload.get(\"candidates\", []), payload.get(\"context\", {}))\n json.dump(out, sys.stdout)\n\nif __name__ == \"__main__\":\n if len(sys.argv) > 1 and sys.argv[1] == \"route\":\n route()\n else:\n env_check()\n";
|
package/dist/ruleset.js
CHANGED
|
@@ -169,7 +169,7 @@ const CODEX_EFFORTS = LAUNCH_EFFORTS.filter((e) => e !== "ultracode" && e !== "m
|
|
|
169
169
|
function effortAllowed(model, effort) {
|
|
170
170
|
if (model === "haiku")
|
|
171
171
|
return effort === HAIKU_EFFORT;
|
|
172
|
-
if (model === "sonnet")
|
|
172
|
+
if (model === "sonnet" || model === "fable")
|
|
173
173
|
return SONNET_EFFORTS.includes(effort);
|
|
174
174
|
if (model === "opus" || model === "opus-4-8") {
|
|
175
175
|
return LAUNCH_EFFORTS.includes(effort);
|
|
@@ -185,7 +185,7 @@ function effortAllowed(model, effort) {
|
|
|
185
185
|
*
|
|
186
186
|
* Per element: string provider/model/effort; provider ∈ {claude, codex};
|
|
187
187
|
* model ∈ launch enum; provider↔model legality (claude↔{haiku,sonnet,opus,
|
|
188
|
-
* opus-4-8}, codex↔gpt-5.5); per-model effort legality incl. haiku→"none".
|
|
188
|
+
* opus-4-8,fable}, codex↔gpt-5.5); per-model effort legality incl. haiku→"none".
|
|
189
189
|
* Extra keys (incl. rank) are ignored on output; duplicates are allowed (the
|
|
190
190
|
* attempt loop just tries them in order). An EMPTY array is VALID — it is the
|
|
191
191
|
* limit case of the allowed filter operation and means "veto the launch"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heretyc/subagent-mcp",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.2",
|
|
4
4
|
"description": "MCP server that launches and manages always-interactive Claude Code and Codex sub-agent sessions (no direct Anthropic/OpenAI API).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"postinstall": "node scripts/postinstall.mjs",
|
|
39
39
|
"prepare": "npm run build",
|
|
40
40
|
"prepublishOnly": "npm test",
|
|
41
|
-
"test": "npm run check:versions && node test/effort.test.mjs && node test/drivers.test.mjs && node test/platform.test.mjs && node test/wait.test.mjs && node test/status.test.mjs && node test/output.test.mjs && node test/stream.test.mjs && node test/routing.test.mjs && node test/deadlock.test.mjs && node test/handler-validation.test.mjs && node test/index-handler.test.mjs && node test/ruleset.test.mjs && node test/ruleset-exec.test.mjs && node test/ruleset-handler.test.mjs && node test/failover.test.mjs && node test/global-concurrency-cap.test.mjs && node test/zombie.test.mjs && node test/orchestration-marker.test.mjs && node test/orchestration-hook-core.test.mjs && node test/orchestration-adapters.test.mjs && node test/orchestration-pretool.test.mjs && node test/orchestration-directives.test.mjs && node test/no-five-call.test.mjs && node test/no-per-provider-cap.test.mjs && node test/rag-pointers.test.mjs && node test/check-worktree-subagent.test.mjs && node test/launch-agent-upsert.test.mjs && node test/claude-session-limit.test.mjs && node test/init-migration.test.mjs && node test/init-global.test.mjs && node test/mirror-fragments.test.mjs && node test/performance-tier-effort.test.mjs && node test/setup-repair.test.mjs && node test/setup-quoting.test.mjs && node test/setup-wire.test.mjs && node test/setup-cli-integration.test.mjs && node test/init.test.mjs && node test/model-selection-mode.test.mjs && node test/cli-args.test.mjs && node test/index-guard.test.mjs && node test/drivers-guard.test.mjs && node test/audit-next13-lb3.test.mjs && node test/zombie-guard.test.mjs && node test/concurrency-guard.test.mjs && node test/hook-core-guard.test.mjs && node scripts/validate_provider.mjs && node scripts/validate_seed_sites.mjs && node scripts/validate_routing_audit.mjs && node test/seed-sites.test.mjs && node test/mcp-compliance.test.mjs"
|
|
41
|
+
"test": "npm run check:versions && node test/effort.test.mjs && node test/drivers.test.mjs && node test/platform.test.mjs && node test/wait.test.mjs && node test/status.test.mjs && node test/output.test.mjs && node test/stream.test.mjs && node test/routing.test.mjs && node test/deadlock.test.mjs && node test/handler-validation.test.mjs && node test/index-handler.test.mjs && node test/ruleset.test.mjs && node test/ruleset-exec.test.mjs && node test/ruleset-handler.test.mjs && node test/failover.test.mjs && node test/global-concurrency-cap.test.mjs && node test/update-check.test.mjs && node test/zombie.test.mjs && node test/orchestration-marker.test.mjs && node test/orchestration-hook-core.test.mjs && node test/orchestration-adapters.test.mjs && node test/orchestration-pretool.test.mjs && node test/orchestration-directives.test.mjs && node test/no-five-call.test.mjs && node test/no-per-provider-cap.test.mjs && node test/rag-pointers.test.mjs && node test/check-worktree-subagent.test.mjs && node test/launch-agent-upsert.test.mjs && node test/claude-session-limit.test.mjs && node test/init-migration.test.mjs && node test/init-global.test.mjs && node test/mirror-fragments.test.mjs && node test/performance-tier-effort.test.mjs && node test/setup-repair.test.mjs && node test/setup-quoting.test.mjs && node test/setup-wire.test.mjs && node test/setup-cli-integration.test.mjs && node test/init.test.mjs && node test/model-selection-mode.test.mjs && node test/cli-args.test.mjs && node test/index-guard.test.mjs && node test/drivers-guard.test.mjs && node test/audit-next13-lb3.test.mjs && node test/zombie-guard.test.mjs && node test/concurrency-guard.test.mjs && node test/hook-core-guard.test.mjs && node scripts/validate_provider.mjs && node scripts/validate_seed_sites.mjs && node scripts/validate_routing_audit.mjs && node test/seed-sites.test.mjs && node test/mcp-compliance.test.mjs"
|
|
42
42
|
},
|
|
43
43
|
"author": "Lexi Blackburn",
|
|
44
44
|
"license": "Apache-2.0",
|