@multiclawcli/core 0.4.0 → 0.5.1

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.
Files changed (47) hide show
  1. package/dist/agents/arch-design-reviewer.d.ts +4 -0
  2. package/dist/agents/architect.d.ts +4 -0
  3. package/dist/agents/backend-developer.d.ts +4 -0
  4. package/dist/agents/code-reviewer.d.ts +4 -0
  5. package/dist/agents/devops.d.ts +4 -0
  6. package/dist/agents/frontend-developer.d.ts +4 -0
  7. package/dist/agents/index.d.ts +26 -0
  8. package/dist/agents/leader.d.ts +4 -0
  9. package/dist/agents/product-manager.d.ts +4 -0
  10. package/dist/agents/reviewer.d.ts +4 -0
  11. package/dist/agents/tester.d.ts +4 -0
  12. package/dist/agents/ui-design-reviewer.d.ts +4 -0
  13. package/dist/agents/ui-designer.d.ts +4 -0
  14. package/dist/config/defineConfig.d.ts +2 -0
  15. package/dist/event/EventBus.d.ts +7 -0
  16. package/dist/index.d.ts +11 -720
  17. package/dist/index.js +463 -165
  18. package/dist/orchestrator/AgentRunner.d.ts +19 -0
  19. package/dist/orchestrator/ContextManager.d.ts +7 -0
  20. package/dist/orchestrator/Orchestrator.d.ts +109 -0
  21. package/dist/orchestrator/StageBuilder.d.ts +2 -0
  22. package/dist/providers/bridge.d.ts +29 -0
  23. package/dist/providers/catalog.d.ts +73 -0
  24. package/dist/providers/detect.d.ts +34 -0
  25. package/dist/providers/index.d.ts +4 -0
  26. package/dist/providers/resolve.d.ts +26 -0
  27. package/dist/runtime/AcpRuntime.d.ts +17 -0
  28. package/dist/runtime/RuntimeBase.d.ts +6 -0
  29. package/dist/runtime/index.d.ts +12 -0
  30. package/dist/types/agent.d.ts +96 -0
  31. package/dist/types/config.d.ts +126 -0
  32. package/dist/types/event.d.ts +115 -0
  33. package/dist/types/index.d.ts +5 -0
  34. package/dist/types/leader.d.ts +36 -0
  35. package/dist/types/runtime.d.ts +146 -0
  36. package/dist/utils/abort.d.ts +4 -0
  37. package/dist/utils/conversation.d.ts +29 -0
  38. package/dist/utils/errors.d.ts +16 -0
  39. package/dist/utils/json.d.ts +10 -0
  40. package/dist/utils/leaderDecision.d.ts +17 -0
  41. package/dist/utils/prompt.d.ts +1 -0
  42. package/dist/utils/resumePlan.d.ts +35 -0
  43. package/dist/utils/retry.d.ts +36 -0
  44. package/dist/utils/timeout.d.ts +1 -0
  45. package/dist/utils/toolSummary.d.ts +13 -0
  46. package/dist/utils/upstream.d.ts +69 -0
  47. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ var TASK = `## Architecture Design
16
16
 
17
17
  ## API Specification
18
18
  {{file:api-spec.md}}
19
+ {{upstreamOutputs}}
19
20
 
20
21
  ## Task
21
22
  Review the architecture design and API specification above. Output arch-review.md:
@@ -93,6 +94,7 @@ var TASK2 = `## Project
93
94
  ## Pipeline Agents
94
95
  The following agents will run in this pipeline (id: name):
95
96
  {{pipelineAgents}}
97
+ {{upstreamOutputs}}
96
98
 
97
99
  ## Task
98
100
  Analyze the requirement and create three files in the current directory:
