@heretyc/subagent-mcp 2.12.9 → 2.12.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,206 +5,166 @@
5
5
  [![node](https://img.shields.io/node/v/@heretyc/subagent-mcp)](https://www.npmjs.com/package/@heretyc/subagent-mcp)
6
6
  [![CI](https://github.com/Heretyc/subagent-mcp/actions/workflows/claude-routine.yml/badge.svg)](https://github.com/Heretyc/subagent-mcp/actions/workflows/claude-routine.yml)
7
7
 
8
- ## Core premise
9
-
10
- subagent-mcp is an MCP (stdio) server that turns any AI coding assistant
11
- (Claude Code, Codex, Gemini CLI) into a **manager/orchestrator of a team of
12
- AI sub-agents** running on macOS, Linux, and Windows. It drives the locally
13
- authenticated `claude` and `codex` CLIs you already signed into **no direct
14
- HTTP API calls and no API keys, ever** (an explicit, permanent non-goal). It
15
- serves developers running big, long-horizon coding jobs who want to escape a
16
- single conversation's context limits and vendor lock-in.
17
-
18
- Its central promise: the orchestrator **monitors but never reads or writes
19
- files itself** — every step is delegated to a fresh sub-agent, so the
20
- orchestrator's context fills only with ≤100-line summaries. This extends
21
- effective context "geometrically" instead of linearly, enabling marathon tasks
22
- with little or no compaction. Guarantees/invariants it promises: a single
23
- machine-global, provider-agnostic concurrency cap (default 20, min 10; rejects
24
- at cap, never queues); fail-safe **ON** on hosts lacking hooks (unknown
25
- orchestration state defaults to ON); orchestration state is authoritative only
26
- via harness-verified `<subagent-mcp state="...">` hook tags, never inferred from
27
- prose (guards against directive drift/hallucination); sub-agents run **gated by
28
- default** (permission ceiling `auto`); and automatic model/provider/effort
29
- routing per task category so the user never picks a model.
30
-
31
- ## Key problems it solves
32
-
33
- - **Context exhaustion / compaction on long tasks** — delegate-only
34
- orchestration keeps the manager's context holding summaries, not raw files,
35
- so long jobs run with minimal compaction.
36
- - **Single-vendor blind spots and lock-in** — mixed Claude + Codex (provider-
37
- agnostic) operation means one vendor's weakness or outage doesn't blind or
38
- block the whole job.
39
- - **No API keys / no direct API calls** — drives locally signed-in vendor CLIs
40
- instead of the Anthropic/OpenAI HTTP APIs, avoiding key management and cost of
41
- a gateway.
42
- - **Directive drift & hallucination over long runs** — operating rules are
43
- re-injected redundantly (MCP `instructions`, INIT_BLOCK, per-turn hooks,
44
- managed AGENTS.md/CLAUDE.md/GEMINI.md blocks) and made authoritative via
45
- harness-verified state tags.
46
- - **Model-selection burden** — a benchmark-derived routing table auto-picks
47
- provider/model/effort from a plain-English prompt + task category.
48
- - **Uncontrolled/unsafe sub-agent actions** — a shared permission engine gates
49
- sub-agent operations (SAFE→allow, DANGER→deny, NEUTRAL→park for a decision)
50
- with `auto`/`manual`/`yolo` ceilings and one-time `respond_permission`.
51
- - **Runaway fan-out / resource contention** — one machine-wide concurrency cap
52
- and worktree isolation (branch-per-task) with a first-line sub-agent
53
- carve-out to prevent fork-bomb recursion.
54
- - **Orchestrator observability** — a fixed set of tools (launch/poll/kill/
55
- send_message/list/wait/respond_permission + orchestration & model modes) plus
56
- a status lifecycle (processing/stalled/finished/errored/stopped/
57
- zombie_killed/permission_requested) so a quiet agent isn't mistaken for dead.
8
+ ## Core Premise
9
+
10
+ subagent-mcp is an MCP stdio server that turns an AI coding assistant (Claude
11
+ Code, Codex, Gemini CLI) into a manager of local Claude and Codex sub-agents on
12
+ macOS, Linux, and Windows. It drives the locally authenticated `claude` and
13
+ `codex` CLIs you already signed into. It does not make direct HTTP API calls and
14
+ does not require API keys.
15
+
16
+ The orchestrator monitors but does not read or write project files itself. Work
17
+ is delegated to fresh sub-agents, so the orchestrator keeps summaries instead
18
+ of raw file context. The main invariants are:
19
+
20
+ - one machine-global, provider-agnostic concurrency cap (default 20, minimum 10)
21
+ - fail-safe orchestration ON on hookless hosts
22
+ - state authority only from harness-verified `<subagent-mcp state="...">` tags
23
+ - sub-agents gated by default with permission ceiling `auto`
24
+ - automatic model, provider, and effort routing per task category
58
25
 
59
26
  ## Install
60
27
 
61
- ### What you need first
28
+ ### What You Need First
62
29
 
63
- - Node.js 18 or newer (`node --version`)
64
- - `claude` CLI installed and signed in (`claude --version`)
65
- - `codex` CLI installed and signed in (`codex --version`; optional if you
66
- only use Claude)
30
+ - Node.js 18 or newer (`node --version`)
31
+ - `claude` CLI, installed and signed in (`claude --version`)
32
+ - `codex` CLI, installed and signed in (`codex --version`; optional if you only
33
+ use Claude)
67
34
 
68
- Building from source needs extra developer tools — see
35
+ Building from source needs extra developer tools. See
69
36
  [CONTRIBUTING.md](CONTRIBUTING.md).
70
37
 
71
- ### Install the package
38
+ ### Install The Package
72
39
 
73
40
  ```bash
74
41
  npm install -g @heretyc/subagent-mcp
75
42
  ```
76
43
 
77
- This is the standard install for everyone. (Organizations pinning the package
78
- through GitHub Packages should see [docs/registration.md](docs/registration.md).)
44
+ This is the standard install. Organizations pinning the package through GitHub
45
+ Packages should see [docs/registration.md](docs/registration.md).
79
46
 
80
- ### Wire it into your assistant
47
+ ### Wire It Into Your Assistant
81
48
 
82
49
  ```bash
83
50
  subagent-mcp setup
84
51
  ```
85
52
 
86
- Installing the package only ships the program it does **not** connect anything
87
- on its own. `subagent-mcp setup` finds your Claude Code / Codex install and
53
+ Installing the package only ships the program. It does not connect anything on
54
+ its own. `subagent-mcp setup` finds your Claude Code or Codex install and
88
55
  registers both the server and the per-turn orchestration hooks. Preview first
89
56
  with `subagent-mcp setup --dry-run`.
90
57
 
91
- ### Restart, then turn on the invariant
58
+ ### Restart, Then Turn On The Invariant
92
59
 
93
- Restart your Claude Code or Codex session so it picks up the new tools. On Codex,
94
- run `/hooks` and trust the new hook. Then (recommended):
60
+ Restart your Claude Code or Codex session so it picks up the new tools. On
61
+ Codex, run `/hooks` and trust the new hook. Then, recommended:
95
62
 
96
63
  ```bash
97
64
  subagent-mcp init --global
98
65
  ```
99
66
 
100
- This writes a small managed "always delegate" rule block into your global
101
- assistant config once, so it works across every project. For a single project
102
- instead, use `subagent-mcp init --root /path/to/project`. Full per-platform
103
- wiring (Gemini CLI, Claude Desktop, manual setup) is in
67
+ This writes a managed "always delegate" rule block into your global assistant
68
+ config once. For one project only, use
69
+ `subagent-mcp init --root /path/to/project`. Full per-platform wiring (Gemini
70
+ CLI, Claude Desktop, manual setup) is in
104
71
  [docs/registration.md](docs/registration.md).
105
72
 
106
- ## How to operate it
73
+ ## How To Operate It
107
74
 
108
- ### Orchestration mode: ON vs OFF
75
+ ### Orchestration Mode
109
76
 
110
- - **ON** your assistant acts as a pure manager. It never reads or writes files
111
- directly; it delegates every step to sub-agents. Best for big, long-running
112
- jobs.
113
- - **OFF** — your assistant works normally, on its own, with no delegation rules.
77
+ - **ON**: your assistant acts as a pure manager. It delegates every step to
78
+ sub-agents. Best for big, long-running jobs.
79
+ - **OFF**: your assistant works normally, with no delegation rules.
114
80
 
115
- Flip it with the `orchestration-mode` tool. (Desktop apps can toggle the mode
116
- but don't receive the per-turn hook reminders.)
81
+ Flip it with the `orchestration-mode` tool. Desktop apps can toggle the mode but
82
+ do not receive per-turn hook reminders.
117
83
 
118
- ### The 9 tools at a glance
84
+ ### Tools
119
85
 
120
- | Tool | What it does |
121
- |---|---|
122
- | `launch_agent` | Start a sub-agent on a task (just give a prompt + a task category) |
123
- | `poll_agent` | Check how an agent is doing and read its latest output |
124
- | `kill_agent` | Stop a running or quiet agent |
125
- | `send_message` | Send a follow-up message to a live agent |
126
- | `list_agents` | See every agent and its current status |
127
- | `wait` | Pause until a specific agent finishes |
128
- | `respond_permission` | Approve or deny a sub-agent's parked permission request |
129
- | `orchestration-mode` | Turn manager-mode ON or OFF |
130
- | `model-selection-mode` | Let the server auto-pick the model, or allow manual overrides |
131
-
132
- You never have to choose a model. Give `launch_agent` a plain-English prompt and
133
- a **task category** (e.g. "coding", "debugging", "security_review") and the
134
- server picks the best provider, model, and effort for that kind of work.
135
-
136
- ### How many run at once
137
-
138
- There is a single machine-wide limit on how many sub-agents run at the same time
139
- across everything on your computer. The **default is 20**. When the limit is
140
- reached, a new `launch_agent` is turned down right away — it does not wait in a
141
- queue. You change the number in the `global-subagent-mcp-config.jsonc` file in
142
- the install folder (minimum 10); the file is re-read on every launch, so no
143
- restart is needed.
144
-
145
- > **Renamed in 2.12.5:** this file was `global-concurrency.jsonc`. The old name
146
- > is still read (with a one-time deprecation notice) when the new file is absent,
147
- > for one major version. Rename it yourself when convenient — nothing auto-renames
148
- > it.
149
-
150
- The same settings file includes `checkForUpdates` (default `true`). When a newer
151
- npm version exists, the per-turn hook can show a throttled notice to run
152
- `subagent-mcp update` and then `subagent-mcp setup`. Set `checkForUpdates` to
153
- `false`, or run with `SUBAGENT_UPDATE_CHECK=0` / `false`, to disable that check.
86
+ The server exposes `launch_agent`, `poll_agent`, `kill_agent`, `send_message`,
87
+ `list_agents`, `wait`, `respond_permission`, `orchestration-mode`, and
88
+ `model-selection-mode`. See [docs/tools.md](docs/tools.md) for the full
89
+ parameter and return reference.
90
+
91
+ You do not have to choose a model. Give `launch_agent` a prompt and a task
92
+ category such as `coding`, `debugging`, or `security_review`; the server picks
93
+ the provider, model, and effort.
94
+
95
+ ### Concurrency
96
+
97
+ There is one machine-wide limit on concurrent sub-agents. The default is 20.
98
+ When the limit is reached, `launch_agent` is rejected immediately and does not
99
+ queue. Change the value in `global-subagent-mcp-config.jsonc` in the install
100
+ folder. The file is re-read on every launch.
101
+
102
+ The config file was renamed from `global-concurrency.jsonc` in 2.12.5. The old
103
+ name is still read, with a one-time deprecation notice, when the new file is
104
+ absent.
105
+
106
+ The same settings file includes `checkForUpdates` (default `true`). Disable it
107
+ with `checkForUpdates: false` or `SUBAGENT_UPDATE_CHECK=0`.
108
+
109
+ ## Configuration
110
+
111
+ Machine-wide defaults live in `global-subagent-mcp-config.jsonc`, installed
112
+ beside the compiled server and re-read on every `launch_agent`. It controls the
113
+ global concurrency cap, update checks, permission ceiling, escalation behavior,
114
+ strict read-parity logging, and Codex sandbox networking.
115
+
116
+ User and repo permission files can only tighten or add scoped permissions on top
117
+ of the global ceiling. See [README/configuration.md](README/configuration.md)
118
+ for the full key table, precedence rules, and mode summary.
154
119
 
155
120
  ## Permissions
156
121
 
157
- Launched sub-agents run **gated** by default (new in 2.12.5). Set
158
- `permissionsCeiling` in `global-subagent-mcp-config.jsonc`:
122
+ Launched sub-agents run gated by default. Set `permissionsCeiling` in
123
+ `global-subagent-mcp-config.jsonc`:
159
124
 
160
125
  | Mode | What a sub-agent can do |
161
126
  |---|---|
162
- | `auto` | **Default.** Safe reads auto-allow, dangerous actions auto-deny, everything else parks for your decision. |
163
- | `manual` | Same, but *every* non-denied action parks for a decision — nothing auto-allows. |
164
- | `yolo` | No gating at all — the pre-2.12.5 behavior. |
127
+ | `auto` | Default. Safe reads auto-allow, dangerous actions auto-deny, everything else parks for your decision. |
128
+ | `manual` | Same, but every non-denied action parks for a decision. |
129
+ | `yolo` | No gating at all. |
165
130
 
166
- When a sub-agent's action parks, its status becomes `permission_requested` and it
167
- shows up in `poll_agent`/`list_agents` and returns early from `wait`. Answer it
168
- with the **`respond_permission`** tool:
131
+ When a sub-agent's action parks, its status becomes `permission_requested` and
132
+ it appears in `poll_agent`, `list_agents`, and `wait`. Answer it with:
169
133
 
134
+ ```text
135
+ respond_permission(agent_id="...", decision="allow" | "deny", reason="...")
170
136
  ```
171
- respond_permission(agent_id="…", decision="allow" | "deny", reason="…")
172
- ```
173
137
 
174
- One-time only (no session-wide grants). Omit `request_id` to answer the oldest
175
- pending request. Unanswered requests auto-deny after 5 minutes; the sub-agent
176
- keeps running either way. Full spec: [docs/spec/permissions.md](docs/spec/permissions.md).
177
-
178
- ## Basic debugging
179
-
180
- - **"An agent looks stuck."** A quiet agent is usually **still alive**, not
181
- dead. After ~10 minutes with no output an agent is marked `stalled` it's
182
- thinking or waiting on a hand-off. It recovers on its own. **Do not kill a
183
- stalled agent**, and `wait` will not return on one.
184
- - **"It won't start a new agent (cap reached)."** You've hit the concurrent
185
- limit. Use `list_agents` to see what's running and `kill_agent` on anything
186
- you no longer need that frees a slot immediately. Raising the number in
187
- `global-subagent-mcp-config.jsonc` also works.
188
- - **"Where are the logs?"** Each agent's recent output is available any time via
189
- `poll_agent`. The server's own diagnostics go to your host's normal MCP
190
- server log (your Claude Code or Codex session logs) — all server logging goes
191
- to stderr, never mixed into results.
138
+ One-time only. Omit `request_id` to answer the oldest pending request.
139
+ Unanswered requests auto-deny after 5 minutes. Full spec:
140
+ [docs/spec/permissions.md](docs/spec/permissions.md).
141
+
142
+ ## Basic Debugging
143
+
144
+ - **An agent looks stuck.** A quiet agent is usually still alive. After about 10
145
+ minutes with no output an agent is marked `stalled`. Prefer `wait` or another
146
+ `poll_agent` over killing it.
147
+ - **Cap reached.** Use `list_agents` to see what is running and `kill_agent` on
148
+ work you no longer need. Raising `globalConcurrentSubagents` also works.
149
+ - **Logs.** Agent output is available through `poll_agent`. Server diagnostics
150
+ go to the host MCP server log on stderr.
192
151
 
193
152
  ## Documentation
194
153
 
195
154
  | Document | Contents |
196
155
  |---|---|
197
- | [docs/spec/arch-rationale.md](docs/spec/arch-rationale.md) | Why it's built this way — the full design rationale |
198
- | [docs/registration.md](docs/registration.md) | Per-platform setup: Claude Code, Codex, Gemini CLI, Claude Desktop |
199
- | [docs/tools.md](docs/tools.md) | Full tool reference — all nine tools, parameters, return shapes |
200
- | [docs/usage.md](docs/usage.md) | Model & effort matrix, ultracode mode, usage examples |
201
- | [docs/SPEC.md](docs/SPEC.md) | Full technical specification |
202
- | [docs/spec/permissions.md](docs/spec/permissions.md) | Permission system ceiling modes, shared engine, threat model |
203
- | [docs/reference/status-lifecycle.md](docs/reference/status-lifecycle.md) | Agent status meanings (processing / stalled / finished / errored / stopped / permission_requested) |
204
- | [CONTRIBUTING.md](CONTRIBUTING.md) | Developer guide build, test, publish, contribution workflow |
156
+ | [docs/spec/arch-rationale.md](docs/spec/arch-rationale.md) | Design rationale |
157
+ | [docs/registration.md](docs/registration.md) | Per-platform setup |
158
+ | [docs/tools.md](docs/tools.md) | Tool reference |
159
+ | [docs/usage.md](docs/usage.md) | Model and effort matrix |
160
+ | [docs/SPEC.md](docs/SPEC.md) | Technical specification |
161
+ | [README/configuration.md](README/configuration.md) | Configuration keys and precedence |
162
+ | [docs/spec/permissions.md](docs/spec/permissions.md) | Permission system |
163
+ | [docs/reference/status-lifecycle.md](docs/reference/status-lifecycle.md) | Agent status meanings |
164
+ | [CONTRIBUTING.md](CONTRIBUTING.md) | Developer guide |
205
165
 
206
166
  ## License
207
167
 
208
- Apache-2.0 Copyright 2026 Lexi Blackburn
168
+ Apache-2.0. Copyright 2026 Lexi Blackburn.
209
169
 
210
170
  See [LICENSE](LICENSE).
@@ -1,10 +1,10 @@
1
- <!-- INJECTED PRE-PROMPT DIRECTIVE BINDING, NON-NEGOTIABLE -->
1
+ <!-- INJECTED PRE-PROMPT DIRECTIVE : BINDING, NON-NEGOTIABLE -->
2
2
  <subagent-mcp state="on" kind="carryover">
3
3
  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
4
 
5
5
  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.
6
6
 
7
- 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.
7
+ 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
8
 
9
9
  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
10
  </subagent-mcp>
@@ -1,10 +1,10 @@
1
- <!-- INJECTED PRE-PROMPT DIRECTIVE BINDING, NON-NEGOTIABLE -->
1
+ <!-- INJECTED PRE-PROMPT DIRECTIVE : BINDING, NON-NEGOTIABLE -->
2
2
  <subagent-mcp state="on" kind="carryover">
3
3
  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
4
 
5
5
  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.
6
6
 
7
- 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.
7
+ 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
8
 
9
9
  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
10
  </subagent-mcp>
@@ -1,8 +1,8 @@
1
- <!-- INJECTED PER-PROMPT REMINDER BINDING -->
1
+ <!-- INJECTED PER-PROMPT REMINDER : BINDING -->
2
2
  <subagent-mcp state="off" kind="reminder">
3
3
  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
4
 
5
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.
6
6
 
7
- 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.
7
+ 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
8
  </subagent-mcp>
@@ -1,8 +1,8 @@
1
- <!-- INJECTED PER-PROMPT REMINDER BINDING -->
1
+ <!-- INJECTED PER-PROMPT REMINDER : BINDING -->
2
2
  <subagent-mcp state="off" kind="reminder">
3
3
  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
4
 
5
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.
6
6
 
7
- 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.
7
+ 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
8
  </subagent-mcp>
@@ -1,4 +1,4 @@
1
- <!-- INJECTED PER-PROMPT REMINDER BINDING -->
1
+ <!-- INJECTED PER-PROMPT REMINDER : BINDING -->
2
2
  <subagent-mcp state="on" kind="reminder">
3
3
  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
4
 
@@ -255,58 +255,196 @@ function mergeRules(target, next) {
255
255
  function unique(items) {
256
256
  return [...new Set(items)];
257
257
  }
258
+ function codexKeyPattern(key) {
259
+ return new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=\\s*(.*)$`);
260
+ }
261
+ function scanBasicString(line, start) {
262
+ for (let i = start + 1; i < line.length; i++) {
263
+ if (line[i] === "\\" && i + 1 < line.length) {
264
+ i++;
265
+ continue;
266
+ }
267
+ if (line[i] === "\"")
268
+ return i;
269
+ }
270
+ throw new Error(`unbalanced TOML quotes: ${line.trim()}`);
271
+ }
272
+ function scanLiteralString(line, start) {
273
+ const end = line.indexOf("'", start + 1);
274
+ if (end === -1)
275
+ throw new Error(`unbalanced TOML quotes: ${line.trim()}`);
276
+ return end;
277
+ }
278
+ function stripTomlCommentsAndMultilineStrings(text) {
279
+ let multiline = null;
280
+ const out = [];
281
+ for (const line of text.split(/\r?\n/)) {
282
+ let sanitized = "";
283
+ let i = 0;
284
+ if (multiline) {
285
+ const end = line.indexOf(multiline);
286
+ if (end === -1) {
287
+ out.push("");
288
+ continue;
289
+ }
290
+ i = end + 3;
291
+ multiline = null;
292
+ }
293
+ while (i < line.length) {
294
+ if (line.startsWith(`"""`, i) || line.startsWith("'''", i)) {
295
+ const delimiter = line.startsWith(`"""`, i) ? `"""` : "'''";
296
+ sanitized += delimiter === `"""` ? `""` : "''";
297
+ i += 3;
298
+ const end = line.indexOf(delimiter, i);
299
+ if (end === -1) {
300
+ multiline = delimiter;
301
+ break;
302
+ }
303
+ i = end + 3;
304
+ continue;
305
+ }
306
+ if (line[i] === "#")
307
+ break;
308
+ if (line[i] === "\"") {
309
+ const end = scanBasicString(line, i);
310
+ sanitized += line.slice(i, end + 1);
311
+ i = end + 1;
312
+ continue;
313
+ }
314
+ if (line[i] === "'") {
315
+ const end = scanLiteralString(line, i);
316
+ sanitized += line.slice(i, end + 1);
317
+ i = end + 1;
318
+ continue;
319
+ }
320
+ sanitized += line[i];
321
+ i++;
322
+ }
323
+ out.push(sanitized);
324
+ }
325
+ if (multiline)
326
+ throw new Error("unterminated TOML multiline string");
327
+ return out.join("\n");
328
+ }
329
+ function countTomlBrackets(line) {
330
+ let opens = 0;
331
+ let closes = 0;
332
+ for (let i = 0; i < line.length; i++) {
333
+ if (line[i] === "\"") {
334
+ i = scanBasicString(line, i);
335
+ continue;
336
+ }
337
+ if (line[i] === "'") {
338
+ i = scanLiteralString(line, i);
339
+ continue;
340
+ }
341
+ if (line[i] === "[")
342
+ opens++;
343
+ if (line[i] === "]")
344
+ closes++;
345
+ }
346
+ return { opens, closes };
347
+ }
348
+ function tomlAssignmentValue(text, key) {
349
+ const keyPattern = codexKeyPattern(key);
350
+ const lines = text.split(/\r?\n/);
351
+ for (let i = 0; i < lines.length; i++) {
352
+ const m = lines[i].match(keyPattern);
353
+ if (!m)
354
+ continue;
355
+ let value = m[1].trim();
356
+ if (!value.startsWith("["))
357
+ return value;
358
+ let balance = 0;
359
+ for (let j = i; j < lines.length; j++) {
360
+ const fragment = j === i ? value : lines[j];
361
+ const { opens, closes } = countTomlBrackets(fragment);
362
+ balance += opens - closes;
363
+ if (j > i)
364
+ value += `\n${fragment.trim()}`;
365
+ if (balance <= 0)
366
+ return value;
367
+ }
368
+ return value;
369
+ }
370
+ return null;
371
+ }
372
+ function parseTomlScalar(value) {
373
+ const trimmed = value.trim();
374
+ if (trimmed.startsWith("\"")) {
375
+ const end = scanBasicString(trimmed, 0);
376
+ return trimmed.slice(1, end);
377
+ }
378
+ if (trimmed.startsWith("'")) {
379
+ const end = scanLiteralString(trimmed, 0);
380
+ return trimmed.slice(1, end);
381
+ }
382
+ const bare = trimmed.match(/^[^\s,]+/);
383
+ return bare ? bare[0] : null;
384
+ }
258
385
  function codexTomlValue(text, key) {
259
- const m = text.match(new RegExp(`^\\s*${key}\\s*=\\s*["']?([^"'\\r\\n#]+)["']?`, "m"));
260
- return m ? m[1].trim() : null;
386
+ const value = tomlAssignmentValue(text, key);
387
+ return value === null ? null : parseTomlScalar(value);
261
388
  }
262
389
  function codexTomlArray(text, key) {
263
- const m = text.match(new RegExp(`^\\s*${key}\\s*=\\s*\\[([^\\]]*)\\]`, "m"));
264
- if (!m)
390
+ const value = tomlAssignmentValue(text, key);
391
+ if (!value || !value.trim().startsWith("["))
265
392
  return [];
266
- return [...m[1].matchAll(/["']([^"']+)["']/g)].map((v) => v[1]);
393
+ const out = [];
394
+ for (let i = 0; i < value.length; i++) {
395
+ if (value[i] === "\"") {
396
+ const end = scanBasicString(value, i);
397
+ out.push(value.slice(i + 1, end));
398
+ i = end;
399
+ }
400
+ else if (value[i] === "'") {
401
+ const end = scanLiteralString(value, i);
402
+ out.push(value.slice(i + 1, end));
403
+ i = end;
404
+ }
405
+ }
406
+ return out;
267
407
  }
268
408
  function assertBasicTomlParsable(text) {
269
- let inArray = false;
270
- for (const line of text.split(/\r?\n/)) {
271
- const trimmed = line.replace(/#.*$/, "").trim();
409
+ const sanitized = stripTomlCommentsAndMultilineStrings(text);
410
+ let arrayDepth = 0;
411
+ for (const line of sanitized.split(/\r?\n/)) {
412
+ const trimmed = line.trim();
272
413
  if (!trimmed)
273
414
  continue;
274
415
  if (/^\[[^\]]+\]$/.test(trimmed))
275
416
  continue;
276
- if (!trimmed.includes("=") && !inArray)
417
+ if (!trimmed.includes("=") && arrayDepth === 0)
277
418
  throw new Error(`malformed TOML line: ${trimmed}`);
278
- const quoteCount = (trimmed.match(/(?<!\\)"/g) ?? []).length + (trimmed.match(/(?<!\\)'/g) ?? []).length;
279
- if (quoteCount % 2 !== 0)
280
- throw new Error(`unbalanced TOML quotes: ${trimmed}`);
281
- const opens = (trimmed.match(/\[/g) ?? []).length;
282
- const closes = (trimmed.match(/\]/g) ?? []).length;
283
- inArray = inArray || opens > closes;
284
- if (closes > opens)
285
- inArray = false;
286
- }
287
- if (inArray)
419
+ const { opens, closes } = countTomlBrackets(trimmed);
420
+ arrayDepth += opens - closes;
421
+ if (arrayDepth < 0)
422
+ throw new Error(`malformed TOML line: ${trimmed}`);
423
+ }
424
+ if (arrayDepth > 0)
288
425
  throw new Error("unterminated TOML array");
426
+ return sanitized;
289
427
  }
290
428
  function readCodexConfig(path) {
291
429
  try {
292
430
  const text = readFileSync(path, "utf8");
293
- assertBasicTomlParsable(text);
431
+ const toml = assertBasicTomlParsable(text);
294
432
  const rules = { allow: [], deny: [], ask: [], additionalDirectories: [], sandboxNetwork: false };
295
- if (codexTomlValue(text, "sandbox_mode") === "read-only") {
433
+ if (codexTomlValue(toml, "sandbox_mode") === "read-only") {
296
434
  rules.deny.push("Edit", "Write", "NotebookEdit");
297
435
  }
298
- if (/\bsandbox_workspace_write\.network_access\s*=\s*true\b/i.test(text) ||
299
- /^\s*\[sandbox_workspace_write\][\s\S]*?^\s*network_access\s*=\s*true\b/im.test(text)) {
436
+ if (/\bsandbox_workspace_write\.network_access\s*=\s*true\b/i.test(toml) ||
437
+ /^\s*\[sandbox_workspace_write\][\s\S]*?^\s*network_access\s*=\s*true\b/im.test(toml)) {
300
438
  rules.sandboxNetwork = true;
301
439
  }
302
- rules.additionalDirectories.push(...codexTomlArray(text, "writable_roots"));
303
- for (const m of text.matchAll(/^\s*path\s*=\s*["']([^"']+)["']\s*\r?\n\s*access\s*=\s*["']deny["']/gm)) {
440
+ rules.additionalDirectories.push(...codexTomlArray(toml, "writable_roots"));
441
+ for (const m of toml.matchAll(/^\s*path\s*=\s*["']([^"']+)["']\s*\r?\n\s*access\s*=\s*["']deny["']/gm)) {
304
442
  rules.deny.push(`Edit(${m[1]})`, `Write(${m[1]})`);
305
443
  }
306
- for (const m of text.matchAll(/^\s*domain\s*=\s*["']([^"']+)["']\s*\r?\n\s*access\s*=\s*["']deny["']/gm)) {
444
+ for (const m of toml.matchAll(/^\s*domain\s*=\s*["']([^"']+)["']\s*\r?\n\s*access\s*=\s*["']deny["']/gm)) {
307
445
  rules.deny.push(`WebFetch(domain:${m[1]})`);
308
446
  }
309
- for (const m of text.matchAll(/^\s*domain\s*=\s*["']([^"']+)["']\s*\r?\n\s*access\s*=\s*["']allow["']/gm)) {
447
+ for (const m of toml.matchAll(/^\s*domain\s*=\s*["']([^"']+)["']\s*\r?\n\s*access\s*=\s*["']allow["']/gm)) {
310
448
  rules.allow.push(`WebFetch(domain:${m[1]})`);
311
449
  rules.sandboxNetwork = true;
312
450
  }
@@ -1,6 +1,7 @@
1
1
  import { realpathSync } from "node:fs";
2
2
  import { pathToFileURL } from "node:url";
3
3
  import { countJsonlType, runHook, } from "../orchestration/hook-core.js";
4
+ import { hasParentMarker } from "../launch-prompt.js";
4
5
  /**
5
6
  * Claude Code UserPromptSubmit hook entry. Reads the JSON payload from stdin,
6
7
  * runs the provider-agnostic core with the Claude adapter, and writes the
@@ -31,7 +32,10 @@ export const claudeAdapter = {
31
32
  return true;
32
33
  }
33
34
  const entrypoint = env.CLAUDE_CODE_ENTRYPOINT;
34
- return typeof entrypoint === "string" && SUBAGENT_ENTRYPOINTS.has(entrypoint);
35
+ if (typeof entrypoint === "string" && SUBAGENT_ENTRYPOINTS.has(entrypoint)) {
36
+ return true;
37
+ }
38
+ return hasParentMarker(payload.prompt);
35
39
  },
36
40
  // Count JSONL lines in the transcript whose parsed object.type === 'user'.
37
41
  // Delegates to the bounded counter (reads at most the trailing window so a
@@ -41,6 +45,7 @@ export const claudeAdapter = {
41
45
  currentTurn(transcriptPath) {
42
46
  return countJsonlType(transcriptPath, "user");
43
47
  },
48
+ anonScope: "claude",
44
49
  fullDirectiveFile: "orchestration-claude.md",
45
50
  shortOnFile: "short-on.md",
46
51
  shortOffFile: "short-off.md",