llm-orchestrator 1.2.3 → 1.2.4
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/.claude-plugin/plugin.json +1 -1
- package/README.md +4 -2
- package/adapters/hooks.mjs +3 -1
- package/hooks/hooks.json +11 -0
- package/lib/flow-gate.mjs +28 -7
- package/package.json +1 -1
- package/schemas/flow-ledger.schema.json +4 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-orchestrator",
|
|
3
3
|
"description": "Write /task once — it plans the work, shards it across parallel subagents, gates every phase and verifies before claiming done. Claude Code, Codex, OpenCode, Kilo.",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.4",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bogdan-Gabriel Torcescu",
|
|
7
7
|
"url": "https://www.linkedin.com/in/bogdantorcescu/"
|
package/README.md
CHANGED
|
@@ -367,8 +367,10 @@ more instructions:
|
|
|
367
367
|
- Trivial is the narrow exception — a one-line change such as a typo or a version bump. A run
|
|
368
368
|
declared trivial that then edits a second file or touches tests gets one reminder to reopen it as
|
|
369
369
|
a typed run, and counts as `trivial_overreach` in the audit. Trivial runs close at the end of the
|
|
370
|
-
turn (`Stop` hook), so single-prompt sessions still reach the history.
|
|
371
|
-
|
|
370
|
+
turn (`Stop` hook), so single-prompt sessions still reach the history. Typed runs stay open across
|
|
371
|
+
turns and close when the session ends (`SessionEnd`), so an unclosed run is never lost.
|
|
372
|
+
- Subagents are counted once each: resuming one (SendMessage) is not a new dispatch.
|
|
373
|
+
- A run opened with two or more shards whose main thread keeps doing the work — two work calls per planned shard, no
|
|
372
374
|
subagent started — gets one more sentence: dispatch the independent shards (searching for the
|
|
373
375
|
Agent tool if it is deferred), or declare the chain inline with
|
|
374
376
|
`run start --type <T> --shards <n> --inline "stateful:<what>"`. Work is inline only while it holds
|
package/adapters/hooks.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { relative, isAbsolute } from 'node:path';
|
|
|
11
11
|
/** Marks the hook entries this package owns inside a user's hooks JSON. */
|
|
12
12
|
export const FLOW_MARKER = 'orchestrate-core:flow';
|
|
13
13
|
|
|
14
|
-
export const FLOW_EVENTS = ['UserPromptSubmit', 'PreToolUse', 'SubagentStart', 'Stop'];
|
|
14
|
+
export const FLOW_EVENTS = ['UserPromptSubmit', 'PreToolUse', 'SubagentStart', 'Stop', 'SessionEnd'];
|
|
15
15
|
|
|
16
16
|
/** Spell the runtime path through $HOME when it lives there, so committed settings stay portable. */
|
|
17
17
|
export function runtimeCliPath(runtimeRoot) {
|
|
@@ -166,6 +166,8 @@ export const OrchestrateFlow = async ({ directory }) => {
|
|
|
166
166
|
}
|
|
167
167
|
const idle = event?.properties?.sessionID;
|
|
168
168
|
if (event?.type === "session.idle" && idle && !parentOf.has(idle)) gate({ hook_event_name: "Stop", session_id: idle });
|
|
169
|
+
const ended = info?.id;
|
|
170
|
+
if (event?.type === "session.deleted" && ended && !parentOf.has(ended)) gate({ hook_event_name: "SessionEnd", session_id: ended });
|
|
169
171
|
} catch {}
|
|
170
172
|
},
|
|
171
173
|
"chat.message": async (input) => {
|
package/hooks/hooks.json
CHANGED
|
@@ -44,6 +44,17 @@
|
|
|
44
44
|
}
|
|
45
45
|
]
|
|
46
46
|
}
|
|
47
|
+
],
|
|
48
|
+
"SessionEnd": [
|
|
49
|
+
{
|
|
50
|
+
"hooks": [
|
|
51
|
+
{
|
|
52
|
+
"type": "command",
|
|
53
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/bin/llm-orchestrator.mjs\" gate 2>/dev/null || true # orchestrate-core:flow",
|
|
54
|
+
"timeout": 3
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
47
58
|
]
|
|
48
59
|
}
|
|
49
60
|
}
|
package/lib/flow-gate.mjs
CHANGED
|
@@ -58,8 +58,9 @@ function pathHash(path) {
|
|
|
58
58
|
return createHash('sha256').update(path).digest('hex').slice(0, 12);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// Main-thread work calls tolerated
|
|
62
|
-
|
|
61
|
+
// Main-thread work calls tolerated per planned shard before the dispatch nudge: a
|
|
62
|
+
// two-shard task that is finished inline in three calls never reached a fixed six.
|
|
63
|
+
export const DISPATCH_CALLS_PER_SHARD = 2;
|
|
63
64
|
|
|
64
65
|
// Evidence-gathering flows: their first phase fans out across independent sources.
|
|
65
66
|
const EVIDENCE_TYPES = new Set(['INCIDENT', 'INVESTIGATION', 'RESEARCH']);
|
|
@@ -188,8 +189,9 @@ export function normalizePayload(raw) {
|
|
|
188
189
|
// plugin and a CLI install; the harness's own event id lets the second be ignored.
|
|
189
190
|
const id = (value) => (typeof value === 'string' && value ? value : null);
|
|
190
191
|
if (eventName === 'UserPromptSubmit') return { kind: 'prompt', session, isSubagent, key: keyOf(eventName, id(payload.prompt_id) ?? id(payload.turn_id)) };
|
|
191
|
-
if (eventName === 'SubagentStart') return { kind: 'subagent', session, isSubagent, key: keyOf(eventName, id(payload.agent_id)) };
|
|
192
|
+
if (eventName === 'SubagentStart') return { kind: 'subagent', session, isSubagent, key: keyOf(eventName, id(payload.agent_id)), agent: id(payload.agent_id) };
|
|
192
193
|
if (eventName === 'Stop') return { kind: 'stop', session, isSubagent };
|
|
194
|
+
if (eventName === 'SessionEnd') return { kind: 'session_end', session, isSubagent };
|
|
193
195
|
if (eventName !== 'PreToolUse') return { kind: 'other', session, isSubagent };
|
|
194
196
|
|
|
195
197
|
const toolName = String(payload.tool_name ?? '');
|
|
@@ -283,6 +285,15 @@ export function decide(previous, event, now) {
|
|
|
283
285
|
return { session, output, history };
|
|
284
286
|
}
|
|
285
287
|
|
|
288
|
+
if (event.kind === 'session_end') {
|
|
289
|
+
// Typed runs outlive turns but not the session: close them so they reach history.
|
|
290
|
+
if (session.run) {
|
|
291
|
+
history.push(closeRun(session, now, 'session_end'));
|
|
292
|
+
session.run = null;
|
|
293
|
+
}
|
|
294
|
+
return { session, output, history };
|
|
295
|
+
}
|
|
296
|
+
|
|
286
297
|
if (event.kind === 'stop') {
|
|
287
298
|
// A trivial run lasts one turn; closing it here keeps single-prompt sessions in history.
|
|
288
299
|
if (session.run?.trivial && !event.isSubagent) {
|
|
@@ -293,8 +304,16 @@ export function decide(previous, event, now) {
|
|
|
293
304
|
}
|
|
294
305
|
|
|
295
306
|
if (event.kind === 'subagent') {
|
|
296
|
-
if (session.run)
|
|
297
|
-
|
|
307
|
+
if (session.run) {
|
|
308
|
+
// Count distinct agents: a resumed subagent fires SubagentStart again, possibly
|
|
309
|
+
// long after its first start has left the seen-window.
|
|
310
|
+
const ids = Array.isArray(session.run.subagent_ids) ? session.run.subagent_ids : [];
|
|
311
|
+
const agentHash = event.agent ? pathHash(event.agent) : null;
|
|
312
|
+
if (!agentHash || !ids.includes(agentHash)) {
|
|
313
|
+
session.run.subagents_started += 1;
|
|
314
|
+
if (agentHash) session.run.subagent_ids = [...ids, agentHash];
|
|
315
|
+
}
|
|
316
|
+
} else session.subagents_without_run += 1;
|
|
298
317
|
return { session, output, history };
|
|
299
318
|
}
|
|
300
319
|
|
|
@@ -317,6 +336,7 @@ export function decide(previous, event, now) {
|
|
|
317
336
|
edited_files: [],
|
|
318
337
|
touched_tests: false,
|
|
319
338
|
overreach_nudged: false,
|
|
339
|
+
subagent_ids: [],
|
|
320
340
|
};
|
|
321
341
|
// The work already done is accounted for on the run itself now.
|
|
322
342
|
session.worked_without_run = false;
|
|
@@ -345,7 +365,7 @@ export function decide(previous, event, now) {
|
|
|
345
365
|
return { session, output, history };
|
|
346
366
|
}
|
|
347
367
|
if ((current.planned_shards ?? 0) >= 2 && current.subagents_started === 0 && !current.inline_reason
|
|
348
|
-
&& !current.dispatch_nudged && current.main_work_calls >=
|
|
368
|
+
&& !current.dispatch_nudged && current.main_work_calls >= DISPATCH_CALLS_PER_SHARD * current.planned_shards) {
|
|
349
369
|
current.dispatch_nudged = true;
|
|
350
370
|
output = { additionalContext: dispatchNudgeFor(undefined, current.planned_shards), kind: 'dispatch', planned: current.planned_shards };
|
|
351
371
|
}
|
|
@@ -533,6 +553,7 @@ export function adherenceSummary(lines) {
|
|
|
533
553
|
runs_without_plan: lines.filter((line) => !line.skipped_flow && !line.trivial && line.planned_shards === null).length,
|
|
534
554
|
inline_declared: lines.filter((line) => Boolean(line.inline_reason)).length,
|
|
535
555
|
trivial_overreach: lines.filter((line) => line.trivial && line.overreach).length,
|
|
536
|
-
below_fan_out: lines.filter((line) => EVIDENCE_TYPES.has(line.task_type) && !line.inline_reason
|
|
556
|
+
below_fan_out: lines.filter((line) => EVIDENCE_TYPES.has(line.task_type) && !line.inline_reason
|
|
557
|
+
&& (line.planned_shards ?? 0) < 2 && (line.subagents_started ?? 0) < 2).length,
|
|
537
558
|
};
|
|
538
559
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-orchestrator",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "Write /task once — it plans the work, shards it across parallel subagents, gates every phase and verifies before claiming done. Claude Code, Codex, OpenCode, Kilo.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"run": {
|
|
9
9
|
"type": "object",
|
|
10
10
|
"additionalProperties": false,
|
|
11
|
-
"required": ["task_id", "task_type", "trivial", "reason", "opened_at", "planned_shards", "subagents_started", "started_outside_flow", "inline_reason", "main_work_calls", "dispatch_nudged", "edited_files", "touched_tests", "overreach_nudged"],
|
|
11
|
+
"required": ["task_id", "task_type", "trivial", "reason", "opened_at", "planned_shards", "subagents_started", "started_outside_flow", "inline_reason", "main_work_calls", "dispatch_nudged", "edited_files", "touched_tests", "overreach_nudged", "subagent_ids"],
|
|
12
12
|
"properties": {
|
|
13
13
|
"task_id": { "type": "string", "minLength": 1 },
|
|
14
14
|
"task_type": { "type": ["string", "null"], "enum": ["INCIDENT", "FEATURE", "BUG_FIX", "REFACTOR", "INVESTIGATION", "DEPLOY", "CONFIG", "REVIEW", "RESEARCH", null] },
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"dispatch_nudged": { "type": "boolean" },
|
|
24
24
|
"edited_files": { "type": "array", "items": { "type": "string", "pattern": "^[0-9a-f]{12}$" }, "description": "Short hashes of edited paths — never the paths themselves." },
|
|
25
25
|
"touched_tests": { "type": "boolean" },
|
|
26
|
-
"overreach_nudged": { "type": "boolean" }
|
|
26
|
+
"overreach_nudged": { "type": "boolean" },
|
|
27
|
+
"subagent_ids": { "type": "array", "items": { "type": "string", "pattern": "^[0-9a-f]{12}$" }, "description": "Short hashes of the distinct subagents started under this run." }
|
|
27
28
|
}
|
|
28
29
|
},
|
|
29
30
|
"session": {
|
|
@@ -62,7 +63,7 @@
|
|
|
62
63
|
"inline_reason": { "type": ["string", "null"] },
|
|
63
64
|
"dispatch_nudged": { "type": "boolean" },
|
|
64
65
|
"overreach": { "type": "boolean", "description": "A trivial run that edited two or more files or its tests." },
|
|
65
|
-
"closed_by": { "enum": ["run_close", "next_prompt", "succession", "turn_end"] }
|
|
66
|
+
"closed_by": { "enum": ["run_close", "next_prompt", "succession", "turn_end", "session_end"] }
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
}
|