llm-orchestrator 1.2.5 → 1.2.6

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.5",
4
+ "version": "1.2.6",
5
5
  "author": {
6
6
  "name": "Bogdan-Gabriel Torcescu",
7
7
  "url": "https://www.linkedin.com/in/bogdantorcescu/"
package/README.md CHANGED
@@ -375,13 +375,14 @@ more instructions:
375
375
  reports `role_dispatches`, `generic_dispatches` and `runs_without_roles`. The Claude Code plugin
376
376
  ships the roles as agents (`llm-orchestrator:<role>`), so they are available without a project
377
377
  install.
378
- - A run opened with two or more shards whose main thread keeps doing the work — two work calls per planned shard, no
378
+ - A run opened with two or more shards whose main thread keeps doing the work — two work calls per planned shard (one per shard in incident, investigation and research flows, whose reads *are* the shards, with one firmer follow-up if the first reminder is ignored), no
379
379
  subagent started — gets one more sentence: dispatch the independent shards (searching for the
380
380
  Agent tool if it is deferred), or declare the chain inline with
381
381
  `run start --type <T> --shards <n> --inline "stateful:<what>"`. Work is inline only while it holds
382
382
  live state a subagent cannot inherit (a browser mid-flow, an interactive shell); independent reads
383
- are never inline. The audit adds `inline_declared` and `below_fan_out` (incident, investigation or
384
- research runs opened with fewer than two shards).
383
+ are never inline. The audit adds `inline_declared`, `inline_after_nudge` (an `--inline` declared only
384
+ after a dispatch reminder — a retroactive justification) and `below_fan_out` (incident, investigation
385
+ or research runs opened with fewer than two shards).
385
386
  - Once the entrypoint is loaded, read-only discovery (reading files, `grep`, `git status`, tool
386
387
  version checks) before `run start` is SKILL.md steps 2–3, not a deviation. An edit, a write, a
387
388
  dispatch or any other shell command before the run is.
package/lib/flow-gate.mjs CHANGED
@@ -35,7 +35,7 @@ export const NUDGE = nudgeFor();
35
35
  * none went to a subagent, and the main thread keeps doing the work itself.
36
36
  */
37
37
  export function dispatchNudgeFor(cli = 'llm-orchestrator', planned = 2, taskType = null) {
38
- const roles = (FLOW_ROLES[taskType] ?? []).slice(0, 4);
38
+ const roles = (EVIDENCE_TYPES.has(taskType) ? EVIDENCE_ROLES[taskType] : FLOW_ROLES[taskType] ?? []).slice(0, 4);
39
39
  const to = roles.length > 0
40
40
  ? ` to this flow's roles (${roles.join(', ')}), not a general-purpose agent`
41
41
  : ' to the orchestrator\'s roles, not a general-purpose agent';
@@ -67,6 +67,18 @@ function pathHash(path) {
67
67
  // Main-thread work calls tolerated per planned shard before the dispatch nudge: a
68
68
  // two-shard task that is finished inline in three calls never reached a fixed six.
69
69
  export const DISPATCH_CALLS_PER_SHARD = 2;
70
+ // Evidence flows: the reads are the shards, so waiting two calls per shard lets the
71
+ // main thread read everything before the reminder arrives. One call per shard there,
72
+ // and one firmer follow-up if the first reminder is ignored.
73
+ export const EVIDENCE_CALLS_PER_SHARD = 1;
74
+ const MAX_DISPATCH_NUDGES = 2;
75
+
76
+ /** The follow-up for an evidence flow whose first dispatch reminder was ignored. */
77
+ export function dispatchFollowupFor(cli = 'llm-orchestrator', planned = 2, taskType = null) {
78
+ const roles = (EVIDENCE_ROLES[taskType] ?? []).slice(0, 3);
79
+ const who = roles.length > 0 ? ` (${roles.join(', ')})` : '';
80
+ return `The dispatch reminder was not acted on, and the main thread is still gathering evidence itself. In an evidence flow those reads are the ${planned} planned shards — independent, W-tier, meant for subagents${who}. Dispatch the remaining sources now, or declare why they must stay inline (\`${cli} run start --type <TYPE> --inline "stateful:<what state>"\`).`;
81
+ }
70
82
 
71
83
  // Evidence-gathering flows: their first phase fans out across independent sources.
72
84
  const EVIDENCE_TYPES = new Set(['INCIDENT', 'INVESTIGATION', 'RESEARCH']);
@@ -85,8 +97,12 @@ const ROLE_IDS = new Set([
85
97
  ...(readRegistry('registries/agent-roles.json')?.roles ?? []).map((role) => role.id),
86
98
  ...(readRegistry('registries/preferred-tools.json')?.tools ?? []).filter((tool) => tool.kind === 'agent_role').map((tool) => tool.id),
87
99
  ]);
88
- const FLOW_ROLES = Object.fromEntries(Object.entries(readRegistry('registries/routing-matrix.json')?.task_flows ?? {})
100
+ const TASK_FLOWS = readRegistry('registries/routing-matrix.json')?.task_flows ?? {};
101
+ const FLOW_ROLES = Object.fromEntries(Object.entries(TASK_FLOWS)
89
102
  .map(([type, flow]) => [type, [...new Set(flow.phases.flatMap((phase) => phase.roles ?? []))].filter((role) => role !== 'orchestrator')]));
103
+ // The evidence phase of each flow: who gathers, as opposed to who fixes later.
104
+ const EVIDENCE_ROLES = Object.fromEntries(Object.entries(TASK_FLOWS)
105
+ .map(([type, flow]) => [type, (flow.phases[0]?.roles ?? []).filter((role) => role !== 'orchestrator')]));
90
106
 
91
107
  /** 'role' for an orchestrator role (plugin-namespaced ids too), 'generic' otherwise, null when unknown. */
92
108
  export function roleKind(agentType) {
@@ -267,6 +283,7 @@ function historyLine(session, fields, now) {
267
283
  inline_reason: fields.inline_reason ?? null,
268
284
  dispatch_nudged: Boolean(fields.dispatch_nudged),
269
285
  overreach: Boolean(fields.overreach_nudged),
286
+ inline_after_nudge: Boolean(fields.inline_after_nudge),
270
287
  role_dispatches: fields.role_dispatches ?? 0,
271
288
  generic_dispatches: fields.generic_dispatches ?? 0,
272
289
  closed_by: fields.closed_by,
@@ -369,6 +386,8 @@ export function decide(previous, event, now) {
369
386
  subagent_ids: [],
370
387
  role_dispatches: 0,
371
388
  generic_dispatches: 0,
389
+ dispatch_nudges: 0,
390
+ inline_after_nudge: Boolean(event.run.inline && session.run?.dispatch_nudged),
372
391
  };
373
392
  // The work already done is accounted for on the run itself now.
374
393
  session.worked_without_run = false;
@@ -396,10 +415,17 @@ export function decide(previous, event, now) {
396
415
  }
397
416
  return { session, output, history };
398
417
  }
418
+ const evidence = EVIDENCE_TYPES.has(current.task_type);
419
+ const step = (evidence ? EVIDENCE_CALLS_PER_SHARD : DISPATCH_CALLS_PER_SHARD) * (current.planned_shards ?? 0);
420
+ const nudges = current.dispatch_nudges ?? (current.dispatch_nudged ? 1 : 0);
421
+ const allowed = evidence ? MAX_DISPATCH_NUDGES : 1;
399
422
  if ((current.planned_shards ?? 0) >= 2 && current.subagents_started === 0 && !current.inline_reason
400
- && !current.dispatch_nudged && current.main_work_calls >= DISPATCH_CALLS_PER_SHARD * current.planned_shards) {
423
+ && nudges < allowed && current.main_work_calls >= step * (nudges + 1)) {
401
424
  current.dispatch_nudged = true;
402
- output = { additionalContext: dispatchNudgeFor(undefined, current.planned_shards, current.task_type), kind: 'dispatch', planned: current.planned_shards, taskType: current.task_type };
425
+ current.dispatch_nudges = nudges + 1;
426
+ output = nudges === 0
427
+ ? { additionalContext: dispatchNudgeFor(undefined, current.planned_shards, current.task_type), kind: 'dispatch', planned: current.planned_shards, taskType: current.task_type }
428
+ : { additionalContext: dispatchFollowupFor(undefined, current.planned_shards, current.task_type), kind: 'dispatch_followup', planned: current.planned_shards, taskType: current.task_type };
403
429
  }
404
430
  return { session, output, history };
405
431
  }
@@ -538,6 +564,7 @@ export async function handleHook({ payload, project, now = Date.now(), cli = 'll
538
564
  if (!result.output) return null;
539
565
  // decide() speaks in the default CLI spelling; the hook swaps in the runnable path.
540
566
  const text = result.output.kind === 'dispatch' ? dispatchNudgeFor(cli, result.output.planned, result.output.taskType)
567
+ : result.output.kind === 'dispatch_followup' ? dispatchFollowupFor(cli, result.output.planned, result.output.taskType)
541
568
  : result.output.kind === 'overreach' ? overreachNudgeFor(cli, result.output.files)
542
569
  : nudgeFor(cli);
543
570
  return { hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: text } };
@@ -584,6 +611,7 @@ export function adherenceSummary(lines) {
584
611
  planned_but_not_dispatched: lines.filter((line) => (line.planned_shards ?? 0) > 1 && line.subagents_started === 0).length,
585
612
  runs_without_plan: lines.filter((line) => !line.skipped_flow && !line.trivial && line.planned_shards === null).length,
586
613
  inline_declared: lines.filter((line) => Boolean(line.inline_reason)).length,
614
+ inline_after_nudge: lines.filter((line) => line.inline_after_nudge).length,
587
615
  trivial_overreach: lines.filter((line) => line.trivial && line.overreach).length,
588
616
  role_dispatches: lines.reduce((sum, line) => sum + (line.role_dispatches ?? 0), 0),
589
617
  generic_dispatches: lines.reduce((sum, line) => sum + (line.generic_dispatches ?? 0), 0),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-orchestrator",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
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", "subagent_ids", "role_dispatches", "generic_dispatches"],
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", "role_dispatches", "generic_dispatches", "dispatch_nudges", "inline_after_nudge"],
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] },
@@ -26,7 +26,9 @@
26
26
  "overreach_nudged": { "type": "boolean" },
27
27
  "subagent_ids": { "type": "array", "items": { "type": "string", "pattern": "^[0-9a-f]{12}$" }, "description": "Short hashes of the distinct subagents started under this run." },
28
28
  "role_dispatches": { "type": "integer", "minimum": 0, "description": "Subagents started as one of the orchestrator's roles." },
29
- "generic_dispatches": { "type": "integer", "minimum": 0, "description": "Subagents started as a generic agent type (general-purpose, Explore, …)." }
29
+ "generic_dispatches": { "type": "integer", "minimum": 0, "description": "Subagents started as a generic agent type (general-purpose, Explore, …)." },
30
+ "dispatch_nudges": { "type": "integer", "minimum": 0, "maximum": 2, "description": "Dispatch reminders sent: one, or two for evidence flows whose first was ignored." },
31
+ "inline_after_nudge": { "type": "boolean", "description": "This run replaced one that had already received a dispatch reminder, and declared --inline: a retroactive justification." }
30
32
  }
31
33
  },
32
34
  "session": {
@@ -48,7 +50,7 @@
48
50
  "historyLine": {
49
51
  "type": "object",
50
52
  "additionalProperties": false,
51
- "required": ["session", "task_id", "task_type", "trivial", "reason", "opened_at", "closed_at", "duration_s", "planned_shards", "subagents_started", "started_outside_flow", "skipped_flow", "inline_reason", "dispatch_nudged", "overreach", "role_dispatches", "generic_dispatches", "closed_by"],
53
+ "required": ["session", "task_id", "task_type", "trivial", "reason", "opened_at", "closed_at", "duration_s", "planned_shards", "subagents_started", "started_outside_flow", "skipped_flow", "inline_reason", "dispatch_nudged", "overreach", "inline_after_nudge", "role_dispatches", "generic_dispatches", "closed_by"],
52
54
  "properties": {
53
55
  "session": { "type": "string", "minLength": 1 },
54
56
  "task_id": { "type": ["string", "null"] },
@@ -65,6 +67,7 @@
65
67
  "inline_reason": { "type": ["string", "null"] },
66
68
  "dispatch_nudged": { "type": "boolean" },
67
69
  "overreach": { "type": "boolean", "description": "A trivial run that edited two or more files or its tests." },
70
+ "inline_after_nudge": { "type": "boolean" },
68
71
  "role_dispatches": { "type": "integer", "minimum": 0 },
69
72
  "generic_dispatches": { "type": "integer", "minimum": 0 },
70
73
  "closed_by": { "enum": ["run_close", "next_prompt", "succession", "turn_end", "session_end"] }