@heretyc/subagent-mcp 2.6.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/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +124 -0
- package/directives/carryover-claude.md +17 -0
- package/directives/carryover-codex.md +17 -0
- package/directives/off-turn-reminder.md +1 -0
- package/directives/orchestration-claude.md +21 -0
- package/directives/orchestration-codex.md +22 -0
- package/dist/advanced-ruleset.py +67 -0
- package/dist/deadlock.js +8 -0
- package/dist/doctor.js +32 -0
- package/dist/effort.js +78 -0
- package/dist/hooks/orchestration-claude.js +88 -0
- package/dist/hooks/orchestration-codex.js +152 -0
- package/dist/index.js +908 -0
- package/dist/orchestration/hook-core.js +208 -0
- package/dist/orchestration/marker.js +139 -0
- package/dist/output-helpers.js +128 -0
- package/dist/platform.js +59 -0
- package/dist/routing-table.json +3821 -0
- package/dist/routing.js +260 -0
- package/dist/ruleset-scaffold.js +2 -0
- package/dist/ruleset.js +319 -0
- package/dist/setup.js +507 -0
- package/dist/status-helpers.js +56 -0
- package/dist/stream-helpers.js +182 -0
- package/dist/wait-helpers.js +21 -0
- package/package.json +51 -0
- package/scripts/postinstall.mjs +102 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { countJsonlType, runHook, } from "../orchestration/hook-core.js";
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code UserPromptSubmit hook entry. Reads the JSON payload from stdin,
|
|
6
|
+
* runs the provider-agnostic core with the Claude adapter, and writes the
|
|
7
|
+
* result to stdout. Always exits 0 — a hook must never fail the host turn.
|
|
8
|
+
*
|
|
9
|
+
* Compiles to dist/hooks/orchestration-claude.js and is invoked as:
|
|
10
|
+
* node "${CLAUDE_PLUGIN_ROOT}/dist/hooks/orchestration-claude.js"
|
|
11
|
+
*/
|
|
12
|
+
// Claude entrypoints that are themselves SUBAGENTS and must NOT be nagged.
|
|
13
|
+
// Top-level entrypoints that SHOULD inject (cli, mcp, claude-vscode) are simply
|
|
14
|
+
// absent from this set. DECISION: claude-desktop is a non-hook host (it has no
|
|
15
|
+
// UserPromptSubmit hook), so whether it appears here never matters there; we do
|
|
16
|
+
// NOT add it, which resolves the prototype/INSTALL.md conflict over whether
|
|
17
|
+
// claude-desktop should be skipped.
|
|
18
|
+
const SUBAGENT_ENTRYPOINTS = new Set([
|
|
19
|
+
"local-agent",
|
|
20
|
+
"sdk-cli",
|
|
21
|
+
"sdk-ts",
|
|
22
|
+
"sdk-py",
|
|
23
|
+
]);
|
|
24
|
+
export const claudeAdapter = {
|
|
25
|
+
isSubagent(payload, env) {
|
|
26
|
+
// subagent-mcp-spawned children inherit this guard and must not claim/nag.
|
|
27
|
+
if (env.SUBAGENT_MCP_SUBAGENT === "1") {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (payload.agent_id) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
const entrypoint = env.CLAUDE_CODE_ENTRYPOINT;
|
|
34
|
+
return typeof entrypoint === "string" && SUBAGENT_ENTRYPOINTS.has(entrypoint);
|
|
35
|
+
},
|
|
36
|
+
// Count JSONL lines in the transcript whose parsed object.type === 'user'.
|
|
37
|
+
// Delegates to the bounded counter (reads at most the trailing window so a
|
|
38
|
+
// huge/attacker-supplied transcript can't stall the inline host turn).
|
|
39
|
+
// Unreadable/missing transcript -> 0, so the hook emits FULL (fail-safe: a
|
|
40
|
+
// visible directive rather than silent suppression).
|
|
41
|
+
currentTurn(transcriptPath) {
|
|
42
|
+
return countJsonlType(transcriptPath, "user");
|
|
43
|
+
},
|
|
44
|
+
fullDirectiveFile: "orchestration-claude.md",
|
|
45
|
+
offTurnFile: "off-turn-reminder.md",
|
|
46
|
+
carryoverDirectiveFile: "carryover-claude.md",
|
|
47
|
+
};
|
|
48
|
+
function readStdin() {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
let data = "";
|
|
51
|
+
process.stdin.setEncoding("utf8");
|
|
52
|
+
process.stdin.on("data", (chunk) => {
|
|
53
|
+
data += chunk;
|
|
54
|
+
});
|
|
55
|
+
process.stdin.on("end", () => resolve(data));
|
|
56
|
+
process.stdin.on("error", () => resolve(data));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function main() {
|
|
60
|
+
let payload = {};
|
|
61
|
+
try {
|
|
62
|
+
const raw = await readStdin();
|
|
63
|
+
if (raw.trim()) {
|
|
64
|
+
payload = JSON.parse(raw);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Bad/empty stdin -> empty payload; runHook degrades to '' safely.
|
|
69
|
+
}
|
|
70
|
+
let out = "";
|
|
71
|
+
try {
|
|
72
|
+
out = runHook(payload, process.env, claudeAdapter);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
out = "";
|
|
76
|
+
}
|
|
77
|
+
if (out) {
|
|
78
|
+
process.stdout.write(out);
|
|
79
|
+
}
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
// Only run the stdin->stdout shim when invoked directly as the hook command,
|
|
83
|
+
// NOT when a test imports `claudeAdapter`. Importing must have no side effects.
|
|
84
|
+
const isMain = process.argv[1] !== undefined &&
|
|
85
|
+
import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
|
|
86
|
+
if (isMain) {
|
|
87
|
+
void main();
|
|
88
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { classifyClaim, countJsonlType, readDirective, runHook, sessionKey, } from "../orchestration/hook-core.js";
|
|
4
|
+
import * as marker from "../orchestration/marker.js";
|
|
5
|
+
/**
|
|
6
|
+
* Codex CLI hook entry. Branches on payload.hook_event_name:
|
|
7
|
+
* - 'SessionStart' -> if active and not a subagent, emit FULL (covers the
|
|
8
|
+
* turn-0 directive before the first UserPromptSubmit).
|
|
9
|
+
* - 'UserPromptSubmit' -> the normal %5 runHook cadence.
|
|
10
|
+
*
|
|
11
|
+
* NOTE: the Python prototype carried inline 'alternating/odd-turn' comments;
|
|
12
|
+
* those were STALE. The operative cadence is every 5th relative turn (%5),
|
|
13
|
+
* matching INSTALL.md. The stale alternating comments are intentionally NOT
|
|
14
|
+
* reproduced here.
|
|
15
|
+
*
|
|
16
|
+
* Compiles to dist/hooks/orchestration-codex.js and is invoked as:
|
|
17
|
+
* node "<PLUGIN_ROOT>/dist/hooks/orchestration-codex.js"
|
|
18
|
+
*/
|
|
19
|
+
// Codex 0.131+ source-string variants that mark a SUBAGENT session.
|
|
20
|
+
const SUBAGENT_SOURCE_STRINGS = new Set([
|
|
21
|
+
"subAgentReview",
|
|
22
|
+
"subAgentCompact",
|
|
23
|
+
"subAgentThreadSpawn",
|
|
24
|
+
"subAgentOther",
|
|
25
|
+
]);
|
|
26
|
+
const PARENT_PROCESS_MARKER = "this is a request from a parent process";
|
|
27
|
+
export const codexAdapter = {
|
|
28
|
+
isSubagent(payload, env) {
|
|
29
|
+
// subagent-mcp-spawned children inherit this guard and must not claim/nag.
|
|
30
|
+
if (env.SUBAGENT_MCP_SUBAGENT === "1") {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
const source = payload.source;
|
|
34
|
+
// 0.131+: source is an object whose keys name the subagent kind.
|
|
35
|
+
if (source && typeof source === "object") {
|
|
36
|
+
if (Object.prototype.hasOwnProperty.call(source, "subagent")) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Older: source is a string enum.
|
|
41
|
+
if (typeof source === "string" && SUBAGENT_SOURCE_STRINGS.has(source)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
// Fallback: a parent-process handoff is detectable from the prompt's first
|
|
45
|
+
// non-empty line (our own subagent contract starts with this sentinel).
|
|
46
|
+
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
47
|
+
const head = prompt.slice(0, 200);
|
|
48
|
+
for (const line of head.split("\n")) {
|
|
49
|
+
const trimmed = line.trim().toLowerCase();
|
|
50
|
+
if (!trimmed)
|
|
51
|
+
continue;
|
|
52
|
+
return trimmed.includes(PARENT_PROCESS_MARKER);
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
},
|
|
56
|
+
// Count JSONL lines whose parsed object.type === 'turn_context'. Delegates to
|
|
57
|
+
// the bounded counter (reads at most the trailing window so a huge/
|
|
58
|
+
// attacker-supplied transcript can't stall the inline host turn). Unreadable
|
|
59
|
+
// -> 0 (fail-safe: emits FULL rather than silently suppressing).
|
|
60
|
+
currentTurn(transcriptPath) {
|
|
61
|
+
return countJsonlType(transcriptPath, "turn_context");
|
|
62
|
+
},
|
|
63
|
+
fullDirectiveFile: "orchestration-codex.md",
|
|
64
|
+
offTurnFile: "off-turn-reminder.md",
|
|
65
|
+
carryoverDirectiveFile: "carryover-codex.md",
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Codex dispatcher. SessionStart fires once before the first prompt; it covers
|
|
69
|
+
* the turn-0 directive (UserPromptSubmit cadence then handles turns 1+).
|
|
70
|
+
*
|
|
71
|
+
* Because the marker now PERSISTS across sessions, SessionStart must classify
|
|
72
|
+
* the claim like runHook does: an inherited marker owned by a prior/other
|
|
73
|
+
* session is a CARRYOVER (emit the CARRYOVER notice prepended to FULL and
|
|
74
|
+
* re-claim); a freshly-enabled marker is FRESH (emit FULL and claim). Either
|
|
75
|
+
* way the marker is re-claimed/baselined so the following UserPromptSubmit turns
|
|
76
|
+
* run SAME-SESSION cadence and the notice fires exactly once.
|
|
77
|
+
*
|
|
78
|
+
* Returns the string to inject, or '' for nothing. Fully fail-safe.
|
|
79
|
+
*/
|
|
80
|
+
export function runCodexHook(payload, env, adapter = codexAdapter) {
|
|
81
|
+
try {
|
|
82
|
+
if (payload.hook_event_name === "SessionStart") {
|
|
83
|
+
if (adapter.isSubagent(payload, env))
|
|
84
|
+
return "";
|
|
85
|
+
const cwd = payload.cwd || process.cwd();
|
|
86
|
+
if (!marker.isActive(cwd))
|
|
87
|
+
return "";
|
|
88
|
+
const current = sessionKey(payload);
|
|
89
|
+
const turn = adapter.currentTurn(payload.transcript_path);
|
|
90
|
+
const m = marker.readMarker(cwd);
|
|
91
|
+
const kind = classifyClaim(m.owner_session, m.baseline_turn, current);
|
|
92
|
+
// Claim/re-claim for this session and baseline at the current turn.
|
|
93
|
+
const firstCarryover = kind === "carryover" && !m.carryover_ack;
|
|
94
|
+
m.baseline_turn = turn;
|
|
95
|
+
m.owner_session = current ?? null;
|
|
96
|
+
if (kind === "carryover") {
|
|
97
|
+
m.provenance = "carried-over";
|
|
98
|
+
m.carryover_ack = true;
|
|
99
|
+
}
|
|
100
|
+
marker.writeMarker(cwd, m);
|
|
101
|
+
return firstCarryover
|
|
102
|
+
? readDirective(env, adapter.carryoverDirectiveFile) +
|
|
103
|
+
readDirective(env, adapter.fullDirectiveFile)
|
|
104
|
+
: readDirective(env, adapter.fullDirectiveFile);
|
|
105
|
+
}
|
|
106
|
+
// UserPromptSubmit (and any other event) -> normal cadence.
|
|
107
|
+
return runHook(payload, env, adapter);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function readStdin() {
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
let data = "";
|
|
116
|
+
process.stdin.setEncoding("utf8");
|
|
117
|
+
process.stdin.on("data", (chunk) => {
|
|
118
|
+
data += chunk;
|
|
119
|
+
});
|
|
120
|
+
process.stdin.on("end", () => resolve(data));
|
|
121
|
+
process.stdin.on("error", () => resolve(data));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async function main() {
|
|
125
|
+
let payload = {};
|
|
126
|
+
try {
|
|
127
|
+
const raw = await readStdin();
|
|
128
|
+
if (raw.trim()) {
|
|
129
|
+
payload = JSON.parse(raw);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Bad/empty stdin -> empty payload; degrades to '' safely.
|
|
134
|
+
}
|
|
135
|
+
let out = "";
|
|
136
|
+
try {
|
|
137
|
+
out = runCodexHook(payload, process.env, codexAdapter);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
out = "";
|
|
141
|
+
}
|
|
142
|
+
if (out) {
|
|
143
|
+
process.stdout.write(out);
|
|
144
|
+
}
|
|
145
|
+
process.exit(0);
|
|
146
|
+
}
|
|
147
|
+
// Only run the shim when invoked directly, not when a test imports the adapter.
|
|
148
|
+
const isMain = process.argv[1] !== undefined &&
|
|
149
|
+
import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
|
|
150
|
+
if (isMain) {
|
|
151
|
+
void main();
|
|
152
|
+
}
|