llm-orchestrator 1.2.6 → 1.2.8

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.6",
4
+ "version": "1.2.8",
5
5
  "author": {
6
6
  "name": "Bogdan-Gabriel Torcescu",
7
7
  "url": "https://www.linkedin.com/in/bogdantorcescu/"
package/README.md CHANGED
@@ -365,7 +365,8 @@ more instructions:
365
365
  it, runs opened without a PlanShard count, runs that planned several shards but started no
366
366
  subagents, and runs still open.
367
367
  - Trivial is the narrow exception — a one-line change such as a typo or a version bump. A run
368
- declared trivial that then edits a second file or touches tests gets one reminder to reopen it as
368
+ declared trivial that then edits a second file, touches tests, queries a live source twice, or keeps
369
+ working past eight calls (an investigation is not trivial) gets one reminder to reopen it as
369
370
  a typed run, and counts as `trivial_overreach` in the audit. Trivial runs close at the end of the
370
371
  turn (`Stop` hook), so single-prompt sessions still reach the history. Typed runs stay open across
371
372
  turns and close when the session ends (`SessionEnd`), so an unclosed run is never lost.
@@ -375,7 +376,7 @@ more instructions:
375
376
  reports `role_dispatches`, `generic_dispatches` and `runs_without_roles`. The Claude Code plugin
376
377
  ships the roles as agents (`llm-orchestrator:<role>`), so they are available without a project
377
378
  install.
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
+ - 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 and the evidence is live — `ssh`, remote databases, cluster/cloud CLIs, HTTP; a few small local files are fine read inline), no
379
380
  subagent started — gets one more sentence: dispatch the independent shards (searching for the
380
381
  Agent tool if it is deferred), or declare the chain inline with
381
382
  `run start --type <T> --shards <n> --inline "stateful:<what>"`. Work is inline only while it holds
package/bin/run.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
  * the project ledger; the command itself only validates and acknowledges, so it is
8
8
  * safe to call with or without the hooks installed.
9
9
  */
10
- import { parseRunArgs, TASK_TYPES } from '../lib/flow-gate.mjs';
10
+ import { INLINE_REASON, parseRunArgs, TASK_TYPES } from '../lib/flow-gate.mjs';
11
11
 
12
12
  const USAGE = `Usage: llm-orchestrator run start --type <${TASK_TYPES.join('|')}> [--shards N] [--inline "stateful:<what>"]
13
13
  llm-orchestrator run start --trivial "<reason>"
@@ -18,7 +18,11 @@ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
18
18
  process.stdout.write(`${USAGE}\n`);
19
19
  } else {
20
20
  const parsed = parseRunArgs(args);
21
- if (!parsed) {
21
+ const inlineAt = args.indexOf('--inline');
22
+ if (!parsed && inlineAt !== -1 && !INLINE_REASON.test(args[inlineAt + 1] ?? '')) {
23
+ process.stderr.write('--inline must name the live state a subagent cannot inherit, as "stateful:<what state>" (a browser session mid-flow, an interactive shell). Independent reads are never inline — dispatch them.\n');
24
+ process.exitCode = 1;
25
+ } else if (!parsed) {
22
26
  process.stderr.write(`${USAGE}\n`);
23
27
  process.exitCode = 1;
24
28
  } else {
package/lib/flow-gate.mjs CHANGED
@@ -47,6 +47,16 @@ export function overreachNudgeFor(cli = 'llm-orchestrator', files = 2) {
47
47
  return `This task was declared trivial, but it now touches ${files} files or its tests. Reopen it as a typed run (\`${cli} run start --type <TYPE> --shards <n>\`) so it is classified, planned and verified like one.`;
48
48
  }
49
49
 
50
+ /** A run declared trivial that has turned into an investigation. */
51
+ export function overreachWorkNudgeFor(cli = 'llm-orchestrator', calls = 9) {
52
+ return `This task was declared trivial, but it has now made ${calls} work calls — trivial is a one-line change, not an investigation. Reopen it as a typed run (\`${cli} run start --type <TYPE> --shards <n>\`) so it is classified, planned and verified like one.`;
53
+ }
54
+
55
+ /** A run declared trivial that is querying live sources. */
56
+ export function overreachLiveNudgeFor(cli = 'llm-orchestrator', live = 2) {
57
+ return `This task was declared trivial, but it has now queried live sources ${live} times — an investigation over production evidence is not trivial. Reopen it as a typed run (\`${cli} run start --type INCIDENT|INVESTIGATION --shards <n>\`), where independent live sources are shards for collectors.`;
58
+ }
59
+
50
60
  // Edit-shaped tools, per harness. Only a short hash of each path is kept.
