@heretyc/subagent-mcp 2.12.13 → 2.12.15
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 +170 -170
- package/dist/advanced-ruleset.py +67 -67
- package/dist/concurrency.js +131 -17
- package/dist/config-scaffold.js +1 -1
- package/dist/drivers.js +13 -6
- package/dist/effort.js +5 -3
- package/dist/global-concurrency.jsonc +43 -43
- package/dist/global-subagent-mcp-config.jsonc +43 -43
- package/dist/hooks/orchestration-codex.js +2 -2
- package/dist/index.js +6 -6
- package/dist/launch-prompt.js +3 -2
- package/dist/orchestration/hook-core.js +10 -8
- package/dist/orchestration/liveness.js +24 -3
- package/dist/orchestration/marker.js +9 -46
- package/dist/orchestration/model-mode.js +1 -1
- package/dist/orchestration/reminder.js +66 -14
- package/dist/output-helpers.js +39 -30
- package/dist/pending-permissions.js +13 -3
- package/dist/platform.js +5 -2
- package/dist/routing-table.json +2133 -2133
- package/dist/ruleset-scaffold.js +1 -1
- package/package.json +70 -70
- package/scripts/postinstall.mjs +102 -102
package/README.md
CHANGED
|
@@ -1,170 +1,170 @@
|
|
|
1
|
-
# subagent-mcp
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@heretyc/subagent-mcp)
|
|
4
|
-
[](LICENSE)
|
|
5
|
-
[](https://www.npmjs.com/package/@heretyc/subagent-mcp)
|
|
6
|
-
[](https://github.com/Heretyc/subagent-mcp/actions/workflows/claude-routine.yml)
|
|
7
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
## Install
|
|
27
|
-
|
|
28
|
-
### What You Need First
|
|
29
|
-
|
|
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)
|
|
34
|
-
|
|
35
|
-
Building from source needs extra developer tools. See
|
|
36
|
-
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
37
|
-
|
|
38
|
-
### Install The Package
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
npm install -g @heretyc/subagent-mcp
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
This is the standard install. Organizations pinning the package through GitHub
|
|
45
|
-
Packages should see [docs/registration.md](docs/registration.md).
|
|
46
|
-
|
|
47
|
-
### Wire It Into Your Assistant
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
subagent-mcp setup
|
|
51
|
-
```
|
|
52
|
-
|
|
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
|
|
55
|
-
registers both the server and the per-turn orchestration hooks. Preview first
|
|
56
|
-
with `subagent-mcp setup --dry-run`.
|
|
57
|
-
|
|
58
|
-
### Restart, Then Turn On The Invariant
|
|
59
|
-
|
|
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:
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
subagent-mcp init --global
|
|
65
|
-
```
|
|
66
|
-
|
|
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
|
|
71
|
-
[docs/registration.md](docs/registration.md).
|
|
72
|
-
|
|
73
|
-
## How To Operate It
|
|
74
|
-
|
|
75
|
-
### Orchestration Mode
|
|
76
|
-
|
|
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.
|
|
80
|
-
|
|
81
|
-
Flip it with the `orchestration-mode` tool. Desktop apps can toggle the mode but
|
|
82
|
-
do not receive per-turn hook reminders.
|
|
83
|
-
|
|
84
|
-
### Tools
|
|
85
|
-
|
|
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.
|
|
119
|
-
|
|
120
|
-
## Permissions
|
|
121
|
-
|
|
122
|
-
Launched sub-agents run gated by default. Set `permissionsCeiling` in
|
|
123
|
-
`global-subagent-mcp-config.jsonc`:
|
|
124
|
-
|
|
125
|
-
| Mode | What a sub-agent can do |
|
|
126
|
-
|---|---|
|
|
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. |
|
|
130
|
-
|
|
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:
|
|
133
|
-
|
|
134
|
-
```text
|
|
135
|
-
respond_permission(agent_id="...", decision="allow" | "deny", reason="...")
|
|
136
|
-
```
|
|
137
|
-
|
|
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.
|
|
151
|
-
|
|
152
|
-
## Documentation
|
|
153
|
-
|
|
154
|
-
| Document | Contents |
|
|
155
|
-
|---|---|
|
|
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 |
|
|
165
|
-
|
|
166
|
-
## License
|
|
167
|
-
|
|
168
|
-
Apache-2.0. Copyright 2026 Lexi Blackburn.
|
|
169
|
-
|
|
170
|
-
See [LICENSE](LICENSE).
|
|
1
|
+
# subagent-mcp
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@heretyc/subagent-mcp)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://www.npmjs.com/package/@heretyc/subagent-mcp)
|
|
6
|
+
[](https://github.com/Heretyc/subagent-mcp/actions/workflows/claude-routine.yml)
|
|
7
|
+
|
|
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
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
### What You Need First
|
|
29
|
+
|
|
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)
|
|
34
|
+
|
|
35
|
+
Building from source needs extra developer tools. See
|
|
36
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
37
|
+
|
|
38
|
+
### Install The Package
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install -g @heretyc/subagent-mcp
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This is the standard install. Organizations pinning the package through GitHub
|
|
45
|
+
Packages should see [docs/registration.md](docs/registration.md).
|
|
46
|
+
|
|
47
|
+
### Wire It Into Your Assistant
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
subagent-mcp setup
|
|
51
|
+
```
|
|
52
|
+
|
|
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
|
|
55
|
+
registers both the server and the per-turn orchestration hooks. Preview first
|
|
56
|
+
with `subagent-mcp setup --dry-run`.
|
|
57
|
+
|
|
58
|
+
### Restart, Then Turn On The Invariant
|
|
59
|
+
|
|
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:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
subagent-mcp init --global
|
|
65
|
+
```
|
|
66
|
+
|
|
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
|
|
71
|
+
[docs/registration.md](docs/registration.md).
|
|
72
|
+
|
|
73
|
+
## How To Operate It
|
|
74
|
+
|
|
75
|
+
### Orchestration Mode
|
|
76
|
+
|
|
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.
|
|
80
|
+
|
|
81
|
+
Flip it with the `orchestration-mode` tool. Desktop apps can toggle the mode but
|
|
82
|
+
do not receive per-turn hook reminders.
|
|
83
|
+
|
|
84
|
+
### Tools
|
|
85
|
+
|
|
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.
|
|
119
|
+
|
|
120
|
+
## Permissions
|
|
121
|
+
|
|
122
|
+
Launched sub-agents run gated by default. Set `permissionsCeiling` in
|
|
123
|
+
`global-subagent-mcp-config.jsonc`:
|
|
124
|
+
|
|
125
|
+
| Mode | What a sub-agent can do |
|
|
126
|
+
|---|---|
|
|
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. |
|
|
130
|
+
|
|
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:
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
respond_permission(agent_id="...", decision="allow" | "deny", reason="...")
|
|
136
|
+
```
|
|
137
|
+
|
|
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.
|
|
151
|
+
|
|
152
|
+
## Documentation
|
|
153
|
+
|
|
154
|
+
| Document | Contents |
|
|
155
|
+
|---|---|
|
|
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 |
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
Apache-2.0. Copyright 2026 Lexi Blackburn.
|
|
169
|
+
|
|
170
|
+
See [LICENSE](LICENSE).
|
package/dist/advanced-ruleset.py
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""advanced-ruleset.py — final-authority model-routing override hook for subagent-mcp.
|
|
3
|
-
|
|
4
|
-
(a) PERFORMANCE WARNING: this script runs synchronously inside EVERY launch_agent
|
|
5
|
-
call. Slow rules slow every agent launch. Keep rules lean and low-latency —
|
|
6
|
-
no network calls, no heavy imports at module top. This is YOUR responsibility;
|
|
7
|
-
you have been warned.
|
|
8
|
-
|
|
9
|
-
(b) OUTPUT CONTRACT (routing mode): print to stdout ONE JSON array — the modified
|
|
10
|
-
candidate list (reorder / filter / replace allowed). Template:
|
|
11
|
-
[
|
|
12
|
-
{"provider": "claude", "model": "sonnet", "effort": "high", "rank": 1},
|
|
13
|
-
{"provider": "codex", "model": "gpt-5.5", "effort": "xhigh", "rank": 2}
|
|
14
|
-
]
|
|
15
|
-
Valid providers: claude, codex. Valid models: haiku, sonnet, opus, opus-4-8, fable (claude);
|
|
16
|
-
gpt-5.5 (codex). Valid efforts: haiku -> "none" only; sonnet -> medium|high|xhigh|max;
|
|
17
|
-
fable -> medium|high|xhigh|max; opus/opus-4-8 -> those plus ultracode; gpt-5.5 -> medium|high|xhigh.
|
|
18
|
-
"rank" on output is ignored. An EMPTY array vetoes the launch. Anything else
|
|
19
|
-
invalid fails the launch hard — the server validates strictly.
|
|
20
|
-
|
|
21
|
-
(c) INPUT CONTRACT (routing mode, invoked as: <python> advanced-ruleset.py route):
|
|
22
|
-
stdin receives one JSON object:
|
|
23
|
-
{ "candidates": [ {"provider","model","effort","rank"} ... ], # rank 1..N best->worst
|
|
24
|
-
"context": { "task_category": str, "cwd": str,
|
|
25
|
-
"selection_mode": "auto"|"provider"|"provider_model"|"explicit",
|
|
26
|
-
"provider": str|None, "model": str|None, "effort": str|None } }
|
|
27
|
-
OS environment variables are visible natively (os.environ).
|
|
28
|
-
|
|
29
|
-
ENV-CHECK MODE (no arguments): prints {"ready": true|false, "load-rules": true|false}.
|
|
30
|
-
Runs once per MCP server process. load-rules false => ruleset silently disabled
|
|
31
|
-
for the rest of the process. Set LOAD_RULES = True below to activate.
|
|
32
|
-
"""
|
|
33
|
-
import json
|
|
34
|
-
import sys
|
|
35
|
-
|
|
36
|
-
LOAD_RULES = False
|
|
37
|
-
|
|
38
|
-
# --- Requirements stub (scaffold itself is stdlib-only) ----------------------
|
|
39
|
-
# List third-party distributions your rules import, e.g.:
|
|
40
|
-
# REQUIREMENTS = ["requests", "pyyaml"]
|
|
41
|
-
# Install with: <python> -m pip install <name> ...
|
|
42
|
-
REQUIREMENTS = []
|
|
43
|
-
|
|
44
|
-
def missing_requirements():
|
|
45
|
-
"""pip-check helper: returns the REQUIREMENTS entries not importable here."""
|
|
46
|
-
import importlib.util
|
|
47
|
-
return [r for r in REQUIREMENTS
|
|
48
|
-
if importlib.util.find_spec(r.replace("-", "_")) is None]
|
|
49
|
-
|
|
50
|
-
def env_check():
|
|
51
|
-
missing = missing_requirements()
|
|
52
|
-
json.dump({"ready": not missing, "load-rules": bool(LOAD_RULES)}, sys.stdout)
|
|
53
|
-
|
|
54
|
-
def apply_rules(candidates, context):
|
|
55
|
-
"""YOUR RULES HERE. Default: passthrough (returns the list unchanged)."""
|
|
56
|
-
return candidates
|
|
57
|
-
|
|
58
|
-
def route():
|
|
59
|
-
payload = json.load(sys.stdin)
|
|
60
|
-
out = apply_rules(payload.get("candidates", []), payload.get("context", {}))
|
|
61
|
-
json.dump(out, sys.stdout)
|
|
62
|
-
|
|
63
|
-
if __name__ == "__main__":
|
|
64
|
-
if len(sys.argv) > 1 and sys.argv[1] == "route":
|
|
65
|
-
route()
|
|
66
|
-
else:
|
|
67
|
-
env_check()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""advanced-ruleset.py — final-authority model-routing override hook for subagent-mcp.
|
|
3
|
+
|
|
4
|
+
(a) PERFORMANCE WARNING: this script runs synchronously inside EVERY launch_agent
|
|
5
|
+
call. Slow rules slow every agent launch. Keep rules lean and low-latency —
|
|
6
|
+
no network calls, no heavy imports at module top. This is YOUR responsibility;
|
|
7
|
+
you have been warned.
|
|
8
|
+
|
|
9
|
+
(b) OUTPUT CONTRACT (routing mode): print to stdout ONE JSON array — the modified
|
|
10
|
+
candidate list (reorder / filter / replace allowed). Template:
|
|
11
|
+
[
|
|
12
|
+
{"provider": "claude", "model": "sonnet", "effort": "high", "rank": 1},
|
|
13
|
+
{"provider": "codex", "model": "gpt-5.5", "effort": "xhigh", "rank": 2}
|
|
14
|
+
]
|
|
15
|
+
Valid providers: claude, codex. Valid models: haiku, sonnet, opus, opus-4-8, fable (claude);
|
|
16
|
+
gpt-5.5 (codex). Valid efforts: haiku -> "none" only; sonnet -> medium|high|xhigh|max;
|
|
17
|
+
fable -> medium|high|xhigh|max; opus/opus-4-8 -> those plus ultracode; gpt-5.5 -> medium|high|xhigh.
|
|
18
|
+
"rank" on output is ignored. An EMPTY array vetoes the launch. Anything else
|
|
19
|
+
invalid fails the launch hard — the server validates strictly.
|
|
20
|
+
|
|
21
|
+
(c) INPUT CONTRACT (routing mode, invoked as: <python> advanced-ruleset.py route):
|
|
22
|
+
stdin receives one JSON object:
|
|
23
|
+
{ "candidates": [ {"provider","model","effort","rank"} ... ], # rank 1..N best->worst
|
|
24
|
+
"context": { "task_category": str, "cwd": str,
|
|
25
|
+
"selection_mode": "auto"|"provider"|"provider_model"|"explicit",
|
|
26
|
+
"provider": str|None, "model": str|None, "effort": str|None } }
|
|
27
|
+
OS environment variables are visible natively (os.environ).
|
|
28
|
+
|
|
29
|
+
ENV-CHECK MODE (no arguments): prints {"ready": true|false, "load-rules": true|false}.
|
|
30
|
+
Runs once per MCP server process. load-rules false => ruleset silently disabled
|
|
31
|
+
for the rest of the process. Set LOAD_RULES = True below to activate.
|
|
32
|
+
"""
|
|
33
|
+
import json
|
|
34
|
+
import sys
|
|
35
|
+
|
|
36
|
+
LOAD_RULES = False
|
|
37
|
+
|
|
38
|
+
# --- Requirements stub (scaffold itself is stdlib-only) ----------------------
|
|
39
|
+
# List third-party distributions your rules import, e.g.:
|
|
40
|
+
# REQUIREMENTS = ["requests", "pyyaml"]
|
|
41
|
+
# Install with: <python> -m pip install <name> ...
|
|
42
|
+
REQUIREMENTS = []
|
|
43
|
+
|
|
44
|
+
def missing_requirements():
|
|
45
|
+
"""pip-check helper: returns the REQUIREMENTS entries not importable here."""
|
|
46
|
+
import importlib.util
|
|
47
|
+
return [r for r in REQUIREMENTS
|
|
48
|
+
if importlib.util.find_spec(r.replace("-", "_")) is None]
|
|
49
|
+
|
|
50
|
+
def env_check():
|
|
51
|
+
missing = missing_requirements()
|
|
52
|
+
json.dump({"ready": not missing, "load-rules": bool(LOAD_RULES)}, sys.stdout)
|
|
53
|
+
|
|
54
|
+
def apply_rules(candidates, context):
|
|
55
|
+
"""YOUR RULES HERE. Default: passthrough (returns the list unchanged)."""
|
|
56
|
+
return candidates
|
|
57
|
+
|
|
58
|
+
def route():
|
|
59
|
+
payload = json.load(sys.stdin)
|
|
60
|
+
out = apply_rules(payload.get("candidates", []), payload.get("context", {}))
|
|
61
|
+
json.dump(out, sys.stdout)
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
if len(sys.argv) > 1 and sys.argv[1] == "route":
|
|
65
|
+
route()
|
|
66
|
+
else:
|
|
67
|
+
env_check()
|
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));
|
|
@@ -486,7 +529,20 @@ function repoConfigDigest(cwd) {
|
|
|
486
529
|
h.update(file).update("\0").update(readFileSync(file));
|
|
487
530
|
return h.digest("hex");
|
|
488
531
|
}
|
|
532
|
+
const FIRST_REPO_DIGEST_LIMIT = 100;
|
|
533
|
+
// ponytail: process-local only; restart persistence is unnecessary for first-seen repo digests.
|
|
489
534
|
const firstRepoDigests = new Map();
|
|
535
|
+
function rememberFirstRepoDigest(cwd, digest) {
|
|
536
|
+
if (firstRepoDigests.has(cwd)) {
|
|
537
|
+
firstRepoDigests.delete(cwd);
|
|
538
|
+
}
|
|
539
|
+
else if (firstRepoDigests.size >= FIRST_REPO_DIGEST_LIMIT) {
|
|
540
|
+
const oldest = firstRepoDigests.keys().next().value;
|
|
541
|
+
if (oldest !== undefined)
|
|
542
|
+
firstRepoDigests.delete(oldest);
|
|
543
|
+
}
|
|
544
|
+
firstRepoDigests.set(cwd, digest);
|
|
545
|
+
}
|
|
490
546
|
export function readMergedPermissionConfig(cwd, path = defaultConfigPath()) {
|
|
491
547
|
const global = readGlobalConfig(path);
|
|
492
548
|
noteLegacyConfigIfUsed(path);
|
|
@@ -537,7 +593,7 @@ export function readMergedPermissionConfig(cwd, path = defaultConfigPath()) {
|
|
|
537
593
|
const first = firstRepoDigests.get(cwd);
|
|
538
594
|
const repoConfigChangedSinceFirstSeen = first !== undefined && first !== digest;
|
|
539
595
|
if (first === undefined)
|
|
540
|
-
|
|
596
|
+
rememberFirstRepoDigest(cwd, digest);
|
|
541
597
|
const selfProtectionDeny = configSelfProtectionDenyRules(path);
|
|
542
598
|
const protectedDirs = new Set([resolve(path), resolve(legacyConfigPath(path))]);
|
|
543
599
|
merged.deny.push(...selfProtectionDeny);
|
|
@@ -591,6 +647,74 @@ export function countSlots(dir = slotDir()) {
|
|
|
591
647
|
return 0;
|
|
592
648
|
}
|
|
593
649
|
}
|
|
650
|
+
function slotPathForIndex(dir, index) {
|
|
651
|
+
return join(dir, `slot-${index}.json`);
|
|
652
|
+
}
|
|
653
|
+
function slotMetadataJson(agentId) {
|
|
654
|
+
const now = Date.now();
|
|
655
|
+
return JSON.stringify({
|
|
656
|
+
schema_version: 1,
|
|
657
|
+
agent_id: agentId,
|
|
658
|
+
server_pid: process.pid,
|
|
659
|
+
child_pid: null,
|
|
660
|
+
cwd: process.cwd(),
|
|
661
|
+
started_at: new Date(now).toISOString(),
|
|
662
|
+
started_at_ms: now,
|
|
663
|
+
last_activity_ms: now,
|
|
664
|
+
status: null,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
function claimSlotPath(dir, agentId, max) {
|
|
668
|
+
let existing;
|
|
669
|
+
try {
|
|
670
|
+
existing = readdirSync(dir).filter((f) => f.startsWith("slot-"));
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
existing = [];
|
|
674
|
+
}
|
|
675
|
+
if (existing.length >= max)
|
|
676
|
+
return null;
|
|
677
|
+
const occupied = new Set(existing);
|
|
678
|
+
const candidateCount = max - existing.length;
|
|
679
|
+
const candidates = [];
|
|
680
|
+
for (let i = 0; i < max && candidates.length < candidateCount; i++) {
|
|
681
|
+
const name = `slot-${i}.json`;
|
|
682
|
+
if (!occupied.has(name))
|
|
683
|
+
candidates.push(slotPathForIndex(dir, i));
|
|
684
|
+
}
|
|
685
|
+
for (const path of candidates) {
|
|
686
|
+
let fd = null;
|
|
687
|
+
try {
|
|
688
|
+
fd = openSync(path, "wx", 0o600);
|
|
689
|
+
writeFileSync(fd, slotMetadataJson(agentId));
|
|
690
|
+
return path;
|
|
691
|
+
}
|
|
692
|
+
catch (e) {
|
|
693
|
+
if (fd !== null) {
|
|
694
|
+
try {
|
|
695
|
+
closeSync(fd);
|
|
696
|
+
}
|
|
697
|
+
catch { }
|
|
698
|
+
fd = null;
|
|
699
|
+
try {
|
|
700
|
+
unlinkSync(path);
|
|
701
|
+
}
|
|
702
|
+
catch { }
|
|
703
|
+
}
|
|
704
|
+
if (e.code !== "EEXIST")
|
|
705
|
+
throw e;
|
|
706
|
+
}
|
|
707
|
+
finally {
|
|
708
|
+
if (fd !== null) {
|
|
709
|
+
try {
|
|
710
|
+
closeSync(fd);
|
|
711
|
+
}
|
|
712
|
+
catch { }
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
594
718
|
export function scheduleForceKill(ms, kill) {
|
|
595
719
|
const timer = setTimeout(kill, ms);
|
|
596
720
|
timer.unref();
|
|
@@ -608,21 +732,11 @@ export function reserveSlot(agentId, max, dir = slotDir(), cullDeps) {
|
|
|
608
732
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
609
733
|
}
|
|
610
734
|
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 { }
|
|
735
|
+
// ponytail: numbered O_EXCL slot claims make admission atomic without queues or a process-wide lock.
|
|
736
|
+
const slotPath = claimSlotPath(dir, agentId, max);
|
|
737
|
+
if (!slotPath)
|
|
624
738
|
return { ok: false, current: countSlots(dir), max };
|
|
625
|
-
|
|
739
|
+
const after = countSlots(dir);
|
|
626
740
|
return { ok: true, slotPath, current: after, max };
|
|
627
741
|
}
|
|
628
742
|
catch (e) {
|