@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 +109 -149
- package/directives/carryover-claude.md +2 -2
- package/directives/carryover-codex.md +2 -2
- package/directives/reminder-off-claude.md +2 -2
- package/directives/reminder-off-codex.md +2 -2
- package/directives/reminder-on.md +1 -1
- package/dist/concurrency.js +165 -27
- package/dist/hooks/orchestration-claude.js +6 -1
- package/dist/hooks/orchestration-codex.js +8 -16
- package/dist/index.js +16 -8
- package/dist/launch-prompt.js +13 -7
- package/dist/orchestration/atomic-write.js +31 -0
- package/dist/orchestration/hook-core.js +48 -16
- package/dist/orchestration/liveness.js +3 -2
- package/dist/orchestration/marker.js +67 -38
- package/dist/orchestration/model-mode.js +3 -2
- package/dist/orchestration/reminder.js +3 -2
- package/dist/orchestration/update-check.js +6 -7
- package/dist/output-helpers.js +5 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -5,206 +5,166 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@heretyc/subagent-mcp)
|
|
6
6
|
[](https://github.com/Heretyc/subagent-mcp/actions/workflows/claude-routine.yml)
|
|
7
7
|
|
|
8
|
-
## Core
|
|
9
|
-
|
|
10
|
-
subagent-mcp is an MCP
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
28
|
+
### What You Need First
|
|
62
29
|
|
|
63
|
-
- Node.js 18 or newer
|
|
64
|
-
- `claude` CLI
|
|
65
|
-
- `codex` CLI
|
|
66
|
-
|
|
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
|
|
35
|
+
Building from source needs extra developer tools. See
|
|
69
36
|
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
70
37
|
|
|
71
|
-
### Install
|
|
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
|
|
78
|
-
|
|
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
|
|
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
|
|
87
|
-
|
|
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,
|
|
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
|
|
94
|
-
run `/hooks` and trust the new hook. Then
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
73
|
+
## How To Operate It
|
|
107
74
|
|
|
108
|
-
### Orchestration
|
|
75
|
+
### Orchestration Mode
|
|
109
76
|
|
|
110
|
-
- **ON
|
|
111
|
-
|
|
112
|
-
|
|
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.
|
|
116
|
-
|
|
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
|
-
###
|
|
84
|
+
### Tools
|
|
119
85
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
158
|
-
`
|
|
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` |
|
|
163
|
-
| `manual` | Same, but
|
|
164
|
-
| `yolo` | No gating at all
|
|
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
|
|
167
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
## Basic
|
|
179
|
-
|
|
180
|
-
- **
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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) |
|
|
198
|
-
| [docs/registration.md](docs/registration.md) | Per-platform setup
|
|
199
|
-
| [docs/tools.md](docs/tools.md) |
|
|
200
|
-
| [docs/usage.md](docs/usage.md) | Model
|
|
201
|
-
| [docs/SPEC.md](docs/SPEC.md) |
|
|
202
|
-
| [
|
|
203
|
-
| [docs/
|
|
204
|
-
| [
|
|
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
|
|
168
|
+
Apache-2.0. Copyright 2026 Lexi Blackburn.
|
|
209
169
|
|
|
210
170
|
See [LICENSE](LICENSE).
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
<!-- INJECTED PRE-PROMPT DIRECTIVE
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/dist/concurrency.js
CHANGED
|
@@ -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
|
|
260
|
-
return
|
|
386
|
+
const value = tomlAssignmentValue(text, key);
|
|
387
|
+
return value === null ? null : parseTomlScalar(value);
|
|
261
388
|
}
|
|
262
389
|
function codexTomlArray(text, key) {
|
|
263
|
-
const
|
|
264
|
-
if (!
|
|
390
|
+
const value = tomlAssignmentValue(text, key);
|
|
391
|
+
if (!value || !value.trim().startsWith("["))
|
|
265
392
|
return [];
|
|
266
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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("=") &&
|
|
417
|
+
if (!trimmed.includes("=") && arrayDepth === 0)
|
|
277
418
|
throw new Error(`malformed TOML line: ${trimmed}`);
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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(
|
|
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(
|
|
299
|
-
/^\s*\[sandbox_workspace_write\][\s\S]*?^\s*network_access\s*=\s*true\b/im.test(
|
|
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(
|
|
303
|
-
for (const m of
|
|
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
|
|
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
|
|
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
|
-
|
|
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",
|