@heretyc/subagent-mcp 2.12.11 → 2.12.13

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.
@@ -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
+ }
@@ -1,6 +1,6 @@
1
- import { realpathSync } from "node:fs";
1
+ import { closeSync, openSync, readFileSync, readSync, realpathSync, statSync, } from "node:fs";
2
2
  import { pathToFileURL } from "node:url";
3
- import { countJsonlType, runHook, } from "../orchestration/hook-core.js";
3
+ import { countJsonlType, runHook, TRANSCRIPT_READ_CAP, } from "../orchestration/hook-core.js";
4
4
  import { hasParentMarker } from "../launch-prompt.js";
5
5
  /**
6
6
  * Claude Code UserPromptSubmit hook entry. Reads the JSON payload from stdin,
@@ -22,6 +22,87 @@ const SUBAGENT_ENTRYPOINTS = new Set([
22
22
  "sdk-ts",
23
23
  "sdk-py",
24
24
  ]);
25
+ function finiteNumber(value) {
26
+ return typeof value === "number" && Number.isFinite(value);
27
+ }
28
+ function readTranscriptTail(transcriptPath) {
29
+ if (!transcriptPath)
30
+ return null;
31
+ let fd;
32
+ try {
33
+ const size = statSync(transcriptPath).size;
34
+ if (size <= TRANSCRIPT_READ_CAP) {
35
+ return readFileSync(transcriptPath, "utf8");
36
+ }
37
+ const start = size - TRANSCRIPT_READ_CAP;
38
+ const buf = Buffer.allocUnsafe(TRANSCRIPT_READ_CAP);
39
+ fd = openSync(transcriptPath, "r");
40
+ let offset = 0;
41
+ let pos = start;
42
+ while (offset < TRANSCRIPT_READ_CAP) {
43
+ const bytes = readSync(fd, buf, offset, TRANSCRIPT_READ_CAP - offset, pos);
44
+ if (bytes <= 0)
45
+ break;
46
+ offset += bytes;
47
+ pos += bytes;
48
+ }
49
+ const raw = buf.toString("utf8", 0, offset);
50
+ const firstNewline = raw.indexOf("\n");
51
+ return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ finally {
57
+ if (fd !== undefined) {
58
+ try {
59
+ closeSync(fd);
60
+ }
61
+ catch {
62
+ // Ignore close failures. The hook must never fail the host turn.
63
+ }
64
+ }
65
+ }
66
+ }
67
+ function liftClaudeUsageFromTranscript(transcriptPath) {
68
+ const raw = readTranscriptTail(transcriptPath);
69
+ if (raw === null || !transcriptPath)
70
+ return null;
71
+ const lines = raw.split("\n");
72
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
73
+ const trimmed = lines[i].trim();
74
+ if (!trimmed)
75
+ continue;
76
+ try {
77
+ const msg = JSON.parse(trimmed);
78
+ if (msg.type !== "assistant" || !msg.message?.usage)
79
+ continue;
80
+ if (typeof msg.message.model !== "string")
81
+ return null;
82
+ const usage = msg.message.usage;
83
+ return {
84
+ harness: "claude",
85
+ model: msg.message.model,
86
+ source_ref: transcriptPath,
87
+ usage: {
88
+ input: finiteNumber(usage.input_tokens) ? usage.input_tokens : 0,
89
+ output: finiteNumber(usage.output_tokens) ? usage.output_tokens : 0,
90
+ cache_creation: finiteNumber(usage.cache_creation_input_tokens)
91
+ ? usage.cache_creation_input_tokens
92
+ : 0,
93
+ cache_read: finiteNumber(usage.cache_read_input_tokens)
94
+ ? usage.cache_read_input_tokens
95
+ : 0,
96
+ },
97
+ harnessPercentage: null,
98
+ };
99
+ }
100
+ catch {
101
+ // Skip malformed transcript lines and keep scanning older completed turns.
102
+ }
103
+ }
104
+ return null;
105
+ }
25
106
  export const claudeAdapter = {
26
107
  isSubagent(payload, env) {
27
108
  // subagent-mcp-spawned children inherit this guard and must not claim/nag.
@@ -45,6 +126,9 @@ export const claudeAdapter = {
45
126
  currentTurn(transcriptPath) {
46
127
  return countJsonlType(transcriptPath, "user");
47
128
  },
129
+ liftUsage(_payload, _env, transcriptPath) {
130
+ return liftClaudeUsageFromTranscript(transcriptPath);
131
+ },
48
132
  anonScope: "claude",
49
133
  fullDirectiveFile: "orchestration-claude.md",
50
134
  shortOnFile: "short-on.md",
@@ -1,7 +1,8 @@
1
- import { realpathSync } from "node:fs";
1
+ import { closeSync, openSync, readFileSync, readSync, realpathSync, statSync, } from "node:fs";
2
2
  import { pathToFileURL } from "node:url";
3
- import { claimAndEmit, classifyOwnerClaim, countJsonlType, cullHookZombies, ownerKey, runHook, } from "../orchestration/hook-core.js";
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
+ import { resolveContextWindow, } from "../orchestration/metering.js";
5
6
  import { hasParentMarker } from "../launch-prompt.js";
6
7
  /**
7
8
  * Codex CLI hook entry. Branches on payload.hook_event_name:
@@ -20,6 +21,142 @@ const SUBAGENT_SOURCE_STRINGS = new Set([
20
21
  "subAgentThreadSpawn",
21
22
  "subAgentOther",
22
23
  ]);
24
+ function finiteNumber(value) {
25
+ return typeof value === "number" && Number.isFinite(value);
26
+ }
27
+ function tailJsonlLines(transcriptPath) {
28
+ if (!transcriptPath)
29
+ return [];
30
+ let fd;
31
+ try {
32
+ const size = statSync(transcriptPath).size;
33
+ let raw;
34
+ let droppedPartialHead = false;
35
+ if (size <= TRANSCRIPT_READ_CAP) {
36
+ raw = readFileSync(transcriptPath, "utf8");
37
+ }
38
+ else {
39
+ const start = size - TRANSCRIPT_READ_CAP;
40
+ const buf = Buffer.allocUnsafe(TRANSCRIPT_READ_CAP);
41
+ fd = openSync(transcriptPath, "r");
42
+ let offset = 0;
43
+ let pos = start;
44
+ while (offset < TRANSCRIPT_READ_CAP) {
45
+ const bytes = readSync(fd, buf, offset, TRANSCRIPT_READ_CAP - offset, pos);
46
+ if (bytes <= 0)
47
+ break;
48
+ offset += bytes;
49
+ pos += bytes;
50
+ }
51
+ raw = buf.toString("utf8", 0, offset);
52
+ droppedPartialHead = true;
53
+ }
54
+ const lines = raw.split("\n");
55
+ if (droppedPartialHead) {
56
+ lines.shift();
57
+ }
58
+ return lines.map((line) => line.trim()).filter(Boolean);
59
+ }
60
+ catch {
61
+ return [];
62
+ }
63
+ finally {
64
+ if (fd !== undefined) {
65
+ try {
66
+ closeSync(fd);
67
+ }
68
+ catch {
69
+ // Best-effort close; never throw.
70
+ }
71
+ }
72
+ }
73
+ }
74
+ function nestedRecord(value, key) {
75
+ if (!value || typeof value !== "object")
76
+ return null;
77
+ const child = value[key];
78
+ return child && typeof child === "object"
79
+ ? child
80
+ : null;
81
+ }
82
+ function stringField(value, key) {
83
+ if (!value || typeof value !== "object")
84
+ return null;
85
+ const child = value[key];
86
+ return typeof child === "string" ? child : null;
87
+ }
88
+ function isTokenCountLine(obj) {
89
+ if (obj.type === "token_count")
90
+ return true;
91
+ const payload = nestedRecord(obj, "payload");
92
+ return payload?.type === "token_count";
93
+ }
94
+ function tokenInfo(obj) {
95
+ const direct = nestedRecord(obj, "info");
96
+ if (direct)
97
+ return direct;
98
+ return nestedRecord(nestedRecord(obj, "payload"), "info");
99
+ }
100
+ function modelFromTurnContext(obj) {
101
+ if (obj.type !== "turn_context")
102
+ return null;
103
+ return stringField(obj, "model") ?? stringField(nestedRecord(obj, "payload"), "model");
104
+ }
105
+ function liftCodexUsageFromRollout(transcriptPath) {
106
+ let latestTokenInfo = null;
107
+ let latestModel = null;
108
+ const lines = tailJsonlLines(transcriptPath);
109
+ for (let i = lines.length - 1; i >= 0; i--) {
110
+ try {
111
+ const obj = JSON.parse(lines[i]);
112
+ if (!obj || typeof obj !== "object")
113
+ continue;
114
+ const record = obj;
115
+ if (!latestTokenInfo && isTokenCountLine(record)) {
116
+ latestTokenInfo = tokenInfo(record);
117
+ }
118
+ if (!latestModel) {
119
+ latestModel = modelFromTurnContext(record);
120
+ }
121
+ if (latestTokenInfo && latestModel)
122
+ break;
123
+ }
124
+ catch {
125
+ // Skip unparseable lines; never throw.
126
+ }
127
+ }
128
+ const total = nestedRecord(latestTokenInfo, "total_token_usage");
129
+ if (!total || !latestModel || !transcriptPath)
130
+ return null;
131
+ const input = total.input_tokens;
132
+ const output = total.output_tokens;
133
+ const cacheRead = total.cached_input_tokens;
134
+ if (!finiteNumber(input) || !finiteNumber(output) || !finiteNumber(cacheRead)) {
135
+ return null;
136
+ }
137
+ const modelContextWindow = latestTokenInfo?.model_context_window;
138
+ const totalTokens = total.total_tokens;
139
+ const harnessPercentage = finiteNumber(modelContextWindow) &&
140
+ modelContextWindow > 0 &&
141
+ finiteNumber(totalTokens)
142
+ ? (totalTokens / modelContextWindow) * 100
143
+ : null;
144
+ if (harnessPercentage === null && resolveContextWindow("codex", latestModel) === null) {
145
+ return null;
146
+ }
147
+ return {
148
+ harness: "codex",
149
+ model: latestModel,
150
+ source_ref: transcriptPath,
151
+ usage: {
152
+ input,
153
+ output,
154
+ cache_creation: 0,
155
+ cache_read: cacheRead,
156
+ },
157
+ harnessPercentage,
158
+ };
159
+ }
23
160
  export const codexAdapter = {
24
161
  isSubagent(payload, env) {
25
162
  // subagent-mcp-spawned children inherit this guard and must not claim/nag.
@@ -47,6 +184,12 @@ export const codexAdapter = {
47
184
  currentTurn(transcriptPath) {
48
185
  return countJsonlType(transcriptPath, "turn_context");
49
186
  },
187
+ liftUsage(_payload, _env, transcriptPath) {
188
+ // Provider-specific lift ONLY. hook-core is the sole writer of the metering
189
+ // record (matching the Claude adapter contract); the adapter must not also
190
+ // self-write, or the same turn would persist the record twice.
191
+ return liftCodexUsageFromRollout(transcriptPath);
192
+ },
50
193
  anonScope: "codex",
51
194
  fullDirectiveFile: "orchestration-codex.md",
52
195
  shortOnFile: "short-on.md",
@@ -78,7 +221,13 @@ export function runCodexHook(payload, env, adapter = codexAdapter) {
78
221
  const cwd = payload.cwd || process.cwd();
79
222
  const current = ownerKey(payload, cwd, adapter);
80
223
  marker.writeCurrentSession(cwd, current);
81
- if (!marker.isActive(cwd, current)) {
224
+ // Gate on the SAME effective-active computation runHook uses (disable /
225
+ // enable / latch / metering fail-safe), not the bare marker. SessionStart
226
+ // is turn 0 (grace window), so no metering fail-safe applies yet; passing
227
+ // false keeps a never-pre-enabled real session eligible for its turn-0
228
+ // directive via the latch/enable paths just like the UserPromptSubmit
229
+ // cadence would.
230
+ if (!computeEffectiveActive(cwd, current, Date.now(), false)) {
82
231
  return "";
83
232
  }
84
233
  const turn = adapter.currentTurn(payload.transcript_path);
package/dist/index.js CHANGED
@@ -22,6 +22,9 @@ import { CONFIG_FILENAME, NONBLOCKING_CULL_DEPS, ZOMBIE_FORCE_GRACE_MS, ZOMBIE_L
22
22
  import { shouldReapTerminalButAlive } from "./zombie.js";
23
23
  import * as orchestrationMarker from "./orchestration/marker.js";
24
24
  import * as modelMode from "./orchestration/model-mode.js";
25
+ import * as handoff from "./orchestration/handoff.js";
26
+ import * as metering from "./orchestration/metering.js";
27
+ import { computeEffectiveActive } from "./orchestration/hook-core.js";
25
28
  import { startLivenessHeartbeat } from "./orchestration/liveness.js";
26
29
  import { checkForNpmUpdate } from "./orchestration/update-check.js";
27
30
  import { ensureParentMarker } from "./launch-prompt.js";
@@ -117,6 +120,21 @@ const TASK_CATEGORY_GLOSS = "REQUIRED. Task shape -> routing category (server pi
117
120
  function errorResult(text) {
118
121
  return { content: [{ type: "text", text }], isError: true };
119
122
  }
123
+ function textResult(text) {
124
+ return { content: [{ type: "text", text }] };
125
+ }
126
+ function computeEffectiveOrchestrationActive(cwd, key) {
127
+ const now = Date.now();
128
+ // Derive the metering fail-safe from the PERSISTED record only: a record that
129
+ // exists but cannot resolve a percentage is undetectable (fail-safe ON). The
130
+ // ABSENCE of a record is NOT fail-safed -- it matches the hook's turn-1 grace
131
+ // window (no completed turn yet), so the tool's report agrees with the hook's
132
+ // tag on turn 1 instead of spuriously flipping ON. Everything else flows
133
+ // through the one shared helper so the tool never diverges from the hook.
134
+ const record = key !== undefined ? metering.readMetering(key) : null;
135
+ const meteringUndetectableFailSafe = record !== null && record.used_percentage === null;
136
+ return computeEffectiveActive(cwd, key, now, meteringUndetectableFailSafe);
137
+ }
120
138
  function currentLaunchDepth(env = process.env) {
121
139
  const raw = env.SUBAGENT_MCP_DEPTH;
122
140
  if (raw !== undefined && raw !== "") {
@@ -550,11 +568,11 @@ reconcileInterval.unref();
550
568
  // test/mirror-fragments.test.mjs while ORCHESTRATION_INSTRUCTIONS below stays
551
569
  // compressed under MCP metadata limits:
552
570
  // READ-ESCALATION LADDER (the orchestrator's only read channels, in order): (1) subagent-mcp `poll_agent` TAIL; (2) if the tail is insufficient, dispatch ONE sub-agent to return a single summary of <=100 lines, trusted as-is (no separate verification step); (3) anything larger: the USER reads the document directly. No reads or writes occur outside these channels. An empty or stalled tail means the agent is ALIVE, not dead — do NOT busy-loop poll_agent; learn completion via `wait`. Large inter-agent data: the orchestrator assigns scratch-file paths (%TEMP% on Windows, /tmp on POSIX) in prompts; the producing sub-agent writes, the consuming sub-agent reads; the orchestrator NEVER reads those files.
553
- const ORCHESTRATION_INSTRUCTIONS = "subagent-mcp - CANONICAL OPERATING MODEL (full spec: docs/spec/dev-loop/orchestration-directive-architecture.md).\n\nPRECEDENCE. The latest <subagent-mcp state=\"...\"> hook tag and repo/system safety rules are jointly binding; genuine conflict => STOP and ask. Only the hook flips ON/OFF; absence of any tag = UNKNOWN => fail-safe ON.\n\nSOLE CHANNEL. Every launch uses launch_agent; never harness Task/Agent or shell-spawned agents.\n\nORCHESTRATION ON. You are a delegate-ONLY orchestrator: use only the structured-question tool (AskUserQuestion on Claude / request-user-input on Codex), subagent-mcp, and /workflows. No direct reads/writes; inline-by-right does not exist. Non-delegable step: ask a one-time exception, do only that step, resume delegating.\n\nSUB-AGENT CONTRACT. Every prompt carries objective + output format + tools/sources + boundaries. SCALE: ~1 agent for a fact-find, 2-4 for comparisons; never one-shot multi-phase work; split into atomic steps, one agent each. FAN-OUT independents, sequence dependents, SERIALIZE writers over shared paths (no cwd lock). VERIFY code and non-trivial steps with a separate sub-agent first.\n\nREAD LADDER. poll_agent tail -> one <=100-line summarizer sub-agent, trusted as-is -> else the USER reads it. Large handoffs use scratch-file paths; producer writes, consumer reads, orchestrator never reads them. Empty/stalled tail means ALIVE; learn finish via wait, do not poll-loop.\n\nORCHESTRATION OFF. If context footprint since last upgrade ask exceeds 200 lines, after that turn STOP and ask whether to enable; reset count only when you ask.\n\nDROPOUT WHILE ON: HALT and ask until restored. SUB-AGENT EXEMPTION: a prompt whose literal FIRST LINE begins \"<this is a request from a parent process>\" skips this regime. DISABLE: user-only, never on your own initiative.\n\nMODEL SELECTION. Default smart auto-picks, rejects provider/model/effort selectors. user-approved-overrides honors them 30 min, expires lazily on launch_agent, needs user authorization.";
571
+ const ORCHESTRATION_INSTRUCTIONS = "subagent-mcp - CANONICAL OPERATING MODEL (full spec: orchestration-directive-architecture.md).\n\nPRECEDENCE. The latest <subagent-mcp state=\"...\"> hook tag and repo/system safety rules are jointly binding; genuine conflict => STOP and ask. Only the hook flips ON/OFF; absence of any tag = UNKNOWN => fail-safe ON.\n\nSOLE CHANNEL. Every launch uses launch_agent; never harness Task/Agent or shell-spawned agents.\n\nORCHESTRATION ON. You are a delegate-ONLY orchestrator: use only the structured-question tool (AskUserQuestion on Claude / request-user-input on Codex), subagent-mcp, and /workflows. No direct reads/writes; inline-by-right does not exist. Non-delegable step: ask a one-time exception, do only that step, resume delegating.\n\nSUB-AGENT CONTRACT. Prompt carries objective + output format + tools/sources + boundaries. SCALE: ~1 fact-find agent, 2-4 for comparisons; split multi-phase work into atomic steps. FAN-OUT independents, sequence dependents, SERIALIZE writers over shared paths (no cwd lock). VERIFY code and non-trivial steps with a separate sub-agent first.\n\nREAD LADDER. poll_agent tail -> one <=100-line summarizer sub-agent, trusted as-is -> else the USER reads it. Large handoffs use scratch-file paths; producer writes, consumer reads, orchestrator never reads them. Empty/stalled tail means ALIVE; learn finish via wait, do not poll-loop.\n\nORCHESTRATION starts OFF each session. Hook meters provider-reported context (never tokenized); at 15% it latches ON and coaches a 5-question plan; at 50% it warns every turn and unlocks handoff-write/handoff-read/handoff-clear. Undetectable context size = fail-safe ON.\n\nDROPOUT WHILE ON: HALT and ask until restored. SUB-AGENT EXEMPTION: a prompt whose literal FIRST LINE begins \"<this is a request from a parent process>\" skips this regime. DISABLE: user-only, never on your own initiative.\n\nMODEL SELECTION. Default smart auto-picks, rejects provider/model/effort selectors. user-approved-overrides honors them 30 min, expires lazily on launch_agent, needs user authorization.";
554
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.";
555
573
  const server = new McpServer({
556
574
  name: "subagent-mcp",
557
- version: "2.12.11",
575
+ version: "2.12.13",
558
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.",
559
577
  }, {
560
578
  instructions: process.env.SUBAGENT_MCP_SUBAGENT === "1"
@@ -1614,22 +1632,27 @@ server.tool("wait", "Blocks until one or more sub-agents reach a reportable stat
1614
1632
  };
1615
1633
  }));
1616
1634
  // Tool 8: orchestration-mode
1617
- server.tool("orchestration-mode", "Toggle or query per-project ORCHESTRATION MODE. `enabled`: true = ON, false = OFF for THIS session only, omit = query. SOLE CHANNEL holds in BOTH states: subagent-mcp is the only sanctioned way to launch sub-agents; toggling OFF does not lift that. WHAT: default-ON mode for LONG-HORIZON work that would fill the context window if run inline; when ON act as a delegate-ONLY orchestrator delegate every step, inline-by-right does not exist, a non-delegable atomic step needs a one-time user-approved exception via the structured-question tool (state which + why). OFF upgrade check: a long-horizon task = any whose TOTAL context footprint (input read + output produced) exceeds 200 lines, measured CUMULATIVELY since your last upgrade ask; after EVERY user turn, if it qualifies, STOP and ask whether to remain enabled (every qualifying turn; a decline does not latch; reset the count only when you ask). PERSISTENCE: a permitted disable is session-keyed only, applies to THIS session only, resumes ON next new session (or after the 2h backstop), no mid-session re-enable; keyless hosts get only the one-time non-persisted conversational opt-out. CARRYOVER: if ON was inherited from a prior session (carried-over, not user-enabled this session), the bundled hook prepends a ONE-TIME notice (once per marker) — notify the user it auto-activated and confirm keeping it ON. DISABLE: never on your own initiative; you may PROPOSE OFF on task-fit mismatch, but only EXPLICIT user permission (AskUserQuestion on Claude, request-user-input on Codex) may set enabled:false. Per-turn injection fires only in CLI hosts that load the bundled hook; desktop hosts toggle the marker but inject nothing (documented degradation). Full operating model is in this server's MCP `instructions` (read once at initialize) — this is the operational summary only.", {
1635
+ server.tool("orchestration-mode", "Toggle or query per-project ORCHESTRATION MODE. `enabled`: true = ON, false = OFF for THIS session only, omit = query. SOLE CHANNEL holds in BOTH states: subagent-mcp is the only sanctioned way to launch sub-agents; toggling OFF does not lift that. WHAT: when ON act as a delegate-ONLY orchestrator; delegate every step, inline-by-right does not exist, a non-delegable atomic step needs a one-time user-approved exception via the structured-question tool. Default is now OFF each session (metering-driven latch/handoff supersede the old manual-upgrade-ask model); enabled:true explicitly turns ON early (before any latch); enabled:false remains a session-scoped 2h-TTL opt-out, HONORED even after the 15% latch or metering fail-safe forces ON. PERSISTENCE: a permitted disable is session-keyed only, applies to THIS session only, resumes ON next new session or after the 2h backstop; keyless hosts get only the one-time non-persisted conversational opt-out. DISABLE: never on your own initiative; you may PROPOSE OFF on task-fit mismatch, but only EXPLICIT user permission may set enabled:false. Per-turn injection fires only in CLI hosts that load the bundled hook; desktop hosts toggle the marker but inject nothing.", {
1618
1636
  enabled: z.boolean().optional(),
1619
1637
  }, withMaintenance(async (params) => {
1620
1638
  const cwd = process.cwd();
1621
1639
  const key = orchestrationMarker.readCurrentSession(cwd);
1622
1640
  if (params.enabled === true) {
1623
- if (!orchestrationMarker.isActive(cwd, key)) {
1624
- return errorResult("orchestration already disabled for this session, cannot re-enable mid-session; resumes ON automatically next new session (or after the 2h backstop).");
1641
+ if (!key) {
1642
+ return errorResult("cannot enable: no session pointer found for this project (the per-turn hook has not fired yet). Enable records are session-keyed only; send one prompt first, or check wiring with `subagent-mcp doctor`.");
1625
1643
  }
1644
+ if (!orchestrationMarker.isSessionScopedKey(key)) {
1645
+ return errorResult("cannot enable: this host supplies no session identity (no session_id/transcript_path in hook payloads) and enable records are session-keyed only. Orchestration remains fail-safe ON for this anonymous host.");
1646
+ }
1647
+ orchestrationMarker.removeDisable(key);
1648
+ orchestrationMarker.writeEnable(key);
1626
1649
  return {
1627
1650
  content: [
1628
1651
  {
1629
1652
  type: "text",
1630
1653
  text: JSON.stringify({
1631
1654
  orchestration_mode: "ON",
1632
- message: "orchestration is ON by default.",
1655
+ message: "orchestration is ON for this session.",
1633
1656
  }),
1634
1657
  },
1635
1658
  ],
@@ -1649,14 +1672,14 @@ server.tool("orchestration-mode", "Toggle or query per-project ORCHESTRATION MOD
1649
1672
  type: "text",
1650
1673
  text: JSON.stringify({
1651
1674
  orchestration_mode: "disabled-this-session",
1652
- message: "orchestration disabled for THIS session only; the next new session resumes ON automatically (or after the 2h backstop); no mid-session re-enable.",
1675
+ message: "orchestration disabled for THIS session only; this opt-out overrides the 15% latch and metering fail-safe until the next new session or the 2h backstop.",
1653
1676
  }),
1654
1677
  },
1655
1678
  ],
1656
1679
  };
1657
1680
  }
1658
1681
  // enabled === undefined -> query only; no marker mutation.
1659
- const active = orchestrationMarker.isActive(cwd, key);
1682
+ const active = computeEffectiveOrchestrationActive(cwd, key);
1660
1683
  return {
1661
1684
  content: [
1662
1685
  {
@@ -1695,6 +1718,51 @@ server.tool("model-selection-mode", "Set or query per-project MODEL SELECTION MO
1695
1718
  ],
1696
1719
  };
1697
1720
  }));
1721
+ // Verbatim handoff post-write response (plan Section 1.1). Defined as a
1722
+ // top-level string constant here so mirror-fragments.test.mjs can assert
1723
+ // byte-identity against handoff.md via source-text regex. Kept in lockstep
1724
+ // with handoff.HANDOFF_WRITE_SUCCESS (single verbatim string, two surfaces).
1725
+ const HANDOFF_WRITE_SUCCESS_MESSAGE = "We are ready to start a new session, to avoid wasting tokens, use the structured question tool to confirm that the user is ready to use the `handoff-resume skill` in the next new session to resume work and has cleared the current /goal (if present) - or you will be compelled to keep working on a potential /goal that needs to be halted for a new session.";
1726
+ // Tool 10: handoff-write
1727
+ server.tool("handoff-write", "Write a handoff for this working directory so the NEXT session can resume cleanly. UNLOCKS only at >=50% context utilization with readable metering; below that, or if context size is undetectable, this tool returns an affirmative unavailable error (never silent). BEFORE calling, ask the user 10 clarifying questions via the structured-question tool to build a /goal prompt for the next session. content <=4000 chars; use overflow (<=8000 more chars) for anything beyond that, referenced by full path inside content. On success, relay the tool's exact response to the user verbatim.", {
1728
+ content: z.string().min(1),
1729
+ overflow: z.string().optional(),
1730
+ }, withMaintenance(async (params) => {
1731
+ const cwd = process.cwd();
1732
+ const key = orchestrationMarker.readCurrentSession(cwd);
1733
+ if (!key)
1734
+ return errorResult(handoff.UNAVAILABLE_NO_METERING);
1735
+ const result = handoff.writeHandoffIfAvailable(cwd, {
1736
+ content: params.content,
1737
+ overflowContent: params.overflow,
1738
+ createdBySession: key,
1739
+ }, metering.readMetering(key));
1740
+ if (!result.ok)
1741
+ return errorResult(result.error);
1742
+ return textResult(HANDOFF_WRITE_SUCCESS_MESSAGE);
1743
+ }));
1744
+ // Tool 11: handoff-read
1745
+ server.tool("handoff-read", "Read the saved handoff for this working directory (if any). BEFORE calling, confirm the user's intent via EXACTLY 5 structured questions (proves legitimacy, clears ambiguity). If found, this session becomes the ONLY session that gets the handoff re-appended to its periodic LONG reminders. If none is saved, explains that the previous session must write one first.", {}, withMaintenance(async () => {
1746
+ const cwd = process.cwd();
1747
+ const key = orchestrationMarker.readCurrentSession(cwd);
1748
+ const record = handoff.readHandoff(cwd);
1749
+ if (record === null)
1750
+ return errorResult(handoff.NO_HANDOFF_FOUND);
1751
+ const marked = key ? handoff.markRead(cwd, key) ?? record : record;
1752
+ const overflowLine = marked.overflow_path
1753
+ ? `\n\nOverflow file: ${marked.overflow_path}`
1754
+ : "";
1755
+ return textResult([
1756
+ "Saved handoff:",
1757
+ marked.content + overflowLine,
1758
+ "Before using this handoff, confirm the user's intent via EXACTLY 5 structured questions. Confirm: resume objective, current blocker, files/state to preserve, next concrete action, and permission to proceed in this session.",
1759
+ ].join("\n\n"));
1760
+ }));
1761
+ // Tool 12: handoff-clear
1762
+ server.tool("handoff-clear", "Delete the saved handoff for this working directory, including any overflow file. The write/read/clear cycle repeats at each successor session's own 50% threshold.", {}, withMaintenance(async () => {
1763
+ handoff.clearHandoff(process.cwd());
1764
+ return textResult("handoff cleared for this directory.");
1765
+ }));
1698
1766
  // Connect the stdio transport only when run as the entry point (the bin), NOT
1699
1767
  // when this module is imported (e.g. test/handler-validation.test.mjs importing
1700
1768
  // the exported validatePresence). Connecting on import would block the test on
package/dist/init.js CHANGED
@@ -4,7 +4,7 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  const MIGRATE_RE = /<!-- subagent-mcp:(?:managed:)?begin\b[^>]*-->[\s\S]*?<!-- subagent-mcp:(?:managed:)?end -->/;
5
5
  const OWNER_CAP = 8;
6
6
  export const INIT_BLOCK = [
7
- "<!-- subagent-mcp:managed:begin schema=3 -->",
7
+ "<!-- subagent-mcp:managed:begin schema=4 -->",
8
8
  "## subagent-mcp invariant — managed block, do not edit between markers",
9
9
  "",
10
10
  "Upserted by `subagent-mcp init` at user request; re-running init keeps it in sync.",
@@ -25,7 +25,7 @@ export const INIT_BLOCK = [
25
25
  "",
26
26
  "READ-ESCALATION LADDER (the orchestrator's only read channels, in order): (1) subagent-mcp `poll_agent` TAIL; (2) if the tail is insufficient, dispatch ONE sub-agent to return a single summary of <=100 lines, trusted as-is (no separate verification step); (3) anything larger: the USER reads the document directly. No reads or writes occur outside these channels. An empty or stalled tail means the agent is ALIVE, not dead — do NOT busy-loop poll_agent; learn completion via `wait`. Large inter-agent data: the orchestrator assigns scratch-file paths (%TEMP% on Windows, /tmp on POSIX) in prompts; the producing sub-agent writes, the consuming sub-agent reads; the orchestrator NEVER reads those files.",
27
27
  "",
28
- "ORCHESTRATION OFF a \"long-horizon task\" = any task whose TOTAL context footprint (input you read + output you produce) exceeds 200 lines of text. After EVERY user turn, measure the CUMULATIVE footprint accumulated since your last upgrade ask; reset that cumulative count to zero ONLY when you actually ask. If it qualifies, ask the user via the structured-question tool whether to switch orchestration ON. Ask on every qualifying turn; a decline does not suppress future asks. Never assert ON yourself — only ask.",
28
+ "ORCHESTRATION OFF BY DEFAULT -- each new session starts with orchestration OFF. A hook meters real provider-reported context usage (never tokenized, never self-estimated). At 15% utilization a persisted latch force-enables orchestration and coaches a 5-question planning stop. At 50% the hook warns every turn to wind down and unlocks handoff-write/handoff-read/handoff-clear for a clean session handoff. If context size cannot be measured, the hook fails safe to ON. Never assert a state yourself -- only the hook tag is authoritative.",
29
29
  "",
30
30
  "DROPOUT WHILE ON: if subagent-mcp stops responding while orchestration is ON, halt and ask the user; do nothing inline. Keep re-checking and stay halted until subagent-mcp is restored (no auto-degrade). The only user choices are keep-waiting (the default) or explicitly abandon the whole task; aborting ends the task, it never switches you to inline work.",
31
31
  "",