@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.
- package/directives/carryover-claude.md +0 -2
- package/directives/carryover-codex.md +0 -2
- package/directives/handoff-claude.md +7 -0
- package/directives/handoff-codex.md +7 -0
- package/directives/latch-claude.md +5 -0
- package/directives/latch-codex.md +5 -0
- package/directives/orchestration-claude.md +0 -2
- package/directives/orchestration-codex.md +0 -2
- package/directives/reminder-off-claude.md +1 -3
- package/directives/reminder-off-codex.md +1 -3
- package/directives/reminder-on.md +0 -2
- package/directives/short-off.md +1 -1
- package/directives/short-on.md +1 -1
- package/directives/tag-template.md +2 -0
- package/dist/concurrency.js +117 -16
- package/dist/drivers.js +13 -6
- package/dist/effort.js +5 -3
- package/dist/hooks/orchestration-claude.js +86 -2
- package/dist/hooks/orchestration-codex.js +152 -3
- package/dist/index.js +81 -13
- package/dist/init.js +2 -2
- package/dist/orchestration/handoff.js +142 -0
- package/dist/orchestration/hook-core.js +184 -16
- package/dist/orchestration/latch.js +47 -0
- package/dist/orchestration/marker.js +55 -1
- package/dist/orchestration/metering.js +158 -0
- package/dist/orchestration/pretool.js +3 -3
- package/dist/orchestration/template.js +31 -0
- package/dist/pending-permissions.js +13 -3
- package/package.json +4 -4
|
@@ -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.
|
|
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.
|
|
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>
|
package/directives/short-off.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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.
|
package/directives/short-on.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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.
|
package/dist/concurrency.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
612
|
-
const
|
|
613
|
-
if (
|
|
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
|
-
|
|
757
|
+
console.error(`[codex app-server driver] declining unhandled elicitation method: ${message.method}`);
|
|
758
|
+
void this.replyJsonRpc(message.id, buildElicitationResult(undefined, "", "decline"));
|
|
755
759
|
}
|
|
756
760
|
if (message.method === "turn/started" && message.params && typeof message.params === "object") {
|
|
757
761
|
const turn = (message.params.turn ?? {});
|
|
@@ -819,7 +823,7 @@ export class CodexAppServerDriver {
|
|
|
819
823
|
}
|
|
820
824
|
fail(error) {
|
|
821
825
|
const msg = error.message;
|
|
822
|
-
const transient =
|
|
826
|
+
const transient = TRANSIENT_FAILURE_RE.test(msg);
|
|
823
827
|
this._definitelyStartedReject(transient ? new ProviderTransientError(msg) : error);
|
|
824
828
|
this.process.fail(error);
|
|
825
829
|
this.rejectPending(error);
|
|
@@ -1028,7 +1032,7 @@ export class ClaudeSdkDriver {
|
|
|
1028
1032
|
catch (error) {
|
|
1029
1033
|
if (!this.process.killed) {
|
|
1030
1034
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1031
|
-
const transient =
|
|
1035
|
+
const transient = TRANSIENT_FAILURE_RE.test(msg);
|
|
1032
1036
|
this._definitelyStartedReject(transient ? new ProviderTransientError(msg) : new Error(msg));
|
|
1033
1037
|
this.closedFlag = true;
|
|
1034
1038
|
this.process.stderr.write(msg);
|
|
@@ -1038,8 +1042,11 @@ export class ClaudeSdkDriver {
|
|
|
1038
1042
|
}
|
|
1039
1043
|
}
|
|
1040
1044
|
export async function createProviderDriver(options) {
|
|
1041
|
-
|
|
1042
|
-
|
|
1045
|
+
const testSeamsEnabled = process.env.NODE_ENV === "test" || process.env.SUBAGENT_MCP_ENABLE_TEST_SEAMS === "1";
|
|
1046
|
+
if (testSeamsEnabled &&
|
|
1047
|
+
((options.provider === "claude" && process.env.SUBAGENT_MOCK_CLAUDE_DRIVER === "jsonl") ||
|
|
1048
|
+
(options.provider === "codex" && process.env.SUBAGENT_MOCK_CODEX_DRIVER === "jsonl"))) {
|
|
1049
|
+
// Test seam: never honor mock-driver script env vars in production by default.
|
|
1043
1050
|
const mockScript = process.env.SUBAGENT_MOCK_DRIVER_SCRIPT;
|
|
1044
1051
|
const child = mockScript
|
|
1045
1052
|
? spawn(process.execPath, [mockScript, options.provider], {
|
package/dist/effort.js
CHANGED
|
@@ -6,9 +6,13 @@ export function mapModel(provider, model) {
|
|
|
6
6
|
if (provider === "claude") {
|
|
7
7
|
if (model === "opus" || model === "opus-4-8")
|
|
8
8
|
return "claude-opus-4-8";
|
|
9
|
+
if (model === "sonnet")
|
|
10
|
+
return "claude-sonnet-4-6";
|
|
11
|
+
if (model === "haiku")
|
|
12
|
+
return "claude-haiku-4-5";
|
|
9
13
|
if (model === "fable")
|
|
10
14
|
return "claude-fable-5";
|
|
11
|
-
return model;
|
|
15
|
+
return model;
|
|
12
16
|
}
|
|
13
17
|
else {
|
|
14
18
|
return model; // gpt-5.5
|
|
@@ -60,10 +64,8 @@ export function buildCommand(provider, model, effort, cwd, agentId) {
|
|
|
60
64
|
const ucSettingsPath = join(ucSettingsDir, "settings.json");
|
|
61
65
|
writeFileSync(ucSettingsPath, '{"ultracode":true}', { mode: 0o600 });
|
|
62
66
|
args.push("--settings", ucSettingsPath);
|
|
63
|
-
args.push("--permission-mode", "bypassPermissions", "--tools", "default", "--max-turns", "50");
|
|
64
67
|
return { args, ucSettingsPath, ucSettingsDir };
|
|
65
68
|
}
|
|
66
|
-
args.push("--permission-mode", "bypassPermissions", "--tools", "default", "--max-turns", "50");
|
|
67
69
|
return { args };
|
|
68
70
|
}
|
|
69
71
|
else {
|
|
@@ -1,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",
|