llm-orchestrator 1.2.3 → 1.2.5

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.5",
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,15 @@ 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. Each is also
373
+ classified by its agent type — one of the orchestrator's roles, or a generic agent such as
374
+ `general-purpose` — and the dispatch reminder names the roles of the task's flow. The audit
375
+ reports `role_dispatches`, `generic_dispatches` and `runs_without_roles`. The Claude Code plugin
376
+ ships the roles as agents (`llm-orchestrator:<role>`), so they are available without a project
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
372
379
  subagent started — gets one more sentence: dispatch the independent shards (searching for the
373
380
  Agent tool if it is deferred), or declare the chain inline with
374
381
  `run start --type <T> --shards <n> --inline "stateful:<what>"`. Work is inline only while it holds
@@ -26,7 +26,7 @@ description: ${role.description}
26
26
  ---
27
27
  ${MD_MARKER}
28
28
 
29
- Mandatory — before acting, load and follow \`.agents/skills/orchestrate/SKILL.md\`.
29
+ Mandatory — before acting, load and follow the \`orchestrate-core\` skill (\`.agents/skills/orchestrate/SKILL.md\` in a project install). You work inside the parent's run: never open or close one.
30
30
  ${profileLine}
31
31
  Best for: ${role.best_for}
32
32
  Never bypass a mandatory capability without declaring the gap first.
@@ -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) => {
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: adversarial-skeptic
3
+ description: Independently challenges a conclusion, diagnosis, or diff before it ships.
4
+ ---
5
+ <!-- llm-orchestrator · created by Bogdan-Gabriel Torcescu · https://www.linkedin.com/in/bogdantorcescu/ · keep this credit when copying or deriving -->
6
+
7
+ Mandatory — before acting, load and follow the `orchestrate-core` skill (`.agents/skills/orchestrate/SKILL.md` in a project install). You work inside the parent's run: never open or close one.
8
+ Permission profile: RO — Read-only. Investigation, evidence collection, review, telemetry.
9
+ Best for: Money, auth, migration, and frozen-build-shaped review seats.
10
+ Never bypass a mandatory capability without declaring the gap first.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: backend-fixer
3
+ description: Implements bounded backend changes: a bug fix behind a validated hypothesis, or a feature/config change behind a failing test.
4
+ ---
5
+ <!-- llm-orchestrator · created by Bogdan-Gabriel Torcescu · https://www.linkedin.com/in/bogdantorcescu/ · keep this credit when copying or deriving -->
6
+
7
+ Mandatory — before acting, load and follow the `orchestrate-core` skill (`.agents/skills/orchestrate/SKILL.md` in a project install). You work inside the parent's run: never open or close one.
8
+ Permission profile: RW — Read-write within an assigned bounded shard.
9
+ Best for: Backend implementation shards — bug fixes with a validated hypothesis, and the build phase of a feature or config flow.
10
+ Never bypass a mandatory capability without declaring the gap first.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: code-reviewer
3
+ description: Performs the review pass at the task's review risk floor.
4
+ ---
5
+ <!-- llm-orchestrator · created by Bogdan-Gabriel Torcescu · https://www.linkedin.com/in/bogdantorcescu/ · keep this credit when copying or deriving -->
6
+
7
+ Mandatory — before acting, load and follow the `orchestrate-core` skill (`.agents/skills/orchestrate/SKILL.md` in a project install). You work inside the parent's run: never open or close one.
8
+ Permission profile: RO — Read-only. Investigation, evidence collection, review, telemetry.
9
+ Best for: The review seat on every review-gated task.
10
+ Never bypass a mandatory capability without declaring the gap first.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: code-simplifier
3
+ description: Simplifies and clarifies recently changed code without changing behavior.
4
+ ---
5
+ <!-- llm-orchestrator · created by Bogdan-Gabriel Torcescu · https://www.linkedin.com/in/bogdantorcescu/ · keep this credit when copying or deriving -->
6
+
7
+ Mandatory — before acting, load and follow the `orchestrate-core` skill (`.agents/skills/orchestrate/SKILL.md` in a project install). You work inside the parent's run: never open or close one.
8
+ Permission profile: RW — Read-write within an assigned bounded shard.
9
+ Best for: Post-implementation cleanup passes.
10
+ Never bypass a mandatory capability without declaring the gap first.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: db-concurrency-specialist
3
+ description: Reviews transactional/locking correctness and concurrency-sensitive schema/code.
4
+ ---
5
+ <!-- llm-orchestrator · created by Bogdan-Gabriel Torcescu · https://www.linkedin.com/in/bogdantorcescu/ · keep this credit when copying or deriving -->
6
+
7
+ Mandatory — before acting, load and follow the `orchestrate-core` skill (`.agents/skills/orchestrate/SKILL.md` in a project install). You work inside the parent's run: never open or close one.
8
+ Permission profile: RO — Read-only. Investigation, evidence collection, review, telemetry.
9
+ Best for: Race conditions, stale claims, lock ordering, transactional boundaries.
10
+ Never bypass a mandatory capability without declaring the gap first.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: db-migration-author
3
+ description: Sole authority for authoring SQL schema migrations.
4
+ ---
5
+ <!-- llm-orchestrator · created by Bogdan-Gabriel Torcescu · https://www.linkedin.com/in/bogdantorcescu/ · keep this credit when copying or deriving -->
6
+
7
+ Mandatory — before acting, load and follow the `orchestrate-core` skill (`.agents/skills/orchestrate/SKILL.md` in a project install). You work inside the parent's run: never open or close one.
8
+ Permission profile: RW — Read-write within an assigned bounded shard.
9
+ Best for: Any new migration file; never hand-write one outside this role.
10
+ Never bypass a mandatory capability without declaring the gap first.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: explore
3
+ description: Read-only breadth search across a codebase: where something is defined, what calls it, which files are involved.
4
+ ---
5
+ <!-- llm-orchestrator · created by Bogdan-Gabriel Torcescu · https://www.linkedin.com/in/bogdantorcescu/ · keep this credit when copying or deriving -->
6
+
7
+ Mandatory — before acting, load and follow the `orchestrate-core` skill (`.agents/skills/orchestrate/SKILL.md` in a project install). You work inside the parent's run: never open or close one.
8
+ Permission profile: RO — Read-only. Investigation, evidence collection, review, telemetry.
9
+ Best for: Broad read-only location work before a decision; never edits.
10
+ Never bypass a mandatory capability without declaring the gap first.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: frontend-fixer
3
+ description: Implements bounded frontend changes: a bug fix behind a validated hypothesis, or a feature change behind a failing test.
4
+ ---
5
+ <!-- llm-orchestrator · created by Bogdan-Gabriel Torcescu · https://www.linkedin.com/in/bogdantorcescu/ · keep this credit when copying or deriving -->
6
+
7
+ Mandatory — before acting, load and follow the `orchestrate-core` skill (`.agents/skills/orchestrate/SKILL.md` in a project install). You work inside the parent's run: never open or close one.
8
+ Permission profile: RW — Read-write within an assigned bounded shard.
9
+ Best for: Frontend/UI implementation shards — bug fixes with a validated hypothesis, and the build phase of a feature flow.
10
+ Never bypass a mandatory capability without declaring the gap first.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: frontend-specialist
3
+ description: Implements frontend changes that touch shared state, realtime or a native bridge, respecting shipped-client compatibility.
4
+ ---
5
+ <!-- llm-orchestrator · created by Bogdan-Gabriel Torcescu · https://www.linkedin.com/in/bogdantorcescu/ · keep this credit when copying or deriving -->
6
+
7
+ Mandatory — before acting, load and follow the `orchestrate-core` skill (`.agents/skills/orchestrate/SKILL.md` in a project install). You work inside the parent's run: never open or close one.
8
+ Permission profile: RW — Read-write within an assigned bounded shard.
9
+ Best for: Complex frontend state, realtime surfaces and native-bridge implementation work.
10
+ Never bypass a mandatory capability without declaring the gap first.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: general
3
+ description: General-purpose bounded worker for tasks that fit no specialist role.
4
+ ---
5
+ <!-- llm-orchestrator · created by Bogdan-Gabriel Torcescu · https://www.linkedin.com/in/bogdantorcescu/ · keep this credit when copying or deriving -->
6
+
7
+ Mandatory — before acting, load and follow the `orchestrate-core` skill (`.agents/skills/orchestrate/SKILL.md` in a project install). You work inside the parent's run: never open or close one.
8
+ Permission profile: RW — Read-write within an assigned bounded shard.
9
+ Best for: Bounded work with no specialist owner; escalate rather than widen scope.
10
+ Never bypass a mandatory capability without declaring the gap first.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: orchestrator
3
+ description: Plans work, resolves capabilities, dispatches bounded shards, integrates results.
4
+ ---
5
+ <!-- llm-orchestrator · created by Bogdan-Gabriel Torcescu · https://www.linkedin.com/in/bogdantorcescu/ · keep this credit when copying or deriving -->
6
+
7
+ Mandatory — before acting, load and follow the `orchestrate-core` skill (`.agents/skills/orchestrate/SKILL.md` in a project install). You work inside the parent's run: never open or close one.
8
+ Permission profile: ORCHESTRATOR — Plans, dispatches, and integrates; owns the overall session state.
9
+ Best for: Any nontrivial task needing more than one shard or a risk-floor review seat.
10
+ Never bypass a mandatory capability without declaring the gap first.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: production-telemetry-collector
3
+ description: Collects production logs/metrics/traces before an incident hypothesis is formed.
4
+ ---
5
+ <!-- llm-orchestrator · created by Bogdan-Gabriel Torcescu · https://www.linkedin.com/in/bogdantorcescu/ · keep this credit when copying or deriving -->
6
+
7
+ Mandatory — before acting, load and follow the `orchestrate-core` skill (`.agents/skills/orchestrate/SKILL.md` in a project install). You work inside the parent's run: never open or close one.
8
+ Permission profile: RO — Read-only. Investigation, evidence collection, review, telemetry.
9
+ Best for: Incident evidence gathering; never forms a fix on its own.
10
+ Never bypass a mandatory capability without declaring the gap first.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: provider-webhook-specialist
3
+ description: Implements and reviews payment/webhook provider integrations (Stripe, Apple, Google).
4
+ ---
5
+ <!-- llm-orchestrator · created by Bogdan-Gabriel Torcescu · https://www.linkedin.com/in/bogdantorcescu/ · keep this credit when copying or deriving -->
6
+
7
+ Mandatory — before acting, load and follow the `orchestrate-core` skill (`.agents/skills/orchestrate/SKILL.md` in a project install). You work inside the parent's run: never open or close one.
8
+ Permission profile: RW — Read-write within an assigned bounded shard.
9
+ Best for: Webhook signature/idempotency, provider state reconciliation, refund delivery.
10
+ Never bypass a mandatory capability without declaring the gap first.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: route-data-flow-tracer
3
+ description: Traces a request/data path across layers (frontend, API, DB, provider) read-only.
4
+ ---
5
+ <!-- llm-orchestrator · created by Bogdan-Gabriel Torcescu · https://www.linkedin.com/in/bogdantorcescu/ · keep this credit when copying or deriving -->
6
+
7
+ Mandatory — before acting, load and follow the `orchestrate-core` skill (`.agents/skills/orchestrate/SKILL.md` in a project install). You work inside the parent's run: never open or close one.
8
+ Permission profile: RO — Read-only. Investigation, evidence collection, review, telemetry.
9
+ Best for: Symptoms that cross architectural layers.
10
+ Never bypass a mandatory capability without declaring the gap first.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: test-engineer
3
+ description: Writes and maintains behavioral/regression tests.
4
+ ---
5
+ <!-- llm-orchestrator · created by Bogdan-Gabriel Torcescu · https://www.linkedin.com/in/bogdantorcescu/ · keep this credit when copying or deriving -->
6
+
7
+ Mandatory — before acting, load and follow the `orchestrate-core` skill (`.agents/skills/orchestrate/SKILL.md` in a project install). You work inside the parent's run: never open or close one.
8
+ Permission profile: RW — Read-write within an assigned bounded shard.
9
+ Best for: Coverage gaps, regression tests for bug fixes, refactor safety nets.
10
+ Never bypass a mandatory capability without declaring the gap first.
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
@@ -15,7 +15,9 @@
15
15
  */
16
16
  import { createHash, randomUUID } from 'node:crypto';
17
17
  import { appendFile, mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
18
- import { join } from 'node:path';
18
+ import { readFileSync } from 'node:fs';
19
+ import { dirname, join } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
19
21
 
20
22
  /**
21
23
  * The one sentence the model gets. `cli` is how to invoke this package from the
@@ -32,8 +34,12 @@ export const NUDGE = nudgeFor();
32
34
  * The second, and last, sentence the model can get per run: the plan has shards,
33
35
  * none went to a subagent, and the main thread keeps doing the work itself.
34
36
  */
35
- export function dispatchNudgeFor(cli = 'llm-orchestrator', planned = 2) {
36
- return `This run planned ${planned} shards and none has been dispatched to a subagent; the main thread is doing the work itself. Dispatch the independent shards (Claude Code: the Agent tool — search for it if it is deferred; Codex: spawn_agent; OpenCode/Kilo: task), or declare why this must stay inline (\`${cli} run start --type <TYPE> --inline "stateful:<what state>"\`).`;
37
+ export function dispatchNudgeFor(cli = 'llm-orchestrator', planned = 2, taskType = null) {
38
+ const roles = (FLOW_ROLES[taskType] ?? []).slice(0, 4);
39
+ const to = roles.length > 0
40
+ ? ` to this flow's roles (${roles.join(', ')}), not a general-purpose agent`
41
+ : ' to the orchestrator\'s roles, not a general-purpose agent';
42
+ return `This run planned ${planned} shards and none has been dispatched to a subagent; the main thread is doing the work itself. Dispatch the independent shards${to} (Claude Code: the Agent tool — search for it if it is deferred; Codex: spawn_agent; OpenCode/Kilo: task), or declare why this must stay inline (\`${cli} run start --type <TYPE> --inline "stateful:<what state>"\`).`;
37
43
  }
38
44
 
39
45
  /** A run declared trivial has outgrown the declaration. */
@@ -58,8 +64,9 @@ function pathHash(path) {
58
64
  return createHash('sha256').update(path).digest('hex').slice(0, 12);
59
65
  }
60
66
 
61
- // Main-thread work calls tolerated after run start before the dispatch nudge.
62
- export const DISPATCH_THRESHOLD = 6;
67
+ // Main-thread work calls tolerated per planned shard before the dispatch nudge: a
68
+ // two-shard task that is finished inline in three calls never reached a fixed six.
69
+ export const DISPATCH_CALLS_PER_SHARD = 2;
63
70
 
64
71
  // Evidence-gathering flows: their first phase fans out across independent sources.
65
72
  const EVIDENCE_TYPES = new Set(['INCIDENT', 'INVESTIGATION', 'RESEARCH']);
@@ -68,6 +75,26 @@ export const TASK_TYPES = ['INCIDENT', 'FEATURE', 'BUG_FIX', 'REFACTOR', 'INVEST
68
75
 
69
76
  export const LEDGER_DIRECTORY = '.orchestrator-run';
70
77
 
78
+ // The orchestrator's own roles, and the roles each task flow dispatches to, read from
79
+ // the registries this runtime ships with. A missing registry only weakens the audit.
80
+ const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
81
+ function readRegistry(path) {
82
+ try { return JSON.parse(readFileSync(join(PACKAGE_ROOT, path), 'utf8')); } catch { return null; }
83
+ }
84
+ const ROLE_IDS = new Set([
85
+ ...(readRegistry('registries/agent-roles.json')?.roles ?? []).map((role) => role.id),
86
+ ...(readRegistry('registries/preferred-tools.json')?.tools ?? []).filter((tool) => tool.kind === 'agent_role').map((tool) => tool.id),
87
+ ]);
88
+ const FLOW_ROLES = Object.fromEntries(Object.entries(readRegistry('registries/routing-matrix.json')?.task_flows ?? {})
89
+ .map(([type, flow]) => [type, [...new Set(flow.phases.flatMap((phase) => phase.roles ?? []))].filter((role) => role !== 'orchestrator')]));
90
+
91
+ /** 'role' for an orchestrator role (plugin-namespaced ids too), 'generic' otherwise, null when unknown. */
92
+ export function roleKind(agentType) {
93
+ if (typeof agentType !== 'string' || !agentType) return null;
94
+ // `plugin:role` (Claude Code plugin agents) and `plugin-role` (preferred-tools ids) both resolve.
95
+ return ROLE_IDS.has(agentType.split(':').pop()) || ROLE_IDS.has(agentType.replaceAll(':', '-')) ? 'role' : 'generic';
96
+ }
97
+
71
98
  const MAX_REASON = 200;
72
99
 
73
100
  // Reading the orchestration instructions is the intended first step, so it must
@@ -188,8 +215,9 @@ export function normalizePayload(raw) {
188
215
  // plugin and a CLI install; the harness's own event id lets the second be ignored.
189
216
  const id = (value) => (typeof value === 'string' && value ? value : null);
190
217
  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)) };