51
61
  const EDIT_TOOLS = new Set(['edit', 'write', 'multiedit', 'notebookedit', 'str_replace_based_edit_tool', 'apply_patch', 'patch']);
52
62
  const TEST_PATH = /(^|\/)(test|tests|__tests__|spec|specs)\/|[._-](test|spec)\.[a-z0-9]+$|(^|\/)test_[^/]+\.py$|_spec\.rb$/i;
@@ -113,6 +123,20 @@ export function roleKind(agentType) {
113
123
 
114
124
  const MAX_REASON = 200;
115
125
 
126
+ export const INLINE_REASON = /^stateful:\s*\S/;
127
+
128
+ // A: a trivial run is a one-line change, not an investigation.
129
+ export const TRIVIAL_WORK_LIMIT = 8;
130
+ // Querying a live source more than once is an investigation, however few calls it takes.
131
+ export const TRIVIAL_LIVE_LIMIT = 2;
132
+
133
+ // C: sources whose reads cost real time and live remotely — the reads that pay to fan out.
134
+ const LIVE_SOURCE = /(^|[\s;&|(])(rtk\s+)?(ssh|scp|psql|mysql|mongosh|mongo|redis-cli|kubectl|docker\s+(exec|logs)|aws|gcloud|az|curl|wget|http|httpie)(\s|$)/;
135
+
136
+ export function isLiveSource(command) {
137
+ return typeof command === 'string' && LIVE_SOURCE.test(command);
138
+ }
139
+
116
140
  // Reading the orchestration instructions is the intended first step, so it must
117
141
  // never count as starting work without a run.
118
142
  const INSTRUCTION_PATH = /(^|\/)(SKILL|AGENTS|CLAUDE|protocol)\.md$|\/orchestrate-core\/|(^|\/)(policies|workflows|registries)\/[^/]+\.(md|json)$/;
@@ -208,10 +232,12 @@ export function parseRunArgs(args) {
208
232
  if (flag === '--type' && value) { type = value.toUpperCase(); index += 1; }
209
233
  else if (flag === '--shards' && value) { shards = Number.parseInt(value, 10); index += 1; }
210
234
  else if (flag === '--trivial' && value !== undefined) { trivial = value.slice(0, MAX_REASON); index += 1; }
211
- else if (flag === '--inline' && value) { inline = value.slice(0, MAX_REASON); index += 1; }
235
+ else if (flag === '--inline' && value !== undefined) { inline = value.slice(0, MAX_REASON); index += 1; }
212
236
  }
213
237
  if (trivial !== null) return { action: 'start', trivial: true, reason: trivial || null, type: null, shards: null, inline: null };
214
238
  if (!TASK_TYPES.includes(type)) return null;
239
+ // Inline is for live state a subagent cannot inherit; the reason has to name it.
240
+ if (inline !== null && !INLINE_REASON.test(inline)) return null;
215
241
  return { action: 'start', trivial: false, reason: null, type, shards: Number.isInteger(shards) && shards > 0 ? shards : null, inline };
216
242
  }
217
243
 
@@ -257,6 +283,7 @@ export function normalizePayload(raw) {
257
283
  nonWork: NON_WORK_TOOLS.has(toolName.toLowerCase()),
258
284
  loadsEntrypoint,
259
285
  readOnly,
286
+ live: isLiveSource(command),
260
287
  edits: editedPaths(toolName, input).map((editedPath) => ({ hash: pathHash(editedPath), test: TEST_PATH.test(editedPath) })),
261
288
  };
262
289
  }
