@heretyc/subagent-mcp 2.9.0 → 2.10.0
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 +121 -57
- package/directives/carryover-claude.md +2 -2
- package/directives/carryover-codex.md +2 -4
- package/dist/concurrency.js +96 -0
- package/dist/config-scaffold.js +2 -0
- package/dist/global-concurrency.jsonc +29 -0
- package/dist/index.js +159 -69
- package/dist/init.js +1 -1
- package/dist/orchestration/hook-core.js +3 -1
- package/dist/orchestration/marker.js +99 -26
- package/dist/routing.js +10 -11
- package/dist/setup.js +1 -0
- package/package.json +18 -3
package/README.md
CHANGED
|
@@ -1,70 +1,115 @@
|
|
|
1
1
|
# subagent-mcp
|
|
2
2
|
|
|
3
|
-
|
|
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)
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
**License:** Apache-2.0 | **Author:** Lexi Blackburn | **Repo:** https://github.com/Heretyc/subagent-mcp
|
|
8
|
+
MCP server that launches and manages always-interactive Claude Code and Codex sub-agent sessions — on macOS, Linux, and Windows. No direct API calls. No API keys.
|
|
8
9
|
|
|
9
10
|
---
|
|
10
11
|
|
|
11
|
-
##
|
|
12
|
+
## Install
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
- Poll status, stream stdout/stderr tails, and enqueue follow-up messages to live sessions
|
|
15
|
-
- Concurrency caps: 5 concurrent Claude agents + 5 concurrent Codex agents (counts only actively-streaming `processing` agents, to limit API rate-limit pressure; quiet `stalled` agents don't reserve a slot)
|
|
16
|
-
- Liveness tracking via the visible provider stream (Claude SDK events, Codex app-server JSONL): agents with no parsed visible provider stream item for 10 minutes enter `stalled` state (still alive, just quiet -- thinking or awaiting a temp-file handoff), and recover to `processing` if the visible stream resumes
|
|
17
|
-
- Ultracode mode for Opus 4.8 -- headless activation via `--settings {"ultracode":true}` (see [docs/usage.md](docs/usage.md))
|
|
18
|
-
- Cross-platform exe resolution (Windows: npm-prefix .exe paths; macOS/Linux: PATH + Homebrew/usr-local fallbacks); immediate `taskkill /t /f` (Windows) / `SIGKILL` (POSIX) force-kill; no graceful shutdown period
|
|
19
|
-
- stdio MCP transport; built with `@modelcontextprotocol/sdk` + `zod`
|
|
20
|
-
- `orchestration-mode` tool — toggles orchestrator directives injected by bundled Claude Code / Codex hooks; Claude also gets a deterministic `PreToolUse` gate (Desktop hosts toggle but do not inject); see [docs/spec/orchestration-mode/_INDEX.md](docs/spec/orchestration-mode/_INDEX.md)
|
|
14
|
+
### Prerequisites (runtime)
|
|
21
15
|
|
|
22
|
-
|
|
16
|
+
To run the published CLI you need:
|
|
23
17
|
|
|
24
|
-
|
|
18
|
+
- Node.js >= 18 (`node --version`)
|
|
19
|
+
- `claude` CLI — globally installed and authenticated (`claude --version`)
|
|
20
|
+
- `codex` CLI — globally installed and authenticated (`codex --version`; optional if you only use Claude paths)
|
|
25
21
|
|
|
26
|
-
|
|
22
|
+
Building from source needs additional developer tooling — see [CONTRIBUTING.md § Prerequisites](CONTRIBUTING.md#prerequisites).
|
|
27
23
|
|
|
28
|
-
|
|
24
|
+
### npmjs (default)
|
|
25
|
+
|
|
26
|
+
No authentication, no `.npmrc` configuration, and no PAT required. The package is publicly available on [npmjs.com](https://www.npmjs.com/package/@heretyc/subagent-mcp).
|
|
29
27
|
|
|
30
28
|
```bash
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
npm install -g @heretyc/subagent-mcp
|
|
30
|
+
```
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
**Use this path for all standard installs.**
|
|
33
|
+
|
|
34
|
+
### GitHub Packages (org-internal pin / supply-chain auditing)
|
|
35
|
+
|
|
36
|
+
Use this path when your organization's `.npmrc` already routes the `@heretyc` scope through GitHub Packages, when you need supply-chain audit tracing against the org-internal artifact, or when you run inside a GitHub Actions workflow whose `GITHUB_TOKEN` carries `read:packages`. Otherwise prefer npmjs above — it is simpler and needs no credentials.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# One-time machine setup
|
|
40
|
+
echo "@heretyc:registry=https://npm.pkg.github.com" >> ~/.npmrc
|
|
35
41
|
echo "//npm.pkg.github.com/:_authToken=YOUR_GITHUB_PAT" >> ~/.npmrc
|
|
42
|
+
# PAT must be a classic PAT with the read:packages scope
|
|
36
43
|
|
|
37
|
-
# 3. Install and wire
|
|
38
44
|
npm install -g @heretyc/subagent-mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> **Note:** GitHub Packages requires authentication even for public packages. If you see `401 Unauthorized`, verify your PAT has the `read:packages` scope and has not expired. GitHub Packages does not render a README page — the live documentation appears on [npmjs.com](https://www.npmjs.com/package/@heretyc/subagent-mcp).
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
**1. Install** (see [Install](#install) above):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install -g @heretyc/subagent-mcp
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**2. Register the MCP server + orchestration hooks:**
|
|
60
|
+
|
|
61
|
+
```bash
|
|
39
62
|
subagent-mcp setup
|
|
40
63
|
```
|
|
41
64
|
|
|
42
|
-
`setup` detects
|
|
65
|
+
Installing the package only ships the binary — it does **not** auto-wire anything. `subagent-mcp setup` auto-detects Claude Code / Codex and registers the MCP server plus the per-turn orchestration-mode hooks. Preview with `subagent-mcp setup --dry-run`.
|
|
43
66
|
|
|
44
|
-
|
|
45
|
-
the managed invariant block into `AGENTS.md`, `CLAUDE.md`, and `GEMINI.md`.
|
|
46
|
-
Use `--dry-run` to preview, `--remove` to uninstall the block, and `--force`
|
|
47
|
-
only if you intentionally run inside this source repo.
|
|
67
|
+
**3. Restart your host.**
|
|
48
68
|
|
|
49
|
-
|
|
50
|
-
instead — `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, `~/.gemini/GEMINI.md` —
|
|
51
|
-
run `subagent-mcp init --global`. It honors `--dry-run`/`--remove`/`--force`
|
|
52
|
-
and is mutually exclusive with `--root`/`--files`/`--copilot`/`--cursor`. See
|
|
53
|
-
[docs/registration.md](docs/registration.md).
|
|
69
|
+
Restart your Claude Code or Codex session so it picks up the new binary. On Codex, run `/hooks` and trust the new hook.
|
|
54
70
|
|
|
55
|
-
|
|
71
|
+
**4. Initialize the orchestration invariant (recommended):**
|
|
56
72
|
|
|
57
|
-
|
|
73
|
+
```bash
|
|
74
|
+
subagent-mcp init --global
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`init --global` writes the managed invariant block into the providers' official global user-config files — `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, `~/.gemini/GEMINI.md` — set up once, works across every project. For a single project instead, use `subagent-mcp init --root /path/to/project` to write into that project's instruction files. `--global` and `--root` are mutually exclusive.
|
|
78
|
+
|
|
79
|
+
For manual wiring, Gemini CLI, Claude Desktop, and developer install from source, see [docs/registration.md](docs/registration.md).
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Features
|
|
84
|
+
|
|
85
|
+
- Start Claude or Codex interactive sessions as managed sub-agents from any MCP host
|
|
86
|
+
- Poll status, stream stdout/stderr tails, and enqueue follow-up messages to live sessions
|
|
87
|
+
- Concurrency caps: 5 concurrent Claude + 5 concurrent Codex agents (only `processing` agents count toward the cap; `stalled` agents do not hold a slot)
|
|
88
|
+
- Liveness tracking via the visible provider stream (Claude SDK events, Codex app-server JSONL): agents with no parsed visible provider stream item for 10 minutes enter `stalled` state (still alive, just quiet — thinking or awaiting a temp-file handoff), and recover to `processing` if the visible stream resumes
|
|
89
|
+
- Ultracode mode for Opus 4.8 via `--settings {"ultracode":true}` — see [docs/usage.md](docs/usage.md)
|
|
90
|
+
- Cross-platform exe resolution (Windows: npm-prefix .exe paths; macOS/Linux: PATH + Homebrew/usr-local fallbacks); immediate `taskkill /t /f` (Windows) / `SIGKILL` (POSIX) force-kill; no graceful shutdown period
|
|
91
|
+
- stdio MCP transport; built with `@modelcontextprotocol/sdk` + `zod`
|
|
92
|
+
- `orchestration-mode` tool — toggles orchestrator directives injected by bundled Claude Code / Codex hooks; Claude also gets a deterministic `PreToolUse` gate
|
|
93
|
+
|
|
94
|
+
---
|
|
58
95
|
|
|
59
|
-
|
|
96
|
+
## Orchestration Mode
|
|
97
|
+
|
|
98
|
+
**ON:** the agent operates as an orchestrator — hook injection governs each session turn, preventing inline reads or writes, and all work is delegated through sub-agent tools.
|
|
99
|
+
|
|
100
|
+
**OFF:** the agent operates normally, with no orchestration constraints.
|
|
101
|
+
|
|
102
|
+
Toggle with the `orchestration-mode` tool. Desktop hosts toggle the mode but receive no hook injection. See [docs/spec/dev-loop/orchestration-directive-architecture.md](docs/spec/dev-loop/orchestration-directive-architecture.md) for full semantics.
|
|
60
103
|
|
|
61
104
|
---
|
|
62
105
|
|
|
63
106
|
## Auto Mode
|
|
64
107
|
|
|
108
|
+
Pass `task_category` to `launch_agent` and the server picks the best provider, model, and effort for that category, with automatic fallback.
|
|
109
|
+
|
|
65
110
|
`launch_agent` supports **auto mode**: pass `prompt` + `task_category` and the server picks the best provider/model/effort for that category from its routing table, silently falling back to the next-best candidate on any launch-time failure.
|
|
66
111
|
|
|
67
|
-
`provider`, `model`, and `effort` are optional overrides — omit them to get auto-selected best combination. Rules: passing `model` requires `provider`; passing `effort` requires both `provider` and `model`.
|
|
112
|
+
`provider`, `model`, and `effort` are optional overrides — omit them to get the auto-selected best combination. Rules: passing `model` requires `provider`; passing `effort` requires both `provider` and `model`.
|
|
68
113
|
|
|
69
114
|
**task_category** (required) — pick one:
|
|
70
115
|
|
|
@@ -92,20 +137,36 @@ The last four are **composite-inferred**: they carry no dedicated benchmark and
|
|
|
92
137
|
|
|
93
138
|
---
|
|
94
139
|
|
|
140
|
+
## Configuration
|
|
141
|
+
|
|
142
|
+
### Global concurrent-subagent cap
|
|
143
|
+
|
|
144
|
+
subagent-mcp enforces a machine-global cap on the number of **live concurrent subagents** across all agentic sessions and MCP server processes on the machine. The count includes agents started by other active sessions and the whole recursive descendant tree; slots free as agents finish or are killed.
|
|
145
|
+
|
|
146
|
+
The cap is configured in `global-concurrency.jsonc`, a dedicated dist-sibling file in the install directory. It is separate from the advanced routing directives file, ships as a commented template, and is retained across installs and updates by the same preserve-user-edits mechanism as the advanced routing directives.
|
|
147
|
+
|
|
148
|
+
Set `globalConcurrentSubagents` in that file. The default is `20`; the minimum valid value is `10`. Validation is forced: `0`, unset, missing, or invalid values reset to `20`, and values `1` through `9` are pinned up to `10`. There is no environment-variable override; the file is the sole source of truth. The file is re-read on every `launch_agent` call, so edits take effect on the next launch with no server restart.
|
|
149
|
+
|
|
150
|
+
When the cap is reached, `launch_agent` is rejected immediately; it never queues. Because the count includes other active sessions and the entire descendant tree, free slots manually with `list_agents` and `kill_agent` before retrying. There is no automatic cleanup or zombie reaping.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
95
154
|
## Tools
|
|
96
155
|
|
|
97
|
-
|
|
156
|
+
Eight tools are exposed over the stdio MCP transport.
|
|
98
157
|
|
|
99
|
-
| Tool |
|
|
100
|
-
|
|
101
|
-
| `launch_agent` | Start a new
|
|
102
|
-
| `poll_agent` |
|
|
103
|
-
| `kill_agent` |
|
|
104
|
-
| `send_message` | Enqueue a
|
|
105
|
-
| `list_agents` | List all agents
|
|
106
|
-
| `wait` | Block until
|
|
158
|
+
| Tool | Description |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `launch_agent` | Start a new sub-agent session with a prompt and optional task category / overrides |
|
|
161
|
+
| `poll_agent` | Check status and stream tail output for a running agent |
|
|
162
|
+
| `kill_agent` | Terminate a running or stalled agent |
|
|
163
|
+
| `send_message` | Enqueue a message into a running agent's stdin |
|
|
164
|
+
| `list_agents` | List all tracked agents and their current status |
|
|
165
|
+
| `wait` | Block until a specific agent reaches a terminal state |
|
|
166
|
+
| `orchestration-mode` | Toggle orchestration-mode ON/OFF; controls whether hook directives are injected into agent sessions |
|
|
167
|
+
| `model-selection-mode` | Control model selection: `smart` (auto-pick) or `user-approved-overrides` (30-min override window) |
|
|
107
168
|
|
|
108
|
-
Full parameters, return shapes, the `alive` / `idle_seconds` / `hint` / `recent_stream` fields
|
|
169
|
+
Full parameters, return shapes, and the `alive` / `idle_seconds` / `hint` / `recent_stream` fields are in [docs/tools.md](docs/tools.md).
|
|
109
170
|
|
|
110
171
|
---
|
|
111
172
|
|
|
@@ -114,30 +175,33 @@ Full parameters, return shapes, the `alive` / `idle_seconds` / `hint` / `recent_
|
|
|
114
175
|
Each agent transitions through these states:
|
|
115
176
|
|
|
116
177
|
| Status | Meaning |
|
|
117
|
-
|
|
118
|
-
| `processing` | Driver alive with a visible provider-stream heartbeat in the last 10 minutes
|
|
119
|
-
| `stalled` | Driver STILL ALIVE but no parsed visible provider stream item for >= 10 minutes
|
|
178
|
+
|---|---|
|
|
179
|
+
| `processing` | Driver alive with a visible provider-stream heartbeat in the last 10 minutes — actively working. Launch time counts as the initial heartbeat |
|
|
180
|
+
| `stalled` | Driver STILL ALIVE but no parsed visible provider stream item for >= 10 minutes — working, thinking, or awaiting a temp-file handoff (not a failure). Recovers to `processing` if the visible stream resumes |
|
|
120
181
|
| `finished` | Current turn completed, or driver exited with code 0 |
|
|
121
182
|
| `errored` | Process exited with non-zero code |
|
|
122
183
|
| `stopped` | Terminated by `kill_agent` |
|
|
123
184
|
|
|
124
|
-
`processing` and `stalled` are live. `
|
|
185
|
+
`processing` and `stalled` are live. `stalled` means the driver is alive but quiet (>= 10 min no visible stream); it recovers automatically and is never auto-killed — do not `kill_agent` a stalled agent. `wait` does not return on `stalled`. Full semantics: [docs/reference/status-lifecycle.md](docs/reference/status-lifecycle.md).
|
|
125
186
|
|
|
126
187
|
---
|
|
127
188
|
|
|
128
189
|
## Documentation
|
|
129
190
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
191
|
+
| Document | Contents |
|
|
192
|
+
|---|---|
|
|
193
|
+
| [docs/registration.md](docs/registration.md) | Per-platform setup: Claude Code, Codex, Gemini CLI, Claude Desktop; manual wiring; developer install from source |
|
|
194
|
+
| [docs/tools.md](docs/tools.md) | Full tool reference — all eight tools, parameters, return shapes |
|
|
195
|
+
| [docs/usage.md](docs/usage.md) | Model & effort matrix, ultracode mode, CLI invocations, usage examples |
|
|
196
|
+
| [docs/SPEC.md](docs/SPEC.md) | Full technical specification — architecture, schemas, status lifecycle, error catalogue |
|
|
197
|
+
| [docs/spec/interactive-drivers.md](docs/spec/interactive-drivers.md) | Always-interactive Claude/Codex driver model |
|
|
198
|
+
| [docs/release-notes.md](docs/release-notes.md) | Operator-facing release notes |
|
|
199
|
+
| [CONTRIBUTING.md](CONTRIBUTING.md) | Contributor guide — dev environment, build, test, publish |
|
|
136
200
|
|
|
137
201
|
---
|
|
138
202
|
|
|
139
203
|
## License
|
|
140
204
|
|
|
141
|
-
Apache-2.0
|
|
205
|
+
Apache-2.0 — Copyright 2026 Lexi Blackburn
|
|
142
206
|
|
|
143
|
-
See [
|
|
207
|
+
See [LICENSE](LICENSE).
|
|
@@ -2,9 +2,9 @@
|
|
|
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
|
-
Orchestration ON carried over from a PRIOR session for this project (
|
|
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
|
|
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 co-supreme with safety-scope; conflict → ask the user.
|
|
10
10
|
</subagent-mcp>
|
|
@@ -2,9 +2,7 @@
|
|
|
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
|
-
Orchestration ON carried over from a PRIOR session for this project (
|
|
5
|
+
Orchestration ON carried over from a PRIOR session for this project (per-session disable only; the next new session resumes ON automatically 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
|
|
8
|
-
|
|
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 co-supreme with safety-scope; conflict → ask the user.
|
|
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 / context-filling → remain enabled; bounded / interactive → propose OFF. Decline → orchestration-mode enabled:false for this session only; no mid-session re-enable. NEVER disable on your own initiative. After the answer the handshake is done; do not re-raise.
|
|
10
8
|
</subagent-mcp>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { CONCURRENCY_SCAFFOLD } from "./config-scaffold.js";
|
|
6
|
+
export const DEFAULT_CAP = 20;
|
|
7
|
+
export const MIN_CAP = 10;
|
|
8
|
+
export const CONFIG_FILENAME = "global-concurrency.jsonc";
|
|
9
|
+
export function clampCap(raw) {
|
|
10
|
+
if (!Number.isInteger(raw))
|
|
11
|
+
return DEFAULT_CAP;
|
|
12
|
+
const v = raw;
|
|
13
|
+
if (v <= 0)
|
|
14
|
+
return DEFAULT_CAP;
|
|
15
|
+
if (v < MIN_CAP)
|
|
16
|
+
return MIN_CAP;
|
|
17
|
+
return v;
|
|
18
|
+
}
|
|
19
|
+
export function stripJsoncComments(text) {
|
|
20
|
+
return text.replace(/^\s*\/\/.*$/gm, "");
|
|
21
|
+
}
|
|
22
|
+
export function parseConcurrencyConfig(text) {
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = JSON.parse(stripJsoncComments(text))?.globalConcurrentSubagents;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
raw = undefined;
|
|
29
|
+
}
|
|
30
|
+
return clampCap(raw);
|
|
31
|
+
}
|
|
32
|
+
export function defaultConfigPath() {
|
|
33
|
+
return fileURLToPath(new URL("./" + CONFIG_FILENAME, import.meta.url));
|
|
34
|
+
}
|
|
35
|
+
export function ensureConcurrencyConfig(path = defaultConfigPath()) {
|
|
36
|
+
try {
|
|
37
|
+
if (existsSync(path))
|
|
38
|
+
return;
|
|
39
|
+
writeFileSync(path, CONCURRENCY_SCAFFOLD);
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
export function readGlobalCap(path = defaultConfigPath()) {
|
|
44
|
+
try {
|
|
45
|
+
ensureConcurrencyConfig(path);
|
|
46
|
+
return parseConcurrencyConfig(readFileSync(path, "utf8"));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return DEFAULT_CAP;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function slotDir() {
|
|
53
|
+
if (platform() === "win32") {
|
|
54
|
+
return join(process.env.ProgramData || process.env.ALLUSERSPROFILE || "C:\\ProgramData", "subagent-mcp", "slots");
|
|
55
|
+
}
|
|
56
|
+
return "/tmp/subagent-mcp/slots";
|
|
57
|
+
}
|
|
58
|
+
export function countSlots(dir = slotDir()) {
|
|
59
|
+
try {
|
|
60
|
+
return readdirSync(dir).filter((f) => f.startsWith("slot-")).length;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function reserveSlot(agentId, max, dir = slotDir()) {
|
|
67
|
+
try {
|
|
68
|
+
mkdirSync(dir, { recursive: true, mode: 0o1777 });
|
|
69
|
+
const slotPath = join(dir, `slot-${agentId}.json`);
|
|
70
|
+
writeFileSync(slotPath, JSON.stringify({ pid: process.pid, cwd: process.cwd(), startedAt: new Date().toISOString() }), { mode: 0o600 });
|
|
71
|
+
const n = countSlots(dir);
|
|
72
|
+
if (n > max) {
|
|
73
|
+
try {
|
|
74
|
+
unlinkSync(slotPath);
|
|
75
|
+
}
|
|
76
|
+
catch { }
|
|
77
|
+
return { ok: false, current: n - 1, max };
|
|
78
|
+
}
|
|
79
|
+
return { ok: true, slotPath, current: n, max };
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
console.error(`[concurrency] reserve failed, failing open: ${e instanceof Error ? e.message : String(e)}`);
|
|
83
|
+
return { ok: true, current: 0, max, slotPath: null };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function releaseSlot(slotPath) {
|
|
87
|
+
if (!slotPath)
|
|
88
|
+
return;
|
|
89
|
+
try {
|
|
90
|
+
unlinkSync(slotPath);
|
|
91
|
+
}
|
|
92
|
+
catch { }
|
|
93
|
+
}
|
|
94
|
+
export function globalCapMessage(current, max, configPath = defaultConfigPath()) {
|
|
95
|
+
return `Global concurrent-subagent limit reached: ${current} of ${max} live subagents are already running across all sessions on this machine. This global count includes agents started by OTHER active agentic sessions and the ENTIRE recursive descendant tree, not just this session's direct children. launch_agent was REJECTED — this cap never queues or blocks; no slot frees itself by waiting. Free a slot manually first: call list_agents to see live agents, then kill_agent to terminate ones you no longer need, and retry. The limit is "globalConcurrentSubagents" in ${configPath} (default 20, minimum 10).`;
|
|
96
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// GENERATED by scripts/gen-ruleset-scaffold.mjs from src/global-concurrency.jsonc — DO NOT EDIT.
|
|
2
|
+
export const CONCURRENCY_SCAFFOLD = "// subagent-mcp — Global Concurrent Subagent Cap\r\n// ------------------------------------------------------------------\r\n// SOLE source of truth for the machine-wide limit on how many subagents\r\n// may be ALIVE AT ONCE across EVERY session, process, and user on this\r\n// machine. There is NO environment-variable override.\r\n//\r\n// The whole recursive descendant tree counts toward this ONE number: a\r\n// subagent that itself launches subagents adds to the same machine-wide\r\n// total, and OTHER active agentic sessions count too.\r\n//\r\n// RE-READ on every launch_agent call — edits take effect immediately, no\r\n// server restart required.\r\n//\r\n// Value rules (forcibly applied to the number below):\r\n// - missing / unset / non-integer / 0 / negative -> reset to default 20\r\n// - 1 through 9 -> forced UP to minimum 10\r\n// - 10 or greater -> used as-is\r\n//\r\n// When the cap is reached, launch_agent is REJECTED (never queued). Free a\r\n// slot with list_agents + kill_agent, then retry.\r\n//\r\n// Slots free automatically as agents finish or are killed. There is NO\r\n// zombie reaping. If a server process CRASHES, its slots stay counted until\r\n// you delete the stale slot files by hand:\r\n// Windows: %ProgramData%\\subagent-mcp\\slots\r\n// macOS/Linux: /tmp/subagent-mcp/slots\r\n{\r\n \"globalConcurrentSubagents\": 20\r\n}\r\n";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// subagent-mcp — Global Concurrent Subagent Cap
|
|
2
|
+
// ------------------------------------------------------------------
|
|
3
|
+
// SOLE source of truth for the machine-wide limit on how many subagents
|
|
4
|
+
// may be ALIVE AT ONCE across EVERY session, process, and user on this
|
|
5
|
+
// machine. There is NO environment-variable override.
|
|
6
|
+
//
|
|
7
|
+
// The whole recursive descendant tree counts toward this ONE number: a
|
|
8
|
+
// subagent that itself launches subagents adds to the same machine-wide
|
|
9
|
+
// total, and OTHER active agentic sessions count too.
|
|
10
|
+
//
|
|
11
|
+
// RE-READ on every launch_agent call — edits take effect immediately, no
|
|
12
|
+
// server restart required.
|
|
13
|
+
//
|
|
14
|
+
// Value rules (forcibly applied to the number below):
|
|
15
|
+
// - missing / unset / non-integer / 0 / negative -> reset to default 20
|
|
16
|
+
// - 1 through 9 -> forced UP to minimum 10
|
|
17
|
+
// - 10 or greater -> used as-is
|
|
18
|
+
//
|
|
19
|
+
// When the cap is reached, launch_agent is REJECTED (never queued). Free a
|
|
20
|
+
// slot with list_agents + kill_agent, then retry.
|
|
21
|
+
//
|
|
22
|
+
// Slots free automatically as agents finish or are killed. There is NO
|
|
23
|
+
// zombie reaping. If a server process CRASHES, its slots stay counted until
|
|
24
|
+
// you delete the stale slot files by hand:
|
|
25
|
+
// Windows: %ProgramData%\subagent-mcp\slots
|
|
26
|
+
// macOS/Linux: /tmp/subagent-mcp/slots
|
|
27
|
+
{
|
|
28
|
+
"globalConcurrentSubagents": 20
|
|
29
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { consumeStreamChunk, flushStream, isTurnCompletedLine, retainLastN, } fr
|
|
|
18
18
|
import { loadRoutingTable, buildCandidates, validatePresence, TASK_CATEGORIES, AUTO_HINT, SPLIT_HINT, } from "./routing.js";
|
|
19
19
|
import { createDeadlockWindow } from "./deadlock.js";
|
|
20
20
|
import { createRulesetGate, RULESET_HARD_FAIL_MSG, } from "./ruleset.js";
|
|
21
|
+
import { CONFIG_FILENAME, defaultConfigPath, globalCapMessage, readGlobalCap, releaseSlot, reserveSlot, } from "./concurrency.js";
|
|
21
22
|
import * as orchestrationMarker from "./orchestration/marker.js";
|
|
22
23
|
import * as modelMode from "./orchestration/model-mode.js";
|
|
23
24
|
import { startLivenessHeartbeat } from "./orchestration/liveness.js";
|
|
@@ -139,11 +140,11 @@ reconcileInterval.unref();
|
|
|
139
140
|
// test/mirror-fragments.test.mjs while ORCHESTRATION_INSTRUCTIONS below stays
|
|
140
141
|
// compressed under MCP metadata limits:
|
|
141
142
|
// READ-ESCALATION LADDER (the orchestrator's only read channels, in order): (1) subagent-mcp `poll_agent` TAIL; (2) if the tail is insufficient, dispatch ONE sub-agent to return a single summary of <=100 lines, trusted as-is (no separate verification step); (3) anything larger: the USER reads the document directly. No reads or writes occur outside these channels. An empty or stalled tail means the agent is ALIVE, not dead — do NOT busy-loop poll_agent; learn completion via `wait`. Large inter-agent data: the orchestrator assigns scratch-file paths (%TEMP% on Windows, /tmp on POSIX) in prompts; the producing sub-agent writes, the consuming sub-agent reads; the orchestrator NEVER reads those files.
|
|
142
|
-
const ORCHESTRATION_INSTRUCTIONS = "subagent-mcp - CANONICAL OPERATING MODEL (full spec: docs/spec/dev-loop/orchestration-directive-architecture.md).\n\nPRECEDENCE. The latest <subagent-mcp state=\"...\"> hook tag and repo/system safety rules are co-supreme; genuine conflict => STOP and ask the user. Only the hook state changes ON/OFF.\n\nSOLE CHANNEL. Every sub-agent launch uses launch_agent; never harness Task/Agent or shell-spawned agents.\n\nORCHESTRATION ON. You are a delegate-ONLY orchestrator. Use only the structured-question tool (AskUserQuestion on Claude / request-user-input on Codex), subagent-mcp, and the /workflows tool. No direct reads/writes; inline-by-right does not exist. Non-delegable atomic step: ask for a one-time exception, do only that step, then resume delegating.\n\nREAD LADDER. poll_agent tail -> one <=100-line summarizer sub-agent, trusted as-is -> else the USER reads it. Large handoffs use scratch-file paths; producer writes, consumer reads; orchestrator never reads those files. Empty/stalled tail means ALIVE; use wait.\n\nORCHESTRATION OFF. If total context footprint since last upgrade ask exceeds 200 lines, after that turn STOP and ask whether to
|
|
143
|
+
const ORCHESTRATION_INSTRUCTIONS = "subagent-mcp - CANONICAL OPERATING MODEL (full spec: docs/spec/dev-loop/orchestration-directive-architecture.md).\n\nPRECEDENCE. The latest <subagent-mcp state=\"...\"> hook tag and repo/system safety rules are co-supreme; genuine conflict => STOP and ask the user. Only the hook state changes ON/OFF.\n\nSOLE CHANNEL. Every sub-agent launch uses launch_agent; never harness Task/Agent or shell-spawned agents.\n\nORCHESTRATION ON. You are a delegate-ONLY orchestrator. Use only the structured-question tool (AskUserQuestion on Claude / request-user-input on Codex), subagent-mcp, and the /workflows tool. No direct reads/writes; inline-by-right does not exist. Non-delegable atomic step: ask for a one-time exception, do only that step, then resume delegating.\n\nREAD LADDER. poll_agent tail -> one <=100-line summarizer sub-agent, trusted as-is -> else the USER reads it. Large handoffs use scratch-file paths; producer writes, consumer reads; orchestrator never reads those files. Empty/stalled tail means ALIVE; use wait.\n\nORCHESTRATION OFF. If total context footprint since last upgrade ask exceeds 200 lines, after that turn STOP and ask whether to remain enabled; reset count only when you ask.\n\nDROPOUT WHILE ON: HALT and ask; stay halted until restored. SUB-AGENT EXEMPTION: a prompt whose literal FIRST LINE begins \"<this is a request from a parent process>\" skips this regime.\n\nMODEL SELECTION MODE. Default smart rejects provider/model/effort selectors; launch_agent auto-picks. user-approved-overrides lasts 30 minutes, expires lazily on launch_agent, and must be enabled only after explicit user authorization via AskUserQuestion/request-user-input.";
|
|
143
144
|
const SUBAGENT_INSTRUCTIONS = "SUB-AGENT SESSION: you are a child process launched by subagent-mcp. Follow the parent prompt. Do not treat yourself as the orchestrator, do not re-trigger orchestration carryover, and do not launch further sub-agents unless the parent prompt explicitly assigns that.\n\nMODEL SELECTION MODE (parallel to orchestration-mode, set via the model-selection-mode tool). DEFAULT is \"smart\" and is used whenever unset: in smart, launch_agent REJECTS any call supplying provider/model/effort selectors and the server auto-picks the best model. \"user-approved-overrides\" opens a 30-MINUTE window where selectors are HONORED, enforced LAZILY (the mode reverts to smart on the next launch_agent call after 30 minutes) and re-enabling does NOT extend an active window. HONOR-BASED: you MUST NOT set \"user-approved-overrides\" without explicit interactive USER authorization via the structured-question tool (AskUserQuestion on Claude / request-user-input on Codex); never enable it on your own initiative.";
|
|
144
145
|
const server = new McpServer({
|
|
145
146
|
name: "subagent-mcp",
|
|
146
|
-
version: "2.
|
|
147
|
+
version: "2.10.0",
|
|
147
148
|
description: "Launches always-interactive local Claude and Codex sub-agent sessions. Claude uses the Claude Agent SDK over the local Claude Code executable; Codex uses `codex app-server` over stdio. The server does not call Anthropic or OpenAI HTTP APIs directly.",
|
|
148
149
|
}, {
|
|
149
150
|
instructions: process.env.SUBAGENT_MCP_SUBAGENT === "1"
|
|
@@ -359,6 +360,8 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
|
|
|
359
360
|
}
|
|
360
361
|
// Always clean up ultracode settings file on close
|
|
361
362
|
cleanupUcSettings(agentState);
|
|
363
|
+
releaseSlot(agentState.slotPath ?? null);
|
|
364
|
+
agentState.slotPath = null;
|
|
362
365
|
// Always record actual close time (unless already finalized)
|
|
363
366
|
if (agentState.exitedAt === null)
|
|
364
367
|
agentState.exitedAt = Date.now();
|
|
@@ -599,69 +602,94 @@ server.tool("launch_agent", "Spawn a sub-agent session. AUTO MODE (mandatory fir
|
|
|
599
602
|
}
|
|
600
603
|
// 6. Attempt loop: best→worst. Register on first successful driver start; silently
|
|
601
604
|
// advance on launch-time failure. Sub-agent task outcome is NEVER a trigger.
|
|
605
|
+
const cap = readGlobalCap();
|
|
606
|
+
const reservationId = randomUUID();
|
|
607
|
+
const reservation = reserveSlot(reservationId, cap);
|
|
608
|
+
if (!reservation.ok) {
|
|
609
|
+
return errorResult(globalCapMessage(reservation.current, cap, defaultConfigPath()));
|
|
610
|
+
}
|
|
602
611
|
const skipped = [];
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
+
let launched = false;
|
|
613
|
+
try {
|
|
614
|
+
for (const candidate of candidates) {
|
|
615
|
+
const outcome = await tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rulesetApplied && rulesetOriginalSelection !== undefined
|
|
616
|
+
? { applied: true, originalSelection: rulesetOriginalSelection }
|
|
617
|
+
: undefined);
|
|
618
|
+
if ("agentId" in outcome) {
|
|
619
|
+
if (branch === "performance") {
|
|
620
|
+
deadlockWindow.consume();
|
|
621
|
+
}
|
|
612
622
|
const registeredAgent = agents.get(outcome.agentId);
|
|
613
623
|
if (registeredAgent) {
|
|
614
|
-
registeredAgent.
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
624
|
+
registeredAgent.slotPath = reservation.slotPath;
|
|
625
|
+
// The child can reach a terminal state DURING tryLaunchCandidate's
|
|
626
|
+
// awaits — before slotPath was set — so its close handler ran
|
|
627
|
+
// releaseSlot(undefined) (a no-op) and left the slot leaked. Detect
|
|
628
|
+
// that here and release now. slotPath is unique per reservation, so
|
|
629
|
+
// this never frees another agent's slot.
|
|
630
|
+
if (registeredAgent.exitCode !== null) {
|
|
631
|
+
releaseSlot(registeredAgent.slotPath);
|
|
632
|
+
registeredAgent.slotPath = null;
|
|
633
|
+
}
|
|
634
|
+
if (skipped.length > 0) {
|
|
635
|
+
registeredAgent.failoverFrom = skipped.map((s) => ({
|
|
636
|
+
provider: s.provider,
|
|
637
|
+
model: s.model,
|
|
638
|
+
effort: s.effort,
|
|
639
|
+
failure_type: s.failure_type,
|
|
640
|
+
}));
|
|
641
|
+
}
|
|
620
642
|
}
|
|
643
|
+
launched = true;
|
|
644
|
+
return {
|
|
645
|
+
content: [
|
|
646
|
+
{
|
|
647
|
+
type: "text",
|
|
648
|
+
text: JSON.stringify({
|
|
649
|
+
agent_id: outcome.agentId,
|
|
650
|
+
status: "processing",
|
|
651
|
+
provider: candidate.provider,
|
|
652
|
+
model: candidate.model,
|
|
653
|
+
effort: candidate.effort,
|
|
654
|
+
task_category,
|
|
655
|
+
...(rulesetApplied
|
|
656
|
+
? {
|
|
657
|
+
ruleset_applied: true,
|
|
658
|
+
ruleset_original_selection: rulesetOriginalSelection,
|
|
659
|
+
}
|
|
660
|
+
: {}),
|
|
661
|
+
...(skipped.length > 0
|
|
662
|
+
? {
|
|
663
|
+
failover_occurred: true,
|
|
664
|
+
failover_from: skipped.map((s) => ({
|
|
665
|
+
provider: s.provider,
|
|
666
|
+
model: s.model,
|
|
667
|
+
effort: s.effort,
|
|
668
|
+
failure_type: s.failure_type,
|
|
669
|
+
})),
|
|
670
|
+
failover_note: buildFailoverNote(skipped, candidate),
|
|
671
|
+
}
|
|
672
|
+
: {}),
|
|
673
|
+
}),
|
|
674
|
+
},
|
|
675
|
+
],
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
skipped.push({
|
|
679
|
+
model: candidate.model,
|
|
680
|
+
effort: candidate.effort,
|
|
681
|
+
provider: candidate.provider,
|
|
682
|
+
reason: outcome.reason,
|
|
683
|
+
failure_type: outcome.failure_type,
|
|
684
|
+
});
|
|
685
|
+
if (!pureAuto) {
|
|
686
|
+
break;
|
|
621
687
|
}
|
|
622
|
-
return {
|
|
623
|
-
content: [
|
|
624
|
-
{
|
|
625
|
-
type: "text",
|
|
626
|
-
text: JSON.stringify({
|
|
627
|
-
agent_id: outcome.agentId,
|
|
628
|
-
status: "processing",
|
|
629
|
-
provider: candidate.provider,
|
|
630
|
-
model: candidate.model,
|
|
631
|
-
effort: candidate.effort,
|
|
632
|
-
task_category,
|
|
633
|
-
...(rulesetApplied
|
|
634
|
-
? {
|
|
635
|
-
ruleset_applied: true,
|
|
636
|
-
ruleset_original_selection: rulesetOriginalSelection,
|
|
637
|
-
}
|
|
638
|
-
: {}),
|
|
639
|
-
...(skipped.length > 0
|
|
640
|
-
? {
|
|
641
|
-
failover_occurred: true,
|
|
642
|
-
failover_from: skipped.map((s) => ({
|
|
643
|
-
provider: s.provider,
|
|
644
|
-
model: s.model,
|
|
645
|
-
effort: s.effort,
|
|
646
|
-
failure_type: s.failure_type,
|
|
647
|
-
})),
|
|
648
|
-
failover_note: buildFailoverNote(skipped, candidate),
|
|
649
|
-
}
|
|
650
|
-
: {}),
|
|
651
|
-
}),
|
|
652
|
-
},
|
|
653
|
-
],
|
|
654
|
-
};
|
|
655
688
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
reason: outcome.reason,
|
|
661
|
-
failure_type: outcome.failure_type,
|
|
662
|
-
});
|
|
663
|
-
if (!pureAuto) {
|
|
664
|
-
break;
|
|
689
|
+
}
|
|
690
|
+
finally {
|
|
691
|
+
if (!launched) {
|
|
692
|
+
releaseSlot(reservation.slotPath);
|
|
665
693
|
}
|
|
666
694
|
}
|
|
667
695
|
// 7. All candidates failed. Override selector modes are single-attempt hard
|
|
@@ -790,6 +818,8 @@ server.tool("kill_agent", "Terminate a live agent/session (status `processing`,
|
|
|
790
818
|
// POSIX, SIGKILL the process (close handler records the real exit code).
|
|
791
819
|
agent.status = "stopped";
|
|
792
820
|
agent.driver.kill();
|
|
821
|
+
releaseSlot(agent.slotPath ?? null);
|
|
822
|
+
agent.slotPath = null;
|
|
793
823
|
if (isWindows && agent.process.pid) {
|
|
794
824
|
spawn("taskkill", ["/pid", String(agent.process.pid), "/t", "/f"], {
|
|
795
825
|
windowsHide: true,
|
|
@@ -1004,24 +1034,52 @@ server.tool("wait", "Blocks until one or more sub-agents reach a reportable stat
|
|
|
1004
1034
|
};
|
|
1005
1035
|
});
|
|
1006
1036
|
// Tool 7: orchestration-mode
|
|
1007
|
-
server.tool("orchestration-mode", "Toggle or query per-project ORCHESTRATION MODE. `enabled`: true = ON, false = OFF, omit = query current state. SOLE CHANNEL: the subagent MCP is the ONLY sanctioned channel for launching sub-agents whether this mode is ON or OFF — toggling OFF does not lift that obligation. When OFF, run the per-turn upgrade check: a long-horizon task = any whose TOTAL context footprint (input read + output produced) exceeds 200 lines, measured CUMULATIVELY since your last upgrade ask; after EVERY user turn, if it qualifies, STOP and ask the user whether to
|
|
1037
|
+
server.tool("orchestration-mode", "Toggle or query per-project ORCHESTRATION MODE. `enabled`: true = ON, false = OFF for THIS session only, omit = query current state. SOLE CHANNEL: the subagent MCP is the ONLY sanctioned channel for launching sub-agents whether this mode is ON or OFF — toggling OFF does not lift that obligation. When OFF, run the per-turn upgrade check: a long-horizon task = any whose TOTAL context footprint (input read + output produced) exceeds 200 lines, measured CUMULATIVELY since your last upgrade ask; after EVERY user turn, if it qualifies, STOP and ask the user whether to remain enabled (ask every qualifying turn; a decline does not latch; reset the count only when you actually ask). The FULL operating model + governance is carried in this server's MCP `instructions` (read once at initialize) — this is the operational summary only; do not act on the mode without that detail. WHAT: a default-ON orchestration mode for LONG-HORIZON work that would fill the context window if run to completion inline; when ON, act as a delegate-ONLY orchestrator: delegate every step; inline-by-right does not exist; a non-delegable atomic step requires a one-time user-approved exception via the structured-question tool (state which + why). PERSISTENCE: orchestration is DEFAULT ON; a permitted disable applies to THIS session only, resumes ON next new session (or after the 2h backstop), and cannot be re-enabled mid-session. CARRYOVER: if ON was inherited from a PRIOR session (provenance = carried-over, not user-enabled this session), the bundled hook prepends a ONE-TIME notice (once per marker, never per turn) — you MUST then notify the user it auto-activated and confirm whether to keep it ON. DISABLE: never on your own initiative; you MAY PROPOSE turning it OFF on task-fit mismatch, but only EXPLICIT user permission (AskUserQuestion on Claude, request-user-input on Codex) may set enabled:false. Per-turn injection fires only in CLI hosts that load the bundled hook; desktop hosts toggle the marker but inject nothing (documented degradation).", {
|
|
1008
1038
|
enabled: z.boolean().optional(),
|
|
1009
1039
|
}, async (params) => {
|
|
1010
1040
|
const cwd = process.cwd();
|
|
1041
|
+
const key = orchestrationMarker.readCurrentSession(cwd);
|
|
1011
1042
|
if (params.enabled === true) {
|
|
1012
|
-
orchestrationMarker.
|
|
1043
|
+
if (!orchestrationMarker.isActive(cwd, key)) {
|
|
1044
|
+
return errorResult("orchestration already disabled for this session, cannot re-enable mid-session; resumes ON automatically next new session (or after the 2h backstop).");
|
|
1045
|
+
}
|
|
1046
|
+
return {
|
|
1047
|
+
content: [
|
|
1048
|
+
{
|
|
1049
|
+
type: "text",
|
|
1050
|
+
text: JSON.stringify({
|
|
1051
|
+
orchestration_mode: "ON",
|
|
1052
|
+
message: "orchestration is ON by default.",
|
|
1053
|
+
}),
|
|
1054
|
+
},
|
|
1055
|
+
],
|
|
1056
|
+
};
|
|
1013
1057
|
}
|
|
1014
1058
|
else if (params.enabled === false) {
|
|
1015
|
-
|
|
1059
|
+
if (key)
|
|
1060
|
+
orchestrationMarker.writeDisable(key);
|
|
1061
|
+
else
|
|
1062
|
+
orchestrationMarker.writeDisableCwd(cwd);
|
|
1063
|
+
return {
|
|
1064
|
+
content: [
|
|
1065
|
+
{
|
|
1066
|
+
type: "text",
|
|
1067
|
+
text: JSON.stringify({
|
|
1068
|
+
orchestration_mode: "disabled-this-session",
|
|
1069
|
+
message: "orchestration disabled for THIS session only; the next new session resumes ON automatically (or after the 2h backstop); no mid-session re-enable.",
|
|
1070
|
+
}),
|
|
1071
|
+
},
|
|
1072
|
+
],
|
|
1073
|
+
};
|
|
1016
1074
|
}
|
|
1017
1075
|
// enabled === undefined -> query only; no marker mutation.
|
|
1076
|
+
const active = orchestrationMarker.isActive(cwd, key);
|
|
1018
1077
|
return {
|
|
1019
1078
|
content: [
|
|
1020
1079
|
{
|
|
1021
1080
|
type: "text",
|
|
1022
1081
|
text: JSON.stringify({
|
|
1023
|
-
orchestration_mode:
|
|
1024
|
-
marker_path: orchestrationMarker.markerPath(cwd),
|
|
1082
|
+
orchestration_mode: active ? "ON" : "disabled-this-session",
|
|
1025
1083
|
}),
|
|
1026
1084
|
},
|
|
1027
1085
|
],
|
|
@@ -1126,7 +1184,9 @@ if (isMain) {
|
|
|
1126
1184
|
}
|
|
1127
1185
|
const installRoot = join(npmRoot.stdout.toString().trim(), ...pkg.name.split("/"));
|
|
1128
1186
|
const rulesetPath = join(installRoot, "dist", "advanced-ruleset.py");
|
|
1187
|
+
const cfgPath = join(installRoot, "dist", CONFIG_FILENAME);
|
|
1129
1188
|
let previousRuleset = null;
|
|
1189
|
+
let previousCfg = null;
|
|
1130
1190
|
if (existsSync(rulesetPath)) {
|
|
1131
1191
|
previousRuleset = readFileSync(rulesetPath);
|
|
1132
1192
|
const backupPath = join(tmpdir(), `advanced-ruleset.py.bak-update-${Date.now()}`);
|
|
@@ -1139,6 +1199,18 @@ if (isMain) {
|
|
|
1139
1199
|
process.exit(1);
|
|
1140
1200
|
}
|
|
1141
1201
|
}
|
|
1202
|
+
if (existsSync(cfgPath)) {
|
|
1203
|
+
previousCfg = readFileSync(cfgPath);
|
|
1204
|
+
const backupPath = join(tmpdir(), `${CONFIG_FILENAME}.bak-update-${Date.now()}`);
|
|
1205
|
+
try {
|
|
1206
|
+
writeFileSync(backupPath, previousCfg);
|
|
1207
|
+
console.log(`backed up user ${CONFIG_FILENAME} to ${backupPath}`);
|
|
1208
|
+
}
|
|
1209
|
+
catch (e) {
|
|
1210
|
+
console.error(`update refused before install: failed to back up ${CONFIG_FILENAME}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1211
|
+
process.exit(1);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1142
1214
|
const r = spawnNpm(npmArgs, "inherit");
|
|
1143
1215
|
if (r.error) {
|
|
1144
1216
|
console.error(`update failed to start npm: ${r.error.message}`);
|
|
@@ -1161,6 +1233,21 @@ if (isMain) {
|
|
|
1161
1233
|
process.exit(1);
|
|
1162
1234
|
}
|
|
1163
1235
|
}
|
|
1236
|
+
if (previousCfg !== null) {
|
|
1237
|
+
try {
|
|
1238
|
+
const freshCfg = existsSync(cfgPath)
|
|
1239
|
+
? readFileSync(cfgPath)
|
|
1240
|
+
: null;
|
|
1241
|
+
if (freshCfg === null || !previousCfg.equals(freshCfg)) {
|
|
1242
|
+
writeFileSync(cfgPath, previousCfg);
|
|
1243
|
+
console.log(`restored user ${CONFIG_FILENAME} (package update never overwrites user edits)`);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
catch (e) {
|
|
1247
|
+
console.error(`update failed to restore ${CONFIG_FILENAME}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1248
|
+
process.exit(1);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1164
1251
|
console.log("Update complete. Restart your CLI sessions so the MCP server picks up the new build.");
|
|
1165
1252
|
}
|
|
1166
1253
|
process.exit(code);
|
|
@@ -1184,11 +1271,14 @@ if (isMain) {
|
|
|
1184
1271
|
process.exit(1);
|
|
1185
1272
|
}
|
|
1186
1273
|
// ORCHESTRATION MODE PERSISTS across restarts/sessions: the server does NOT
|
|
1187
|
-
// clear the marker on startup. DEFAULT
|
|
1188
|
-
//
|
|
1189
|
-
//
|
|
1190
|
-
//
|
|
1191
|
-
// (
|
|
1274
|
+
// clear the marker on startup. DEFAULT ON now means ABSENCE of a disable
|
|
1275
|
+
// record — a project stays ON with no marker write needed; OFF is only a
|
|
1276
|
+
// per-session disable record that holds while it is active, cleared with
|
|
1277
|
+
// explicit user permission. On a new session a carried-over legacy ON marker
|
|
1278
|
+
// (if any) triggers a one-time prompt asking whether to remain enabled; under
|
|
1279
|
+
// default-ON this rarely fires.
|
|
1280
|
+
// (the tool's enabled:false writes a disable record via writeDisable /
|
|
1281
|
+
// writeDisableCwd; it does not call disable().)
|
|
1192
1282
|
startLivenessHeartbeat();
|
|
1193
1283
|
const transport = new StdioServerTransport();
|
|
1194
1284
|
await server.connect(transport);
|
package/dist/init.js
CHANGED
|
@@ -29,7 +29,7 @@ export const INIT_BLOCK = [
|
|
|
29
29
|
"",
|
|
30
30
|
'NO-HOOK / UNKNOWN STATE: if no harness-hook injection bearing a <subagent-mcp state="..."> tag is present this session (e.g. Gemini, desktop apps, or any host that fires no hook), the state is UNKNOWN — represented by the ABSENCE of any tag, never by a tag value. Emit this UNKNOWN-STATE WARNING to the user: "subagent-mcp: no hook injection detected — orchestration state unknown; defaulting to ON". Briefly explain WHY: no hook injection detected — cannot verify orchestration state; defaulting to ON to prevent uncontrolled inline execution. Then DEFAULT TO ORCHESTRATION ON (fail-safe). One-time opt-out: if you are not currently running an orchestration workflow, you may explicitly opt out of ON for this session by saying so now; this opt-out does not persist and is not recorded. The sub-agent first-line exemption is the ONLY automatic suppressor of this default.',
|
|
31
31
|
"",
|
|
32
|
-
"DISABLE: never on your own initiative; you MAY propose OFF on task-fit mismatch via the structured-question tool, and only explicit user approval may
|
|
32
|
+
"DISABLE: never on your own initiative; you MAY propose OFF on task-fit mismatch via the structured-question tool, and only explicit user approval may set enabled:false — per-session only; the next new session resumes ON; no mid-session re-enable.",
|
|
33
33
|
"<!-- subagent-mcp:managed:end -->",
|
|
34
34
|
].join("\n");
|
|
35
35
|
function detectEol(s) {
|
|
@@ -234,7 +234,9 @@ export function runHook(payload, env, adapter) {
|
|
|
234
234
|
}
|
|
235
235
|
const cwd = payload.cwd || process.cwd();
|
|
236
236
|
const current = sessionKey(payload);
|
|
237
|
-
if (
|
|
237
|
+
if (current)
|
|
238
|
+
marker.writeCurrentSession(cwd, current);
|
|
239
|
+
if (!marker.isActive(cwd, current)) {
|
|
238
240
|
// OFF: no claim machinery — just the per-prompt reminder cadence.
|
|
239
241
|
const r = reminder.advance(cwd, current);
|
|
240
242
|
return cadenceEmit(env, adapter, adapter.reminderOffFile, adapter.shortOffFile, r.count, r.persisted);
|
|
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
const markerDir = join(tmpdir(), "subagent-mcp");
|
|
6
|
+
export const ORCH_DISABLE_TTL_MS = 2 * 60 * 60 * 1000; // 2h GC backstop ONLY (independent of model-mode WINDOW_MS)
|
|
6
7
|
/**
|
|
7
8
|
* Shared per-project state dir for ALL hook state files (marker + reminder
|
|
8
9
|
* counter). Exported so sibling state modules key off the SAME location — a
|
|
@@ -33,15 +34,27 @@ export function normalizeCwd(cwd) {
|
|
|
33
34
|
}
|
|
34
35
|
return p;
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
+
function hashKey(key) {
|
|
37
38
|
return createHash("sha256")
|
|
38
|
-
.update(
|
|
39
|
+
.update(key, "utf8")
|
|
39
40
|
.digest("hex")
|
|
40
41
|
.slice(0, 16);
|
|
41
42
|
}
|
|
43
|
+
export function cwdHash(cwd) {
|
|
44
|
+
return hashKey(normalizeCwd(cwd));
|
|
45
|
+
}
|
|
42
46
|
export function markerPath(cwd) {
|
|
43
47
|
return join(markerDir, "orch-" + cwdHash(cwd) + ".flag");
|
|
44
48
|
}
|
|
49
|
+
export function disablePath(sessionKey) {
|
|
50
|
+
return join(stateDir, `orch-disable-${hashKey(sessionKey)}.json`);
|
|
51
|
+
}
|
|
52
|
+
function cwdDisablePath(cwd) {
|
|
53
|
+
return join(stateDir, `orch-disable-${cwdHash(cwd)}.json`);
|
|
54
|
+
}
|
|
55
|
+
export function sessionPointerPath(cwd) {
|
|
56
|
+
return join(stateDir, `orch-session-${cwdHash(cwd)}.json`);
|
|
57
|
+
}
|
|
45
58
|
/**
|
|
46
59
|
* Enable orchestration for cwd. ALWAYS overwrites — re-enabling re-baselines by
|
|
47
60
|
* clearing owner_session/baseline_turn back to null so the next hook turn
|
|
@@ -62,44 +75,105 @@ export function enable(cwd) {
|
|
|
62
75
|
carryover_ack: false,
|
|
63
76
|
};
|
|
64
77
|
writeFileSync(markerPath(cwd), JSON.stringify(state), { encoding: "utf8", mode: 0o600 });
|
|
78
|
+
try {
|
|
79
|
+
unlinkSync(cwdDisablePath(cwd));
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
if (e?.code !== "ENOENT") {
|
|
83
|
+
// Fail-safe: enable still succeeds if stale disable cleanup fails.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
65
86
|
}
|
|
66
87
|
catch {
|
|
67
88
|
// Fail-safe: never throw to the caller.
|
|
68
89
|
}
|
|
69
90
|
}
|
|
70
91
|
/**
|
|
71
|
-
* Disable orchestration for cwd
|
|
72
|
-
*
|
|
73
|
-
* No existsSync() guard: that only opens a TOCTOU window where a concurrent
|
|
74
|
-
* clearForCwd/disable for the same cwd removes the file between the check and
|
|
75
|
-
* the unlink. We just unlink and swallow ENOENT (already-gone is success).
|
|
76
|
-
*
|
|
77
|
-
* KNOWN LIMITATION: the marker is keyed by cwd, NOT by session. Two CLI
|
|
78
|
-
* sessions in the same project share one marker, so their enable/disable
|
|
79
|
-
* interleave and the last writer wins. Per-session isolation would require
|
|
80
|
-
* keying the marker by cwd+session_id; not done here because LOCKED DECISION 1
|
|
81
|
-
* keys the marker by working directory alone. (The hook tracks the owning
|
|
82
|
-
* session via owner_session so a carried-over marker is re-claimed, but the
|
|
83
|
-
* marker file itself is still shared per cwd.)
|
|
92
|
+
* Disable orchestration for cwd using cwd-keyed shared fallback state. The
|
|
93
|
+
* hook's session-keyed path uses writeDisable(sessionKey) instead.
|
|
84
94
|
*/
|
|
85
95
|
export function disable(cwd) {
|
|
86
96
|
try {
|
|
87
|
-
|
|
97
|
+
mkdirSync(markerDir, { recursive: true, mode: 0o700 });
|
|
98
|
+
writeFileSync(cwdDisablePath(cwd), JSON.stringify({ disabled_at: Date.now() }), {
|
|
99
|
+
encoding: "utf8",
|
|
100
|
+
mode: 0o600,
|
|
101
|
+
});
|
|
88
102
|
}
|
|
89
|
-
catch
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
catch {
|
|
104
|
+
// Fail-safe: never throw to the caller.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function writeDisable(sessionKey) {
|
|
108
|
+
try {
|
|
109
|
+
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
110
|
+
writeFileSync(disablePath(sessionKey), JSON.stringify({ disabled_at: Date.now() }), {
|
|
111
|
+
encoding: "utf8",
|
|
112
|
+
mode: 0o600,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Fail-safe: never throw to the caller.
|
|
94
117
|
}
|
|
95
118
|
}
|
|
96
|
-
export function
|
|
119
|
+
export function writeDisableCwd(cwd) {
|
|
97
120
|
try {
|
|
98
|
-
|
|
121
|
+
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
122
|
+
writeFileSync(cwdDisablePath(cwd), JSON.stringify({ disabled_at: Date.now() }), {
|
|
123
|
+
encoding: "utf8",
|
|
124
|
+
mode: 0o600,
|
|
125
|
+
});
|
|
99
126
|
}
|
|
100
127
|
catch {
|
|
128
|
+
// Fail-safe: never throw to the caller.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export function writeCurrentSession(cwd, sessionKey) {
|
|
132
|
+
try {
|
|
133
|
+
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
134
|
+
writeFileSync(sessionPointerPath(cwd), JSON.stringify({ session_key: sessionKey }), {
|
|
135
|
+
encoding: "utf8",
|
|
136
|
+
mode: 0o600,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Fail-safe: never throw to the caller.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export function readCurrentSession(cwd) {
|
|
144
|
+
try {
|
|
145
|
+
const raw = readFileSync(sessionPointerPath(cwd), "utf8");
|
|
146
|
+
const parsed = JSON.parse(raw);
|
|
147
|
+
return typeof parsed.session_key === "string" ? parsed.session_key : undefined;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function isDisableActive(path, now) {
|
|
154
|
+
if (!existsSync(path)) {
|
|
101
155
|
return false;
|
|
102
156
|
}
|
|
157
|
+
const raw = readFileSync(path, "utf8");
|
|
158
|
+
const parsed = JSON.parse(raw);
|
|
159
|
+
if (typeof parsed.disabled_at !== "number") {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
if (now - parsed.disabled_at <= ORCH_DISABLE_TTL_MS) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
// Lazy GC side-effect: the disable has expired, so remove it.
|
|
166
|
+
unlinkSync(path);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
export function isActive(cwd, sessionKey) {
|
|
170
|
+
try {
|
|
171
|
+
const path = sessionKey === undefined ? cwdDisablePath(cwd) : disablePath(sessionKey);
|
|
172
|
+
return !isDisableActive(path, Date.now());
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
103
177
|
}
|
|
104
178
|
export function readMarker(cwd) {
|
|
105
179
|
try {
|
|
@@ -136,9 +210,8 @@ export function writeMarker(cwd, obj) {
|
|
|
136
210
|
}
|
|
137
211
|
}
|
|
138
212
|
/**
|
|
139
|
-
*
|
|
140
|
-
* marker explicitly (e.g. the tool's enabled:false path).
|
|
141
|
-
* longer calls this on startup — orchestration mode now PERSISTS across sessions.
|
|
213
|
+
* Cwd-keyed disable alias, identical to disable. RETAINED for callers that
|
|
214
|
+
* clear legacy marker state explicitly (e.g. the tool's enabled:false path).
|
|
142
215
|
*/
|
|
143
216
|
export function clearForCwd(cwd) {
|
|
144
217
|
disable(cwd);
|
package/dist/routing.js
CHANGED
|
@@ -120,10 +120,10 @@ export function normalizeEffort(provider, model, effort) {
|
|
|
120
120
|
* explicit (provider+model+effort all present): one candidate from the user's
|
|
121
121
|
* triple; the table is NOT read (works with a null table).
|
|
122
122
|
*
|
|
123
|
-
* auto/provider/provider_model: read
|
|
124
|
-
* array directly
|
|
125
|
-
*
|
|
126
|
-
*
|
|
123
|
+
* auto/provider/provider_model: read <branch>.<task_category> as the pairings
|
|
124
|
+
* array directly, sort by rank asc, map each model to a launch id, normalize
|
|
125
|
+
* effort, and drop non-launchable / unknown-effort pairings. An empty result
|
|
126
|
+
* sets noCandidates:true.
|
|
127
127
|
*/
|
|
128
128
|
export function buildCandidates(table, taskCategory, overrides, branch = DEFAULT_BRANCH) {
|
|
129
129
|
const { provider, model, effort } = overrides;
|
|
@@ -188,12 +188,7 @@ function readPairings(table, taskCategory, branch = DEFAULT_BRANCH) {
|
|
|
188
188
|
const perf = table[branch];
|
|
189
189
|
if (!perf || typeof perf !== "object")
|
|
190
190
|
return [];
|
|
191
|
-
|
|
192
|
-
// Defensive: tolerate a {pairings:[...]} wrapper even though the contract
|
|
193
|
-
// says the value IS the array directly.
|
|
194
|
-
if (raw && !Array.isArray(raw) && typeof raw === "object" && "pairings" in raw) {
|
|
195
|
-
raw = raw.pairings;
|
|
196
|
-
}
|
|
191
|
+
const raw = perf[taskCategory];
|
|
197
192
|
if (!Array.isArray(raw))
|
|
198
193
|
return [];
|
|
199
194
|
const entries = raw.filter((e) => !!e && typeof e === "object" && typeof e.model === "string");
|
|
@@ -251,7 +246,11 @@ export function validatePresence(p) {
|
|
|
251
246
|
if (model && !provider) {
|
|
252
247
|
return `Error: provider is required when model is given. You passed model=${model} without provider. Either also pass provider, or omit both.\n${AUTO_HINT}`;
|
|
253
248
|
}
|
|
254
|
-
//
|
|
249
|
+
// fallback_default is valid only for fully explicit launches.
|
|
250
|
+
if (task_category === "fallback_default" && !(provider && model && effort)) {
|
|
251
|
+
return `Error: fallback_default is a split hint sentinel, not a launchable routing-table category.\n${SPLIT_HINT}\n${AUTO_HINT}`;
|
|
252
|
+
}
|
|
253
|
+
// provider+model must satisfy the existing match rule.
|
|
255
254
|
if (provider && model) {
|
|
256
255
|
if (provider === "claude" && !["haiku", "sonnet", "opus", "opus-4-8"].includes(model)) {
|
|
257
256
|
return `Error: Claude provider only supports haiku, sonnet, opus, or opus-4-8. Got: ${model}`;
|
package/dist/setup.js
CHANGED
|
@@ -227,6 +227,7 @@ export function verifyInstall(root = INSTALL_ROOT) {
|
|
|
227
227
|
const required = [
|
|
228
228
|
"dist/index.js",
|
|
229
229
|
"dist/advanced-ruleset.py",
|
|
230
|
+
"dist/global-concurrency.jsonc",
|
|
230
231
|
"dist/hooks/orchestration-claude.js",
|
|
231
232
|
"dist/hooks/orchestration-claude-pretool.js",
|
|
232
233
|
"dist/hooks/orchestration-codex.js",
|
package/package.json
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heretyc/subagent-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "MCP server that launches and manages always-interactive Claude Code and Codex sub-agent sessions (no direct Anthropic/OpenAI API).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"mcp-server",
|
|
9
|
+
"claude",
|
|
10
|
+
"claude-code",
|
|
11
|
+
"codex",
|
|
12
|
+
"orchestration",
|
|
13
|
+
"subagent",
|
|
14
|
+
"sub-agent",
|
|
15
|
+
"multi-agent",
|
|
16
|
+
"ai-agents",
|
|
17
|
+
"anthropic",
|
|
18
|
+
"openai"
|
|
19
|
+
],
|
|
5
20
|
"type": "module",
|
|
6
21
|
"main": "dist/index.js",
|
|
7
22
|
"bin": {
|
|
@@ -22,7 +37,7 @@
|
|
|
22
37
|
"postinstall": "node scripts/postinstall.mjs",
|
|
23
38
|
"prepare": "npm run build",
|
|
24
39
|
"prepublishOnly": "npm test",
|
|
25
|
-
"test": "npm run check:versions && node test/effort.test.mjs && node test/drivers.test.mjs && node test/platform.test.mjs && node test/wait.test.mjs && node test/status.test.mjs && node test/output.test.mjs && node test/stream.test.mjs && node test/routing.test.mjs && node test/deadlock.test.mjs && node test/handler-validation.test.mjs && node test/index-handler.test.mjs && node test/ruleset.test.mjs && node test/ruleset-exec.test.mjs && node test/ruleset-handler.test.mjs && node test/failover.test.mjs && node test/orchestration-marker.test.mjs && node test/orchestration-hook-core.test.mjs && node test/orchestration-adapters.test.mjs && node test/orchestration-pretool.test.mjs && node test/orchestration-directives.test.mjs && node test/no-five-call.test.mjs && node test/check-worktree-subagent.test.mjs && node test/launch-agent-upsert.test.mjs && node test/claude-session-limit.test.mjs && node test/init-migration.test.mjs && node test/init-global.test.mjs && node test/mirror-fragments.test.mjs && node test/performance-tier-effort.test.mjs && node test/setup-repair.test.mjs && node test/setup-quoting.test.mjs && node test/setup-wire.test.mjs && node test/setup-cli-integration.test.mjs && node test/init.test.mjs && node test/model-selection-mode.test.mjs && node test/cli-args.test.mjs && node scripts/validate_provider.mjs && node scripts/validate_seed_sites.mjs && node scripts/validate_routing_audit.mjs && node test/seed-sites.test.mjs && node test/mcp-compliance.test.mjs"
|
|
40
|
+
"test": "npm run check:versions && node test/effort.test.mjs && node test/drivers.test.mjs && node test/platform.test.mjs && node test/wait.test.mjs && node test/status.test.mjs && node test/output.test.mjs && node test/stream.test.mjs && node test/routing.test.mjs && node test/deadlock.test.mjs && node test/handler-validation.test.mjs && node test/index-handler.test.mjs && node test/ruleset.test.mjs && node test/ruleset-exec.test.mjs && node test/ruleset-handler.test.mjs && node test/failover.test.mjs && node test/global-concurrency-cap.test.mjs && node test/orchestration-marker.test.mjs && node test/orchestration-hook-core.test.mjs && node test/orchestration-adapters.test.mjs && node test/orchestration-pretool.test.mjs && node test/orchestration-directives.test.mjs && node test/no-five-call.test.mjs && node test/check-worktree-subagent.test.mjs && node test/launch-agent-upsert.test.mjs && node test/claude-session-limit.test.mjs && node test/init-migration.test.mjs && node test/init-global.test.mjs && node test/mirror-fragments.test.mjs && node test/performance-tier-effort.test.mjs && node test/setup-repair.test.mjs && node test/setup-quoting.test.mjs && node test/setup-wire.test.mjs && node test/setup-cli-integration.test.mjs && node test/init.test.mjs && node test/model-selection-mode.test.mjs && node test/cli-args.test.mjs && node scripts/validate_provider.mjs && node scripts/validate_seed_sites.mjs && node scripts/validate_routing_audit.mjs && node test/seed-sites.test.mjs && node test/mcp-compliance.test.mjs"
|
|
26
41
|
},
|
|
27
42
|
"author": "Lexi Blackburn",
|
|
28
43
|
"license": "Apache-2.0",
|
|
@@ -47,7 +62,7 @@
|
|
|
47
62
|
"node": ">=18"
|
|
48
63
|
},
|
|
49
64
|
"publishConfig": {
|
|
50
|
-
"registry": "https://
|
|
65
|
+
"registry": "https://registry.npmjs.org",
|
|
51
66
|
"access": "public"
|
|
52
67
|
}
|
|
53
68
|
}
|