@heretyc/subagent-mcp 2.12.11 → 2.12.13
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/directives/carryover-claude.md +0 -2
- package/directives/carryover-codex.md +0 -2
- package/directives/handoff-claude.md +7 -0
- package/directives/handoff-codex.md +7 -0
- package/directives/latch-claude.md +5 -0
- package/directives/latch-codex.md +5 -0
- package/directives/orchestration-claude.md +0 -2
- package/directives/orchestration-codex.md +0 -2
- package/directives/reminder-off-claude.md +1 -3
- package/directives/reminder-off-codex.md +1 -3
- package/directives/reminder-on.md +0 -2
- package/directives/short-off.md +1 -1
- package/directives/short-on.md +1 -1
- package/directives/tag-template.md +2 -0
- package/dist/advanced-ruleset.py +67 -67
- package/dist/config-scaffold.js +1 -1
- package/dist/global-concurrency.jsonc +43 -43
- package/dist/global-subagent-mcp-config.jsonc +43 -43
- package/dist/hooks/orchestration-claude.js +86 -2
- package/dist/hooks/orchestration-codex.js +152 -3
- package/dist/index.js +76 -8
- package/dist/init.js +2 -2
- package/dist/orchestration/handoff.js +142 -0
- package/dist/orchestration/hook-core.js +184 -16
- package/dist/orchestration/latch.js +47 -0
- package/dist/orchestration/marker.js +55 -1
- package/dist/orchestration/metering.js +158 -0
- package/dist/orchestration/pretool.js +3 -3
- package/dist/orchestration/template.js +31 -0
- package/dist/routing-table.json +2133 -3561
- package/dist/routing.js +3 -2
- package/dist/ruleset-scaffold.js +1 -1
- package/package.json +70 -70
- package/scripts/postinstall.mjs +102 -102
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
// subagent-mcp - Global Subagent MCP Config
|
|
2
|
-
// ------------------------------------------------------------------
|
|
3
|
-
// SOLE source of truth for machine-wide subagent-mcp defaults that must be
|
|
4
|
-
// available beside the compiled server.
|
|
5
|
-
//
|
|
6
|
-
// globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE
|
|
7
|
-
// across EVERY session, process, and user on this machine. There is NO
|
|
8
|
-
// environment-variable override for the cap.
|
|
9
|
-
//
|
|
10
|
-
// RE-READ on every launch_agent call - edits take effect immediately, no
|
|
11
|
-
// server restart required.
|
|
12
|
-
//
|
|
13
|
-
// Cap value rules:
|
|
14
|
-
// - missing / unset / non-integer / 0 / negative -> reset to default 20
|
|
15
|
-
// - 1 through 9 -> forced UP to minimum 10
|
|
16
|
-
// - 10 or greater -> used as-is
|
|
17
|
-
//
|
|
18
|
-
// checkForUpdates controls the silent npmjs update check started when the MCP
|
|
19
|
-
// server connects. Default true. Set to false to skip the registry fetch and
|
|
20
|
-
// suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.
|
|
21
|
-
//
|
|
22
|
-
// permissionsCeiling controls launch-time permissions posture:
|
|
23
|
-
// - auto (default): shared engine gates unsafe/residue actions
|
|
24
|
-
// - manual: human approval for residue while still auto-denying DANGER
|
|
25
|
-
// - yolo: preserve the historical bypass/danger-full-access path, except
|
|
26
|
-
// for non-bypassable config-file self-protection
|
|
27
|
-
//
|
|
28
|
-
// escalation applies only in auto mode. irreversible-only routes irreversible
|
|
29
|
-
// NEUTRAL residue to a human; off leaves residue to orchestrator judgment.
|
|
30
|
-
//
|
|
31
|
-
// strictReadParity controls logging only. Unparseable Codex approval payloads
|
|
32
|
-
// always fail closed to ask; warn logs the construct, off silences that log.
|
|
33
|
-
//
|
|
34
|
-
// sandboxNetwork controls Codex workspace-write network access. Default false.
|
|
35
|
-
// Set true only when sub-agents need network after permission approval.
|
|
36
|
-
{
|
|
37
|
-
"globalConcurrentSubagents": 20,
|
|
38
|
-
"checkForUpdates": true,
|
|
39
|
-
"permissionsCeiling": "auto",
|
|
40
|
-
"escalation": "irreversible-only",
|
|
41
|
-
"strictReadParity": "warn",
|
|
42
|
-
"sandboxNetwork": false
|
|
43
|
-
}
|
|
1
|
+
// subagent-mcp - Global Subagent MCP Config
|
|
2
|
+
// ------------------------------------------------------------------
|
|
3
|
+
// SOLE source of truth for machine-wide subagent-mcp defaults that must be
|
|
4
|
+
// available beside the compiled server.
|
|
5
|
+
//
|
|
6
|
+
// globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE
|
|
7
|
+
// across EVERY session, process, and user on this machine. There is NO
|
|
8
|
+
// environment-variable override for the cap.
|
|
9
|
+
//
|
|
10
|
+
// RE-READ on every launch_agent call - edits take effect immediately, no
|
|
11
|
+
// server restart required.
|
|
12
|
+
//
|
|
13
|
+
// Cap value rules:
|
|
14
|
+
// - missing / unset / non-integer / 0 / negative -> reset to default 20
|
|
15
|
+
// - 1 through 9 -> forced UP to minimum 10
|
|
16
|
+
// - 10 or greater -> used as-is
|
|
17
|
+
//
|
|
18
|
+
// checkForUpdates controls the silent npmjs update check started when the MCP
|
|
19
|
+
// server connects. Default true. Set to false to skip the registry fetch and
|
|
20
|
+
// suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.
|
|
21
|
+
//
|
|
22
|
+
// permissionsCeiling controls launch-time permissions posture:
|
|
23
|
+
// - auto (default): shared engine gates unsafe/residue actions
|
|
24
|
+
// - manual: human approval for residue while still auto-denying DANGER
|
|
25
|
+
// - yolo: preserve the historical bypass/danger-full-access path, except
|
|
26
|
+
// for non-bypassable config-file self-protection
|
|
27
|
+
//
|
|
28
|
+
// escalation applies only in auto mode. irreversible-only routes irreversible
|
|
29
|
+
// NEUTRAL residue to a human; off leaves residue to orchestrator judgment.
|
|
30
|
+
//
|
|
31
|
+
// strictReadParity controls logging only. Unparseable Codex approval payloads
|
|
32
|
+
// always fail closed to ask; warn logs the construct, off silences that log.
|
|
33
|
+
//
|
|
34
|
+
// sandboxNetwork controls Codex workspace-write network access. Default false.
|
|
35
|
+
// Set true only when sub-agents need network after permission approval.
|
|
36
|
+
{
|
|
37
|
+
"globalConcurrentSubagents": 20,
|
|
38
|
+
"checkForUpdates": true,
|
|
39
|
+
"permissionsCeiling": "auto",
|
|
40
|
+
"escalation": "irreversible-only",
|
|
41
|
+
"strictReadParity": "warn",
|
|
42
|
+
"sandboxNetwork": false
|
|
43
|
+
}
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
// subagent-mcp - Global Subagent MCP Config
|
|
2
|
-
// ------------------------------------------------------------------
|
|
3
|
-
// SOLE source of truth for machine-wide subagent-mcp defaults that must be
|
|
4
|
-
// available beside the compiled server.
|
|
5
|
-
//
|
|
6
|
-
// globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE
|
|
7
|
-
// across EVERY session, process, and user on this machine. There is NO
|
|
8
|
-
// environment-variable override for the cap.
|
|
9
|
-
//
|
|
10
|
-
// RE-READ on every launch_agent call - edits take effect immediately, no
|
|
11
|
-
// server restart required.
|
|
12
|
-
//
|
|
13
|
-
// Cap value rules:
|
|
14
|
-
// - missing / unset / non-integer / 0 / negative -> reset to default 20
|
|
15
|
-
// - 1 through 9 -> forced UP to minimum 10
|
|
16
|
-
// - 10 or greater -> used as-is
|
|
17
|
-
//
|
|
18
|
-
// checkForUpdates controls the silent npmjs update check started when the MCP
|
|
19
|
-
// server connects. Default true. Set to false to skip the registry fetch and
|
|
20
|
-
// suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.
|
|
21
|
-
//
|
|
22
|
-
// permissionsCeiling controls launch-time permissions posture:
|
|
23
|
-
// - auto (default): shared engine gates unsafe/residue actions
|
|
24
|
-
// - manual: human approval for residue while still auto-denying DANGER
|
|
25
|
-
// - yolo: preserve the historical bypass/danger-full-access path, except
|
|
26
|
-
// for non-bypassable config-file self-protection
|
|
27
|
-
//
|
|
28
|
-
// escalation applies only in auto mode. irreversible-only routes irreversible
|
|
29
|
-
// NEUTRAL residue to a human; off leaves residue to orchestrator judgment.
|
|
30
|
-
//
|
|
31
|
-
// strictReadParity controls logging only. Unparseable Codex approval payloads
|
|
32
|
-
// always fail closed to ask; warn logs the construct, off silences that log.
|
|
33
|
-
//
|
|
34
|
-
// sandboxNetwork controls Codex workspace-write network access. Default false.
|
|
35
|
-
// Set true only when sub-agents need network after permission approval.
|
|
36
|
-
{
|
|
37
|
-
"globalConcurrentSubagents": 20,
|
|
38
|
-
"checkForUpdates": true,
|
|
39
|
-
"permissionsCeiling": "auto",
|
|
40
|
-
"escalation": "irreversible-only",
|
|
41
|
-
"strictReadParity": "warn",
|
|
42
|
-
"sandboxNetwork": false
|
|
43
|
-
}
|
|
1
|
+
// subagent-mcp - Global Subagent MCP Config
|
|
2
|
+
// ------------------------------------------------------------------
|
|
3
|
+
// SOLE source of truth for machine-wide subagent-mcp defaults that must be
|
|
4
|
+
// available beside the compiled server.
|
|
5
|
+
//
|
|
6
|
+
// globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE
|
|
7
|
+
// across EVERY session, process, and user on this machine. There is NO
|
|
8
|
+
// environment-variable override for the cap.
|
|
9
|
+
//
|
|
10
|
+
// RE-READ on every launch_agent call - edits take effect immediately, no
|
|
11
|
+
// server restart required.
|
|
12
|
+
//
|
|
13
|
+
// Cap value rules:
|
|
14
|
+
// - missing / unset / non-integer / 0 / negative -> reset to default 20
|
|
15
|
+
// - 1 through 9 -> forced UP to minimum 10
|
|
16
|
+
// - 10 or greater -> used as-is
|
|
17
|
+
//
|
|
18
|
+
// checkForUpdates controls the silent npmjs update check started when the MCP
|
|
19
|
+
// server connects. Default true. Set to false to skip the registry fetch and
|
|
20
|
+
// suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.
|
|
21
|
+
//
|
|
22
|
+
// permissionsCeiling controls launch-time permissions posture:
|
|
23
|
+
// - auto (default): shared engine gates unsafe/residue actions
|
|
24
|
+
// - manual: human approval for residue while still auto-denying DANGER
|
|
25
|
+
// - yolo: preserve the historical bypass/danger-full-access path, except
|
|
26
|
+
// for non-bypassable config-file self-protection
|
|
27
|
+
//
|
|
28
|
+
// escalation applies only in auto mode. irreversible-only routes irreversible
|
|
29
|
+
// NEUTRAL residue to a human; off leaves residue to orchestrator judgment.
|
|
30
|
+
//
|
|
31
|
+
// strictReadParity controls logging only. Unparseable Codex approval payloads
|
|
32
|
+
// always fail closed to ask; warn logs the construct, off silences that log.
|
|
33
|
+
//
|
|
34
|
+
// sandboxNetwork controls Codex workspace-write network access. Default false.
|
|
35
|
+
// Set true only when sub-agents need network after permission approval.
|
|
36
|
+
{
|
|
37
|
+
"globalConcurrentSubagents": 20,
|
|
38
|
+
"checkForUpdates": true,
|
|
39
|
+
"permissionsCeiling": "auto",
|
|
40
|
+
"escalation": "irreversible-only",
|
|
41
|
+
"strictReadParity": "warn",
|
|
42
|
+
"sandboxNetwork": false
|
|
43
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { realpathSync } from "node:fs";
|
|
1
|
+
import { closeSync, openSync, readFileSync, readSync, realpathSync, statSync, } from "node:fs";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
|
-
import { countJsonlType, runHook, } from "../orchestration/hook-core.js";
|
|
3
|
+
import { countJsonlType, runHook, TRANSCRIPT_READ_CAP, } from "../orchestration/hook-core.js";
|
|
4
4
|
import { hasParentMarker } from "../launch-prompt.js";
|
|
5
5
|
/**
|
|
6
6
|
* Claude Code UserPromptSubmit hook entry. Reads the JSON payload from stdin,
|
|
@@ -22,6 +22,87 @@ const SUBAGENT_ENTRYPOINTS = new Set([
|
|
|
22
22
|
"sdk-ts",
|
|
23
23
|
"sdk-py",
|
|
24
24
|
]);
|
|
25
|
+
function finiteNumber(value) {
|
|
26
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
27
|
+
}
|
|
28
|
+
function readTranscriptTail(transcriptPath) {
|
|
29
|
+
if (!transcriptPath)
|
|
30
|
+
return null;
|
|
31
|
+
let fd;
|
|
32
|
+
try {
|
|
33
|
+
const size = statSync(transcriptPath).size;
|
|
34
|
+
if (size <= TRANSCRIPT_READ_CAP) {
|
|
35
|
+
return readFileSync(transcriptPath, "utf8");
|
|
36
|
+
}
|
|
37
|
+
const start = size - TRANSCRIPT_READ_CAP;
|
|
38
|
+
const buf = Buffer.allocUnsafe(TRANSCRIPT_READ_CAP);
|
|
39
|
+
fd = openSync(transcriptPath, "r");
|
|
40
|
+
let offset = 0;
|
|
41
|
+
let pos = start;
|
|
42
|
+
while (offset < TRANSCRIPT_READ_CAP) {
|
|
43
|
+
const bytes = readSync(fd, buf, offset, TRANSCRIPT_READ_CAP - offset, pos);
|
|
44
|
+
if (bytes <= 0)
|
|
45
|
+
break;
|
|
46
|
+
offset += bytes;
|
|
47
|
+
pos += bytes;
|
|
48
|
+
}
|
|
49
|
+
const raw = buf.toString("utf8", 0, offset);
|
|
50
|
+
const firstNewline = raw.indexOf("\n");
|
|
51
|
+
return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
if (fd !== undefined) {
|
|
58
|
+
try {
|
|
59
|
+
closeSync(fd);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Ignore close failures. The hook must never fail the host turn.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function liftClaudeUsageFromTranscript(transcriptPath) {
|
|
68
|
+
const raw = readTranscriptTail(transcriptPath);
|
|
69
|
+
if (raw === null || !transcriptPath)
|
|
70
|
+
return null;
|
|
71
|
+
const lines = raw.split("\n");
|
|
72
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
73
|
+
const trimmed = lines[i].trim();
|
|
74
|
+
if (!trimmed)
|
|
75
|
+
continue;
|
|
76
|
+
try {
|
|
77
|
+
const msg = JSON.parse(trimmed);
|
|
78
|
+
if (msg.type !== "assistant" || !msg.message?.usage)
|
|
79
|
+
continue;
|
|
80
|
+
if (typeof msg.message.model !== "string")
|
|
81
|
+
return null;
|
|
82
|
+
const usage = msg.message.usage;
|
|
83
|
+
return {
|
|
84
|
+
harness: "claude",
|
|
85
|
+
model: msg.message.model,
|
|
86
|
+
source_ref: transcriptPath,
|
|
87
|
+
usage: {
|
|
88
|
+
input: finiteNumber(usage.input_tokens) ? usage.input_tokens : 0,
|
|
89
|
+
output: finiteNumber(usage.output_tokens) ? usage.output_tokens : 0,
|
|
90
|
+
cache_creation: finiteNumber(usage.cache_creation_input_tokens)
|
|
91
|
+
? usage.cache_creation_input_tokens
|
|
92
|
+
: 0,
|
|
93
|
+
cache_read: finiteNumber(usage.cache_read_input_tokens)
|
|
94
|
+
? usage.cache_read_input_tokens
|
|
95
|
+
: 0,
|
|
96
|
+
},
|
|
97
|
+
harnessPercentage: null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Skip malformed transcript lines and keep scanning older completed turns.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
25
106
|
export const claudeAdapter = {
|
|
26
107
|
isSubagent(payload, env) {
|
|
27
108
|
// subagent-mcp-spawned children inherit this guard and must not claim/nag.
|
|
@@ -45,6 +126,9 @@ export const claudeAdapter = {
|
|
|
45
126
|
currentTurn(transcriptPath) {
|
|
46
127
|
return countJsonlType(transcriptPath, "user");
|
|
47
128
|
},
|
|
129
|
+
liftUsage(_payload, _env, transcriptPath) {
|
|
130
|
+
return liftClaudeUsageFromTranscript(transcriptPath);
|
|
131
|
+
},
|
|
48
132
|
anonScope: "claude",
|
|
49
133
|
fullDirectiveFile: "orchestration-claude.md",
|
|
50
134
|
shortOnFile: "short-on.md",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { realpathSync } from "node:fs";
|
|
1
|
+
import { closeSync, openSync, readFileSync, readSync, realpathSync, statSync, } from "node:fs";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
|
-
import { claimAndEmit, classifyOwnerClaim, countJsonlType, cullHookZombies, ownerKey, runHook, } from "../orchestration/hook-core.js";
|
|
3
|
+
import { claimAndEmit, classifyOwnerClaim, computeEffectiveActive, countJsonlType, cullHookZombies, ownerKey, runHook, TRANSCRIPT_READ_CAP, } from "../orchestration/hook-core.js";
|
|
4
4
|
import * as marker from "../orchestration/marker.js";
|
|
5
|
+
import { resolveContextWindow, } from "../orchestration/metering.js";
|
|
5
6
|
import { hasParentMarker } from "../launch-prompt.js";
|
|
6
7
|
/**
|
|
7
8
|
* Codex CLI hook entry. Branches on payload.hook_event_name:
|
|
@@ -20,6 +21,142 @@ const SUBAGENT_SOURCE_STRINGS = new Set([
|
|
|
20
21
|
"subAgentThreadSpawn",
|
|
21
22
|
"subAgentOther",
|
|
22
23
|
]);
|
|
24
|
+
function finiteNumber(value) {
|
|
25
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
26
|
+
}
|
|
27
|
+
function tailJsonlLines(transcriptPath) {
|
|
28
|
+
if (!transcriptPath)
|
|
29
|
+
return [];
|
|
30
|
+
let fd;
|
|
31
|
+
try {
|
|
32
|
+
const size = statSync(transcriptPath).size;
|
|
33
|
+
let raw;
|
|
34
|
+
let droppedPartialHead = false;
|
|
35
|
+
if (size <= TRANSCRIPT_READ_CAP) {
|
|
36
|
+
raw = readFileSync(transcriptPath, "utf8");
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const start = size - TRANSCRIPT_READ_CAP;
|
|
40
|
+
const buf = Buffer.allocUnsafe(TRANSCRIPT_READ_CAP);
|
|
41
|
+
fd = openSync(transcriptPath, "r");
|
|
42
|
+
let offset = 0;
|
|
43
|
+
let pos = start;
|
|
44
|
+
while (offset < TRANSCRIPT_READ_CAP) {
|
|
45
|
+
const bytes = readSync(fd, buf, offset, TRANSCRIPT_READ_CAP - offset, pos);
|
|
46
|
+
if (bytes <= 0)
|
|
47
|
+
break;
|
|
48
|
+
offset += bytes;
|
|
49
|
+
pos += bytes;
|
|
50
|
+
}
|
|
51
|
+
raw = buf.toString("utf8", 0, offset);
|
|
52
|
+
droppedPartialHead = true;
|
|
53
|
+
}
|
|
54
|
+
const lines = raw.split("\n");
|
|
55
|
+
if (droppedPartialHead) {
|
|
56
|
+
lines.shift();
|
|
57
|
+
}
|
|
58
|
+
return lines.map((line) => line.trim()).filter(Boolean);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
if (fd !== undefined) {
|
|
65
|
+
try {
|
|
66
|
+
closeSync(fd);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Best-effort close; never throw.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function nestedRecord(value, key) {
|
|
75
|
+
if (!value || typeof value !== "object")
|
|
76
|
+
return null;
|
|
77
|
+
const child = value[key];
|
|
78
|
+
return child && typeof child === "object"
|
|
79
|
+
? child
|
|
80
|
+
: null;
|
|
81
|
+
}
|
|
82
|
+
function stringField(value, key) {
|
|
83
|
+
if (!value || typeof value !== "object")
|
|
84
|
+
return null;
|
|
85
|
+
const child = value[key];
|
|
86
|
+
return typeof child === "string" ? child : null;
|
|
87
|
+
}
|
|
88
|
+
function isTokenCountLine(obj) {
|
|
89
|
+
if (obj.type === "token_count")
|
|
90
|
+
return true;
|
|
91
|
+
const payload = nestedRecord(obj, "payload");
|
|
92
|
+
return payload?.type === "token_count";
|
|
93
|
+
}
|
|
94
|
+
function tokenInfo(obj) {
|
|
95
|
+
const direct = nestedRecord(obj, "info");
|
|
96
|
+
if (direct)
|
|
97
|
+
return direct;
|
|
98
|
+
return nestedRecord(nestedRecord(obj, "payload"), "info");
|
|
99
|
+
}
|
|
100
|
+
function modelFromTurnContext(obj) {
|
|
101
|
+
if (obj.type !== "turn_context")
|
|
102
|
+
return null;
|
|
103
|
+
return stringField(obj, "model") ?? stringField(nestedRecord(obj, "payload"), "model");
|
|
104
|
+
}
|
|
105
|
+
function liftCodexUsageFromRollout(transcriptPath) {
|
|
106
|
+
let latestTokenInfo = null;
|
|
107
|
+
let latestModel = null;
|
|
108
|
+
const lines = tailJsonlLines(transcriptPath);
|
|
109
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
110
|
+
try {
|
|
111
|
+
const obj = JSON.parse(lines[i]);
|
|
112
|
+
if (!obj || typeof obj !== "object")
|
|
113
|
+
continue;
|
|
114
|
+
const record = obj;
|
|
115
|
+
if (!latestTokenInfo && isTokenCountLine(record)) {
|
|
116
|
+
latestTokenInfo = tokenInfo(record);
|
|
117
|
+
}
|
|
118
|
+
if (!latestModel) {
|
|
119
|
+
latestModel = modelFromTurnContext(record);
|
|
120
|
+
}
|
|
121
|
+
if (latestTokenInfo && latestModel)
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Skip unparseable lines; never throw.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const total = nestedRecord(latestTokenInfo, "total_token_usage");
|
|
129
|
+
if (!total || !latestModel || !transcriptPath)
|
|
130
|
+
return null;
|
|
131
|
+
const input = total.input_tokens;
|
|
132
|
+
const output = total.output_tokens;
|
|
133
|
+
const cacheRead = total.cached_input_tokens;
|
|
134
|
+
if (!finiteNumber(input) || !finiteNumber(output) || !finiteNumber(cacheRead)) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const modelContextWindow = latestTokenInfo?.model_context_window;
|
|
138
|
+
const totalTokens = total.total_tokens;
|
|
139
|
+
const harnessPercentage = finiteNumber(modelContextWindow) &&
|
|
140
|
+
modelContextWindow > 0 &&
|
|
141
|
+
finiteNumber(totalTokens)
|
|
142
|
+
? (totalTokens / modelContextWindow) * 100
|
|
143
|
+
: null;
|
|
144
|
+
if (harnessPercentage === null && resolveContextWindow("codex", latestModel) === null) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
harness: "codex",
|
|
149
|
+
model: latestModel,
|
|
150
|
+
source_ref: transcriptPath,
|
|
151
|
+
usage: {
|
|
152
|
+
input,
|
|
153
|
+
output,
|
|
154
|
+
cache_creation: 0,
|
|
155
|
+
cache_read: cacheRead,
|
|
156
|
+
},
|
|
157
|
+
harnessPercentage,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
23
160
|
export const codexAdapter = {
|
|
24
161
|
isSubagent(payload, env) {
|
|
25
162
|
// subagent-mcp-spawned children inherit this guard and must not claim/nag.
|
|
@@ -47,6 +184,12 @@ export const codexAdapter = {
|
|
|
47
184
|
currentTurn(transcriptPath) {
|
|
48
185
|
return countJsonlType(transcriptPath, "turn_context");
|
|
49
186
|
},
|
|
187
|
+
liftUsage(_payload, _env, transcriptPath) {
|
|
188
|
+
// Provider-specific lift ONLY. hook-core is the sole writer of the metering
|
|
189
|
+
// record (matching the Claude adapter contract); the adapter must not also
|
|
190
|
+
// self-write, or the same turn would persist the record twice.
|
|
191
|
+
return liftCodexUsageFromRollout(transcriptPath);
|
|
192
|
+
},
|
|
50
193
|
anonScope: "codex",
|
|
51
194
|
fullDirectiveFile: "orchestration-codex.md",
|
|
52
195
|
shortOnFile: "short-on.md",
|
|
@@ -78,7 +221,13 @@ export function runCodexHook(payload, env, adapter = codexAdapter) {
|
|
|
78
221
|
const cwd = payload.cwd || process.cwd();
|
|
79
222
|
const current = ownerKey(payload, cwd, adapter);
|
|
80
223
|
marker.writeCurrentSession(cwd, current);
|
|
81
|
-
|
|
224
|
+
// Gate on the SAME effective-active computation runHook uses (disable /
|
|
225
|
+
// enable / latch / metering fail-safe), not the bare marker. SessionStart
|
|
226
|
+
// is turn 0 (grace window), so no metering fail-safe applies yet; passing
|
|
227
|
+
// false keeps a never-pre-enabled real session eligible for its turn-0
|
|
228
|
+
// directive via the latch/enable paths just like the UserPromptSubmit
|
|
229
|
+
// cadence would.
|
|
230
|
+
if (!computeEffectiveActive(cwd, current, Date.now(), false)) {
|
|
82
231
|
return "";
|
|
83
232
|
}
|
|
84
233
|
const turn = adapter.currentTurn(payload.transcript_path);
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,9 @@ import { CONFIG_FILENAME, NONBLOCKING_CULL_DEPS, ZOMBIE_FORCE_GRACE_MS, ZOMBIE_L
|
|
|
22
22
|
import { shouldReapTerminalButAlive } from "./zombie.js";
|
|
23
23
|
import * as orchestrationMarker from "./orchestration/marker.js";
|
|
24
24
|
import * as modelMode from "./orchestration/model-mode.js";
|
|
25
|
+
import * as handoff from "./orchestration/handoff.js";
|
|
26
|
+
import * as metering from "./orchestration/metering.js";
|
|
27
|
+
import { computeEffectiveActive } from "./orchestration/hook-core.js";
|
|
25
28
|
import { startLivenessHeartbeat } from "./orchestration/liveness.js";
|
|
26
29
|
import { checkForNpmUpdate } from "./orchestration/update-check.js";
|
|
27
30
|
import { ensureParentMarker } from "./launch-prompt.js";
|
|
@@ -117,6 +120,21 @@ const TASK_CATEGORY_GLOSS = "REQUIRED. Task shape -> routing category (server pi
|
|
|
117
120
|
function errorResult(text) {
|
|
118
121
|
return { content: [{ type: "text", text }], isError: true };
|
|
119
122
|
}
|
|
123
|
+
function textResult(text) {
|
|
124
|
+
return { content: [{ type: "text", text }] };
|
|
125
|
+
}
|
|
126
|
+
function computeEffectiveOrchestrationActive(cwd, key) {
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
// Derive the metering fail-safe from the PERSISTED record only: a record that
|
|
129
|
+
// exists but cannot resolve a percentage is undetectable (fail-safe ON). The
|
|
130
|
+
// ABSENCE of a record is NOT fail-safed -- it matches the hook's turn-1 grace
|
|
131
|
+
// window (no completed turn yet), so the tool's report agrees with the hook's
|
|
132
|
+
// tag on turn 1 instead of spuriously flipping ON. Everything else flows
|
|
133
|
+
// through the one shared helper so the tool never diverges from the hook.
|
|
134
|
+
const record = key !== undefined ? metering.readMetering(key) : null;
|
|
135
|
+
const meteringUndetectableFailSafe = record !== null && record.used_percentage === null;
|
|
136
|
+
return computeEffectiveActive(cwd, key, now, meteringUndetectableFailSafe);
|
|
137
|
+
}
|
|
120
138
|
function currentLaunchDepth(env = process.env) {
|
|
121
139
|
const raw = env.SUBAGENT_MCP_DEPTH;
|
|
122
140
|
if (raw !== undefined && raw !== "") {
|
|
@@ -550,11 +568,11 @@ reconcileInterval.unref();
|
|
|
550
568
|
// test/mirror-fragments.test.mjs while ORCHESTRATION_INSTRUCTIONS below stays
|
|
551
569
|
// compressed under MCP metadata limits:
|
|
552
570
|
// 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.
|
|
553
|
-
const ORCHESTRATION_INSTRUCTIONS = "subagent-mcp - CANONICAL OPERATING MODEL (full spec:
|
|
571
|
+
const ORCHESTRATION_INSTRUCTIONS = "subagent-mcp - CANONICAL OPERATING MODEL (full spec: orchestration-directive-architecture.md).\n\nPRECEDENCE. The latest <subagent-mcp state=\"...\"> hook tag and repo/system safety rules are jointly binding; genuine conflict => STOP and ask. Only the hook flips ON/OFF; absence of any tag = UNKNOWN => fail-safe ON.\n\nSOLE CHANNEL. Every 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 /workflows. No direct reads/writes; inline-by-right does not exist. Non-delegable step: ask a one-time exception, do only that step, resume delegating.\n\nSUB-AGENT CONTRACT. Prompt carries objective + output format + tools/sources + boundaries. SCALE: ~1 fact-find agent, 2-4 for comparisons; split multi-phase work into atomic steps. FAN-OUT independents, sequence dependents, SERIALIZE writers over shared paths (no cwd lock). VERIFY code and non-trivial steps with a separate sub-agent first.\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 them. Empty/stalled tail means ALIVE; learn finish via wait, do not poll-loop.\n\nORCHESTRATION starts OFF each session. Hook meters provider-reported context (never tokenized); at 15% it latches ON and coaches a 5-question plan; at 50% it warns every turn and unlocks handoff-write/handoff-read/handoff-clear. Undetectable context size = fail-safe ON.\n\nDROPOUT WHILE ON: HALT and ask until restored. SUB-AGENT EXEMPTION: a prompt whose literal FIRST LINE begins \"<this is a request from a parent process>\" skips this regime. DISABLE: user-only, never on your own initiative.\n\nMODEL SELECTION. Default smart auto-picks, rejects provider/model/effort selectors. user-approved-overrides honors them 30 min, expires lazily on launch_agent, needs user authorization.";
|
|
554
572
|
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. launch_agent is code-capped at 2 spawn levels below the main orchestrator: depth 1 may launch depth 2 workers; depth 2 workers cannot spawn further.\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.";
|
|
555
573
|
const server = new McpServer({
|
|
556
574
|
name: "subagent-mcp",
|
|
557
|
-
version: "2.12.
|
|
575
|
+
version: "2.12.13",
|
|
558
576
|
description: "Launches always-interactive local Claude and Codex sub-agent sessions and is the orchestrator's sole launch channel. Claude runs via the Claude Agent SDK over the local Claude Code executable; Codex via `codex app-server` over stdio. The server never calls Anthropic or OpenAI HTTP APIs directly.",
|
|
559
577
|
}, {
|
|
560
578
|
instructions: process.env.SUBAGENT_MCP_SUBAGENT === "1"
|
|
@@ -1614,22 +1632,27 @@ server.tool("wait", "Blocks until one or more sub-agents reach a reportable stat
|
|
|
1614
1632
|
};
|
|
1615
1633
|
}));
|
|
1616
1634
|
// Tool 8: orchestration-mode
|
|
1617
|
-
server.tool("orchestration-mode", "Toggle or query per-project ORCHESTRATION MODE. `enabled`: true = ON, false = OFF for THIS session only, omit = query. SOLE CHANNEL holds in BOTH states: subagent-mcp is the only sanctioned way to launch sub-agents; toggling OFF does not lift that. WHAT:
|
|
1635
|
+
server.tool("orchestration-mode", "Toggle or query per-project ORCHESTRATION MODE. `enabled`: true = ON, false = OFF for THIS session only, omit = query. SOLE CHANNEL holds in BOTH states: subagent-mcp is the only sanctioned way to launch sub-agents; toggling OFF does not lift that. WHAT: when ON act as a delegate-ONLY orchestrator; delegate every step, inline-by-right does not exist, a non-delegable atomic step needs a one-time user-approved exception via the structured-question tool. Default is now OFF each session (metering-driven latch/handoff supersede the old manual-upgrade-ask model); enabled:true explicitly turns ON early (before any latch); enabled:false remains a session-scoped 2h-TTL opt-out, HONORED even after the 15% latch or metering fail-safe forces ON. PERSISTENCE: a permitted disable is session-keyed only, applies to THIS session only, resumes ON next new session or after the 2h backstop; keyless hosts get only the one-time non-persisted conversational opt-out. DISABLE: never on your own initiative; you may PROPOSE OFF on task-fit mismatch, but only EXPLICIT user permission 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.", {
|
|
1618
1636
|
enabled: z.boolean().optional(),
|
|
1619
1637
|
}, withMaintenance(async (params) => {
|
|
1620
1638
|
const cwd = process.cwd();
|
|
1621
1639
|
const key = orchestrationMarker.readCurrentSession(cwd);
|
|
1622
1640
|
if (params.enabled === true) {
|
|
1623
|
-
if (!
|
|
1624
|
-
return errorResult("
|
|
1641
|
+
if (!key) {
|
|
1642
|
+
return errorResult("cannot enable: no session pointer found for this project (the per-turn hook has not fired yet). Enable records are session-keyed only; send one prompt first, or check wiring with `subagent-mcp doctor`.");
|
|
1625
1643
|
}
|
|
1644
|
+
if (!orchestrationMarker.isSessionScopedKey(key)) {
|
|
1645
|
+
return errorResult("cannot enable: this host supplies no session identity (no session_id/transcript_path in hook payloads) and enable records are session-keyed only. Orchestration remains fail-safe ON for this anonymous host.");
|
|
1646
|
+
}
|
|
1647
|
+
orchestrationMarker.removeDisable(key);
|
|
1648
|
+
orchestrationMarker.writeEnable(key);
|
|
1626
1649
|
return {
|
|
1627
1650
|
content: [
|
|
1628
1651
|
{
|
|
1629
1652
|
type: "text",
|
|
1630
1653
|
text: JSON.stringify({
|
|
1631
1654
|
orchestration_mode: "ON",
|
|
1632
|
-
message: "orchestration is ON
|
|
1655
|
+
message: "orchestration is ON for this session.",
|
|
1633
1656
|
}),
|
|
1634
1657
|
},
|
|
1635
1658
|
],
|
|
@@ -1649,14 +1672,14 @@ server.tool("orchestration-mode", "Toggle or query per-project ORCHESTRATION MOD
|
|
|
1649
1672
|
type: "text",
|
|
1650
1673
|
text: JSON.stringify({
|
|
1651
1674
|
orchestration_mode: "disabled-this-session",
|
|
1652
|
-
message: "orchestration disabled for THIS session only; the next new session
|
|
1675
|
+
message: "orchestration disabled for THIS session only; this opt-out overrides the 15% latch and metering fail-safe until the next new session or the 2h backstop.",
|
|
1653
1676
|
}),
|
|
1654
1677
|
},
|
|
1655
1678
|
],
|
|
1656
1679
|
};
|
|
1657
1680
|
}
|
|
1658
1681
|
// enabled === undefined -> query only; no marker mutation.
|
|
1659
|
-
const active =
|
|
1682
|
+
const active = computeEffectiveOrchestrationActive(cwd, key);
|
|
1660
1683
|
return {
|
|
1661
1684
|
content: [
|
|
1662
1685
|
{
|
|
@@ -1695,6 +1718,51 @@ server.tool("model-selection-mode", "Set or query per-project MODEL SELECTION MO
|
|
|
1695
1718
|
],
|
|
1696
1719
|
};
|
|
1697
1720
|
}));
|
|
1721
|
+
// Verbatim handoff post-write response (plan Section 1.1). Defined as a
|
|
1722
|
+
// top-level string constant here so mirror-fragments.test.mjs can assert
|
|
1723
|
+
// byte-identity against handoff.md via source-text regex. Kept in lockstep
|
|
1724
|
+
// with handoff.HANDOFF_WRITE_SUCCESS (single verbatim string, two surfaces).
|
|
1725
|
+
const HANDOFF_WRITE_SUCCESS_MESSAGE = "We are ready to start a new session, to avoid wasting tokens, use the structured question tool to confirm that the user is ready to use the `handoff-resume skill` in the next new session to resume work and has cleared the current /goal (if present) - or you will be compelled to keep working on a potential /goal that needs to be halted for a new session.";
|
|
1726
|
+
// Tool 10: handoff-write
|
|
1727
|
+
server.tool("handoff-write", "Write a handoff for this working directory so the NEXT session can resume cleanly. UNLOCKS only at >=50% context utilization with readable metering; below that, or if context size is undetectable, this tool returns an affirmative unavailable error (never silent). BEFORE calling, ask the user 10 clarifying questions via the structured-question tool to build a /goal prompt for the next session. content <=4000 chars; use overflow (<=8000 more chars) for anything beyond that, referenced by full path inside content. On success, relay the tool's exact response to the user verbatim.", {
|
|
1728
|
+
content: z.string().min(1),
|
|
1729
|
+
overflow: z.string().optional(),
|
|
1730
|
+
}, withMaintenance(async (params) => {
|
|
1731
|
+
const cwd = process.cwd();
|
|
1732
|
+
const key = orchestrationMarker.readCurrentSession(cwd);
|
|
1733
|
+
if (!key)
|
|
1734
|
+
return errorResult(handoff.UNAVAILABLE_NO_METERING);
|
|
1735
|
+
const result = handoff.writeHandoffIfAvailable(cwd, {
|
|
1736
|
+
content: params.content,
|
|
1737
|
+
overflowContent: params.overflow,
|
|
1738
|
+
createdBySession: key,
|
|
1739
|
+
}, metering.readMetering(key));
|
|
1740
|
+
if (!result.ok)
|
|
1741
|
+
return errorResult(result.error);
|
|
1742
|
+
return textResult(HANDOFF_WRITE_SUCCESS_MESSAGE);
|
|
1743
|
+
}));
|
|
1744
|
+
// Tool 11: handoff-read
|
|
1745
|
+
server.tool("handoff-read", "Read the saved handoff for this working directory (if any). BEFORE calling, confirm the user's intent via EXACTLY 5 structured questions (proves legitimacy, clears ambiguity). If found, this session becomes the ONLY session that gets the handoff re-appended to its periodic LONG reminders. If none is saved, explains that the previous session must write one first.", {}, withMaintenance(async () => {
|
|
1746
|
+
const cwd = process.cwd();
|
|
1747
|
+
const key = orchestrationMarker.readCurrentSession(cwd);
|
|
1748
|
+
const record = handoff.readHandoff(cwd);
|
|
1749
|
+
if (record === null)
|
|
1750
|
+
return errorResult(handoff.NO_HANDOFF_FOUND);
|
|
1751
|
+
const marked = key ? handoff.markRead(cwd, key) ?? record : record;
|
|
1752
|
+
const overflowLine = marked.overflow_path
|
|
1753
|
+
? `\n\nOverflow file: ${marked.overflow_path}`
|
|
1754
|
+
: "";
|
|
1755
|
+
return textResult([
|
|
1756
|
+
"Saved handoff:",
|
|
1757
|
+
marked.content + overflowLine,
|
|
1758
|
+
"Before using this handoff, confirm the user's intent via EXACTLY 5 structured questions. Confirm: resume objective, current blocker, files/state to preserve, next concrete action, and permission to proceed in this session.",
|
|
1759
|
+
].join("\n\n"));
|
|
1760
|
+
}));
|
|
1761
|
+
// Tool 12: handoff-clear
|
|
1762
|
+
server.tool("handoff-clear", "Delete the saved handoff for this working directory, including any overflow file. The write/read/clear cycle repeats at each successor session's own 50% threshold.", {}, withMaintenance(async () => {
|
|
1763
|
+
handoff.clearHandoff(process.cwd());
|
|
1764
|
+
return textResult("handoff cleared for this directory.");
|
|
1765
|
+
}));
|
|
1698
1766
|
// Connect the stdio transport only when run as the entry point (the bin), NOT
|
|
1699
1767
|
// when this module is imported (e.g. test/handler-validation.test.mjs importing
|
|
1700
1768
|
// the exported validatePresence). Connecting on import would block the test on
|
package/dist/init.js
CHANGED
|
@@ -4,7 +4,7 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
|
4
4
|
const MIGRATE_RE = /<!-- subagent-mcp:(?:managed:)?begin\b[^>]*-->[\s\S]*?<!-- subagent-mcp:(?:managed:)?end -->/;
|
|
5
5
|
const OWNER_CAP = 8;
|
|
6
6
|
export const INIT_BLOCK = [
|
|
7
|
-
"<!-- subagent-mcp:managed:begin schema=
|
|
7
|
+
"<!-- subagent-mcp:managed:begin schema=4 -->",
|
|
8
8
|
"## subagent-mcp invariant — managed block, do not edit between markers",
|
|
9
9
|
"",
|
|
10
10
|
"Upserted by `subagent-mcp init` at user request; re-running init keeps it in sync.",
|
|
@@ -25,7 +25,7 @@ export const INIT_BLOCK = [
|
|
|
25
25
|
"",
|
|
26
26
|
"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.",
|
|
27
27
|
"",
|
|
28
|
-
"ORCHESTRATION OFF
|
|
28
|
+
"ORCHESTRATION OFF BY DEFAULT -- each new session starts with orchestration OFF. A hook meters real provider-reported context usage (never tokenized, never self-estimated). At 15% utilization a persisted latch force-enables orchestration and coaches a 5-question planning stop. At 50% the hook warns every turn to wind down and unlocks handoff-write/handoff-read/handoff-clear for a clean session handoff. If context size cannot be measured, the hook fails safe to ON. Never assert a state yourself -- only the hook tag is authoritative.",
|
|
29
29
|
"",
|
|
30
30
|
"DROPOUT WHILE ON: if subagent-mcp stops responding while orchestration is ON, halt and ask the user; do nothing inline. Keep re-checking and stay halted until subagent-mcp is restored (no auto-degrade). The only user choices are keep-waiting (the default) or explicitly abandon the whole task; aborting ends the task, it never switches you to inline work.",
|
|
31
31
|
"",
|