@heretyc/subagent-mcp 2.7.1 → 2.8.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 -201
- package/NOTICE +5 -5
- package/README.md +124 -124
- package/directives/carryover-claude.md +17 -17
- package/directives/carryover-codex.md +17 -17
- package/directives/off-turn-reminder.md +1 -1
- package/directives/orchestration-claude.md +21 -21
- package/directives/orchestration-codex.md +22 -22
- package/directives/reminder-off-claude.md +12 -0
- package/directives/reminder-off-codex.md +12 -0
- package/directives/reminder-on.md +9 -0
- package/dist/advanced-ruleset.py +67 -67
- package/dist/hooks/orchestration-claude.js +4 -2
- package/dist/hooks/orchestration-codex.js +14 -23
- package/dist/orchestration/hook-core.js +85 -37
- package/dist/orchestration/marker.js +6 -0
- package/dist/orchestration/reminder.js +72 -0
- package/dist/routing-table.json +2957 -3821
- package/dist/ruleset-scaffold.js +1 -1
- package/dist/setup.js +3 -0
- package/package.json +51 -51
- package/scripts/postinstall.mjs +102 -102
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import { realpathSync } from "node:fs";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
|
-
import { classifyClaim, countJsonlType,
|
|
3
|
+
import { claimAndEmit, classifyClaim, countJsonlType, runHook, sessionKey, } from "../orchestration/hook-core.js";
|
|
4
4
|
import * as marker from "../orchestration/marker.js";
|
|
5
5
|
/**
|
|
6
6
|
* Codex CLI hook entry. Branches on payload.hook_event_name:
|
|
7
|
-
* - 'SessionStart' -> if active and not a subagent, emit FULL
|
|
8
|
-
* turn-0 directive before
|
|
9
|
-
*
|
|
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.
|
|
7
|
+
* - 'SessionStart' -> if active and not a subagent, emit FULL + the ON
|
|
8
|
+
* reminder block (covers the turn-0 directive before
|
|
9
|
+
* the first UserPromptSubmit).
|
|
10
|
+
* - 'UserPromptSubmit' -> the normal per-prompt reminder cadence (runHook).
|
|
15
11
|
*
|
|
16
12
|
* Compiles to dist/hooks/orchestration-codex.js and is invoked as:
|
|
17
13
|
* node "<PLUGIN_ROOT>/dist/hooks/orchestration-codex.js"
|
|
@@ -56,13 +52,16 @@ export const codexAdapter = {
|
|
|
56
52
|
// Count JSONL lines whose parsed object.type === 'turn_context'. Delegates to
|
|
57
53
|
// the bounded counter (reads at most the trailing window so a huge/
|
|
58
54
|
// attacker-supplied transcript can't stall the inline host turn). Unreadable
|
|
59
|
-
// -> 0 (fail-safe:
|
|
55
|
+
// -> 0 (fail-safe: the claim baseline stamps at 0; cadence is counter-driven
|
|
56
|
+
// and unaffected). Read on claim turns only.
|
|
60
57
|
currentTurn(transcriptPath) {
|
|
61
58
|
return countJsonlType(transcriptPath, "turn_context");
|
|
62
59
|
},
|
|
63
60
|
fullDirectiveFile: "orchestration-codex.md",
|
|
64
61
|
offTurnFile: "off-turn-reminder.md",
|
|
65
62
|
carryoverDirectiveFile: "carryover-codex.md",
|
|
63
|
+
reminderOnFile: "reminder-on.md",
|
|
64
|
+
reminderOffFile: "reminder-off-codex.md",
|
|
66
65
|
};
|
|
67
66
|
/**
|
|
68
67
|
* Codex dispatcher. SessionStart fires once before the first prompt; it covers
|
|
@@ -89,19 +88,11 @@ export function runCodexHook(payload, env, adapter = codexAdapter) {
|
|
|
89
88
|
const turn = adapter.currentTurn(payload.transcript_path);
|
|
90
89
|
const m = marker.readMarker(cwd);
|
|
91
90
|
const kind = classifyClaim(m.owner_session, m.baseline_turn, current);
|
|
92
|
-
// Claim/re-claim
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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);
|
|
91
|
+
// Claim/re-claim + emit via the SHARED claim path (one copy of the
|
|
92
|
+
// semantics — FULL + ON reminder, ack-latched CARRYOVER prepend, counter
|
|
93
|
+
// re-baseline). SessionStart claims even on SAME-SESSION (resume) so
|
|
94
|
+
// turn 0 is always covered.
|
|
95
|
+
return claimAndEmit(cwd, current, turn, m, kind, env, adapter);
|
|
105
96
|
}
|
|
106
97
|
// UserPromptSubmit (and any other event) -> normal cadence.
|
|
107
98
|
return runHook(payload, env, adapter);
|
|
@@ -3,6 +3,31 @@ import { closeSync, openSync, readFileSync, readSync, statSync, } from "node:fs"
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import * as marker from "./marker.js";
|
|
6
|
+
import * as reminder from "./reminder.js";
|
|
7
|
+
/**
|
|
8
|
+
* Provider-agnostic core of the UserPromptSubmit / SessionStart hook.
|
|
9
|
+
*
|
|
10
|
+
* The MCP tool only ever WRITES the marker. A SEPARATE hook process (one per
|
|
11
|
+
* turn) READS the marker here and decides what to inject. The hook now emits in
|
|
12
|
+
* BOTH marker states, on a per-prompt counter (reminder.ts): every
|
|
13
|
+
* REMINDER_PERIOD-th prompt injects the LONG mode-specific
|
|
14
|
+
* <ORCHESTRATION-REMINDER-INVARIANT> block, every prompt between injects the
|
|
15
|
+
* one-line pointer at it. Marker ON adds the claim machinery: the claim turn
|
|
16
|
+
* (fresh enable or carryover re-claim) emits the FULL directive plus the ON
|
|
17
|
+
* reminder block and re-baselines the counter. (Supersedes LOCKED DECISION 2's
|
|
18
|
+
* same-session rel%5 FULL re-emission — owner directive 2026-06-11: steady
|
|
19
|
+
* state is the leaner tagged reminder, FULL fires on claim turns only.) The
|
|
20
|
+
* marker PERSISTS across sessions/restarts, so the first turn of a new session
|
|
21
|
+
* that inherits an already-ON marker emits a CARRYOVER notice (prepended to
|
|
22
|
+
* FULL) once per project marker, ack-latched in marker state, and re-claims
|
|
23
|
+
* for that session.
|
|
24
|
+
*
|
|
25
|
+
* The entire run is wrapped in try/catch: on ANY error we emit nothing. A hook
|
|
26
|
+
* must never crash or stall the host turn. "Emit" means RETURN the string; the
|
|
27
|
+
* entry shim is what writes it to process.stdout.
|
|
28
|
+
*/
|
|
29
|
+
/** Long-reminder cadence: every Nth counted prompt is a LONG turn. */
|
|
30
|
+
export const REMINDER_PERIOD = 5;
|
|
6
31
|
/**
|
|
7
32
|
* Resolve the repo-root `directives/` dir at runtime. Honors an explicit plugin
|
|
8
33
|
* root (Claude sets CLAUDE_PLUGIN_ROOT; a generic PLUGIN_ROOT is also accepted)
|
|
@@ -147,20 +172,60 @@ export function classifyClaim(owner_session, baseline_turn, current) {
|
|
|
147
172
|
}
|
|
148
173
|
return "same";
|
|
149
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Per-prompt reminder cadence emission: the LONG block (longFile) on every
|
|
177
|
+
* REMINDER_PERIOD-th counted prompt, the one-line pointer between. When the
|
|
178
|
+
* counter could NOT persist, emit the LONG block — fail VISIBLE: a host whose
|
|
179
|
+
* temp dir cannot hold the state file would otherwise inject the pointer on
|
|
180
|
+
* every prompt at a block that never arrives.
|
|
181
|
+
*/
|
|
182
|
+
function cadenceEmit(env, adapter, longFile, count, persisted) {
|
|
183
|
+
return !persisted || count % REMINDER_PERIOD === 0
|
|
184
|
+
? readDirective(env, longFile)
|
|
185
|
+
: readDirective(env, adapter.offTurnFile);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Claim (or re-claim) an active marker for the current session and emit the
|
|
189
|
+
* claim-turn payload: FULL directive + ON reminder block, with the CARRYOVER
|
|
190
|
+
* notice prepended on the first foreign-owner claim of a marker (ack-latched,
|
|
191
|
+
* so sub-agent/parallel-session marker ping-pong cannot re-fire it). The
|
|
192
|
+
* reminder counter re-baselines to 0 — the claim turn IS a LONG turn, so the
|
|
193
|
+
* next LONG fires exactly REMINDER_PERIOD prompts later. Shared by runHook's
|
|
194
|
+
* claim branch and the Codex SessionStart dispatcher (one copy of the claim
|
|
195
|
+
* semantics, no drift).
|
|
196
|
+
*/
|
|
197
|
+
export function claimAndEmit(cwd, current, turn, m, kind, env, adapter) {
|
|
198
|
+
const firstCarryover = kind === "carryover" && !m.carryover_ack;
|
|
199
|
+
m.baseline_turn = turn;
|
|
200
|
+
m.owner_session = current ?? null;
|
|
201
|
+
if (kind === "carryover") {
|
|
202
|
+
m.provenance = "carried-over";
|
|
203
|
+
m.carryover_ack = true;
|
|
204
|
+
}
|
|
205
|
+
marker.writeMarker(cwd, m);
|
|
206
|
+
reminder.rebase(cwd, current, 0);
|
|
207
|
+
const full = readDirective(env, adapter.fullDirectiveFile) +
|
|
208
|
+
readDirective(env, adapter.reminderOnFile);
|
|
209
|
+
return firstCarryover
|
|
210
|
+
? readDirective(env, adapter.carryoverDirectiveFile) + full
|
|
211
|
+
: full;
|
|
212
|
+
}
|
|
150
213
|
/**
|
|
151
214
|
* Core hook logic. Returns the string to inject, or '' to inject nothing.
|
|
152
215
|
*
|
|
153
216
|
* Order:
|
|
154
|
-
* 1. subagent -> '' (a subagent must never be nagged to delegate
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
217
|
+
* 1. subagent -> '' (a subagent must never be nagged to delegate; the counter
|
|
218
|
+
* does not advance).
|
|
219
|
+
* 2. marker not active for cwd -> OFF cadence: advance the session's counter
|
|
220
|
+
* (per-owner; a new session starts its own), LONG OFF-variant reminder when
|
|
221
|
+
* count % REMINDER_PERIOD === 0, else the one-line pointer.
|
|
222
|
+
* 3. marker active: classify the claim from marker state.
|
|
223
|
+
* 4. FRESH / CARRYOVER -> claimAndEmit (FULL + ON reminder; CARRYOVER notice
|
|
224
|
+
* prepended once per marker; counter re-baselined). The transcript turn is
|
|
225
|
+
* read ONLY here — claim turns are the only consumer of the baseline, and
|
|
226
|
+
* the tail read is too expensive for the per-prompt steady state.
|
|
227
|
+
* 5. SAME-SESSION -> ON cadence: LONG ON-variant reminder when
|
|
228
|
+
* count % REMINDER_PERIOD === 0, else the one-line pointer.
|
|
164
229
|
*/
|
|
165
230
|
export function runHook(payload, env, adapter) {
|
|
166
231
|
try {
|
|
@@ -168,38 +233,21 @@ export function runHook(payload, env, adapter) {
|
|
|
168
233
|
return "";
|
|
169
234
|
}
|
|
170
235
|
const cwd = payload.cwd || process.cwd();
|
|
236
|
+
const current = sessionKey(payload);
|
|
171
237
|
if (!marker.isActive(cwd)) {
|
|
172
|
-
|
|
238
|
+
// OFF: no claim machinery — just the per-prompt reminder cadence.
|
|
239
|
+
const r = reminder.advance(cwd, current);
|
|
240
|
+
return cadenceEmit(env, adapter, adapter.reminderOffFile, r.count, r.persisted);
|
|
173
241
|
}
|
|
174
|
-
const current = sessionKey(payload);
|
|
175
|
-
const turn = adapter.currentTurn(payload.transcript_path);
|
|
176
242
|
const m = marker.readMarker(cwd);
|
|
177
243
|
const kind = classifyClaim(m.owner_session, m.baseline_turn, current);
|
|
178
|
-
if (kind === "fresh") {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
marker.writeMarker(cwd, m);
|
|
182
|
-
return readDirective(env, adapter.fullDirectiveFile);
|
|
183
|
-
}
|
|
184
|
-
if (kind === "carryover") {
|
|
185
|
-
// Re-claim for the current session and re-baseline at this turn so the
|
|
186
|
-
// notice fires once per project marker. The ack survives re-claims, so
|
|
187
|
-
// sub-agent/parallel-session marker ping-pong cannot re-fire it.
|
|
188
|
-
const firstTime = !m.carryover_ack;
|
|
189
|
-
m.baseline_turn = turn;
|
|
190
|
-
m.owner_session = current ?? null;
|
|
191
|
-
m.provenance = "carried-over";
|
|
192
|
-
m.carryover_ack = true;
|
|
193
|
-
marker.writeMarker(cwd, m);
|
|
194
|
-
return firstTime
|
|
195
|
-
? readDirective(env, adapter.carryoverDirectiveFile) +
|
|
196
|
-
readDirective(env, adapter.fullDirectiveFile)
|
|
197
|
-
: readDirective(env, adapter.fullDirectiveFile);
|
|
244
|
+
if (kind === "fresh" || kind === "carryover") {
|
|
245
|
+
const turn = adapter.currentTurn(payload.transcript_path);
|
|
246
|
+
return claimAndEmit(cwd, current, turn, m, kind, env, adapter);
|
|
198
247
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
: readDirective(env, adapter.offTurnFile);
|
|
248
|
+
// SAME-SESSION: per-prompt reminder cadence, ON variant.
|
|
249
|
+
const r = reminder.advance(cwd, current);
|
|
250
|
+
return cadenceEmit(env, adapter, adapter.reminderOnFile, r.count, r.persisted);
|
|
203
251
|
}
|
|
204
252
|
catch {
|
|
205
253
|
// Any failure -> inject nothing. Never crash or stall the host turn.
|
|
@@ -3,6 +3,12 @@ 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
|
+
/**
|
|
7
|
+
* Shared per-project state dir for ALL hook state files (marker + reminder
|
|
8
|
+
* counter). Exported so sibling state modules key off the SAME location — a
|
|
9
|
+
* future move edits one constant, not N copies.
|
|
10
|
+
*/
|
|
11
|
+
export const stateDir = markerDir;
|
|
6
12
|
/**
|
|
7
13
|
* Canonicalize a working directory so two spellings of the same path hash
|
|
8
14
|
* identically. Strip a leading Windows \\?\ extended-length prefix FIRST (on
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { cwdHash, stateDir } from "./marker.js";
|
|
4
|
+
/** Bound the per-owner map so a busy multi-session cwd cannot grow it without
|
|
5
|
+
* limit; evicting ALL entries on overflow is crude but rare and self-heals. */
|
|
6
|
+
const OWNER_CAP = 8;
|
|
7
|
+
function ownerKey(current) {
|
|
8
|
+
return current ?? "null";
|
|
9
|
+
}
|
|
10
|
+
export function reminderPath(cwd) {
|
|
11
|
+
return join(stateDir, "remind-" + cwdHash(cwd) + ".json");
|
|
12
|
+
}
|
|
13
|
+
export function readReminder(cwd) {
|
|
14
|
+
try {
|
|
15
|
+
const raw = readFileSync(reminderPath(cwd), "utf8");
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
const counts = {};
|
|
18
|
+
if (parsed.counts && typeof parsed.counts === "object") {
|
|
19
|
+
for (const [owner, count] of Object.entries(parsed.counts)) {
|
|
20
|
+
if (typeof count === "number" && Number.isFinite(count)) {
|
|
21
|
+
counts[owner] = count;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { counts };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Missing/corrupt state -> safe default (no owners counted yet).
|
|
29
|
+
return { counts: {} };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Persist the state. Returns true on success, false on any write failure. */
|
|
33
|
+
export function writeReminder(cwd, obj) {
|
|
34
|
+
try {
|
|
35
|
+
// Owner-only perms (see marker.enable()): the state persists session keys.
|
|
36
|
+
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
37
|
+
writeFileSync(reminderPath(cwd), JSON.stringify(obj), {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
mode: 0o600,
|
|
40
|
+
});
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Fail-safe: report the failure; never throw.
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Count one user prompt for the current session and persist. Returns the
|
|
50
|
+
* session's advanced count plus whether the state persisted (persisted=false
|
|
51
|
+
* lets the caller fail visible instead of never reaching the LONG cadence).
|
|
52
|
+
*/
|
|
53
|
+
export function advance(cwd, current) {
|
|
54
|
+
const owner = ownerKey(current);
|
|
55
|
+
const state = readReminder(cwd);
|
|
56
|
+
if (!(owner in state.counts) && Object.keys(state.counts).length >= OWNER_CAP) {
|
|
57
|
+
state.counts = {};
|
|
58
|
+
}
|
|
59
|
+
const count = (state.counts[owner] ?? 0) + 1;
|
|
60
|
+
state.counts[owner] = count;
|
|
61
|
+
const persisted = writeReminder(cwd, state);
|
|
62
|
+
return { count, persisted };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Re-baseline the current session's count (claim turns set 0: the claim turn
|
|
66
|
+
* IS a LONG turn, so the next LONG fires exactly REMINDER_PERIOD prompts on).
|
|
67
|
+
*/
|
|
68
|
+
export function rebase(cwd, current, count) {
|
|
69
|
+
const state = readReminder(cwd);
|
|
70
|
+
state.counts[ownerKey(current)] = count;
|
|
71
|
+
writeReminder(cwd, state);
|
|
72
|
+
}
|