@@ -284,6 +311,7 @@ function historyLine(session, fields, now) {
284
311
  dispatch_nudged: Boolean(fields.dispatch_nudged),
285
312
  overreach: Boolean(fields.overreach_nudged),
286
313
  inline_after_nudge: Boolean(fields.inline_after_nudge),
314
+ live_calls: fields.live_calls ?? 0,
287
315
  role_dispatches: fields.role_dispatches ?? 0,
288
316
  generic_dispatches: fields.generic_dispatches ?? 0,
289
317
  closed_by: fields.closed_by,
@@ -387,6 +415,7 @@ export function decide(previous, event, now) {
387
415
  role_dispatches: 0,
388
416
  generic_dispatches: 0,
389
417
  dispatch_nudges: 0,
418
+ live_calls: 0,
390
419
  inline_after_nudge: Boolean(event.run.inline && session.run?.dispatch_nudged),
391
420
  };
392
421
  // The work already done is accounted for on the run itself now.
@@ -406,19 +435,29 @@ export function decide(previous, event, now) {
406
435
  if (session.run) {
407
436
  const current = session.run;
408
437
  current.main_work_calls = (current.main_work_calls ?? 0) + 1;
438
+ // Live reads count on every run, trivial ones included — they are what fan-out is for.
439
+ if (event.live) current.live_calls = (current.live_calls ?? 0) + 1;
409
440
  if (current.trivial) {
410
441
  current.edited_files = [...new Set([...(current.edited_files ?? []), ...event.edits.map((entry) => entry.hash)])];
411
442
  current.touched_tests = Boolean(current.touched_tests) || event.edits.some((entry) => entry.test);
412
- if (!current.overreach_nudged && (current.edited_files.length >= 2 || current.touched_tests)) {
443
+ const grewByEdits = current.edited_files.length >= 2 || current.touched_tests;
444
+ const grewByWork = current.main_work_calls > TRIVIAL_WORK_LIMIT;
445
+ const grewByLive = (current.live_calls ?? 0) >= TRIVIAL_LIVE_LIMIT;
446
+ if (!current.overreach_nudged && (grewByEdits || grewByWork || grewByLive)) {
413
447
  current.overreach_nudged = true;
414
- output = { additionalContext: overreachNudgeFor(undefined, current.edited_files.length), kind: 'overreach', files: current.edited_files.length };
448
+ output = grewByEdits
449
+ ? { additionalContext: overreachNudgeFor(undefined, current.edited_files.length), kind: 'overreach', files: current.edited_files.length }
450
+ : grewByLive
451
+ ? { additionalContext: overreachLiveNudgeFor(undefined, current.live_calls), kind: 'overreach', live: current.live_calls }
452
+ : { additionalContext: overreachWorkNudgeFor(undefined, current.main_work_calls), kind: 'overreach', calls: current.main_work_calls };
415
453
  }
416
454
  return { session, output, history };
417
455
  }
418
456
  const evidence = EVIDENCE_TYPES.has(current.task_type);
419
457
  const step = (evidence ? EVIDENCE_CALLS_PER_SHARD : DISPATCH_CALLS_PER_SHARD) * (current.planned_shards ?? 0);
420
458
  const nudges = current.dispatch_nudges ?? (current.dispatch_nudged ? 1 : 0);
421
- const allowed = evidence ? MAX_DISPATCH_NUDGES : 1;
459
+ // The follow-up is for live evidence; a few small local files are fine read inline.
460
+ const allowed = evidence && (current.live_calls ?? 0) > 0 ? MAX_DISPATCH_NUDGES : 1;
422
461
  if ((current.planned_shards ?? 0) >= 2 && current.subagents_started === 0 && !current.inline_reason
423
462
  && nudges < allowed && current.main_work_calls >= step * (nudges + 1)) {
424
463
  current.dispatch_nudged = true;
@@ -565,7 +604,8 @@ export async function handleHook({ payload, project, now = Date.now(), cli = 'll
565
604
  // decide() speaks in the default CLI spelling; the hook swaps in the runnable path.
566
605
  const text = result.output.kind === 'dispatch' ? dispatchNudgeFor(cli, result.output.planned, result.output.taskType)
567
606
  : result.output.kind === 'dispatch_followup' ? dispatchFollowupFor(cli, result.output.planned, result.output.taskType)
568
- : result.output.kind === 'overreach' ? overreachNudgeFor(cli, result.output.files)
607
+ : result.output.kind === 'overreach' ? (result.output.live ? overreachLiveNudgeFor(cli, result.output.live)
608
+ : result.output.calls ? overreachWorkNudgeFor(cli, result.output.calls) : overreachNudgeFor(cli, result.output.files))
569
609
  : nudgeFor(cli);
570
610
  return { hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: text } };
571
611
  }
@@ -616,7 +656,7 @@ export function adherenceSummary(lines) {
616
656
  role_dispatches: lines.reduce((sum, line) => sum + (line.role_dispatches ?? 0), 0),
617
657
  generic_dispatches: lines.reduce((sum, line) => sum + (line.generic_dispatches ?? 0), 0),
618
658
  runs_without_roles: lines.filter((line) => (line.subagents_started ?? 0) > 0 && line.role_dispatches === 0 && (line.generic_dispatches ?? 0) > 0).length,
619
- below_fan_out: lines.filter((line) => EVIDENCE_TYPES.has(line.task_type) && !line.inline_reason
659
+ below_fan_out: lines.filter((line) => EVIDENCE_TYPES.has(line.task_type) && !line.inline_reason && (line.live_calls ?? 0) > 0
620
660
  && (line.planned_shards ?? 0) < 2 && (line.subagents_started ?? 0) < 2).length,
621
661
  };
622
662
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-orchestrator",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
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": {
@@ -294,12 +294,18 @@ Everything around that chain is still sharded:
294
294
  - Independent reads are never inline: log, metric and database queries, code search, config
295
295
  lookups. An SSH query that only reads is a stateless command, not a session — five of them are
296
296
  five W-tier evidence shards, not one inline chain.
297
+ - Size the fan-out to the evidence, not to the task type. **Live or large sources** — anything
298
+ behind `ssh`, a remote database, a cluster or cloud CLI, an HTTP API, or logs too long to read
299
+ whole — are where parallel W-tier collectors pay, and where the incident/investigation/research
300
+ minimums apply. A handful of small local files is read faster inline than dispatched; plan it as
301
+ one shard and do not pad the fan-out to look thorough.
297
302
  - "Cheaper inline" is not a reason. The main thread runs at the flow's highest tier; the same reads
298
303
  on a W-tier subagent cost less per token and keep the orchestrator's context for synthesis.
299
304
  - A tool that is not visible is not absent. On Claude Code the Agent tool can be deferred — search
300
305
  for it (`tool.discovery`) before concluding dispatch is unavailable.
301
306
  - Declare the inline chain when opening the run: `run start --type <T> --shards <n> --inline
302
- "stateful:<what state>"`. Undeclared, a planned multi-shard run whose main thread keeps working
307
+ "stateful:<what state>"`. The reason must name the live state; `run start` rejects any other
308
+ reason, and an `--inline` declared only after a dispatch reminder is recorded as retroactive. Undeclared, a planned multi-shard run whose main thread keeps working
303
309
  with no subagent started gets one reminder from the flow hooks, and the audit counts it.
304
310
  - Name the real seam you will split at, e.g. iOS simulator vs Android emulator: independent devices
305
311
  with independent state are parallel shards even when each one is inline inside.
@@ -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", "dispatch_nudges", "inline_after_nudge"],
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", "live_calls"],
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] },
@@ -18,7 +18,7 @@
18
18
  "planned_shards": { "type": ["integer", "null"], "minimum": 1 },
19
19
  "subagents_started": { "type": "integer", "minimum": 0 },
20
20
  "started_outside_flow": { "type": "boolean" },
21
- "inline_reason": { "type": ["string", "null"], "description": "Why the shards stay inline, e.g. stateful:browser — a live state no subagent can inherit." },
21
+ "inline_reason": { "type": ["string", "null"], "pattern": "^stateful:\\s*\\S", "description": "Why the shards stay inline, e.g. stateful:browser — a live state no subagent can inherit." },
22
22
  "main_work_calls": { "type": "integer", "minimum": 0 },
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." },
@@ -28,7 +28,8 @@
28
28
  "role_dispatches": { "type": "integer", "minimum": 0, "description": "Subagents started as one of the orchestrator's roles." },
29
29
  "generic_dispatches": { "type": "integer", "minimum": 0, "description": "Subagents started as a generic agent type (general-purpose, Explore, …)." },
30
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." }
31
+ "inline_after_nudge": { "type": "boolean", "description": "This run replaced one that had already received a dispatch reminder, and declared --inline: a retroactive justification." },
32
+ "live_calls": { "type": "integer", "minimum": 0, "description": "Main-thread calls that read a live source (ssh, remote databases, cluster/cloud CLIs, HTTP clients)." }
32
33
  }
33
34
  },
34
35
  "session": {
@@ -50,7 +51,7 @@
50
51
  "historyLine": {
51
52
  "type": "object",
52
53
  "additionalProperties": false,
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"],
54
+ "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", "live_calls", "role_dispatches", "generic_dispatches", "closed_by"],
54
55
  "properties": {
55
56
  "session": { "type": "string", "minLength": 1 },
56
57
  "task_id": { "type": ["string", "null"] },
@@ -68,6 +69,7 @@
68
69
  "dispatch_nudged": { "type": "boolean" },
69
70
  "overreach": { "type": "boolean", "description": "A trivial run that edited two or more files or its tests." },
70
71
  "inline_after_nudge": { "type": "boolean" },
72
+ "live_calls": { "type": "integer", "minimum": 0 },
71
73
  "role_dispatches": { "type": "integer", "minimum": 0 },
72
74
  "generic_dispatches": { "type": "integer", "minimum": 0 },
73
75
  "closed_by": { "enum": ["run_close", "next_prompt", "succession", "turn_end", "session_end"] }