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.
@@ -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.3",
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
- - A run opened with two or more shards whose main thread keeps doing the work — six work calls, no
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
@@ -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 after run start before the dispatch nudge.
62
- export const DISPATCH_THRESHOLD = 6;
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) session.run.subagents_started += 1;
297
- else session.subagents_without_run += 1;
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 >= DISPATCH_THRESHOLD) {
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 && (line.planned_shards ?? 0) < 2).length,
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",
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
  }