218
+ if (eventName === 'SubagentStart') return { kind: 'subagent', session, isSubagent, key: keyOf(eventName, id(payload.agent_id)), agent: id(payload.agent_id), role: roleKind(payload.agent_type) };
192
219
  if (eventName === 'Stop') return { kind: 'stop', session, isSubagent };
220
+ if (eventName === 'SessionEnd') return { kind: 'session_end', session, isSubagent };
193
221
  if (eventName !== 'PreToolUse') return { kind: 'other', session, isSubagent };
194
222
 
195
223
  const toolName = String(payload.tool_name ?? '');
@@ -239,6 +267,8 @@ function historyLine(session, fields, now) {
239
267
  inline_reason: fields.inline_reason ?? null,
240
268
  dispatch_nudged: Boolean(fields.dispatch_nudged),
241
269
  overreach: Boolean(fields.overreach_nudged),
270
+ role_dispatches: fields.role_dispatches ?? 0,
271
+ generic_dispatches: fields.generic_dispatches ?? 0,
242
272
  closed_by: fields.closed_by,
243
273
  };
244
274
  }
@@ -283,6 +313,15 @@ export function decide(previous, event, now) {
283
313
  return { session, output, history };
284
314
  }
285
315
 
316
+ if (event.kind === 'session_end') {
317
+ // Typed runs outlive turns but not the session: close them so they reach history.
318
+ if (session.run) {
319
+ history.push(closeRun(session, now, 'session_end'));
320
+ session.run = null;
321
+ }
322
+ return { session, output, history };
323
+ }
324
+
286
325
  if (event.kind === 'stop') {
287
326
  // A trivial run lasts one turn; closing it here keeps single-prompt sessions in history.
288
327
  if (session.run?.trivial && !event.isSubagent) {
@@ -293,8 +332,18 @@ export function decide(previous, event, now) {
293
332
  }
294
333
 
295
334
  if (event.kind === 'subagent') {
296
- if (session.run) session.run.subagents_started += 1;
297
- else session.subagents_without_run += 1;
335
+ if (session.run) {
336
+ // Count distinct agents: a resumed subagent fires SubagentStart again, possibly
337
+ // long after its first start has left the seen-window.
338
+ const ids = Array.isArray(session.run.subagent_ids) ? session.run.subagent_ids : [];
339
+ const agentHash = event.agent ? pathHash(event.agent) : null;
340
+ if (!agentHash || !ids.includes(agentHash)) {
341
+ session.run.subagents_started += 1;
342
+ if (agentHash) session.run.subagent_ids = [...ids, agentHash];
343
+ if (event.role === 'role') session.run.role_dispatches = (session.run.role_dispatches ?? 0) + 1;
344
+ if (event.role === 'generic') session.run.generic_dispatches = (session.run.generic_dispatches ?? 0) + 1;
345
+ }
346
+ } else session.subagents_without_run += 1;
298
347
  return { session, output, history };
299
348
  }
300
349
 
@@ -317,6 +366,9 @@ export function decide(previous, event, now) {
317
366
  edited_files: [],
318
367
  touched_tests: false,
319
368
  overreach_nudged: false,
369
+ subagent_ids: [],
370
+ role_dispatches: 0,
371
+ generic_dispatches: 0,
320
372
  };
321
373
  // The work already done is accounted for on the run itself now.
322
374
  session.worked_without_run = false;
@@ -345,9 +397,9 @@ export function decide(previous, event, now) {
345
397
  return { session, output, history };
346
398
  }
347
399
  if ((current.planned_shards ?? 0) >= 2 && current.subagents_started === 0 && !current.inline_reason
348
- && !current.dispatch_nudged && current.main_work_calls >= DISPATCH_THRESHOLD) {
400
+ && !current.dispatch_nudged && current.main_work_calls >= DISPATCH_CALLS_PER_SHARD * current.planned_shards) {
349
401
  current.dispatch_nudged = true;
350
- output = { additionalContext: dispatchNudgeFor(undefined, current.planned_shards), kind: 'dispatch', planned: current.planned_shards };
402
+ output = { additionalContext: dispatchNudgeFor(undefined, current.planned_shards, current.task_type), kind: 'dispatch', planned: current.planned_shards, taskType: current.task_type };
351
403
  }
352
404
  return { session, output, history };
353
405
  }
@@ -485,7 +537,7 @@ export async function handleHook({ payload, project, now = Date.now(), cli = 'll
485
537
  });
486
538
  if (!result.output) return null;
487
539
  // decide() speaks in the default CLI spelling; the hook swaps in the runnable path.
488
- const text = result.output.kind === 'dispatch' ? dispatchNudgeFor(cli, result.output.planned)
540
+ const text = result.output.kind === 'dispatch' ? dispatchNudgeFor(cli, result.output.planned, result.output.taskType)
489
541
  : result.output.kind === 'overreach' ? overreachNudgeFor(cli, result.output.files)
490
542
  : nudgeFor(cli);
491
543
  return { hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: text } };