@@ -112,6 +114,7 @@ function architect(overrides) {
112
114
  name: "Architect",
113
115
  icon: "\u25C6",
114
116
  description: "designs the architecture",
117
+ role: "planner",
115
118
  runtime: "acp",
116
119
  models: { acp: "opus" },
117
120
  modelTier: "strong",
@@ -141,6 +144,7 @@ var TASK3 = `## Task Scope
141
144
 
142
145
  ## Plan
143
146
  {{agentPlan}}
147
+ {{upstreamOutputs}}
144
148
 
145
149
  ---
146
150
 
@@ -200,6 +204,7 @@ var TASK4 = `## Project
200
204
 
201
205
  ## API Specification
202
206
  {{file:api-spec.md}}
207
+ {{upstreamOutputs}}
203
208
 
204
209
  ## Task
205
210
  Perform a thorough code review of all implementation in the current directory and output code-review.md:
@@ -252,6 +257,7 @@ var TASK5 = `## Task Scope
252
257
 
253
258
  ## Plan
254
259
  {{agentPlan}}
260
+ {{upstreamOutputs}}
255
261
 
256
262
  ---
257
263
 
@@ -302,6 +308,7 @@ var TASK6 = `## Task Scope
302
308
 
303
309
  ## Plan
304
310
  {{agentPlan}}
311
+ {{upstreamOutputs}}
305
312
 
306
313
  ---
307
314
 
@@ -356,7 +363,7 @@ The console hands you the turns that came before this one. A requirement is ofte
356
363
  Output ONLY a single JSON object on its own line \u2014 no prose, no markdown fences:
357
364
  {"mode": "reply", "message": "your answer"}
358
365
  {"mode": "ask", "message": "your question"}
359
- {"mode": "run", "run": ["agent-id-1", "agent-id-2"]}`;
366
+ {"mode": "run", "reason": "one sentence on why this set", "run": ["agent-id-1", "agent-id-2"]}`;
360
367
  var TASK7 = `## Project
361
368
  {{projectName}}
362
369
 
@@ -371,7 +378,7 @@ Read the codebase to understand the current state, then decide how to handle the
371
378
  Output a single JSON object \u2014 exactly one of:
372
379
  {"mode": "reply", "message": "your answer"}
373
380
  {"mode": "ask", "message": "your question"}
374
- {"mode": "run", "run": ["agent-id", ...]}
381
+ {"mode": "run", "reason": "one sentence on why this set", "run": ["agent-id", ...]}
375
382
  Agent IDs must come from the Available Agents list above.`;
376
383
  function leader(overrides) {
377
384
  return {
@@ -403,6 +410,7 @@ Deliverables (save to the working directory):
403
410
  Principles: Output the document directly without preamble. Mark uncertain items as OP- (open questions) rather than guessing. Do not do technical architecture design. Do not write feature code.`;
404
411
  var TASK8 = `## Business Context
405
412
  {{requirement}}
413
+ {{upstreamOutputs}}
406
414
 
407
415
  ## Task
408
416
  Based on the context above, create requirements.md in the current directory containing:
@@ -448,6 +456,7 @@ var TASK9 = `## Project
448
456
 
449
457
  ## Requirement
450
458
  {{requirement}}
459
+ {{upstreamOutputs}}
451
460
 
452
461
  ## Task
453
462
  Perform an independent code review of all implementation in the current directory and output review.md:
@@ -494,6 +503,7 @@ var TASK10 = `## Task Scope
494
503
 
495
504
  ## Plan
496
505
  {{agentPlan}}
506
+ {{upstreamOutputs}}
497
507
 
498
508
  ---
499
509
 
@@ -548,6 +558,7 @@ var TASK11 = `## Requirement
548
558
 
549
559
  ## UI Design Specification
550
560
  {{file:ui-design.md}}
561
+ {{upstreamOutputs}}
551
562
 
552
563
  ## Task
553
564
  Review the UI design specification above and output ui-review.md:
@@ -596,6 +607,7 @@ var TASK12 = `## Task Scope
596
607
 
597
608
  ## Plan
598
609
  {{agentPlan}}
610
+ {{upstreamOutputs}}
599
611
 
600
612
  ---
601
613
 
@@ -684,112 +696,6 @@ var EventBus = class {
684
696
  import * as fs4 from "fs";
685
697
  import * as path5 from "path";
686
698
 
687
- // src/utils/conversation.ts
688
- var MAX_CONVERSATION_TURNS = 6;
689
- var MAX_CONVERSATION_CHARS = 4e3;
690
- var MAX_ENTRY_CHARS = 600;
691
- var ELLIPSIS = "\u2026";
692
- function clip(text, max) {
693
- const trimmed = text.trim();
694
- if (trimmed.length <= max) return trimmed;
695
- return `${trimmed.slice(0, Math.max(0, max - 1)).trimEnd()}${ELLIPSIS}`;
696
- }
697
- function entry(turn, index) {
698
- const lines = [`${index}. user: ${clip(turn.requirement, MAX_ENTRY_CHARS)}`];
699
- if (turn.reply) {
700
- const verb = turn.reply.mode === "ask" ? "asked" : "answered";
701
- lines.push(` you ${verb}: ${clip(turn.reply.message, MAX_ENTRY_CHARS)}`);
702
- }
703
- return lines.join("\n");
704
- }
705
- function renderConversation(history) {
706
- if (!history || history.length === 0) return "";
707
- const recent = history.slice(-MAX_CONVERSATION_TURNS);
708
- const first = history.length - recent.length;
709
- const entries = recent.map((turn, i) => entry(turn, first + i + 1));
710
- while (entries.length > 1 && entries.join("\n").length > MAX_CONVERSATION_CHARS) {
711
- entries.shift();
712
- }
713
- const body = clip(entries.join("\n"), MAX_CONVERSATION_CHARS);
714
- return `
715
- ## Conversation so far
716
-
717
- Earlier turns of this session, oldest first:
718
-
719
- ${body}
720
- `;
721
- }
722
-
723
- // src/utils/json.ts
724
- function extractJsonObjects(text) {
725
- const objects = [];
726
- let searchFrom = 0;
727
- while (searchFrom < text.length) {
728
- const start = text.indexOf("{", searchFrom);
729
- if (start === -1) break;
730
- const end = findMatchingBrace(text, start);
731
- if (end !== -1) {
732
- try {
733
- objects.push(JSON.parse(text.slice(start, end + 1)));
734
- searchFrom = end + 1;
735
- continue;
736
- } catch {
737
- }
738
- }
739
- searchFrom = start + 1;
740
- }
741
- return objects;
742
- }
743
- function findMatchingBrace(text, start) {
744
- let depth = 0;
745
- let inString = false;
746
- let escaped = false;
747
- for (let i = start; i < text.length; i++) {
748
- const ch = text[i];
749
- if (inString) {
750
- if (escaped) escaped = false;
751
- else if (ch === "\\") escaped = true;
752
- else if (ch === '"') inString = false;
753
- continue;
754
- }
755
- if (ch === '"') inString = true;
756
- else if (ch === "{") depth++;
757
- else if (ch === "}") {
758
- depth--;
759
- if (depth === 0) return i;
760
- }
761
- }
762
- return -1;
763
- }
764
-
765
- // src/utils/leaderDecision.ts
766
- function parseLeaderDecision(text) {
767
- for (const value of extractJsonObjects(text)) {
768
- const decision = asDecision(value);
769
- if (decision) return decision;
770
- }
771
- return void 0;
772
- }
773
- function asDecision(value) {
774
- if (typeof value !== "object" || value === null) return void 0;
775
- const record = value;
776
- const mode = record.mode;
777
- if (mode === "reply" || mode === "ask") {
778
- const message = typeof record.message === "string" ? record.message.trim() : "";
779
- return message ? { mode, message } : void 0;
780
- }
781
- if (mode === "run" || mode === void 0) {
782
- const run = record.run;
783
- if (!Array.isArray(run) || !run.every((id) => typeof id === "string")) return void 0;
784
- return { mode: "run", run };
785
- }
786
- return void 0;
787
- }
788
-
789
- // src/orchestrator/AgentRunner.ts
790
- import * as fs3 from "fs";
791
- import * as path4 from "path";
792
-
793
699
  // src/providers/catalog.ts
794
700
  import * as path from "path";
795
701
  function normalizeCommandKey(command) {
@@ -933,7 +839,10 @@ function resolveCommand(agent, config) {
933
839
  return agent.command ?? config.defaults?.command ?? specForProvider(resolveProvider(agent, config))?.command ?? DEFAULT_COMMAND;
934
840
  }
935
841
  function resolveArgs(agent, config) {
936
- return agent.args ?? specForProvider(resolveProvider(agent, config))?.args;
842
+ if (agent.args) return agent.args;
843
+ const command = agent.command ?? config.defaults?.command;
844
+ if (command) return specForCommand(command)?.args;
845
+ return specForProvider(resolveProvider(agent, config))?.args;
937
846
  }
938
847
  function resolveModel(agent, config) {
939
848
  if (agent.model) return agent.model;
@@ -960,6 +869,153 @@ function resolveAgentRuntime(agent, config) {
960
869
  };
961
870
  }
962
871
 
872
+ // src/utils/conversation.ts
873
+ var MAX_CONVERSATION_TURNS = 6;
874
+ var MAX_CONVERSATION_CHARS = 4e3;
875
+ var MAX_ENTRY_CHARS = 600;
876
+ var ELLIPSIS = "\u2026";
877
+ function clip(text, max) {
878
+ const trimmed = text.trim();
879
+ if (trimmed.length <= max) return trimmed;
880
+ return `${trimmed.slice(0, Math.max(0, max - 1)).trimEnd()}${ELLIPSIS}`;
881
+ }
882
+ function entry(turn, index) {
883
+ const lines = [`${index}. user: ${clip(turn.requirement, MAX_ENTRY_CHARS)}`];
884
+ if (turn.reply) {
885
+ const verb = turn.reply.mode === "ask" ? "asked" : "answered";
886
+ lines.push(` you ${verb}: ${clip(turn.reply.message, MAX_ENTRY_CHARS)}`);
887
+ }
888
+ return lines.join("\n");
889
+ }
890
+ function renderConversation(history) {
891
+ if (!history || history.length === 0) return "";
892
+ const recent = history.slice(-MAX_CONVERSATION_TURNS);
893
+ const first = history.length - recent.length;
894
+ const entries = recent.map((turn, i) => entry(turn, first + i + 1));
895
+ while (entries.length > 1 && entries.join("\n").length > MAX_CONVERSATION_CHARS) {
896
+ entries.shift();
897
+ }
898
+ const body = clip(entries.join("\n"), MAX_CONVERSATION_CHARS);
899
+ return `
900
+ ## Conversation so far
901
+
902
+ Earlier turns of this session, oldest first:
903
+
904
+ ${body}
905
+ `;
906
+ }
907
+
908
+ // src/utils/json.ts
909
+ function extractJsonObjects(text) {
910
+ const objects = [];
911
+ let searchFrom = 0;
912
+ while (searchFrom < text.length) {
913
+ const start = text.indexOf("{", searchFrom);
914
+ if (start === -1) break;
915
+ const end = findMatchingBrace(text, start);
916
+ if (end !== -1) {
917
+ try {
918
+ objects.push(JSON.parse(text.slice(start, end + 1)));
919
+ searchFrom = end + 1;
920
+ continue;
921
+ } catch {
922
+ }
923
+ }
924
+ searchFrom = start + 1;
925
+ }
926
+ return objects;
927
+ }
928
+ function findMatchingBrace(text, start) {
929
+ let depth = 0;
930
+ let inString = false;
931
+ let escaped = false;
932
+ for (let i = start; i < text.length; i++) {
933
+ const ch = text[i];
934
+ if (inString) {
935
+ if (escaped) escaped = false;
936
+ else if (ch === "\\") escaped = true;
937
+ else if (ch === '"') inString = false;
938
+ continue;
939
+ }
940
+ if (ch === '"') inString = true;
941
+ else if (ch === "{") depth++;
942
+ else if (ch === "}") {
943
+ depth--;
944
+ if (depth === 0) return i;
945
+ }
946
+ }
947
+ return -1;
948
+ }
949
+
950
+ // src/utils/leaderDecision.ts
951
+ function parseLeaderDecision(text) {
952
+ for (const value of extractJsonObjects(text)) {
953
+ const decision = asDecision(value);
954
+ if (decision) return decision;
955
+ }
956
+ return void 0;
957
+ }
958
+ function asDecision(value) {
959
+ if (typeof value !== "object" || value === null) return void 0;
960
+ const record = value;
961
+ const mode = record.mode;
962
+ if (mode === "reply" || mode === "ask") {
963
+ const message = typeof record.message === "string" ? record.message.trim() : "";
964
+ return message ? { mode, message } : void 0;
965
+ }
966
+ if (mode === "run" || mode === void 0) {
967
+ const run = record.run;
968
+ if (!Array.isArray(run) || !run.every((id) => typeof id === "string")) return void 0;
969
+ const reason = typeof record.reason === "string" ? record.reason.trim() : "";
970
+ return reason ? { mode: "run", run, reason } : { mode: "run", run };
971
+ }
972
+ return void 0;
973
+ }
974
+
975
+ // src/utils/upstream.ts
976
+ var MAX_UPSTREAM_ENTRY_CHARS = 600;
977
+ var MAX_UPSTREAM_CHARS = 4e3;
978
+ var ELLIPSIS2 = "\u2026";
979
+ var HEADER = "\n## What upstream produced\n\n";
980
+ var TRAILER = "\n";
981
+ function block(entries) {
982
+ return `${HEADER}${entries.join("\n")}${TRAILER}`;
983
+ }
984
+ function compact(text, max) {
985
+ const trimmed = text.trim();
986
+ if (trimmed.length <= max) return trimmed;
987
+ const marker = (count) => `${ELLIPSIS2} (${count} characters elided) ${ELLIPSIS2}`;
988
+ const reserved = marker(trimmed.length).length;
989
+ const keep = Math.max(0, max - reserved);
990
+ const head = Math.ceil(keep / 2);
991
+ const tail = keep - head;
992
+ const elided = trimmed.length - keep;
993
+ return `${trimmed.slice(0, head)}${marker(elided)}${trimmed.slice(trimmed.length - tail)}`;
994
+ }
995
+ function entry2(output) {
996
+ const label = `- ${output.name} (${output.agentId}):
997
+ `;
998
+ const budget = MAX_UPSTREAM_ENTRY_CHARS - label.length;
999
+ let body = compact(output.output, budget);
1000
+ const newlines = body.length - body.replace(/\n/g, "").length;
1001
+ if (label.length + body.length + 2 * newlines > MAX_UPSTREAM_ENTRY_CHARS) {
1002
+ body = compact(output.output, budget - 2 * newlines);
1003
+ }
1004
+ return `${label}${body.replace(/\n/g, "\n ")}`;
1005
+ }
1006
+ function renderUpstreamOutputs(outputs) {
1007
+ if (outputs.length === 0) return "";
1008
+ const entries = outputs.map(entry2);
1009
+ while (entries.length > 1 && block(entries).length > MAX_UPSTREAM_CHARS) {
1010
+ entries.shift();
1011
+ }
1012
+ return block(entries);
1013
+ }
1014
+
1015
+ // src/orchestrator/AgentRunner.ts
1016
+ import * as fs3 from "fs";
1017
+ import * as path4 from "path";
1018
+
963
1019
  // src/runtime/AcpRuntime.ts
964
1020
  import { spawn } from "child_process";
965
1021
  import { randomUUID } from "crypto";
@@ -1019,6 +1075,17 @@ function isAbortError(err) {
1019
1075
  return err instanceof Error && err.name === ABORT_ERROR_NAME;
1020
1076
  }
1021
1077
 
1078
+ // src/utils/errors.ts
1079
+ var PermanentError = class extends Error {
1080
+ constructor(message) {
1081
+ super(message);
1082
+ this.name = "PermanentError";
1083
+ }
1084
+ };
1085
+ function isPermanent(err) {
1086
+ return err instanceof PermanentError;
1087
+ }
1088
+
1022
1089
  // src/runtime/RuntimeBase.ts
1023
1090
  var RuntimeBase = class {
1024
1091
  };
@@ -1103,6 +1170,9 @@ function withAuthHint(acp, err) {
1103
1170
  }
1104
1171
  return error;
1105
1172
  }
1173
+ function errorMessage(err) {
1174
+ return err instanceof Error ? err.message : String(err);
1175
+ }
1106
1176
  var AcpRuntime = class extends RuntimeBase {
1107
1177
  constructor() {
1108
1178
  super(...arguments);
@@ -1120,7 +1190,7 @@ var AcpRuntime = class extends RuntimeBase {
1120
1190
  capabilities: {
1121
1191
  ...BASE_CAPABILITIES,
1122
1192
  tools: specForCommand(command)?.tools ?? false,
1123
- models: (specForCommand(command)?.modelSelection ?? "none") !== "none"
1193
+ models: specForCommand(command)?.modelSelection === "config-option"
1124
1194
  }
1125
1195
  };
1126
1196
  }
@@ -1185,6 +1255,14 @@ ${task.prompt}` : task.prompt;
1185
1255
  const emit = (event) => {
1186
1256
  task.onEvent?.({ ...event, runId, agentId: task.agentId, seq: seq++ });
1187
1257
  };
1258
+ const warn = (message) => {
1259
+ if (task.onWarning) {
1260
+ task.onWarning(message);
1261
+ return;
1262
+ }
1263
+ process.stderr.write(`[multiclaw] Warning: ${message}
1264
+ `);
1265
+ };
1188
1266
  let sessionId;
1189
1267
  let agentText = "";
1190
1268
  let errorOutput = "";
@@ -1255,7 +1333,7 @@ ${task.prompt}` : task.prompt;
1255
1333
  break;
1256
1334
  }
1257
1335
  case "plan": {
1258
- emit({ type: "plan", entries: update.entries.map((entry2) => entry2.content) });
1336
+ emit({ type: "plan", entries: update.entries.map((entry3) => entry3.content) });
1259
1337
  break;
1260
1338
  }
1261
1339
  case "usage_update": {
@@ -1346,7 +1424,7 @@ ${task.prompt}` : task.prompt;
1346
1424
  settleReject(new Error(`${command} exited with code ${code}: ${errorOutput}`));
1347
1425
  });
1348
1426
  child.on("error", (err) => {
1349
- settleReject(new Error(`Failed to start ${command}: ${err.message}`));
1427
+ settleReject(new PermanentError(`Failed to start ${command}: ${err.message}`));
1350
1428
  });
1351
1429
  const run = async () => {
1352
1430
  const init = await ctx.request(acp.methods.agent.initialize, {
@@ -1355,7 +1433,7 @@ ${task.prompt}` : task.prompt;
1355
1433
  emit({ type: "run_started", model: task.model });
1356
1434
  if (task.sessionId) {
1357
1435
  if (init.agentCapabilities?.loadSession !== true) {
1358
- throw new Error(
1436
+ throw new PermanentError(
1359
1437
  `Agent does not support session/load (loadSession=false); cannot resume session ${task.sessionId}`
1360
1438
  );
1361
1439
  }
@@ -1389,9 +1467,8 @@ ${task.prompt}` : task.prompt;
1389
1467
  if (task.model && spec?.modelSelection === "config-option" && sessionId) {
1390
1468
  const known = discoveredModels.get(command);
1391
1469
  if (known && !known.includes(task.model)) {
1392
- process.stderr.write(
1393
- `[multiclaw] Warning: model "${task.model}" is not offered by ${command}; running with provider default.
1394
- `
1470
+ warn(
1471
+ `model "${task.model}" is not offered by ${command}; running with the provider's default.`
1395
1472
  );
1396
1473
  } else {
1397
1474
  try {
@@ -1400,7 +1477,10 @@ ${task.prompt}` : task.prompt;
1400
1477
  configId: "model",
1401
1478
  value: task.model
1402
1479
  });
1403
- } catch {
1480
+ } catch (err) {
1481
+ warn(
1482
+ `Could not set model "${task.model}" on ${command}; running with the provider's default (${errorMessage(err)}).`
1483
+ );
1404
1484
  }
1405
1485
  }
1406
1486
  }
@@ -1492,6 +1572,7 @@ async function renderPrompt(template, context, workDir) {
1492
1572
  }
1493
1573
 
1494
1574
  // src/utils/retry.ts
1575
+ var DEFAULT_RETRY_DELAY_MS = 2e3;
1495
1576
  function sleep(ms, signal) {
1496
1577
  if (signal?.aborted) return Promise.reject(abortError());
1497
1578
  return new Promise((resolve4, reject) => {
@@ -1506,7 +1587,7 @@ function sleep(ms, signal) {
1506
1587
  signal?.addEventListener("abort", onAbort, { once: true });
1507
1588
  });
1508
1589
  }
1509
- async function withRetry(fn, retries, onRetry, delayMs = 2e3, signal) {
1590
+ async function withRetry(fn, retries, onRetry, delayMs = DEFAULT_RETRY_DELAY_MS, signal, shouldRetry) {
1510
1591
  let lastError = new Error("Unknown error");
1511
1592
  for (let attempt = 1; attempt <= retries + 1; attempt++) {
1512
1593
  if (signal?.aborted) throw abortError();
@@ -1515,6 +1596,7 @@ async function withRetry(fn, retries, onRetry, delayMs = 2e3, signal) {
1515
1596
  } catch (err) {
1516
1597
  lastError = err;
1517
1598
  if (signal?.aborted) throw lastError;
1599
+ if (shouldRetry && !shouldRetry(err, attempt)) throw lastError;
1518
1600
  if (attempt <= retries) {
1519
1601
  onRetry?.(attempt, err);
1520
1602
  await sleep(delayMs * attempt, signal);
@@ -1523,6 +1605,9 @@ async function withRetry(fn, retries, onRetry, delayMs = 2e3, signal) {
1523
1605
  }
1524
1606
  throw lastError;
1525
1607
  }
1608
+ function retryBudgetMs(perAttemptMs, retries, delayMs = DEFAULT_RETRY_DELAY_MS) {
1609
+ return perAttemptMs * (retries + 1) + delayMs * retries * (retries + 1) / 2;
1610
+ }
1526
1611
 
1527
1612
  // src/utils/timeout.ts
1528
1613
  function withTimeout(factory, ms, label, signal) {
@@ -1550,6 +1635,31 @@ function withTimeout(factory, ms, label, signal) {
1550
1635
  });
1551
1636
  }
1552
1637
 
1638
+ // src/utils/toolSummary.ts
1639
+ var MAX_SUMMARY_COLUMNS = 120;
1640
+ function stringifySafe(value) {
1641
+ if (typeof value === "string") return value;
1642
+ if (value === void 0 || value === null) return "";
1643
+ try {
1644
+ return JSON.stringify(value) ?? "";
1645
+ } catch {
1646
+ return String(value);
1647
+ }
1648
+ }
1649
+ function firstLineTruncated(value) {
1650
+ const text = stringifySafe(value);
1651
+ const newline = text.indexOf("\n");
1652
+ const firstLine = newline === -1 ? text : text.slice(0, newline);
1653
+ if (firstLine.length <= MAX_SUMMARY_COLUMNS) return firstLine;
1654
+ return `${firstLine.slice(0, MAX_SUMMARY_COLUMNS - 1)}\u2026`;
1655
+ }
1656
+ function summarizeToolInput(input) {
1657
+ return firstLineTruncated(input);
1658
+ }
1659
+ function summarizeToolOutput(output) {
1660
+ return firstLineTruncated(output);
1661
+ }
1662
+
1553
1663
  // src/orchestrator/AgentRunner.ts
1554
1664
  var AgentRunner = class {
1555
1665
  constructor(eventBus, contextManager, globalWorkDir, config) {
@@ -1574,6 +1684,8 @@ var AgentRunner = class {
1574
1684
  });
1575
1685
  let attempts = 0;
1576
1686
  let output = "";
1687
+ let usage;
1688
+ let costUsd;
1577
1689
  try {
1578
1690
  const workDir = agent.workDir ? path4.resolve(this.globalWorkDir, agent.workDir) : this.globalWorkDir;
1579
1691
  fs3.mkdirSync(workDir, { recursive: true });
@@ -1581,7 +1693,7 @@ var AgentRunner = class {
1581
1693
  const retries = agent.retries ?? 0;
1582
1694
  const timeout = agent.timeout ?? 6e5;
1583
1695
  const tools = agent.tools ?? ["Read", "Write", "Bash"];
1584
- output = await withTimeout(
1696
+ const completed = await withTimeout(
1585
1697
  (innerSignal) => withRetry(
1586
1698
  async (attempt) => {
1587
1699
  attempts = attempt;
@@ -1596,7 +1708,7 @@ var AgentRunner = class {
1596
1708
  agentPlan: agent.agentPlan ?? "No step-by-step plan was provided \u2014 use your judgement."
1597
1709
  };
1598
1710
  const prompt = await renderPrompt(agent.taskPrompt, agentContext, this.globalWorkDir);
1599
- return (await runtime.execute({
1711
+ return await runtime.execute({
1600
1712
  agentId: agent.id,
1601
1713
  systemPrompt: agent.systemPrompt,
1602
1714
  prompt,
@@ -1605,6 +1717,11 @@ var AgentRunner = class {
1605
1717
  timeout,
1606
1718
  signal: innerSignal,
1607
1719
  model,
1720
+ // Only the caller knows which session to continue; absent means a
1721
+ // fresh one. It must stay `undefined` rather than `""` — the ACP
1722
+ // runtime tests `if (task.sessionId)`, so an empty string would
1723
+ // read as "load this session" for a session that does not exist.
1724
+ sessionId: this.config.resume?.sessions?.[agent.id],
1608
1725
  // Resolve through provider/defaults exactly as the CLI's
1609
1726
  // availability gate does — an agent that pins `provider` (or
1610
1727
  // leans on `defaults`) must launch that provider's bridge, not
@@ -1612,16 +1729,44 @@ var AgentRunner = class {
1612
1729
  // here sent a `provider: "claude"` config straight to `opencode`.
1613
1730
  command: resolveCommand(agent, this.config),
1614
1731
  args: resolveArgs(agent, this.config),
1732
+ // A runtime's diagnostics travel on the event bus rather than
1733
+ // stderr: under the TUI's alternate screen a stderr write is
1734
+ // never seen. The agent name prefixes the line so it is
1735
+ // locatable in the one-row status area.
1736
+ onWarning: (message) => {
1737
+ this.eventBus.emit({
1738
+ type: "orchestration:warning",
1739
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1740
+ payload: { message: `[${agent.name}] ${message}` }
1741
+ });
1742
+ },
1615
1743
  onEvent: (e) => {
1616
1744
  if ((e.type === "message_delta" || e.type === "message") && e.role === "agent" && e.text) {
1617
- this.eventBus.emit({
1618
- type: "agent:output",
1619
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1620
- payload: { agentId: agent.id, chunk: e.text }
1621
- });
1745
+ this.emitOutput(agent.id, e.text);
1746
+ return;
1747
+ }
1748
+ if (e.type === "tool_call") {
1749
+ const summary = summarizeToolInput(e.input);
1750
+ this.emitOutput(
1751
+ agent.id,
1752
+ `
1753
+ \u203A ${e.name}${summary ? ` ${summary}` : ""}
1754
+ `,
1755
+ "tool"
1756
+ );
1757
+ return;
1758
+ }
1759
+ if (e.type === "tool_result") {
1760
+ const summary = summarizeToolOutput(e.output);
1761
+ this.emitOutput(
1762
+ agent.id,
1763
+ ` ${e.isError ? "\u2717" : "\u2713"} ${summary}
1764
+ `,
1765
+ e.isError ? "tool" : "tool-detail"
1766
+ );
1622
1767
  }
1623
1768
  }
1624
- })).output;
1769
+ });
1625
1770
  },
1626
1771
  retries,
1627
1772
  (attempt, err) => {
@@ -1637,13 +1782,21 @@ var AgentRunner = class {
1637
1782
  }
1638
1783
  });
1639
1784
  },
1640
- 2e3,
1641
- innerSignal
1785
+ DEFAULT_RETRY_DELAY_MS,
1786
+ innerSignal,
1787
+ // A permanent failure (a missing binary, an unsupported capability)
1788
+ // will fail the same way on every attempt, so let the loop fail fast
1789
+ // instead of burning the whole backoff budget on a doomed run.
1790
+ (err) => !isPermanent(err)
1642
1791
  ),
1643
- timeout,
1792
+ retryBudgetMs(timeout + KILL_ESCALATION_MS, retries),
1644
1793
  agent.name,
1645
1794
  signal
1646
1795
  );
1796
+ output = completed.output;
1797
+ usage = completed.usage;
1798
+ costUsd = completed.costUsd;
1799
+ const sessionId = completed.sessionId;
1647
1800
  const endTime = /* @__PURE__ */ new Date();
1648
1801
  const result = {
1649
1802
  agentId: agent.id,
@@ -1653,7 +1806,11 @@ var AgentRunner = class {
1653
1806
  duration: endTime.getTime() - startTime.getTime(),
1654
1807
  attempts,
1655
1808
  startTime: startTime.toISOString(),
1656
- endTime: endTime.toISOString()
1809
+ endTime: endTime.toISOString(),
1810
+ model,
1811
+ ...usage ? { usage } : {},
1812
+ ...costUsd !== void 0 ? { costUsd } : {},
1813
+ ...sessionId ? { sessionId } : {}
1657
1814
  };
1658
1815
  this.eventBus.emit({
1659
1816
  type: "agent:complete",
@@ -1666,13 +1823,16 @@ var AgentRunner = class {
1666
1823
  const result = {
1667
1824
  agentId: agent.id,
1668
1825
  agentName: agent.name,
1669
- status: "failed",
1826
+ // An abort is a cancellation, not a failure: the team stopped because it
1827
+ // was told to, and the roster must not paint every agent as broken.
1828
+ status: isAbortError(err) ? "cancelled" : "failed",
1670
1829
  output,
1671
1830
  duration: endTime.getTime() - startTime.getTime(),
1672
1831
  attempts,
1673
1832
  error: err.message,
1674
1833
  startTime: startTime.toISOString(),
1675
- endTime: endTime.toISOString()
1834
+ endTime: endTime.toISOString(),
1835
+ model
1676
1836
  };
1677
1837
  this.eventBus.emit({
1678
1838
  type: "agent:failed",
@@ -1682,6 +1842,19 @@ var AgentRunner = class {
1682
1842
  return result;
1683
1843
  }
1684
1844
  }
1845
+ /**
1846
+ * Publish one chunk of agent output.
1847
+ *
1848
+ * `kind` is omitted for text so the payload shape is unchanged for every
1849
+ * consumer written before tool activity was surfaced.
1850
+ */
1851
+ emitOutput(agentId, chunk, kind) {
1852
+ this.eventBus.emit({
1853
+ type: "agent:output",
1854
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1855
+ payload: { agentId, chunk, ...kind ? { kind } : {} }
1856
+ });
1857
+ }
1685
1858
  };
1686
1859
 
1687
1860
  // src/orchestrator/ContextManager.ts
@@ -1736,8 +1909,33 @@ var Orchestrator = class {
1736
1909
  fs4.mkdirSync(this.workDir, { recursive: true });
1737
1910
  this.contextManager = new ContextManager(config.context);
1738
1911
  this.contextManager.set("conversation", renderConversation(config.history));
1912
+ this.contextManager.set("upstreamOutputs", "");
1739
1913
  this.runner = new AgentRunner(this.eventBus, this.contextManager, this.workDir, config);
1740
1914
  }
1915
+ async preview(options = {}) {
1916
+ this.clearStaleTaskPlan();
1917
+ const resolution = await this.resolveTurn(options.signal);
1918
+ if (resolution.kind === "message") {
1919
+ return {
1920
+ name: this.config.name,
1921
+ totalAgents: 0,
1922
+ stages: [],
1923
+ reply: { mode: resolution.mode, message: resolution.message },
1924
+ ...this.requirementField()
1925
+ };
1926
+ }
1927
+ const agents2 = resolution.agents;
1928
+ this.contextManager.set("pipelineAgents", agents2.map((a) => `${a.id}: ${a.name}`).join("\n"));
1929
+ const stages = this.buildStagesOrFallback(agents2, "Could not order the configured agents");
1930
+ const stageInfos = this.stageInfos(stages);
1931
+ return {
1932
+ name: this.config.name,
1933
+ totalAgents: agents2.length,
1934
+ stages: stageInfos,
1935
+ ...this.reasonField(resolution.reason),
1936
+ ...this.requirementField()
1937
+ };
1938
+ }
1741
1939
  async run(options = {}) {
1742
1940
  const startTime = /* @__PURE__ */ new Date();
1743
1941
  this.clearStaleTaskPlan();
@@ -1749,20 +1947,7 @@ var Orchestrator = class {
1749
1947
  if (agents2.length === 0) return this.finishEmpty(startTime);
1750
1948
  this.contextManager.set("pipelineAgents", agents2.map((a) => `${a.id}: ${a.name}`).join("\n"));
1751
1949
  let stages = this.buildStagesOrFallback(agents2, "Could not order the configured agents");
1752
- const getModel = (agent) => {
1753
- return agent.model ?? agent.models?.[agent.runtime ?? "acp"];
1754
- };
1755
- const toStageInfos = (list) => list.map((stage, i) => ({
1756
- stageIndex: i,
1757
- agents: stage.map((a) => ({
1758
- id: a.id,
1759
- name: a.name,
1760
- icon: a.icon,
1761
- model: getModel(a),
1762
- description: a.description,
1763
- taskTitle: a.taskTitle
1764
- }))
1765
- }));
1950
+ const toStageInfos = (list) => this.stageInfos(list);
1766
1951
  this.eventBus.emit({
1767
1952
  type: "orchestration:start",
1768
1953
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1770,12 +1955,14 @@ var Orchestrator = class {
1770
1955
  name: this.config.name,
1771
1956
  requirement: this.config.context?.requirement,
1772
1957
  totalAgents: agents2.length,
1773
- stages: toStageInfos(stages)
1958
+ stages: toStageInfos(stages),
1959
+ ...this.reasonField(resolution.reason)
1774
1960
  }
1775
1961
  });
1776
1962
  const results = [];
1777
1963
  let allSuccess = true;
1778
1964
  let cancelled = false;
1965
+ let planApplied = false;
1779
1966
  for (let i = 0; i < stages.length; i++) {
1780
1967
  const stage = stages[i];
1781
1968
  this.eventBus.emit({
@@ -1785,12 +1972,19 @@ var Orchestrator = class {
1785
1972
  });
1786
1973
  const stageResults = await this.runStage(stage, options.signal);
1787
1974
  results.push(...stageResults);
1975
+ this.contextManager.set(
1976
+ "upstreamOutputs",
1977
+ renderUpstreamOutputs(
1978
+ results.filter((r) => r.status === "success").map((r) => ({ agentId: r.agentId, name: r.agentName, output: r.output }))
1979
+ )
1980
+ );
1788
1981
  if (options.signal?.aborted) {
1789
1982
  cancelled = true;
1790
1983
  allSuccess = false;
1791
1984
  break;
1792
1985
  }
1793
- if (i === 0 && stage.some((a) => a.id === "architect") && stageResults.some((r) => r.status === "success")) {
1986
+ if (!planApplied && stage.some((a) => this.isPlanner(a)) && stageResults.some((r) => r.status === "success")) {
1987
+ planApplied = true;
1794
1988
  await this.applyTaskPlan(agents2);
1795
1989
  stages = this.buildStagesOrFallback(
1796
1990
  agents2,
@@ -1827,6 +2021,7 @@ var Orchestrator = class {
1827
2021
  }
1828
2022
  }
1829
2023
  const endTime = /* @__PURE__ */ new Date();
2024
+ const totalCostUsd = sumCosts(results);
1830
2025
  const result = {
1831
2026
  name: this.config.name,
1832
2027
  success: allSuccess,
@@ -1834,7 +2029,10 @@ var Orchestrator = class {
1834
2029
  agentResults: results,
1835
2030
  startTime: startTime.toISOString(),
1836
2031
  endTime: endTime.toISOString(),
1837
- ...cancelled ? { cancelled: true } : {}
2032
+ ...cancelled ? { cancelled: true } : {},
2033
+ ...totalCostUsd !== void 0 ? { totalCostUsd } : {},
2034
+ ...this.reasonField(resolution.reason),
2035
+ ...this.requirementField()
1838
2036
  };
1839
2037
  this.eventBus.emit({
1840
2038
  type: "orchestration:complete",
@@ -1843,6 +2041,20 @@ var Orchestrator = class {
1843
2041
  });
1844
2042
  return result;
1845
2043
  }
2044
+ stageInfos(stages) {
2045
+ const getModel = (agent) => resolveModel(agent, this.config);
2046
+ return stages.map((stage, i) => ({
2047
+ stageIndex: i,
2048
+ agents: stage.map((a) => ({
2049
+ id: a.id,
2050
+ name: a.name,
2051
+ icon: a.icon,
2052
+ model: getModel(a),
2053
+ description: a.description,
2054
+ taskTitle: a.taskTitle
2055
+ }))
2056
+ }));
2057
+ }
1846
2058
  /**
1847
2059
  * Order the agents into stages, degrading to declaration order if the graph is
1848
2060
  * broken.
@@ -1856,7 +2068,7 @@ var Orchestrator = class {
1856
2068
  try {
1857
2069
  return buildStages(agents2);
1858
2070
  } catch (err) {
1859
- this.warn(`${context} (${errorMessage(err)}) \u2014 running the agents in declaration order.`);
2071
+ this.warn(`${context} (${errorMessage2(err)}) \u2014 running the agents in declaration order.`);
1860
2072
  return [agents2];
1861
2073
  }
1862
2074
  }
@@ -1880,6 +2092,12 @@ var Orchestrator = class {
1880
2092
  * job, and that is what `message` records.
1881
2093
  */
1882
2094
  async resolveTurn(signal) {
2095
+ if (this.config.resume) {
2096
+ return {
2097
+ kind: "run",
2098
+ agents: this.selectAgents(this.config.resume.agents, "The resume list")
2099
+ };
2100
+ }
1883
2101
  if (!this.config.leader || this.config.useLeader === false) {
1884
2102
  return { kind: "run", agents: this.configuredAgents() };
1885
2103
  }
@@ -1897,7 +2115,7 @@ var Orchestrator = class {
1897
2115
  if (decision.mode !== "run") {
1898
2116
  return { kind: "message", mode: decision.mode, message: decision.message };
1899
2117
  }
1900
- return { kind: "run", agents: this.selectAgents(decision.run) };
2118
+ return { kind: "run", agents: this.selectAgents(decision.run), reason: decision.reason };
1901
2119
  }
1902
2120
  const message = result.output.trim();
1903
2121
  if (!message) {
@@ -1916,21 +2134,27 @@ var Orchestrator = class {
1916
2134
  * prevent, but for the in-memory fields rather than the file.
1917
2135
  */
1918
2136
  configuredAgents() {
1919
- return this.config.agents.map((a) => ({ ...a }));
2137
+ return this.config.agents.map(
2138
+ (a) => a.dependsOn ? { ...a, dependsOn: [...a.dependsOn] } : { ...a }
2139
+ );
1920
2140
  }
1921
2141
  /**
1922
- * Narrow the configured team to the ids the leader named.
2142
+ * Narrow the configured team to the ids `namedBy` asked for.
1923
2143
  *
1924
2144
  * Always a subset: an id that is not in the config is reported and dropped
1925
2145
  * rather than added, so a typo in a prompt cannot conjure an agent that has no
1926
2146
  * definition behind it.
2147
+ *
2148
+ * `namedBy` labels the warning. A resume names agents too — with no leader in
2149
+ * the run at all — so blaming "the leader" for a bad resumed id would send the
2150
+ * reader looking in the wrong place.
1927
2151
  */
1928
- selectAgents(run) {
2152
+ selectAgents(run, namedBy = "The leader") {
1929
2153
  const runSet = new Set(run);
1930
2154
  const known = new Set(this.config.agents.map((a) => a.id));
1931
2155
  for (const id of run) {
1932
2156
  if (!known.has(id)) {
1933
- this.warn(`The leader named "${id}", which is not in the config \u2014 ignoring it.`);
2157
+ this.warn(`${namedBy} named "${id}", which is not in the config \u2014 ignoring it.`);
1934
2158
  }
1935
2159
  }
1936
2160
  const skippedIds = new Set(this.config.agents.filter((a) => !runSet.has(a.id)).map((a) => a.id));
@@ -1951,13 +2175,28 @@ var Orchestrator = class {
1951
2175
  if (!maxConcurrency || agents2.length <= maxConcurrency) {
1952
2176
  return Promise.all(agents2.map((a) => this.runner.run(a, signal)));
1953
2177
  }
1954
- const results = [];
1955
- for (let i = 0; i < agents2.length; i += maxConcurrency) {
1956
- const batch = agents2.slice(i, i + maxConcurrency);
1957
- results.push(...await Promise.all(batch.map((a) => this.runner.run(a, signal))));
1958
- }
2178
+ const results = new Array(agents2.length);
2179
+ let next = 0;
2180
+ const workers = Array.from({ length: Math.min(maxConcurrency, agents2.length) }, async () => {
2181
+ while (next < agents2.length) {
2182
+ const index = next;
2183
+ next += 1;
2184
+ results[index] = await this.runner.run(agents2[index], signal);
2185
+ }
2186
+ });
2187
+ await Promise.all(workers);
1959
2188
  return results;
1960
2189
  }
2190
+ /**
2191
+ * Whether `agent` owns task decomposition.
2192
+ *
2193
+ * The explicit `role` is the source of truth, so a config can name its planner
2194
+ * anything. The id `"architect"` is kept as a fallback so every config written
2195
+ * before `role` existed keeps working unchanged.
2196
+ */
2197
+ isPlanner(agent) {
2198
+ return agent.role === "planner" || agent.id === "architect";
2199
+ }
1961
2200
  async applyTaskPlan(agents2) {
1962
2201
  const taskPlanPath = path5.join(this.workDir, "task-plan.json");
1963
2202
  if (!fs4.existsSync(taskPlanPath)) return;
@@ -1966,7 +2205,7 @@ var Orchestrator = class {
1966
2205
  taskPlan = JSON.parse(fs4.readFileSync(taskPlanPath, "utf-8"));
1967
2206
  } catch (err) {
1968
2207
  this.warn(
1969
- `Could not parse task-plan.json \u2014 continuing with the original agent set: ${errorMessage(err)}`
2208
+ `Could not parse task-plan.json \u2014 continuing with the original agent set: ${errorMessage2(err)}`
1970
2209
  );
1971
2210
  return;
1972
2211
  }
@@ -1996,6 +2235,26 @@ var Orchestrator = class {
1996
2235
  }
1997
2236
  }
1998
2237
  }
2238
+ /**
2239
+ * The `requirement` field for a result, present only when the caller set one.
2240
+ *
2241
+ * Spread into every result builder so all three outcomes carry it the same
2242
+ * way: a completion event doubles as the report a resume driver reads back,
2243
+ * and a requirement that never reaches the report cannot be resumed.
2244
+ */
2245
+ requirementField() {
2246
+ const requirement = this.config.context?.requirement;
2247
+ return requirement !== void 0 ? { requirement } : {};
2248
+ }
2249
+ /**
2250
+ * The `selectionReason` field, present only when the leader explained itself.
2251
+ *
2252
+ * Omitted rather than sent as `undefined`/`""` so a run without an
2253
+ * explanation stays byte-identical to one from before the field existed.
2254
+ */
2255
+ reasonField(reason) {
2256
+ return reason ? { selectionReason: reason } : {};
2257
+ }
1999
2258
  /**
2000
2259
  * Close out a turn the leader answered rather than planned.
2001
2260
  *
@@ -2012,7 +2271,8 @@ var Orchestrator = class {
2012
2271
  agentResults: [],
2013
2272
  startTime: startTime.toISOString(),
2014
2273
  endTime: endTime.toISOString(),
2015
- reply: { mode, message }
2274
+ reply: { mode, message },
2275
+ ...this.requirementField()
2016
2276
  };
2017
2277
  this.eventBus.emit({
2018
2278
  type: "orchestration:complete",
@@ -2047,7 +2307,8 @@ var Orchestrator = class {
2047
2307
  totalDuration: endTime.getTime() - startTime.getTime(),
2048
2308
  agentResults: [],
2049
2309
  startTime: startTime.toISOString(),
2050
- endTime: endTime.toISOString()
2310
+ endTime: endTime.toISOString(),
2311
+ ...this.requirementField()
2051
2312
  };
2052
2313
  this.eventBus.emit({
2053
2314
  type: "orchestration:complete",
@@ -2057,9 +2318,20 @@ var Orchestrator = class {
2057
2318
  return result;
2058
2319
  }
2059
2320
  };
2060
- function errorMessage(err) {
2321
+ function errorMessage2(err) {
2061
2322
  return err instanceof Error ? err.message : String(err);
2062
2323
  }
2324
+ function sumCosts(results) {
2325
+ let total = 0;
2326
+ let reported = false;
2327
+ for (const result of results) {
2328
+ if (typeof result.costUsd === "number") {
2329
+ total += result.costUsd;
2330
+ reported = true;
2331
+ }
2332
+ }
2333
+ return reported ? total : void 0;
2334
+ }
2063
2335
 
2064
2336
  // src/providers/detect.ts
2065
2337
  import { spawn as spawn2 } from "child_process";
@@ -2091,21 +2363,28 @@ async function detectProviders(opts) {
2091
2363
  const candidates = [spec.command, ...spec.aliases ?? []];
2092
2364
  let matched;
2093
2365
  let version;
2366
+ let source = "path";
2367
+ let resolvedPath;
2094
2368
  for (const cmd of candidates) {
2095
2369
  const exe = resolveExecutable(cmd);
2096
2370
  if (!exe) continue;
2371
+ const bundled = exe !== cmd;
2097
2372
  if (spec.confirm) {
2098
2373
  if (opts?.withVersion) {
2099
2374
  const v = await versionFor(exe);
2100
2375
  if (v && spec.confirm(v)) {
2101
2376
  matched = cmd;
2102
2377
  version = v;
2378
+ source = bundled ? "bundled" : "path";
2379
+ resolvedPath = bundled ? exe : void 0;
2103
2380
  break;
2104
2381
  }
2105
2382
  }
2106
2383
  continue;
2107
2384
  }
2108
2385
  matched = cmd;
2386
+ source = bundled ? "bundled" : "path";
2387
+ resolvedPath = bundled ? exe : void 0;
2109
2388
  if (opts?.withVersion) {
2110
2389
  version = await versionFor(exe);
2111
2390
  }
@@ -2118,7 +2397,9 @@ async function detectProviders(opts) {
2118
2397
  command: matched,
2119
2398
  status: "present",
2120
2399
  version,
2121
- note: spec.auth.hint || void 0
2400
+ note: spec.auth.hint || void 0,
2401
+ source,
2402
+ ...resolvedPath !== void 0 ? { resolvedPath } : {}
2122
2403
  });
2123
2404
  } else {
2124
2405
  results.push({
@@ -2135,6 +2416,22 @@ async function detectProviders(opts) {
2135
2416
  function presentProviders(detected) {
2136
2417
  return detected.filter((d) => d.status === "present").map((d) => d.id);
2137
2418
  }
2419
+
2420
+ // src/utils/resumePlan.ts
2421
+ function describeResumePlan({ agents: agents2, sessions = {}, team }) {
2422
+ const teamById = new Map(team.map((member) => [member.id, member]));
2423
+ const running = new Set(agents2);
2424
+ const clauses = agents2.map(
2425
+ (id) => sessions[id] ? `${id} continues its session` : `${id} starts fresh`
2426
+ );
2427
+ for (const id of agents2) {
2428
+ const dropped = (teamById.get(id)?.dependsOn ?? []).filter(
2429
+ (dep) => teamById.has(dep) && !running.has(dep)
2430
+ );
2431
+ if (dropped.length > 0) clauses.push(`${id} runs without ${dropped.join(", ")}`);
2432
+ }
2433
+ return clauses.join("; ");
2434
+ }
2138
2435
  export {
2139
2436
  ABORT_ERROR_NAME,
2140
2437
  AcpRuntime,
@@ -2147,6 +2444,7 @@ export {
2147
2444
  agents,
2148
2445
  buildStages,
2149
2446
  defineConfig,
2447
+ describeResumePlan,
2150
2448
  detectProviders,
2151
2449
  isAbortError,
2152
2450
  presentProviders,