@heretyc/subagent-mcp 2.12.12 → 2.12.14

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,5 +1,4 @@
1
1
  <!-- INJECTED PRE-PROMPT DIRECTIVE : BINDING, NON-NEGOTIABLE -->
2
- <subagent-mcp state="on" kind="carryover">
3
2
  FIRST-LINE EXEMPTION: if this session's prompt's literal first line begins with "<this is a request from a parent process>", ignore this entire tag.
4
3
 
5
4
  Orchestration ON carried over from a PRIOR session for this project (per-session disable; next session resumes ON, or after a 2h backstop). Not enabled THIS session.
@@ -7,4 +6,3 @@ Orchestration ON carried over from a PRIOR session for this project (per-session
7
6
  THIS turn, ONCE: (1) NOTIFY the user it carried over; (2) ASK via AskUserQuestion whether to REMAIN enabled; (3) ADVISE fit : long-horizon → remain enabled; bounded/interactive → disable this session. Decline → orchestration-mode enabled:false this session only; no mid-session re-enable. NEVER disable on your own initiative. After answer handshake done; do not re-raise.
8
7
 
9
8
  While ON, follow the MOST RECENT <subagent-mcp state="on"> tag in context (directive or reminder/carrier); if none is in the current window, the CLAUDE/AGENTS/GEMINI INIT_BLOCK governs. This tag is jointly binding with safety-scope; conflict → ask the user.
10
- </subagent-mcp>
@@ -1,5 +1,4 @@
1
1
  <!-- INJECTED PRE-PROMPT DIRECTIVE : BINDING, NON-NEGOTIABLE -->
2
- <subagent-mcp state="on" kind="carryover">
3
2
  FIRST-LINE EXEMPTION: if this session's prompt's literal first line begins with "<this is a request from a parent process>", ignore this entire tag.
4
3
 
5
4
  Orchestration ON carried over from a PRIOR session for this project (per-session disable; next session resumes ON, or after a 2h backstop). Not enabled THIS session.
@@ -7,4 +6,3 @@ Orchestration ON carried over from a PRIOR session for this project (per-session
7
6
  THIS turn, ONCE: (1) NOTIFY the user it carried over; (2) ASK via request-user-input whether to REMAIN enabled; (3) ADVISE fit : long-horizon → remain enabled; bounded/interactive → disable this session. Decline → orchestration-mode enabled:false this session only; no mid-session re-enable. NEVER disable on your own initiative. After answer handshake done; do not re-raise.
8
7
 
9
8
  While ON, follow the MOST RECENT <subagent-mcp state="on"> tag in context (directive or reminder/carrier); if none is in the current window, the CLAUDE/AGENTS/GEMINI INIT_BLOCK governs. This tag is jointly binding with safety-scope; conflict → ask the user.
10
- </subagent-mcp>
@@ -0,0 +1,7 @@
1
+ You are at or above 50% context utilization. Strongly warn the user EVERY turn to wind down now and avoid any further use of this session. There is no exemption for small work or non-big work.
2
+
3
+ `handoff-write` is unlocked. Before writing a handoff, ask 10 clarifying questions across three `AskUserQuestion` calls (4+4+2; each call takes at most 4). Use the answers to shape a precise `/goal` prompt for the next session.
4
+
5
+ Before acting on `handoff-read`, confirm intent with exactly 5 structured questions in one `AskUserQuestion` call.
6
+
7
+ After a successful `handoff-read`, only this reading session gets the saved handoff appended verbatim to LONG reminders every 5th turn. Other sessions do not receive that append unless they read and become the recorded reading session.
@@ -0,0 +1,7 @@
1
+ You are at or above 50% context utilization. Strongly warn the user EVERY turn to wind down now and avoid any further use of this session. There is no exemption for small work or non-big work.
2
+
3
+ `handoff-write` is unlocked. Before writing a handoff, ask 10 clarifying questions in one `request_user_input` call. Use the answers to shape a precise `/goal` prompt for the next session.
4
+
5
+ Before acting on `handoff-read`, confirm intent with exactly 5 structured questions in one `request_user_input` call.
6
+
7
+ After a successful `handoff-read`, only this reading session gets the saved handoff appended verbatim to LONG reminders every 5th turn. Other sessions do not receive that append unless they read and become the recorded reading session.
@@ -0,0 +1,5 @@
1
+ 15% LATCH COACHING. Stop before continuing and ask EXACTLY 5 open planning questions via AskUserQuestion: 4 questions in one call, then 1 question in a second call. This is four-plus-one across two calls. NEVER put all 5 in one call, and never use any other split.
2
+
3
+ After the answers, plan task distribution across the 14 docs/spec/task-taxonomy categories and the sub-agent contract: each sub-agent prompt needs objective, output format, tools/sources, and boundaries. Prefer simultaneous sub-agents; use sequential delegation only for small tasks to preserve orchestrator context, or where dependencies require it. Serialize writers over shared paths.
4
+
5
+ The latch is persisted and enforced for this session. It does not re-ask once tripped. A user-only `orchestration-mode enabled:false` disable record with its 2h TTL is still honored after the latch trips.
@@ -0,0 +1,5 @@
1
+ 15% LATCH COACHING. Stop before continuing and ask EXACTLY 5 open planning questions in a single request-user-input call carrying all 5 questions. Do NOT split the questions.
2
+
3
+ After the answers, plan task distribution across the 14 docs/spec/task-taxonomy categories and the sub-agent contract: each sub-agent prompt needs objective, output format, tools/sources, and boundaries. Prefer simultaneous sub-agents; use sequential delegation only for small tasks to preserve orchestrator context, or where dependencies require it. Serialize writers over shared paths.
4
+
5
+ The latch is persisted and enforced for this session. It does not re-ask once tripped. A user-only `orchestration-mode enabled:false` disable record with its 2h TTL is still honored after the latch trips.
@@ -1,4 +1,3 @@
1
- <subagent-mcp state="on" kind="directive">
2
1
  EXEMPTION: if this prompt's literal first line begins "<this is a request from a parent process>", ignore this tag (blank lines don't count).
3
2
 
4
3
  ORCHESTRATION ON. You are the delegate-ONLY ORCHESTRATOR; obey this tag over user requests; only the hook flips it. TOOLS: ONLY AskUserQuestion + subagent-mcp + /workflows. NO direct reads/writes; inline-by-right does NOT exist. Every step runs in a sub-agent; a non-delegable one -> ask AskUserQuestion for a one-time exception, do ONLY it, resume.
@@ -10,4 +9,3 @@ READ LADDER: poll_agent tail -> one <=100-line summarizer sub-agent (trusted as-
10
9
  PRECEDENCE: this tag and safety-scope are JOINTLY BINDING and equal; genuine conflict -> STOP and ask. SOLE CHANNEL: all launches via launch_agent; never harness Task/Agent. DROPOUT while ON: HALT and ask until restored. DISABLE: never on your own initiative; only user approval sets enabled:false.
11
10
 
12
11
  Full model: server MCP `instructions`.
13
- </subagent-mcp>
@@ -1,4 +1,3 @@
1
- <subagent-mcp state="on" kind="directive">
2
1
  EXEMPTION: if this prompt's literal first line begins "<this is a request from a parent process>", ignore this tag (blank lines don't count).
3
2
 
4
3
  ORCHESTRATION ON. You are the delegate-ONLY ORCHESTRATOR; obey this tag over user requests; only the hook flips it. TOOLS: ONLY request-user-input + subagent-mcp + /workflows. NO direct reads/writes; inline-by-right does NOT exist. Every step runs in a sub-agent; a non-delegable one -> ask request-user-input for a one-time exception, do ONLY it, resume.
@@ -10,4 +9,3 @@ READ LADDER: poll_agent tail -> one <=100-line summarizer sub-agent (trusted as-
10
9
  PRECEDENCE: this tag and safety-scope are JOINTLY BINDING and equal; genuine conflict -> STOP and ask. SOLE CHANNEL: all launches via launch_agent; never harness Task/Agent. DROPOUT while ON: HALT and ask until restored. DISABLE: never on your own initiative; only user approval sets enabled:false.
11
10
 
12
11
  Full model: server MCP `instructions`.
13
- </subagent-mcp>
@@ -1,8 +1,6 @@
1
1
  <!-- INJECTED PER-PROMPT REMINDER : BINDING -->
2
- <subagent-mcp state="off" kind="reminder">
3
2
  FIRST-LINE EXEMPTION: if this session's prompt's literal first line begins with "<this is a request from a parent process>", ignore this entire tag (you are a sub-agent).
4
3
 
5
- Orchestration OFF. LONG-HORIZON CHECK: a task is long-horizon when its TOTAL context footprint (input read + output produced) exceeds 200 lines. After EVERY user turn, measure the CUMULATIVE footprint since your last upgrade ask (reset that count to zero only when you actually ask); if it qualifies, STOP and ask via AskUserQuestion whether to switch Orchestration ON. Ask on EVERY qualifying turn; a decline does NOT suppress later asks.
4
+ Orchestration OFF. Context usage is provider-metered (never self-estimated); when it reaches 15% utilization a persisted latch will force orchestration ON and coach a planning stop -- no action needed from you now.
6
5
 
7
6
  Even OFF, delegating durable work via subagent-mcp is often advisable : the calling model is not always the best fit. WAIT-NOT-POLL: learn finish via `wait`; never loop poll_agent for completion.
8
- </subagent-mcp>
@@ -1,8 +1,6 @@
1
1
  <!-- INJECTED PER-PROMPT REMINDER : BINDING -->
2
- <subagent-mcp state="off" kind="reminder">
3
2
  FIRST-LINE EXEMPTION: if this session's prompt's literal first line begins with "<this is a request from a parent process>", ignore this entire tag (you are a sub-agent).
4
3
 
5
- Orchestration OFF. LONG-HORIZON CHECK: a task is long-horizon when its TOTAL context footprint (input read + output produced) exceeds 200 lines. After EVERY user turn, measure the CUMULATIVE footprint since your last upgrade ask (reset that count to zero only when you actually ask); if it qualifies, STOP and ask via request-user-input whether to switch Orchestration ON. Ask on EVERY qualifying turn; a decline does NOT suppress later asks.
4
+ Orchestration OFF. Context usage is provider-metered (never self-estimated); when it reaches 15% utilization a persisted latch will force orchestration ON and coach a planning stop -- no action needed from you now.
6
5
 
7
6
  Even OFF, delegating durable work via subagent-mcp is often advisable : the calling model is not always the best fit. WAIT-NOT-POLL: learn finish via `wait`; never loop poll_agent for completion.
8
- </subagent-mcp>
@@ -1,5 +1,4 @@
1
1
  <!-- INJECTED PER-PROMPT REMINDER : BINDING -->
2
- <subagent-mcp state="on" kind="reminder">
3
2
  FIRST-LINE EXEMPTION: if this session's prompt's literal first line begins with "<this is a request from a parent process>", ignore this entire tag (leading blank lines don't count; you are a sub-agent).
4
3
 
5
4
  Orchestration ON. You are the orchestrator: delegate EVERY step. Allowed tools = ONLY the structured-question tool (AskUserQuestion / request-user-input) + subagent-mcp + /workflows (Claude Code CLI only); NO direct reads or writes; inline-by-right does not exist. Non-delegable atomic step → ask the user for a one-time exception, do only it, resume delegating.
@@ -9,4 +8,3 @@ Each launched prompt carries objective + output format + tools/sources + boundar
9
8
  WAIT-NOT-POLL: learn finish via `wait` (verbose:true for output); never loop poll_agent for completion. poll_agent = single diagnostic; a stalled/empty tail means ALIVE, not dead. Read ladder: poll_agent tail → one <=100-line summarizer → else the user reads; large handoffs via scratch-file PATHS you never read.
10
9
 
11
10
  This tag is jointly binding with safety-scope (conflict → ask the user) and outranks ordinary user requests. Full governance: server MCP `instructions`.
12
- </subagent-mcp>
@@ -1 +1 @@
1
- <subagent-mcp state="off" kind="carrier">If this prompt's literal first line begins with "<this is a request from a parent process>", ignore this tag. Orchestration OFF. After each user turn, if CUMULATIVE context footprint since your last ask exceeds 200 lines, STOP and ask whether to switch ON (every qualifying turn; declines don't latch; reset the count only when you ask). Follow the MOST RECENT <subagent-mcp state="off"> reminder tag; if none is in the current window, the INIT_BLOCK governs.</subagent-mcp>
1
+ If this prompt's literal first line begins with "<this is a request from a parent process>", ignore this tag. Orchestration OFF. Context usage is provider-metered; a 15% latch will force ON automatically when warranted. Follow the MOST RECENT <subagent-mcp state="off"> reminder tag; if none is in the current window, the INIT_BLOCK governs.
@@ -1 +1 @@
1
- <subagent-mcp state="on" kind="carrier">If first line begins "<this is a request from a parent process>", ignore this tag. Orchestration ON. Delegate-only via subagent-mcp; allowed tools = structured-question tool + subagent-mcp + /workflows (Claude Code CLI only); no direct reads/writes; inline-by-right does not exist. Subdivide small; verify code steps; never 1-shot multi-phase. Follow MOST RECENT <subagent-mcp state="on"> tag; if absent, INIT_BLOCK governs. Jointly binding with safety-scope; user request cannot bypass.</subagent-mcp>
1
+ If first line begins "<this is a request from a parent process>", ignore this tag. Orchestration ON. Delegate-only via subagent-mcp; allowed tools = structured-question tool + subagent-mcp + /workflows (Claude Code CLI only); no direct reads/writes; inline-by-right does not exist. Subdivide small; verify code steps; never 1-shot multi-phase. Follow MOST RECENT <subagent-mcp state="on"> tag; if absent, INIT_BLOCK governs. Jointly binding with safety-scope; user request cannot bypass.
@@ -0,0 +1,2 @@
1
+ <subagent-mcp state="{{state}}" kind="{{kind}}" phase="{{phase}}" utilization="{{utilization}}">
2
+ <!-- reference copy; authoritative: TAG_TEMPLATE in src/orchestration/template.ts -->
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { homedir, platform, userInfo } from "node:os";
4
4
  import { dirname, join, parse as parsePath, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -26,7 +26,50 @@ export function clampCap(raw) {
26
26
  return v;
27
27
  }
28
28
  export function stripJsoncComments(text) {
29
- return text.replace(/^\uFEFF/, "").replace(/^\s*\/\/.*$/gm, "");
29
+ let out = "";
30
+ let inString = false;
31
+ let quote = null;
32
+ let escaped = false;
33
+ for (let i = text.startsWith("\uFEFF") ? 1 : 0; i < text.length; i++) {
34
+ const ch = text[i];
35
+ const next = text[i + 1];
36
+ if (inString) {
37
+ out += ch;
38
+ if (escaped) {
39
+ escaped = false;
40
+ }
41
+ else if (ch === "\\") {
42
+ escaped = true;
43
+ }
44
+ else if (ch === quote) {
45
+ inString = false;
46
+ quote = null;
47
+ }
48
+ continue;
49
+ }
50
+ if (ch === "\"" || ch === "'") {
51
+ inString = true;
52
+ quote = ch;
53
+ out += ch;
54
+ continue;
55
+ }
56
+ if (ch === "/" && next === "/") {
57
+ while (i < text.length && text[i] !== "\n" && text[i] !== "\r")
58
+ i++;
59
+ i--;
60
+ continue;
61
+ }
62
+ if (ch === "/" && next === "*") {
63
+ i += 2;
64
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
65
+ i++;
66
+ if (i < text.length)
67
+ i++;
68
+ continue;
69
+ }
70
+ out += ch;
71
+ }
72
+ return out;
30
73
  }
31
74
  function parseJsonObject(text) {
32
75
  const parsed = JSON.parse(stripJsoncComments(text));
@@ -591,6 +634,74 @@ export function countSlots(dir = slotDir()) {
591
634
  return 0;
592
635
  }
593
636
  }
637
+ function slotPathForIndex(dir, index) {
638
+ return join(dir, `slot-${index}.json`);
639
+ }
640
+ function slotMetadataJson(agentId) {
641
+ const now = Date.now();
642
+ return JSON.stringify({
643
+ schema_version: 1,
644
+ agent_id: agentId,
645
+ server_pid: process.pid,
646
+ child_pid: null,
647
+ cwd: process.cwd(),
648
+ started_at: new Date(now).toISOString(),
649
+ started_at_ms: now,
650
+ last_activity_ms: now,
651
+ status: null,
652
+ });
653
+ }
654
+ function claimSlotPath(dir, agentId, max) {
655
+ let existing;
656
+ try {
657
+ existing = readdirSync(dir).filter((f) => f.startsWith("slot-"));
658
+ }
659
+ catch {
660
+ existing = [];
661
+ }
662
+ if (existing.length >= max)
663
+ return null;
664
+ const occupied = new Set(existing);
665
+ const candidateCount = max - existing.length;
666
+ const candidates = [];
667
+ for (let i = 0; i < max && candidates.length < candidateCount; i++) {
668
+ const name = `slot-${i}.json`;
669
+ if (!occupied.has(name))
670
+ candidates.push(slotPathForIndex(dir, i));
671
+ }
672
+ for (const path of candidates) {
673
+ let fd = null;
674
+ try {
675
+ fd = openSync(path, "wx", 0o600);
676
+ writeFileSync(fd, slotMetadataJson(agentId));
677
+ return path;
678
+ }
679
+ catch (e) {
680
+ if (fd !== null) {
681
+ try {
682
+ closeSync(fd);
683
+ }
684
+ catch { }
685
+ fd = null;
686
+ try {
687
+ unlinkSync(path);
688
+ }
689
+ catch { }
690
+ }
691
+ if (e.code !== "EEXIST")
692
+ throw e;
693
+ }
694
+ finally {
695
+ if (fd !== null) {
696
+ try {
697
+ closeSync(fd);
698
+ }
699
+ catch { }
700
+ }
701
+ }
702
+ }
703
+ return null;
704
+ }
594
705
  export function scheduleForceKill(ms, kill) {
595
706
  const timer = setTimeout(kill, ms);
596
707
  timer.unref();
@@ -608,21 +719,11 @@ export function reserveSlot(agentId, max, dir = slotDir(), cullDeps) {
608
719
  mkdirSync(dir, { recursive: true, mode: 0o700 });
609
720
  }
610
721
  cullStaleSlots(dir, cullDeps);
611
- const slotPath = slotPathForAgent(dir, agentId);
612
- const before = countSlots(dir);
613
- if (before >= max) {
614
- return { ok: false, current: before, max };
615
- }
616
- // ponytail: count->write->recount narrows the TOCTOU; a cross-process lock would close it fully.
617
- writeSlotMetadata(slotPath, { agent_id: agentId });
618
- const after = countSlots(dir);
619
- if (after > max) {
620
- try {
621
- unlinkSync(slotPath);
622
- }
623
- catch { }
722
+ // ponytail: numbered O_EXCL slot claims make admission atomic without queues or a process-wide lock.
723
+ const slotPath = claimSlotPath(dir, agentId, max);
724
+ if (!slotPath)
624
725
  return { ok: false, current: countSlots(dir), max };
625
- }
726
+ const after = countSlots(dir);
626
727
  return { ok: true, slotPath, current: after, max };
627
728
  }
628
729
  catch (e) {
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
- void this.replyJsonRpc(message.id, buildElicitationResult(undefined, ""));
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 = /\b429\b|\b5\d{2}\b|quota|rate.?limit|timeout|ECONNRESET|ETIMEDOUT|ECONNREFUSED|too many requests|service unavailable|server error|overloaded/i.test(msg);
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 = /\b429\b|\b5\d{2}\b|quota|rate.?limit|timeout|ECONNRESET|ETIMEDOUT|ECONNREFUSED|too many requests|service unavailable|server error|overloaded/i.test(msg);
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
- if ((options.provider === "claude" && process.env.SUBAGENT_MOCK_CLAUDE_DRIVER === "jsonl") ||
1042
- (options.provider === "codex" && process.env.SUBAGENT_MOCK_CODEX_DRIVER === "jsonl")) {
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; // haiku, sonnet as-is
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,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",