@@ -533,6 +585,10 @@ export function adherenceSummary(lines) {
533
585
  runs_without_plan: lines.filter((line) => !line.skipped_flow && !line.trivial && line.planned_shards === null).length,
534
586
  inline_declared: lines.filter((line) => Boolean(line.inline_reason)).length,
535
587
  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,
588
+ role_dispatches: lines.reduce((sum, line) => sum + (line.role_dispatches ?? 0), 0),
589
+ generic_dispatches: lines.reduce((sum, line) => sum + (line.generic_dispatches ?? 0), 0),
590
+ runs_without_roles: lines.filter((line) => (line.subagents_started ?? 0) > 0 && line.role_dispatches === 0 && (line.generic_dispatches ?? 0) > 0).length,
591
+ below_fan_out: lines.filter((line) => EVIDENCE_TYPES.has(line.task_type) && !line.inline_reason
592
+ && (line.planned_shards ?? 0) < 2 && (line.subagents_started ?? 0) < 2).length,
537
593
  };
538
594
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-orchestrator",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
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": {
@@ -48,6 +48,7 @@
48
48
  "NOTICE",
49
49
  "skills",
50
50
  "hooks",
51
+ "agents",
51
52
  ".claude-plugin"
52
53
  ],
53
54
  "scripts": {
@@ -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", "role_dispatches", "generic_dispatches"],
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,10 @@
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." },
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, …)." }
27
30
  }
28
31
  },
29
32
  "session": {
@@ -45,7 +48,7 @@
45
48
  "historyLine": {
46
49
  "type": "object",
47
50
  "additionalProperties": false,
48
- "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", "closed_by"],
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"],
49
52
  "properties": {
50
53
  "session": { "type": "string", "minLength": 1 },
51
54
  "task_id": { "type": ["string", "null"] },
@@ -62,7 +65,9 @@
62
65
  "inline_reason": { "type": ["string", "null"] },
63
66
  "dispatch_nudged": { "type": "boolean" },
64
67
  "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"] }
68
+ "role_dispatches": { "type": "integer", "minimum": 0 },
69
+ "generic_dispatches": { "type": "integer", "minimum": 0 },
70
+ "closed_by": { "enum": ["run_close", "next_prompt", "succession", "turn_end", "session_end"] }
66
71
  }
67
72
  }
68
73
  }