@ordewell/core 0.9.0 → 0.10.0

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 (63) hide show
  1. package/dist/{IFileSystem-DpaaFdit.d.ts → IFileSystem-BCJBHFwB.d.ts} +3 -1
  2. package/dist/{IFileSystem-_m7HdoxJ.d.mts → IFileSystem-Bj6fy1cG.d.mts} +3 -1
  3. package/dist/{IRunner-BiQio8gJ.d.ts → IRunner-C7_SSpzG.d.ts} +614 -77
  4. package/dist/{IRunner-CxiymkP_.d.mts → IRunner-hWAJC4hg.d.mts} +614 -77
  5. package/dist/ModeResolver-m1d8Cb-j.d.mts +35 -0
  6. package/dist/ModeResolver-u5ZhRC8x.d.ts +35 -0
  7. package/dist/{Task-1Q19y7sZ.d.mts → Task-CxwnCJLi.d.mts} +346 -127
  8. package/dist/{Task-1Q19y7sZ.d.ts → Task-CxwnCJLi.d.ts} +346 -127
  9. package/dist/{chunk-UUU46PVQ.mjs → chunk-2IIRSBEQ.mjs} +109 -142
  10. package/dist/chunk-2IIRSBEQ.mjs.map +1 -0
  11. package/dist/{chunk-QUARDKVT.mjs → chunk-CH5IGWFS.mjs} +546 -967
  12. package/dist/chunk-CH5IGWFS.mjs.map +1 -0
  13. package/dist/chunk-DJAPDO6E.mjs +1358 -0
  14. package/dist/chunk-DJAPDO6E.mjs.map +1 -0
  15. package/dist/chunk-DNFT4SQG.mjs +97 -0
  16. package/dist/chunk-DNFT4SQG.mjs.map +1 -0
  17. package/dist/index.d.mts +3204 -3926
  18. package/dist/index.d.ts +3204 -3926
  19. package/dist/index.js +22283 -16472
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +20220 -14690
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/parsing.d.mts +55 -3
  24. package/dist/parsing.d.ts +55 -3
  25. package/dist/parsing.js +311 -124
  26. package/dist/parsing.js.map +1 -1
  27. package/dist/parsing.mjs +86 -6
  28. package/dist/parsing.mjs.map +1 -1
  29. package/dist/plan-utils-B6yJdpgi.d.ts +2902 -0
  30. package/dist/plan-utils-CKXdD1AD.d.mts +2902 -0
  31. package/dist/plan-utils.d.mts +4 -5
  32. package/dist/plan-utils.d.ts +4 -5
  33. package/dist/plan-utils.js +471 -86
  34. package/dist/plan-utils.js.map +1 -1
  35. package/dist/plan-utils.mjs +68 -19
  36. package/dist/testing.d.mts +22 -4
  37. package/dist/testing.d.ts +22 -4
  38. package/dist/testing.js +46 -9
  39. package/dist/testing.js.map +1 -1
  40. package/dist/testing.mjs +47 -11
  41. package/dist/testing.mjs.map +1 -1
  42. package/package.json +2 -10
  43. package/skills/tdd/SKILL.md +0 -1
  44. package/dist/ModeResolver-DE-o2lRF.d.mts +0 -48
  45. package/dist/ModeResolver-EWlpmVAy.d.ts +0 -48
  46. package/dist/chunk-2M4X3VB7.mjs +0 -432
  47. package/dist/chunk-2M4X3VB7.mjs.map +0 -1
  48. package/dist/chunk-H5O74L77.mjs +0 -185
  49. package/dist/chunk-H5O74L77.mjs.map +0 -1
  50. package/dist/chunk-QUARDKVT.mjs.map +0 -1
  51. package/dist/chunk-UUU46PVQ.mjs.map +0 -1
  52. package/dist/chunk-V3JHS7AX.mjs +0 -27
  53. package/dist/chunk-V3JHS7AX.mjs.map +0 -1
  54. package/dist/order-labels.d.mts +0 -30
  55. package/dist/order-labels.d.ts +0 -30
  56. package/dist/order-labels.js +0 -54
  57. package/dist/order-labels.js.map +0 -1
  58. package/dist/order-labels.mjs +0 -13
  59. package/dist/order-labels.mjs.map +0 -1
  60. package/dist/parsing-CZdp2p_n.d.ts +0 -154
  61. package/dist/parsing-GgUJoYKs.d.mts +0 -154
  62. package/dist/plan-utils-6rUr7ZOx.d.ts +0 -1222
  63. package/dist/plan-utils-P3PIEwNa.d.mts +0 -1222
@@ -1,27 +1,19 @@
1
1
  import {
2
- PlanParseError,
3
- TASK_OPS_ENVELOPE_KEY,
4
- escapeControlCharsInStrings,
5
- extractObjectsWithKey,
6
- stripTrailingCommas
7
- } from "./chunk-H5O74L77.mjs";
8
- import {
9
- addTaskToPlan,
10
- createTask,
11
- flattenTasks,
12
- inheritedOps,
13
- inheritedSkills,
14
- opsFlag,
15
- removeTaskFromPlan,
16
- renumberTasks,
17
- skillNames,
18
- updateTaskInPlan
19
- } from "./chunk-UUU46PVQ.mjs";
2
+ NOT_STARTED,
3
+ approvalLabel,
4
+ awaitingLabel,
5
+ isTaskRunning,
6
+ resumeLabel,
7
+ taskStatusKind
8
+ } from "./chunk-DJAPDO6E.mjs";
20
9
  import {
10
+ pausedOnLimit,
11
+ rejoinPlanTree,
21
12
  taskOrderLabel,
22
- taskRef,
23
- titledTaskRef
24
- } from "./chunk-V3JHS7AX.mjs";
13
+ taskRefOf,
14
+ titledTaskRef,
15
+ titledTaskRefOf
16
+ } from "./chunk-2IIRSBEQ.mjs";
25
17
 
26
18
  // src/models/Usage.ts
27
19
  function partedPromptUsage(parts) {
@@ -45,13 +37,21 @@ function addUsage(totals, record) {
45
37
  return next;
46
38
  }
47
39
  function addPlannerUsage(usage, record) {
48
- const next = { ...usage, totals: addUsage(usage.totals, record) };
40
+ const next = { ...usage, totals: addUsage(usage.totals, record), byModel: addModelUsage(usage.byModel ?? [], record) };
49
41
  if (record.subagentId) {
50
42
  next.bySubagent = { ...usage.bySubagent, [record.subagentId]: addUsage(usage.bySubagent?.[record.subagentId] ?? {}, record) };
51
43
  return next;
52
44
  }
45
+ if (!record.aggregate) {
46
+ if (record.source !== usage.source || record.model !== usage.model) {
47
+ delete next.lastPromptTokens;
48
+ delete next.contextWindow;
49
+ }
50
+ next.source = record.source;
51
+ next.model = record.model;
52
+ }
53
53
  if (record.inputTokens !== void 0) next.lastPromptTokens = record.inputTokens;
54
- if (record.contextWindow !== void 0 && record.contextWindow > 0) next.contextWindow = record.contextWindow;
54
+ if (record.contextWindow !== void 0) next.contextWindow = record.contextWindow > 0 ? record.contextWindow : void 0;
55
55
  return next;
56
56
  }
57
57
  function isMeasured(totals) {
@@ -61,6 +61,8 @@ function usageLine(usage) {
61
61
  const contextFill = plannerContextFill(usage);
62
62
  return {
63
63
  totals: usage.totals,
64
+ ...usage.byModel ? { byModel: usage.byModel } : {},
65
+ ...usage.model ? { model: usage.model } : {},
64
66
  ...usage.bySubagent ? { bySubagent: usage.bySubagent } : {},
65
67
  ...contextFill ? { contextFill } : {}
66
68
  };
@@ -69,6 +71,48 @@ function plannerContextFill(usage) {
69
71
  if (usage.lastPromptTokens === void 0 || usage.contextWindow === void 0 || usage.contextWindow <= 0) return void 0;
70
72
  return { usedTokens: usage.lastPromptTokens, windowTokens: usage.contextWindow };
71
73
  }
74
+ function addModelUsage(models, record) {
75
+ const index = models.findIndex((m) => m.source === record.source && m.model === record.model);
76
+ if (index < 0) return [...models, { source: record.source, ...record.model ? { model: record.model } : {}, totals: addUsage({}, record) }];
77
+ return models.map((m, i) => i === index ? { ...m, totals: addUsage(m.totals, record) } : m);
78
+ }
79
+ function startAttemptUsage(attempt, runner, startedAt) {
80
+ return { attempt, runner, startedAt, wallTimeMs: 0, totals: {}, byModel: [] };
81
+ }
82
+ function addAttemptUsage(usage, record, now) {
83
+ const folded = addPlannerUsage(usage, record);
84
+ return { ...usage, ...folded, lastPromptTokens: folded.lastPromptTokens, contextWindow: folded.contextWindow, wallTimeMs: Math.max(0, Date.parse(now) - Date.parse(usage.startedAt)) };
85
+ }
86
+ function endAttemptUsage(usage, now) {
87
+ return { ...usage, endedAt: now, wallTimeMs: Math.max(0, Date.parse(now) - Date.parse(usage.startedAt)) };
88
+ }
89
+ function addUsageTotals(totals, addition) {
90
+ let next = addUsage(totals, { source: "", inputTokens: addition.inputTokens, outputTokens: addition.outputTokens, cachedInputTokens: addition.cachedInputTokens });
91
+ for (const [currency, amount] of Object.entries(addition.reportedCost ?? {})) next = addUsage(next, { source: "", reportedCost: { currency, amount } });
92
+ return next;
93
+ }
94
+ function planUsage(tasks) {
95
+ const result = { totals: {}, byModel: [], wallTimeMs: 0, attempts: 0 };
96
+ for (const task of tasks) {
97
+ const child = planUsage(task.subtasks ?? []);
98
+ result.totals = addUsageTotals(result.totals, child.totals);
99
+ result.wallTimeMs += child.wallTimeMs;
100
+ result.attempts += child.attempts;
101
+ const models = [...child.byModel];
102
+ for (const usage of task.attemptUsage ?? []) {
103
+ result.totals = addUsageTotals(result.totals, usage.totals);
104
+ result.wallTimeMs += usage.wallTimeMs;
105
+ result.attempts++;
106
+ models.push(...usage.byModel ?? []);
107
+ }
108
+ for (const model of models) {
109
+ const existing = result.byModel.find((m) => m.source === model.source && m.model === model.model);
110
+ if (existing) existing.totals = addUsageTotals(existing.totals, model.totals);
111
+ else result.byModel.push({ ...model, totals: addUsageTotals({}, model.totals) });
112
+ }
113
+ }
114
+ return result;
115
+ }
72
116
 
73
117
  // src/services/harness/agentTools.ts
74
118
  var KNOWN_TOOLS = {
@@ -253,13 +297,23 @@ function diffSummary({ added, removed }) {
253
297
  return `Added ${lines(added)}, removed ${lines(removed)}`;
254
298
  }
255
299
 
300
+ // src/utils/sameMessage.ts
301
+ function sameMessage(a, b) {
302
+ const bare = (value) => value.trim().replace(/\.+$/, "").trimEnd();
303
+ return bare(a) !== "" && bare(a) === bare(b);
304
+ }
305
+
256
306
  // src/conversation/records.ts
257
307
  function settledMessage(id, role, text2, turnId) {
258
308
  return { type: "message", id, role, text: text2, streaming: false, ...turnId ? { turnId } : {} };
259
309
  }
260
- function userMessage(id, text2, skills, turnId) {
310
+ function userMessage(id, text2, skills, { turnId, forced } = {}) {
261
311
  const block = settledMessage(id, "user", text2, turnId);
262
- return skills.length > 0 ? { ...block, skills: skills.map((s) => s.name) } : block;
312
+ return {
313
+ ...block,
314
+ ...skills.length > 0 ? { skills: skills.map((s) => s.name) } : {},
315
+ ...forced ? { forced: true } : {}
316
+ };
263
317
  }
264
318
  function skillLoadNotice({ invokedBy, name, source, path, attaches }) {
265
319
  return { invokedBy, name, source, path, ...attaches ? { attaches } : {} };
@@ -293,8 +347,8 @@ function subagentBlock(id, { toolCallId, model, usage, turnId, ...fields }) {
293
347
  ...turnId ? { turnId } : {}
294
348
  };
295
349
  }
296
- function usageBlock(id, { totals, bySubagent, contextFill }) {
297
- return { type: "usage", id, totals, ...bySubagent ? { bySubagent } : {}, ...contextFill ? { contextFill } : {} };
350
+ function usageBlock(id, { totals, bySubagent, byModel, model, contextFill }) {
351
+ return { type: "usage", id, totals, ...byModel ? { byModel } : {}, ...model ? { model } : {}, ...bySubagent ? { bySubagent } : {}, ...contextFill ? { contextFill } : {} };
298
352
  }
299
353
  var STATUS_OF_OUTCOME = {
300
354
  success: "ok",
@@ -488,10 +542,17 @@ function streamWhole(view, lane, key, text2, make) {
488
542
  return appendOutput(next, lane, make);
489
543
  }
490
544
  function agentText(view, text2) {
491
- return streamWhole(view, null, TEXT_STREAM, text2, (id) => settledMessage(id, "agent", blockText(text2)));
545
+ const next = streamWhole(view, null, TEXT_STREAM, text2, (id) => settledMessage(id, "agent", blockText(text2)));
546
+ return { ...next, latestText: next.nextId > view.nextId ? `b${view.nextId}` : view.streams[TEXT_STREAM] };
492
547
  }
493
548
  function agentTextDelta(view, text2) {
494
- return streamDelta(view, null, TEXT_STREAM, text2, (id, t) => ({ type: "message", id, role: "agent", text: t, streaming: true }));
549
+ const next = streamDelta(view, null, TEXT_STREAM, text2, (id, t) => ({ type: "message", id, role: "agent", text: t, streaming: true }));
550
+ return { ...next, latestText: next.streams[TEXT_STREAM] };
551
+ }
552
+ function failTurn(view, message) {
553
+ const repeated = view.blocks.find((b) => b.id === view.latestText);
554
+ const settled = repeated?.type === "message" && repeated.role === "agent" && sameMessage(repeated.text, message) ? withoutStream({ ...view, blocks: view.blocks.filter((b) => b !== repeated) }, TEXT_STREAM) : view;
555
+ return appendOutput({ ...settled, latestText: void 0 }, null, (id) => settledMessage(id, "error", message));
495
556
  }
496
557
  function thinkingBlock(id, text2, streaming, subagentId) {
497
558
  return { type: "thinking", id, text: text2, streaming, ...subagentId ? { subagentId } : {} };
@@ -556,19 +617,22 @@ function runnerToolSubject(tool, args) {
556
617
  const headline = toolHeadline(mapped, parsed ? JSON.stringify(normalizeAgentArgs(mapped, parsed)) : args, toolLabel);
557
618
  return headline.keyArg ? `${headline.name}(${headline.keyArg})` : headline.name;
558
619
  }
559
- function requestApproval(view, { approvalId, tool, args, allowForTask, toolCallId }) {
620
+ function requestApproval(view, { approvalId, tool, args, allowForTask, toolCallId, decided }) {
560
621
  if (view.blocks.some((b) => b.type === "approval" && b.approvalId === approvalId)) return view;
561
- return append(view, (id) => ({
562
- type: "approval",
563
- id,
564
- approvalId,
565
- kind: "runner_tool",
566
- subject: runnerToolSubject(tool, args),
567
- scope: tool,
568
- status: "pending",
569
- ...allowForTask ? { allowForTask } : {},
570
- ...toolCallId ? { toolCallId } : {}
571
- }));
622
+ return append(view, (id) => {
623
+ const block = {
624
+ type: "approval",
625
+ id,
626
+ approvalId,
627
+ kind: "runner_tool",
628
+ subject: runnerToolSubject(tool, args),
629
+ scope: tool,
630
+ status: "pending",
631
+ ...allowForTask ? { allowForTask } : {},
632
+ ...toolCallId ? { toolCallId } : {}
633
+ };
634
+ return decided ? answered(block, decided) : block;
635
+ });
572
636
  }
573
637
  function settleApproval(view, approvalId, settle) {
574
638
  const i = findLastIndex(view.blocks, (b) => b.type === "approval" && b.approvalId === approvalId);
@@ -576,36 +640,73 @@ function settleApproval(view, approvalId, settle) {
576
640
  if (block?.type !== "approval" || block.status !== "pending") return view;
577
641
  return { ...view, blocks: replaceAt(view.blocks, i, settle(block)) };
578
642
  }
579
- function decideApproval(view, { approvalId, decision, note }) {
580
- return settleApproval(view, approvalId, (block) => ({
643
+ function answered(block, decision, note) {
644
+ return {
581
645
  ...block,
582
646
  status: decision === "deny" ? "denied" : "granted",
583
647
  decidedBy: "asked",
584
648
  ...decision === "allowForTask" ? { forTask: true } : {},
585
649
  ...note ? { note } : {}
586
- }));
650
+ };
651
+ }
652
+ function decideApproval(view, { approvalId, decision, note }) {
653
+ return settleApproval(view, approvalId, (block) => answered(block, decision, note));
587
654
  }
588
- function startTurn(view, { message, messageId }) {
655
+ function startTurn(view, { message, messageId, from, forced }) {
589
656
  const queued = messageId ? view.queued.filter((m) => m.id !== messageId) : view.queued;
590
- const working = { ...view, queued, working: true };
591
- return message ? append(working, (id) => settledMessage(id, "user", message)) : working;
657
+ const working = { ...view, queued, working: true, latestText: void 0 };
658
+ return message ? append(working, (id) => ({ ...sentMessage(id, message, from), ...forced ? { forced: true } : {} })) : working;
659
+ }
660
+ function sentMessage(id, text2, from) {
661
+ return from === "ordewell" ? settledMessage(id, "system", `Ordewell: ${text2}`) : settledMessage(id, "user", text2);
662
+ }
663
+ function startCommand(view, { name, args, messageId }) {
664
+ const queued = messageId ? view.queued.filter((m) => m.id !== messageId) : view.queued;
665
+ return append({ ...view, queued, working: true, latestText: void 0 }, (id) => ({
666
+ type: "command",
667
+ id,
668
+ name,
669
+ ...args ? { args } : {},
670
+ status: "running",
671
+ output: ""
672
+ }));
673
+ }
674
+ function runningCommand(view) {
675
+ return findLastIndex(view.blocks, (b) => b.type === "command" && b.status === "running");
676
+ }
677
+ function commandOutput(view, { text: text2 }) {
678
+ const i = runningCommand(view);
679
+ const block = view.blocks[i];
680
+ if (block?.type !== "command") return append(view, (id) => settledMessage(id, "system", text2));
681
+ return { ...view, blocks: replaceAt(view.blocks, i, { ...block, output: block.output ? `${block.output}
682
+ ${text2}` : text2 }) };
683
+ }
684
+ function appendStep(view, make) {
685
+ const next = append(view, make);
686
+ return sealLane(next, null, findLastIndex(next.blocks, (b) => b.type !== "usage"));
592
687
  }
593
688
  var CUT = { completed: null, interrupted: "stopped", failed: "failed" };
689
+ var COMMAND_END = { completed: "done", interrupted: "stopped", failed: "failed" };
594
690
  function endTurn(view, { reason }) {
595
- const closed = closeOpenBlocks({ ...view, working: false, lastTurnEnd: reason, streams: {} }, CUT[reason]);
691
+ const i = runningCommand(view);
692
+ const command = view.blocks[i];
693
+ const settled = command?.type === "command" ? { ...view, blocks: replaceAt(view.blocks, i, { ...command, status: COMMAND_END[reason] }) } : view;
694
+ const closed = closeOpenBlocks({ ...settled, working: false, lastTurnEnd: reason, streams: {}, latestText: void 0 }, CUT[reason]);
596
695
  return reason === "interrupted" ? append(closed, (id) => settledMessage(id, "system", "Interrupted.")) : closed;
597
696
  }
598
- function queueMessage(view, { messageId, text: text2, forced }) {
697
+ function queueMessage(view, { messageId, text: text2, forced, from }) {
599
698
  const listed = view.queued.find((m) => m.id === messageId);
699
+ const sender = from ? { from } : {};
600
700
  if (forced) {
601
701
  const rest = view.queued.filter((m) => m !== listed);
602
702
  const at = rest.findIndex((m) => !m.forced);
603
703
  const placed = at < 0 ? rest.length : at;
604
- return { ...view, queued: [...rest.slice(0, placed), { id: messageId, text: text2, forced: true }, ...rest.slice(placed)] };
704
+ const handedOver = listed?.handedOver ? { handedOver: true } : {};
705
+ return { ...view, queued: [...rest.slice(0, placed), { id: messageId, text: text2, forced: true, ...handedOver, ...sender }, ...rest.slice(placed)] };
605
706
  }
606
- if (!listed) return { ...view, queued: [...view.queued, { id: messageId, text: text2 }] };
707
+ if (!listed) return { ...view, queued: [...view.queued, { id: messageId, text: text2, ...sender }] };
607
708
  if (!listed.handedOver) return view;
608
- return { ...view, queued: view.queued.map((m) => m === listed ? { id: m.id, text: m.text } : m) };
709
+ return { ...view, queued: view.queued.map((m) => m === listed ? { id: m.id, text: m.text, ...m.forced ? { forced: true } : {}, ...sender } : m) };
609
710
  }
610
711
  function handOver(view, { messageId }) {
611
712
  const listed = view.queued.find((m) => m.id === messageId);
@@ -613,18 +714,44 @@ function handOver(view, { messageId }) {
613
714
  return { ...view, queued: view.queued.map((m) => m === listed ? { ...m, handedOver: true } : m) };
614
715
  }
615
716
  function deliverMidTurn(view, event) {
616
- return appendOutput(unqueueMessage(view, event), null, (id) => settledMessage(id, "user", event.text));
717
+ return appendOutput(unqueueMessage(view, event), null, (id) => sentMessage(id, event.text, event.from));
617
718
  }
618
719
  function unqueueMessage(view, { messageId }) {
619
720
  const queued = view.queued.filter((m) => m.id !== messageId);
620
721
  return queued.length === view.queued.length ? view : { ...view, queued };
621
722
  }
723
+ function endCall(block) {
724
+ return block.type === "tool" && block.status === "pending" ? finishedTool(block, "", "not_executed") : block;
725
+ }
726
+ function endOpenCalls(view) {
727
+ let changed = false;
728
+ const blocks = view.blocks.map((block) => {
729
+ const ended = block.type === "subagent" ? block.children.some((c) => c.type === "tool" && c.status === "pending") ? { ...block, children: block.children.map(endCall) } : block : block.type === "tool" ? endCall(block) : block;
730
+ if (ended !== block) changed = true;
731
+ return ended;
732
+ });
733
+ return changed ? { ...view, blocks } : view;
734
+ }
622
735
  function reduceTaskLog(view, event) {
623
736
  switch (event.type) {
624
737
  case "task_skills":
625
738
  return append(view, (id) => settledMessage(id, "system", `Skills: ${event.skills.map((s) => `${s.name} (${s.path})`).join(", ")}`));
626
739
  case "turn_start":
627
740
  return startTurn(view, event);
741
+ case "command":
742
+ return startCommand(view, event);
743
+ case "command_output":
744
+ return commandOutput(view, event);
745
+ case "compaction":
746
+ return appendStep(view, (id) => ({
747
+ type: "compaction",
748
+ id,
749
+ trigger: event.trigger,
750
+ ...event.preTokens !== void 0 ? { preTokens: event.preTokens } : {},
751
+ ...event.postTokens !== void 0 ? { postTokens: event.postTokens } : {}
752
+ }));
753
+ case "runner_loaded":
754
+ return append(view, (id) => ({ type: "runner_loaded", id, commands: event.commands, mcpServers: event.mcpServers }));
628
755
  case "turn_end":
629
756
  return endTurn(view, event);
630
757
  case "text_delta":
@@ -656,13 +783,15 @@ function reduceTaskLog(view, event) {
656
783
  case "message_undelivered":
657
784
  return append(unqueueMessage(view, event), (id) => settledMessage(id, "system", `${event.text} \xB7 not delivered`));
658
785
  case "error":
659
- return appendOutput(view, null, (id) => settledMessage(id, "error", event.message));
786
+ return failTurn(view, event.message);
660
787
  case "approval_requested":
661
788
  return requestApproval(view, event);
662
789
  case "approval_decided":
663
790
  return decideApproval(view, event);
664
791
  case "approval_withdrawn":
665
792
  return settleApproval(view, event.approvalId, (block) => ({ ...block, status: "withdrawn" }));
793
+ case "runner_ended":
794
+ return endOpenCalls(view);
666
795
  default:
667
796
  return view;
668
797
  }
@@ -676,848 +805,6 @@ function capConflictFiles(files, max = 5) {
676
805
  return files.length <= max ? files.join(", ") : `${files.slice(0, max).join(", ")}, +${files.length - max} more`;
677
806
  }
678
807
 
679
- // src/services/ModelAllowlistResolver.ts
680
- function effectiveAllowlist(allowlist, runner, modelsByRunner) {
681
- if (!allowlist || allowlist.length === 0) return void 0;
682
- if (!modelsByRunner) return allowlist;
683
- const own = new Set((modelsByRunner[runner] ?? []).map((m) => m.modelId));
684
- const elsewhere = /* @__PURE__ */ new Set();
685
- for (const [other, models] of Object.entries(modelsByRunner)) {
686
- if (other === runner) continue;
687
- for (const m of models ?? []) elsewhere.add(m.modelId);
688
- }
689
- const kept = allowlist.filter((id) => own.has(id) || !elsewhere.has(id));
690
- return kept.length > 0 ? kept : void 0;
691
- }
692
- function filterModelsForPrompt(modelsByRunner, perRunnerAllowlist) {
693
- const result = {};
694
- for (const [runner, models] of Object.entries(modelsByRunner)) {
695
- if (!models) continue;
696
- const allowed = effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
697
- if (!allowed) {
698
- result[runner] = models;
699
- continue;
700
- }
701
- const allowedSet = new Set(allowed);
702
- const intersection = models.filter((m) => allowedSet.has(m.modelId));
703
- const covered = new Set(intersection.map((m) => m.modelId));
704
- const missing = allowed.filter((id) => !covered.has(id)).map((id) => ({ modelId: id, modelLabel: id.split("/").pop() || id, variants: [] }));
705
- result[runner] = [...intersection, ...missing];
706
- }
707
- return result;
708
- }
709
- var EFFORT_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
710
- var EFFORT_ALIASES = { disabled: "none", adaptive: "medium" };
711
- function clampThinkingEffort(effort, variants) {
712
- if (!effort) return void 0;
713
- if (variants.length === 0) return void 0;
714
- const ids = new Set(variants.map((v) => v.id));
715
- if (ids.has(effort)) return effort;
716
- const canon = EFFORT_ALIASES[effort] ?? effort;
717
- if (ids.has(canon)) return canon;
718
- const idx = EFFORT_LADDER.indexOf(canon);
719
- if (idx < 0) return void 0;
720
- for (let d = 1; d < EFFORT_LADDER.length; d++) {
721
- const lower = EFFORT_LADDER[idx - d];
722
- if (lower && ids.has(lower)) return lower;
723
- const higher = EFFORT_LADDER[idx + d];
724
- if (higher && ids.has(higher)) return higher;
725
- }
726
- return void 0;
727
- }
728
- function coerceAssignments(tasks, perRunnerAllowlist, allowedRunners, modelsByRunner) {
729
- const clampVariant = (task) => {
730
- if (!task.assignedModel || !modelsByRunner) return task;
731
- const model = modelsByRunner[task.assignedRunner]?.find(
732
- (m) => m.modelId === task.assignedModel.modelId
733
- );
734
- if (!model) return task;
735
- const clamped = clampThinkingEffort(task.assignedModel.thinkingEffort, model.variants);
736
- return {
737
- ...task,
738
- assignedModel: {
739
- ...task.assignedModel,
740
- thinkingEffort: clamped,
741
- availableVariants: model.variants.map((v) => v.id)
742
- }
743
- };
744
- };
745
- const allowedFor = (runner) => effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
746
- const assignmentFor = (runner, modelId) => {
747
- const known = modelsByRunner?.[runner]?.find((m) => m.modelId === modelId);
748
- return {
749
- modelId,
750
- modelLabel: known?.modelLabel ?? modelId,
751
- thinkingEffort: void 0,
752
- ...known ? { availableVariants: known.variants.map((v) => v.id) } : {}
753
- };
754
- };
755
- return tasks.map((task) => {
756
- if (task.type === "user") return task;
757
- if (allowedRunners && !allowedRunners.includes(task.assignedRunner)) {
758
- const fallbackRunner = allowedRunners[0];
759
- const fallbackModels = allowedFor(fallbackRunner);
760
- const fallbackModel = fallbackModels?.length ? assignmentFor(fallbackRunner, fallbackModels[0]) : modelsByRunner?.[fallbackRunner]?.[0] ? assignmentFor(fallbackRunner, modelsByRunner[fallbackRunner][0].modelId) : task.assignedModel;
761
- return clampVariant({
762
- ...task,
763
- assignedRunner: fallbackRunner,
764
- assignedModel: fallbackModel
765
- });
766
- }
767
- const allowlist = allowedFor(task.assignedRunner);
768
- if (!task.assignedModel) {
769
- if (allowlist && allowlist.length > 0) {
770
- return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) });
771
- }
772
- const discovered = modelsByRunner?.[task.assignedRunner]?.[0];
773
- if (discovered) {
774
- return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, discovered.modelId) });
775
- }
776
- return task;
777
- }
778
- if (!allowlist || allowlist.length === 0) return clampVariant(task);
779
- if (allowlist.includes(task.assignedModel.modelId)) return clampVariant(task);
780
- return { ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) };
781
- });
782
- }
783
-
784
- // src/taskRow/status.ts
785
- var isTaskRunning = (task) => task.status === "in_progress" || task.status === "running";
786
- var NOT_STARTED = /* @__PURE__ */ new Set(["pending", "approved", "blocked"]);
787
- function taskStatusKind(task) {
788
- switch (task.status) {
789
- case "completed":
790
- return "done";
791
- case "failed":
792
- return "failed";
793
- case "blocked":
794
- return "blocked";
795
- case "awaiting_user":
796
- return "awaiting";
797
- }
798
- if (!isTaskRunning(task)) return "todo";
799
- return task.status === "in_progress" && task.idleSince ? "quiet" : "running";
800
- }
801
- var AWAITING_LABELS = {
802
- input: "Waiting for your input",
803
- checkpoint: "Checkpoint",
804
- conflict: "Merge conflict",
805
- "files-changed": "Changed tracked files"
806
- };
807
- function awaitingLabel(task) {
808
- return task.status === "awaiting_user" && task.awaitingReason ? AWAITING_LABELS[task.awaitingReason] : null;
809
- }
810
- function approvalLabel(count) {
811
- if (!count) return null;
812
- return count > 1 ? `Waiting for approval (${count})` : "Waiting for approval";
813
- }
814
-
815
- // src/services/TaskOps.ts
816
- import { v4 as uuidv4 } from "uuid";
817
-
818
- // src/services/TaskEditValidator.ts
819
- var AI_ONLY_FIELDS = ["assignedModel", "thinkingEffort", "taskMode", "autonomy", "ops"];
820
- function checkModelAndModeValidity(runner, assignedModel, taskMode, catalog) {
821
- if (!catalog) return { ok: true };
822
- if (assignedModel) {
823
- const models = catalog.modelsByRunner[runner];
824
- if (models) {
825
- const modelId = assignedModel.modelId;
826
- const allowed = effectiveAllowlist(catalog.perRunnerAllowlist?.[runner], runner, catalog.modelsByRunner);
827
- if (!models.some((m) => m.modelId === modelId) && !allowed?.includes(modelId)) {
828
- return { ok: false, error: `Runner "${runner}" does not offer model "${modelId}"` };
829
- }
830
- if (allowed && !allowed.includes(modelId)) {
831
- return { ok: false, error: `Model "${modelId}" is excluded by the allowlist for runner "${runner}"` };
832
- }
833
- }
834
- }
835
- if (taskMode !== void 0) {
836
- const modes = catalog.runnerModes[runner];
837
- if (modes && modes.length > 0 && !modes.some((m) => m.id === taskMode)) {
838
- return { ok: false, error: `Runner "${runner}" has no task mode "${taskMode}" \u2014 valid modes: ${modes.map((m) => m.id).join(", ")}` };
839
- }
840
- }
841
- return { ok: true };
842
- }
843
- function typeCoherenceCheck(target, changes) {
844
- if (!changes.type || changes.type === target.type) return null;
845
- if (changes.type === "user") {
846
- if (!Array.isArray(changes.userSteps) || changes.userSteps.length === 0) {
847
- return { ok: false, error: `"${target.title}" cannot become a manual task without user steps \u2014 include "userSteps" in this edit` };
848
- }
849
- return { ok: true, clear: AI_ONLY_FIELDS.filter((f) => target[f] !== void 0) };
850
- }
851
- if (!changes.prompt || !changes.prompt.trim()) {
852
- return { ok: false, error: `"${target.title}" cannot become an AI task without a prompt \u2014 include "prompt" in this edit` };
853
- }
854
- return { ok: true, clear: target.userSteps && target.userSteps.length > 0 ? ["userSteps"] : [] };
855
- }
856
- function opsCheck(tasks, target, changes) {
857
- if (!("ops" in changes) || !!changes.ops === !!target.ops) return null;
858
- if (!NOT_STARTED.has(target.status)) {
859
- return { ok: false, error: `"${target.title}" has started, so whether it is an ops task is fixed` };
860
- }
861
- if (!changes.ops) return null;
862
- if ((changes.type ?? target.type) !== "ai") return { ok: false, error: `"${target.title}" is a manual task \u2014 only an AI task can be an ops task` };
863
- if (tasks.some((t) => t.subtasks?.some((s) => s.id === target.id))) {
864
- return { ok: false, error: `"${target.title}" is a subtask, which runs with its parent \u2014 only a top-level task can be an ops task` };
865
- }
866
- return null;
867
- }
868
- function validateTaskEdit(actor, tasks, taskId, changes, catalog) {
869
- const target = tasks.find((t) => t.id === taskId);
870
- if (!target) return { ok: false, error: "Task not found" };
871
- if (actor === "planner") {
872
- if (target.status === "in_progress") return { ok: false, error: `"${target.title}" is running and cannot be modified` };
873
- if (target.status === "completed") return { ok: false, error: `"${target.title}" is completed and cannot be modified` };
874
- }
875
- if (changes.dependencies) {
876
- const depCheck = canSetDependencies(tasks, taskId, changes.dependencies);
877
- if (!depCheck.ok) return depCheck;
878
- }
879
- const coherence = typeCoherenceCheck(target, changes);
880
- if (coherence && !coherence.ok) return coherence;
881
- const ops = opsCheck(tasks, target, changes);
882
- if (ops) return ops;
883
- const runner = changes.assignedRunner ?? target.assignedRunner;
884
- const validity = checkModelAndModeValidity(runner, changes.assignedModel, changes.taskMode, catalog);
885
- if (!validity.ok) return validity;
886
- return coherence ?? { ok: true };
887
- }
888
-
889
- // src/services/TaskOps.ts
890
- var UPDATABLE_FIELDS = [
891
- "title",
892
- "description",
893
- "prompt",
894
- "dependencies",
895
- "assignedRunner",
896
- "assignedModel",
897
- "taskMode",
898
- "thinkingEffort",
899
- "type",
900
- "userSteps",
901
- "autonomy",
902
- "sliceType",
903
- "ops",
904
- "skills"
905
- ];
906
- function taskOpsProtocol(executionNote = "", tools = false) {
907
- const updateChanges = UPDATABLE_FIELDS.map((f) => f === "dependencies" ? '"dependencies"?:["<id or #order>"]' : `"${f}"?`).join(",");
908
- return [
909
- tools ? "- To modify specific tasks, call edit_plan, never a JSON reply. Its argument is:" : "- To modify specific tasks, reply with ONLY this JSON object:",
910
- tools ? ' {"ops": [' : ' {"taskOps": [',
911
- ` {"op":"update","taskId":"<id or #order>","changes":{${updateChanges}}},`,
912
- ' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?,"ops"?},"handle"?:"<name>"},',
913
- ' {"op":"remove","taskId":"<id or #order>"},',
914
- ' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]}, // only to re-prioritise INDEPENDENT tasks',
915
- ' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...},"handle"?:"<name>"},',
916
- ' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...],"handle"?:"<name>"},',
917
- ` {"op":"rearm","taskId":"<id or #order>","changes"?:{${updateChanges}}}`,
918
- " ]}",
919
- tools ? "- For sweeping changes, you may instead call submit_plan with the full plan." : '- For sweeping changes, you may instead emit a full {"tasks":[...]} plan JSON.',
920
- 'Every "<id or #order>" ref in a batch resolves against the plan shown above, before any op in the batch runs \u2014 an earlier remove/merge/split never shifts what a later "#N" means.',
921
- 'Give "add", "merge", or "split" a "handle" (any name you choose, unused elsewhere in this batch) to let a LATER op in the same batch reference the task it produces \u2014 for "split", the handle names its last part. A handle used before its defining op is rejected.',
922
- `When creating or changing tasks, set "assignedRunner" and "assignedModel" ({"modelId","modelLabel","thinkingEffort"?}) using only the runners and models ${tools ? "list_runners and list_models return" : "listed in <available_models>"}.`,
923
- 'Keep dependencies consistent: no cycles, no references to removed tasks, and never touch running or completed tasks \u2014 "rearm" is the one exception, below.',
924
- ...tools ? ["edit_plan checks the batch when you call it and answers with what it will change, or with the error to fix \u2014 call it again in the same reply. The edit is applied when your reply ends; edits you make in one reply join one batch."] : [],
925
- 'Just declare the dependencies you want \u2014 display order is repaired for you afterwards, so a rewire or a newly added prerequisite never needs a "reorder" op. Only a task that is running or completed cannot be shifted, so an edit that would need one to move is rejected.' + executionNote,
926
- 'Flipping "type" between "ai" and "user" is a content change, not just a label: an update to "user" needs "userSteps" in the SAME op, and an update to "ai" needs "prompt" in the SAME op \u2014 the model/mode/effort/autonomy fields (flipping to "user") or the userSteps (flipping to "ai") are cleared automatically.',
927
- `"skills" in "changes" REPLACES the task's whole skill list. To add one, send the names the plan block shows in its skills:[...] plus the new one; to drop one, send the rest; [] removes them all.`,
928
- '"rearm" puts a failed OR completed task back to pending \u2014 verdict and output summary are cleared, any dependents it had blocked are released, and it may carry field changes (e.g. a corrected "prompt") applied in the same op. A running task cannot be re-armed. Never rearm a task just to relabel it \u2014 only when it should actually run again.'
929
- ];
930
- }
931
- function textHasTaskOps(text2) {
932
- return text2.includes(`"${TASK_OPS_ENVELOPE_KEY}"`);
933
- }
934
- function parseTaskOpsJson(text2) {
935
- const { matches, fallback } = extractObjectsWithKey(text2, TASK_OPS_ENVELOPE_KEY);
936
- const candidates = matches.length > 0 ? [...matches].reverse() : [fallback.json];
937
- let lastError = null;
938
- for (const json of candidates) {
939
- try {
940
- return parseTaskOpsObject(json, text2);
941
- } catch (err) {
942
- if (!(err instanceof PlanParseError)) throw err;
943
- if (!lastError) lastError = err;
944
- }
945
- }
946
- throw lastError ?? new PlanParseError("No JSON object found in task-ops reply", text2);
947
- }
948
- function parseTaskOpsObject(json, text2) {
949
- let parsed;
950
- try {
951
- parsed = JSON.parse(escapeControlCharsInStrings(stripTrailingCommas(json)));
952
- } catch (err) {
953
- throw new PlanParseError(`Task-ops JSON is invalid: ${err instanceof Error ? err.message : String(err)}`, text2);
954
- }
955
- const ops = parsed.taskOps;
956
- if (!Array.isArray(ops) || ops.length === 0) {
957
- throw new PlanParseError('"taskOps" must be a non-empty array', text2);
958
- }
959
- for (const op of ops) {
960
- if (typeof op !== "object" || op === null || typeof op.op !== "string") {
961
- throw new PlanParseError('Every task op needs an "op" field (update | add | remove | reorder | merge | split)', text2);
962
- }
963
- }
964
- return ops;
965
- }
966
- function resolveRef(ref, originalScope, liveScope, handleOwner, handleId, opIndex) {
967
- if (typeof ref !== "string" && typeof ref !== "number") return { error: `invalid reference "${String(ref)}"` };
968
- const s = String(ref).trim();
969
- const ownerIdx = handleOwner.get(s);
970
- if (ownerIdx !== void 0) {
971
- if (ownerIdx >= opIndex) {
972
- return { error: `handle "${s}" is defined later in this batch (op ${ownerIdx + 1}) and cannot be referenced yet` };
973
- }
974
- const id = handleId.get(s);
975
- if (!id) return { error: `handle "${s}" was never resolved` };
976
- return { id };
977
- }
978
- const byLiveId = liveScope.find((t) => t.id === s);
979
- if (byLiveId) return { id: byLiveId.id };
980
- const orderStr = s.startsWith("#") ? s.slice(1) : s;
981
- if (/^\d+$/.test(orderStr)) {
982
- const order = Number(orderStr);
983
- const byOrder = originalScope.filter((t) => t.order === order);
984
- if (byOrder.length === 1) return { id: byOrder[0].id };
985
- }
986
- const byTitle = originalScope.find((t) => t.title === s);
987
- if (byTitle) return { id: byTitle.id };
988
- return { error: `task "${s}" not found` };
989
- }
990
- function taskOpRefs(op) {
991
- switch (op.op) {
992
- case "update":
993
- case "remove":
994
- case "split":
995
- case "rearm":
996
- return [op.taskId];
997
- case "merge":
998
- case "reorder":
999
- return op.taskIds;
1000
- case "add":
1001
- return [];
1002
- }
1003
- }
1004
- function refMatchesTask(ref, task) {
1005
- if (typeof ref !== "string" && typeof ref !== "number") return false;
1006
- const s = String(ref).trim();
1007
- if (task.id === s) return true;
1008
- const orderStr = s.startsWith("#") ? s.slice(1) : s;
1009
- return /^\d+$/.test(orderStr) && task.order === Number(orderStr) || task.title === s;
1010
- }
1011
- function refCollidesWithExisting(name, originalFlatAll) {
1012
- if (originalFlatAll.some((t) => t.id === name)) return true;
1013
- const orderStr = name.startsWith("#") ? name.slice(1) : name;
1014
- if (/^\d+$/.test(orderStr) && originalFlatAll.some((t) => t.order === Number(orderStr))) return true;
1015
- return originalFlatAll.some((t) => t.title === name);
1016
- }
1017
- function updatableChanges(raw) {
1018
- const changes = {};
1019
- for (const key of UPDATABLE_FIELDS) {
1020
- if (raw && key in raw) changes[key] = raw[key];
1021
- }
1022
- if ("ops" in changes) changes.ops = opsFlag(changes.ops);
1023
- if ("skills" in changes) changes.skills = skillNames(changes.skills);
1024
- return changes;
1025
- }
1026
- function withSkillNames(subtasks) {
1027
- if (!Array.isArray(subtasks)) return subtasks;
1028
- return subtasks.map((sub) => {
1029
- if (typeof sub !== "object" || sub === null) return sub;
1030
- const { skills, ...rest } = sub;
1031
- const names = skillNames(skills);
1032
- return {
1033
- ...rest,
1034
- ...names ? { skills: names } : {},
1035
- ..."subtasks" in rest ? { subtasks: withSkillNames(rest.subtasks) } : {}
1036
- };
1037
- });
1038
- }
1039
- function findInTasks(tasks, id) {
1040
- return flattenTasks(tasks).find((t) => t.id === id);
1041
- }
1042
- function findTopLevel(tasks, id) {
1043
- return tasks.find((t) => t.id === id);
1044
- }
1045
- function detectCycle(tasks) {
1046
- const byId = new Map(tasks.map((t) => [t.id, t]));
1047
- const visiting = /* @__PURE__ */ new Set();
1048
- const done = /* @__PURE__ */ new Set();
1049
- const visit = (id) => {
1050
- if (done.has(id)) return null;
1051
- if (visiting.has(id)) return id;
1052
- visiting.add(id);
1053
- for (const dep of byId.get(id)?.dependencies ?? []) {
1054
- const hit = visit(dep);
1055
- if (hit) return hit;
1056
- }
1057
- visiting.delete(id);
1058
- done.add(id);
1059
- return null;
1060
- };
1061
- for (const t of tasks) {
1062
- const hit = visit(t.id);
1063
- if (hit) return byId.get(hit)?.title ?? hit;
1064
- }
1065
- return null;
1066
- }
1067
- function isPinned(t) {
1068
- return t.status === "in_progress" || t.status === "completed";
1069
- }
1070
- function repairOrder(tasks) {
1071
- const byId = new Map(tasks.map((t) => [t.id, t]));
1072
- const placed = /* @__PURE__ */ new Set();
1073
- const movable = tasks.filter((t) => !isPinned(t));
1074
- const result = [];
1075
- const blockerOf = (t) => {
1076
- const id = t.dependencies.find((d) => byId.has(d) && !placed.has(d));
1077
- return id ? byId.get(id) : void 0;
1078
- };
1079
- const place = (t) => {
1080
- result.push(t);
1081
- placed.add(t.id);
1082
- };
1083
- for (const occupant of tasks) {
1084
- if (isPinned(occupant)) {
1085
- const blocker = blockerOf(occupant);
1086
- if (blocker) return { error: pinnedRefusal(blocker, occupant, "before") };
1087
- place(occupant);
1088
- continue;
1089
- }
1090
- const next = movable.findIndex((t) => !blockerOf(t));
1091
- if (next < 0) {
1092
- const [stuck, pin] = trailToPin(movable[0], blockerOf);
1093
- return { error: pinnedRefusal(stuck, pin, "after") };
1094
- }
1095
- place(movable.splice(next, 1)[0]);
1096
- }
1097
- return { tasks: result };
1098
- }
1099
- function trailToPin(task, blockerOf) {
1100
- let at = task;
1101
- for (; ; ) {
1102
- const blocker = blockerOf(at);
1103
- if (!blocker || isPinned(blocker)) return [at, blocker ?? at];
1104
- at = blocker;
1105
- }
1106
- }
1107
- function describeMoves(before, after) {
1108
- const wasAt = new Map(before.map((t, i) => [t.id, i + 1]));
1109
- const moved = after.map((t, i) => ({ t, from: wasAt.get(t.id), to: i + 1 })).filter((m) => m.from !== m.to);
1110
- return moved.length ? moved.map((m) => `"${m.t.title}" #${m.from}\u2192#${m.to}`).join(", ") : null;
1111
- }
1112
- function pinnedRefusal(other, pin, side) {
1113
- const word = pin.status === "in_progress" ? "running" : "completed";
1114
- return `"${other.title}" would have to run ${side} "${pin.title}", which is ${word} and cannot be moved`;
1115
- }
1116
- function canMergeTasks(tasks, selectedIds) {
1117
- if (selectedIds.length < 2) return { ok: false, error: "Select at least two tasks to merge" };
1118
- const idSet = new Set(selectedIds);
1119
- const toMerge = tasks.filter((t) => idSet.has(t.id));
1120
- if (toMerge.length < 2) return { ok: false, error: "At least two valid tasks are required to merge" };
1121
- for (const t of toMerge) {
1122
- if (t.status === "in_progress") return { ok: false, error: `"${t.title}" is running and cannot be merged` };
1123
- if (t.status === "completed") return { ok: false, error: `"${t.title}" is completed and cannot be merged` };
1124
- }
1125
- const ordered = [...toMerge].sort((a, b) => a.order - b.order);
1126
- const survivorId = "__merge_sim__";
1127
- const sim = tasks.filter((t) => !idSet.has(t.id)).map((t) => ({ ...t, dependencies: t.dependencies.map((d) => idSet.has(d) ? survivorId : d).filter((d) => d !== t.id) }));
1128
- const unionDeps = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));
1129
- sim.push({ ...ordered[0], id: survivorId, title: "__merge_sim__", order: ordered[0].order, dependencies: unionDeps });
1130
- const cycle = detectCycle(sim);
1131
- if (cycle) return { ok: false, error: `Merging would create a dependency cycle involving "${cycle}"` };
1132
- return { ok: true };
1133
- }
1134
- function dependentsOf(tasks, taskId) {
1135
- return tasks.filter((t) => t.id !== taskId && t.dependencies.includes(taskId));
1136
- }
1137
- function dependencyCandidates(tasks, taskId) {
1138
- if (!taskId) return [...tasks];
1139
- const target = tasks.find((t) => t.id === taskId);
1140
- return target ? tasks.filter((t) => t.order < target.order) : [];
1141
- }
1142
- function canSetDependencies(tasks, taskId, dependencies) {
1143
- const target = tasks.find((t) => t.id === taskId);
1144
- if (!target) return { ok: false, error: "Task not found" };
1145
- const allowed = new Set(dependencyCandidates(tasks, taskId).map((t) => t.id));
1146
- for (const dep of dependencies) {
1147
- if (dep === taskId) return { ok: false, error: `"${target.title}" cannot depend on itself` };
1148
- const found = tasks.find((t) => t.id === dep);
1149
- if (!found) return { ok: false, error: `Unknown dependency "${dep}"` };
1150
- if (!allowed.has(dep)) {
1151
- return { ok: false, error: `"${target.title}" cannot depend on "${found.title}", which comes after it in the plan` };
1152
- }
1153
- }
1154
- return { ok: true };
1155
- }
1156
- function canSplitTask(tasks, taskId) {
1157
- const target = tasks.find((t) => t.id === taskId);
1158
- if (!target) return { ok: false, error: "Task not found" };
1159
- if (target.status === "in_progress") return { ok: false, error: `"${target.title}" is running and cannot be split` };
1160
- if (target.status === "completed") return { ok: false, error: `"${target.title}" is completed and cannot be split` };
1161
- return { ok: true };
1162
- }
1163
- function applyTaskOps(currentTasks, ops, runners, catalog) {
1164
- let tasks = JSON.parse(JSON.stringify(currentTasks));
1165
- const errors = [];
1166
- const summary = [];
1167
- const originalFlatAll = flattenTasks(currentTasks);
1168
- const handleOwner = /* @__PURE__ */ new Map();
1169
- const handleId = /* @__PURE__ */ new Map();
1170
- for (const [i, op] of ops.entries()) {
1171
- const handle = op.handle;
1172
- if (handle === void 0) continue;
1173
- if (typeof handle !== "string" || !handle.trim()) {
1174
- errors.push(`op ${i + 1} (${op.op}): handle must be a non-empty string`);
1175
- continue;
1176
- }
1177
- if (handleOwner.has(handle)) {
1178
- errors.push(`op ${i + 1} (${op.op}): handle "${handle}" is already used earlier in this batch`);
1179
- continue;
1180
- }
1181
- if (refCollidesWithExisting(handle, originalFlatAll)) {
1182
- errors.push(`op ${i + 1} (${op.op}): handle "${handle}" collides with an existing task reference`);
1183
- continue;
1184
- }
1185
- handleOwner.set(handle, i);
1186
- }
1187
- if (errors.length) return { ok: false, tasks: currentTasks, errors, summary: [] };
1188
- const resolveDeps = (deps, opIndex) => {
1189
- const ids = [];
1190
- const bad = [];
1191
- const liveFlat = flattenTasks(tasks);
1192
- for (const d of Array.isArray(deps) ? deps : []) {
1193
- const r = resolveRef(d, originalFlatAll, liveFlat, handleOwner, handleId, opIndex);
1194
- if ("error" in r) {
1195
- bad.push(`${String(d)} (${r.error})`);
1196
- continue;
1197
- }
1198
- if (!liveFlat.some((t) => t.id === r.id)) {
1199
- bad.push(`${String(d)} (no longer exists in this batch)`);
1200
- continue;
1201
- }
1202
- ids.push(r.id);
1203
- }
1204
- return { ids, bad };
1205
- };
1206
- const prepareEdit = (op, opIndex, label, audience, refuseRunning = false) => {
1207
- const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, opIndex);
1208
- if ("error" in ref) return { error: `${label}: ${ref.error}` };
1209
- const target = findInTasks(tasks, ref.id);
1210
- if (!target) return { error: `${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)` };
1211
- if (refuseRunning && target.status === "in_progress") return { error: `${label}: "${target.title}" is running and cannot be re-armed` };
1212
- const changes = updatableChanges(op.changes);
1213
- if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
1214
- return { error: `${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]` };
1215
- }
1216
- const changesForCheck = { ...changes };
1217
- delete changesForCheck.dependencies;
1218
- const check = validateTaskEdit(audience, flattenTasks(tasks), target.id, changesForCheck, catalog);
1219
- if (!check.ok) return { error: `${label}: ${check.error}` };
1220
- for (const field of check.clear ?? []) changes[field] = void 0;
1221
- if ("dependencies" in changes) {
1222
- const { ids, bad } = resolveDeps(changes.dependencies, opIndex);
1223
- if (bad.length) return { error: `${label}: unknown dependencies: ${bad.join(", ")}` };
1224
- changes.dependencies = ids.filter((id) => id !== target.id);
1225
- }
1226
- return { target, changes, check };
1227
- };
1228
- for (const [i, op] of ops.entries()) {
1229
- const label = `op ${i + 1} (${op.op})`;
1230
- switch (op.op) {
1231
- case "update": {
1232
- const prep = prepareEdit(op, i, label, "planner");
1233
- if ("error" in prep) {
1234
- errors.push(prep.error);
1235
- break;
1236
- }
1237
- const { target, changes, check } = prep;
1238
- if (Object.keys(changes).length === 0) {
1239
- errors.push(`${label}: no valid changes provided`);
1240
- break;
1241
- }
1242
- tasks = updateTaskInPlan(tasks, target.id, changes);
1243
- const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
1244
- summary.push(`Updated "${target.title}" (${Object.keys(changes).join(", ")})${clearedNote}`);
1245
- break;
1246
- }
1247
- case "add": {
1248
- const spec = op.task ?? {};
1249
- if (!spec.title) {
1250
- errors.push(`${label}: new task needs a title`);
1251
- break;
1252
- }
1253
- const { ids, bad } = resolveDeps(spec.dependencies, i);
1254
- if (bad.length) {
1255
- errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
1256
- break;
1257
- }
1258
- const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : runners[0];
1259
- const validity = checkModelAndModeValidity(runner, spec.assignedModel, spec.taskMode, catalog);
1260
- if (!validity.ok) {
1261
- errors.push(`${label}: ${validity.error}`);
1262
- break;
1263
- }
1264
- const newId = uuidv4();
1265
- tasks = addTaskToPlan(tasks, {
1266
- ...spec,
1267
- subtasks: withSkillNames(spec.subtasks),
1268
- id: newId,
1269
- dependencies: ids,
1270
- assignedRunner: runner,
1271
- description: spec.description ?? spec.title,
1272
- prompt: spec.prompt ?? spec.description ?? spec.title
1273
- });
1274
- if (op.handle) handleId.set(op.handle, newId);
1275
- summary.push(`Added "${spec.title}"`);
1276
- break;
1277
- }
1278
- case "remove": {
1279
- const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
1280
- if ("error" in ref) {
1281
- errors.push(`${label}: ${ref.error}`);
1282
- break;
1283
- }
1284
- const target = findInTasks(tasks, ref.id);
1285
- if (!target) {
1286
- errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
1287
- break;
1288
- }
1289
- if (target.status === "in_progress") {
1290
- errors.push(`${label}: "${target.title}" is running and cannot be removed`);
1291
- break;
1292
- }
1293
- if (target.status === "completed") {
1294
- errors.push(`${label}: "${target.title}" is completed and cannot be removed`);
1295
- break;
1296
- }
1297
- tasks = removeTaskFromPlan(tasks, target.id);
1298
- summary.push(`Removed "${target.title}"`);
1299
- break;
1300
- }
1301
- case "reorder": {
1302
- const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
1303
- const badRef = refs.find((r) => "error" in r);
1304
- if (badRef) {
1305
- errors.push(`${label}: ${badRef.error}`);
1306
- break;
1307
- }
1308
- const resolved = refs.map((r) => findTopLevel(tasks, r.id));
1309
- if (resolved.some((t) => !t)) {
1310
- errors.push(`${label}: unknown task in reorder list`);
1311
- break;
1312
- }
1313
- const orderIds = resolved.map((t) => t.id);
1314
- if (new Set(orderIds).size !== tasks.length) {
1315
- errors.push(`${label}: reorder must list every top-level task exactly once (${tasks.length} tasks)`);
1316
- break;
1317
- }
1318
- const byId = new Map(tasks.map((t) => [t.id, t]));
1319
- tasks = renumberTasks(orderIds.map((id) => byId.get(id)));
1320
- summary.push("Reordered tasks");
1321
- break;
1322
- }
1323
- case "merge": {
1324
- const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
1325
- const badRef = refs.find((r) => "error" in r);
1326
- if (badRef) {
1327
- errors.push(`${label}: ${badRef.error}`);
1328
- break;
1329
- }
1330
- const resolved = refs.map((r) => findTopLevel(tasks, r.id));
1331
- if (resolved.some((t) => !t)) {
1332
- errors.push(`${label}: unknown task in merge list (already removed, merged, or split by an earlier op)`);
1333
- break;
1334
- }
1335
- const toMerge = [...new Map(resolved.map((t) => [t.id, t])).values()].sort((a, b) => a.order - b.order);
1336
- if (toMerge.length < 2) {
1337
- errors.push(`${label}: merge needs at least two tasks`);
1338
- break;
1339
- }
1340
- const locked = toMerge.find((t) => t.status === "in_progress" || t.status === "completed");
1341
- if (locked) {
1342
- errors.push(`${label}: "${locked.title}" is ${locked.status === "in_progress" ? "running" : "completed"} and cannot be merged`);
1343
- break;
1344
- }
1345
- const idSet = new Set(toMerge.map((t) => t.id));
1346
- const unionDepsRaw = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));
1347
- const { ids, bad } = resolveDeps(unionDepsRaw, i);
1348
- if (bad.length) {
1349
- errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
1350
- break;
1351
- }
1352
- const spec = op.merged ?? {};
1353
- if (!spec.title) {
1354
- errors.push(`${label}: merged task needs a title`);
1355
- break;
1356
- }
1357
- const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : toMerge[0].assignedRunner;
1358
- const mergedTask = createTask({
1359
- title: spec.title,
1360
- description: spec.description ?? toMerge.map((t) => t.description).filter(Boolean).join("\n\n"),
1361
- prompt: spec.prompt ?? (toMerge.map((t) => t.prompt).filter(Boolean).join("\n\n---\n\n") || void 0),
1362
- type: spec.type ?? (toMerge.some((t) => t.type === "user") ? "user" : "ai"),
1363
- dependencies: ids,
1364
- assignedRunner: runner,
1365
- assignedModel: spec.assignedModel ?? toMerge.find((t) => t.assignedModel)?.assignedModel,
1366
- thinkingEffort: spec.thinkingEffort ?? toMerge.find((t) => t.thinkingEffort)?.thinkingEffort,
1367
- taskMode: spec.taskMode ?? toMerge[0].taskMode,
1368
- autonomy: spec.autonomy ?? toMerge.find((t) => t.autonomy)?.autonomy,
1369
- sliceType: spec.sliceType ?? toMerge.find((t) => t.sliceType)?.sliceType,
1370
- ops: inheritedOps(toMerge, spec.ops),
1371
- skills: inheritedSkills(spec.skills, toMerge.flatMap((t) => t.skills ?? [])),
1372
- userStoriesCovered: spec.userStoriesCovered ?? (toMerge.flatMap((t) => t.userStoriesCovered ?? []).length ? [...new Set(toMerge.flatMap((t) => t.userStoriesCovered ?? []))] : void 0)
1373
- });
1374
- const mergedId = mergedTask.id;
1375
- const firstId = toMerge[0].id;
1376
- const result = [];
1377
- let inserted = false;
1378
- for (const t of tasks) {
1379
- if (idSet.has(t.id)) {
1380
- if (!inserted && t.id === firstId) {
1381
- result.push(mergedTask);
1382
- inserted = true;
1383
- }
1384
- continue;
1385
- }
1386
- result.push({ ...t, dependencies: t.dependencies.map((d) => idSet.has(d) ? mergedId : d) });
1387
- }
1388
- if (!inserted) result.push(mergedTask);
1389
- tasks = renumberTasks(result);
1390
- if (op.handle) handleId.set(op.handle, mergedId);
1391
- summary.push(`Merged ${toMerge.length} tasks into "${spec.title}"`);
1392
- break;
1393
- }
1394
- case "split": {
1395
- const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, i);
1396
- if ("error" in ref) {
1397
- errors.push(`${label}: ${ref.error}`);
1398
- break;
1399
- }
1400
- const target = findTopLevel(tasks, ref.id);
1401
- if (!target) {
1402
- errors.push(`${label}: task "${op.taskId}" not found`);
1403
- break;
1404
- }
1405
- if (target.status === "in_progress") {
1406
- errors.push(`${label}: "${target.title}" is running and cannot be split`);
1407
- break;
1408
- }
1409
- if (target.status === "completed") {
1410
- errors.push(`${label}: "${target.title}" is completed and cannot be split`);
1411
- break;
1412
- }
1413
- const parts = op.parts ?? [];
1414
- if (parts.length < 2) {
1415
- errors.push(`${label}: split needs at least two parts`);
1416
- break;
1417
- }
1418
- const newTasks = [];
1419
- let prevId;
1420
- let buildError = false;
1421
- for (let p = 0; p < parts.length; p++) {
1422
- const spec = parts[p];
1423
- if (!spec || !spec.title) {
1424
- errors.push(`${label}: split part ${p + 1} needs a title`);
1425
- buildError = true;
1426
- break;
1427
- }
1428
- let deps;
1429
- if (p === 0) {
1430
- const { ids, bad } = resolveDeps(target.dependencies, i);
1431
- if (bad.length) {
1432
- errors.push(`${label}: split part 1 unknown dependencies: ${bad.join(", ")}`);
1433
- buildError = true;
1434
- break;
1435
- }
1436
- deps = ids;
1437
- } else {
1438
- deps = prevId ? [prevId] : [];
1439
- }
1440
- const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : target.assignedRunner;
1441
- const nt = createTask({
1442
- title: spec.title,
1443
- description: spec.description ?? spec.title,
1444
- prompt: spec.prompt ?? spec.description ?? spec.title,
1445
- type: spec.type ?? target.type,
1446
- dependencies: deps,
1447
- assignedRunner: runner,
1448
- assignedModel: spec.assignedModel ?? target.assignedModel,
1449
- taskMode: spec.taskMode ?? target.taskMode,
1450
- autonomy: spec.autonomy ?? target.autonomy,
1451
- sliceType: spec.sliceType ?? target.sliceType,
1452
- ops: inheritedOps([target], spec.ops),
1453
- skills: inheritedSkills(spec.skills, target.skills),
1454
- userStoriesCovered: spec.userStoriesCovered ?? target.userStoriesCovered
1455
- });
1456
- newTasks.push(nt);
1457
- prevId = nt.id;
1458
- }
1459
- if (buildError) break;
1460
- const tailId = newTasks[newTasks.length - 1].id;
1461
- const result = [];
1462
- for (const t of tasks) {
1463
- if (t.id === target.id) {
1464
- result.push(...newTasks);
1465
- continue;
1466
- }
1467
- result.push({ ...t, dependencies: t.dependencies.map((d) => d === target.id ? tailId : d) });
1468
- }
1469
- tasks = renumberTasks(result);
1470
- if (op.handle) handleId.set(op.handle, tailId);
1471
- summary.push(`Split "${target.title}" into ${newTasks.length} tasks`);
1472
- break;
1473
- }
1474
- case "rearm": {
1475
- const prep = prepareEdit(op, i, label, "direct", true);
1476
- if ("error" in prep) {
1477
- errors.push(prep.error);
1478
- break;
1479
- }
1480
- const { target, changes, check } = prep;
1481
- tasks = updateTaskInPlan(tasks, target.id, { ...changes, status: "pending", verdict: void 0, outputSummary: void 0, awaitingReason: void 0 });
1482
- for (const dep of flattenTasks(tasks)) {
1483
- if (dep.status === "blocked" && dep.dependencies.includes(target.id)) {
1484
- tasks = updateTaskInPlan(tasks, dep.id, { status: "pending" });
1485
- }
1486
- }
1487
- const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
1488
- const changedNote = Object.keys(changes).length ? ` (${Object.keys(changes).join(", ")})` : "";
1489
- summary.push(`Re-armed "${target.title}"${changedNote}${clearedNote}`);
1490
- break;
1491
- }
1492
- default:
1493
- errors.push(`${label}: unknown op "${op.op}"`);
1494
- }
1495
- if (errors.length) break;
1496
- }
1497
- if (!errors.length) {
1498
- const flat = flattenTasks(tasks);
1499
- const idSet = new Set(flat.map((t) => t.id));
1500
- for (const t of flat) {
1501
- for (const dep of t.dependencies) {
1502
- if (!idSet.has(dep)) errors.push(`"${t.title}" depends on a task that no longer exists (${dep})`);
1503
- }
1504
- }
1505
- const cycleAt = detectCycle(flat);
1506
- if (cycleAt) errors.push(`Dependency cycle detected involving "${cycleAt}"`);
1507
- if (!errors.length) {
1508
- const repaired = repairOrder(tasks);
1509
- if ("error" in repaired) {
1510
- errors.push(repaired.error);
1511
- } else {
1512
- const moves = describeMoves(tasks, repaired.tasks);
1513
- if (moves) summary.push(`Reordered to keep dependencies first: ${moves}`);
1514
- tasks = renumberTasks(repaired.tasks);
1515
- }
1516
- }
1517
- }
1518
- return errors.length ? { ok: false, tasks: currentTasks, errors, summary: [] } : { ok: true, tasks, errors: [], summary };
1519
- }
1520
-
1521
808
  // src/services/researchStepSummary.ts
1522
809
  function ellipsize(text2, max) {
1523
810
  const oneLine2 = text2.replace(/\s+/g, " ").trim();
@@ -1580,13 +867,51 @@ function summarizeToolCall(tool, argsJson, toolLabel) {
1580
867
  }
1581
868
  function classifyOutcome(success, output) {
1582
869
  if (!success) {
1583
- if (/^Command refused:/.test(output) || /is not available\. You are a read-only research subagent/.test(output)) return "refused";
870
+ if (/^(Command|Fetch|Web search) refused:/.test(output) || /is not available\. You are a read-only research subagent/.test(output)) return "refused";
1584
871
  if (/not approved/i.test(output) || /^Access denied:/.test(output) || /needs user approval/i.test(output)) return "denied";
1585
872
  return "failure";
1586
873
  }
1587
874
  return "success";
1588
875
  }
1589
876
 
877
+ // src/services/blocking.ts
878
+ var UNSTARTED = /* @__PURE__ */ new Set(["pending", "approved", "blocked"]);
879
+ function blockingFailures(tasks) {
880
+ const byId = /* @__PURE__ */ new Map();
881
+ const index = (list) => {
882
+ for (const t of list) {
883
+ byId.set(t.id, t);
884
+ index(t.subtasks ?? []);
885
+ }
886
+ };
887
+ index(tasks);
888
+ const memo = /* @__PURE__ */ new Map();
889
+ const failuresOf = (id, visiting) => {
890
+ const known = memo.get(id);
891
+ if (known) return known;
892
+ const task = byId.get(id);
893
+ if (!task || !UNSTARTED.has(task.status) || visiting.has(id)) return [];
894
+ visiting.add(id);
895
+ const failed = /* @__PURE__ */ new Set();
896
+ for (const depId of task.dependencies) {
897
+ const dep = byId.get(depId);
898
+ if (!dep || dep.status === "completed" || dep.status === "skipped") continue;
899
+ if (dep.status === "failed") failed.add(depId);
900
+ else for (const root of failuresOf(depId, visiting)) failed.add(root);
901
+ }
902
+ visiting.delete(id);
903
+ const result = [...failed];
904
+ memo.set(id, result);
905
+ return result;
906
+ };
907
+ const blocked = /* @__PURE__ */ new Map();
908
+ for (const id of byId.keys()) {
909
+ const failures = failuresOf(id, /* @__PURE__ */ new Set());
910
+ if (failures.length > 0) blocked.set(id, failures);
911
+ }
912
+ return blocked;
913
+ }
914
+
1590
915
  // src/services/continuation.ts
1591
916
  function continuability(task) {
1592
917
  const refuse = (reason) => ({ ok: false, reason: `Task "${task.title}" cannot be continued: ${reason}` });
@@ -1594,7 +919,8 @@ function continuability(task) {
1594
919
  if (task.status === "awaiting_user" && task.awaitingReason === "conflict") {
1595
920
  return refuse("its work is waiting on a merge conflict. Resolve or repair the conflict instead.");
1596
921
  }
1597
- if (task.status !== "completed" && task.status !== "failed") return refuse("only a completed or failed task can be.");
922
+ const finished = task.status === "completed" || task.status === "failed" || task.status === "skipped";
923
+ if (!finished && !pausedOnLimit(task)) return refuse("only a completed, failed or skipped task, or one paused on a usage limit, can be.");
1598
924
  if (!task.runnerSessionId) return refuse("its runner left no saved session to resume. Use Retry instead.");
1599
925
  return { ok: true, sessionId: task.runnerSessionId };
1600
926
  }
@@ -1602,6 +928,24 @@ function canContinue(task) {
1602
928
  return continuability(task).ok;
1603
929
  }
1604
930
 
931
+ // src/services/runSettingsText.ts
932
+ var SUPERVISOR_LEVEL_LABEL = { off: "Off", watch: "Watch", act: "Act" };
933
+ var plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
934
+ var onOff = (value) => value ? "on" : "off";
935
+ var supervisorSummary = (run) => `Supervisor: ${SUPERVISOR_LEVEL_LABEL[run.supervisorLevel]}, ${plural(run.supervisorRetryLimit, "retry", "retries")} per task, ${plural(run.wakeUpReadBudget, "read", "reads")} per wake-up`;
936
+ var automergeSummary = (run) => `Merge automatically (gates, end of run): ${onOff(run.mergeAtGatesAutomatically)}`;
937
+ var autorunSummary = (run) => `Autorun: ${onOff(run.autorun)}`;
938
+ var ACT_RISK = "Tasks it adds run too, ops tasks included, which can act on real systems unattended.";
939
+ var AUTORUN_RISK = "Plans start without review, ops tasks included, which can act on real systems unattended. In Full, the planner may also start tasks it adds later.";
940
+ function parseSwitch(arg) {
941
+ return arg === "on" ? true : arg === "off" ? false : null;
942
+ }
943
+ function parseCount(arg, min) {
944
+ if (arg === void 0 || !/^\d+$/.test(arg)) return null;
945
+ const n = Number(arg);
946
+ return n >= min ? n : null;
947
+ }
948
+
1605
949
  // src/services/SessionMessage.ts
1606
950
  function serializeTask(t) {
1607
951
  return {
@@ -1609,6 +953,7 @@ function serializeTask(t) {
1609
953
  order: t.order,
1610
954
  title: t.title,
1611
955
  type: t.type,
956
+ status: t.status,
1612
957
  description: t.description,
1613
958
  dependencies: t.dependencies || [],
1614
959
  assignedRunner: t.assignedRunner,
@@ -1617,12 +962,16 @@ function serializeTask(t) {
1617
962
  prompt: t.prompt || null,
1618
963
  subtasks: (t.subtasks || []).map(serializeTask),
1619
964
  userSteps: t.userSteps || void 0,
1620
- thinkingEffort: t.thinkingEffort || void 0,
1621
965
  autonomy: t.autonomy || void 0,
1622
966
  sliceType: t.sliceType || void 0,
1623
967
  userStoriesCovered: t.userStoriesCovered || void 0,
968
+ ...t.status === "awaiting_user" && t.awaitingReason ? { awaitingReason: t.awaitingReason } : {},
969
+ ...t.status === "awaiting_user" && t.awaitingReason === "usage-limit" && t.resumeAt ? { resumeAt: t.resumeAt } : {},
970
+ ...t.status === "skipped" && t.skipReason ? { skipReason: t.skipReason } : {},
1624
971
  ...t.ops ? { ops: true } : {},
1625
- ...t.skills?.length ? { skills: [...t.skills] } : {}
972
+ ...t.attemptUsage ? { attemptUsage: structuredClone(t.attemptUsage) } : {},
973
+ ...t.skills?.length ? { skills: [...t.skills] } : {},
974
+ ...t.addedBy ? { addedBy: t.addedBy } : {}
1626
975
  };
1627
976
  }
1628
977
  function serializeTaskStatus(t, idleSince = null, isolation = null, queued = [], awaitingApproval = 0, mergeGate = [], checkpoint = "") {
@@ -1631,9 +980,12 @@ function serializeTaskStatus(t, idleSince = null, isolation = null, queued = [],
1631
980
  status: t.status,
1632
981
  verdict: t.verdict ? { outcome: t.verdict.outcome, reason: t.verdict.reason, checks: t.verdict.checks || [] } : null,
1633
982
  idleSince,
983
+ ...t.attemptUsage ? { attemptUsage: structuredClone(t.attemptUsage) } : {},
1634
984
  ...isolation ? { isolation } : {},
1635
985
  ...t.status === "awaiting_user" && t.awaitingReason ? { awaitingReason: t.awaitingReason } : {},
986
+ ...t.status === "awaiting_user" && t.awaitingReason === "usage-limit" && t.resumeAt ? { resumeAt: t.resumeAt } : {},
1636
987
  ...t.status === "awaiting_user" && t.awaitingReason === "checkpoint" && checkpoint ? { checkpoint } : {},
988
+ ...t.status === "skipped" && t.skipReason ? { skipReason: t.skipReason } : {},
1637
989
  ...queued.length > 0 ? { queued: queued.map((m) => ({ ...m })) } : {},
1638
990
  ...canContinue(t) ? { continuable: true } : {},
1639
991
  ...awaitingApproval > 0 ? { awaitingApproval } : {},
@@ -1644,8 +996,8 @@ function serializeTaskStatus(t, idleSince = null, isolation = null, queued = [],
1644
996
  function surfaceConversation(history) {
1645
997
  return history?.map(({ skill, ...m }) => skill ? { ...m, skill: skillLoadNotice(skill) } : m);
1646
998
  }
1647
- function surfaceQueued(queued) {
1648
- return queued?.map(({ skills, ...m }) => skills ? { ...m, skills: skills.map(skillLoadNotice) } : m);
999
+ function surfacePendingTurns(pending) {
1000
+ return pending?.map(({ skills, ...t }) => skills ? { ...t, skills: skills.map(skillLoadNotice) } : t);
1649
1001
  }
1650
1002
  function surfaceTask({ attemptSkills, subtasks, runnerSessionId: _runnerSessionId, ...task }) {
1651
1003
  return {
@@ -1654,12 +1006,12 @@ function surfaceTask({ attemptSkills, subtasks, runnerSessionId: _runnerSessionI
1654
1006
  subtasks: (subtasks ?? []).map(surfaceTask)
1655
1007
  };
1656
1008
  }
1657
- function surfacePlan(plan) {
1009
+ function surfacePlan({ queuedMessages: _carriedOverAtLoad, ...plan }) {
1658
1010
  return {
1659
1011
  ...plan,
1660
1012
  tasks: plan.tasks.map(surfaceTask),
1661
1013
  conversationHistory: surfaceConversation(plan.conversationHistory),
1662
- queuedMessages: surfaceQueued(plan.queuedMessages),
1014
+ pendingTurns: surfacePendingTurns(plan.pendingTurns),
1663
1015
  researchLog: plan.researchLog?.map((e) => "type" in e ? e : surfaceStep(e))
1664
1016
  };
1665
1017
  }
@@ -1671,16 +1023,19 @@ function serializePlan(plan) {
1671
1023
  tasks: plan.tasks.map(serializeTask),
1672
1024
  runners: plan.runners,
1673
1025
  generatedAt: plan.generatedAt,
1026
+ runSettings: plan.runSettings,
1674
1027
  conversationHistory: surfaceConversation(plan.conversationHistory),
1675
- prdMarkdown: plan.prdMarkdown,
1676
- queuedMessages: surfaceQueued(plan.queuedMessages)
1028
+ prdMarkdown: plan.prdMarkdown
1677
1029
  };
1678
1030
  }
1679
1031
  function executionSummary(tasks) {
1032
+ const blocked = blockingFailures(tasks);
1680
1033
  return {
1681
1034
  total: tasks.length,
1682
1035
  completed: tasks.filter((t) => t.status === "completed").length,
1683
- failed: tasks.filter((t) => t.status === "failed").length
1036
+ failed: tasks.filter((t) => t.status === "failed").length,
1037
+ blocked: tasks.filter((t) => t.status === "blocked" || blocked.has(t.id)).length,
1038
+ skipped: tasks.filter((t) => t.status === "skipped").length
1684
1039
  };
1685
1040
  }
1686
1041
  var CHECKPOINT_TRUNCATE_LENGTH = 120;
@@ -1690,6 +1045,152 @@ function truncateCheckpointSummary(summary) {
1690
1045
  return firstLine.slice(0, CHECKPOINT_TRUNCATE_LENGTH - 1) + "\u2026";
1691
1046
  }
1692
1047
 
1048
+ // src/services/runView.ts
1049
+ var EMPTY_RUN = { tasks: [], runMerge: null, handoff: null, mergeResult: null };
1050
+ function split({ verdict, idleSince, isolation, checkpoint, queued, continuable, awaitingApproval, mergeGate, forcedPastGate, ...plan }) {
1051
+ return { plan, status: { verdict, idleSince, isolation, checkpoint, queued, continuable, awaitingApproval, mergeGate, forcedPastGate } };
1052
+ }
1053
+ function refresh(previous, tasks) {
1054
+ const held = /* @__PURE__ */ new Map();
1055
+ const index = (list) => list.forEach((t) => {
1056
+ held.set(t.id, t);
1057
+ index(t.subtasks);
1058
+ });
1059
+ index(previous);
1060
+ const fold = ({ subtasks, ...task }) => {
1061
+ const was = held.get(task.id);
1062
+ const { checkpoint, ...status } = was ? split(was).status : {};
1063
+ const waits = task.status === "awaiting_user" && task.awaitingReason === "checkpoint";
1064
+ return { ...task, ...status, ...waits && checkpoint ? { checkpoint } : {}, subtasks: subtasks.map(fold) };
1065
+ };
1066
+ return tasks.map(fold);
1067
+ }
1068
+ function withStatus({ awaitingReason: _reason, resumeAt: _resumeAt, skipReason: _skipped, attemptUsage: _usage, ...plan }, { id: _id, ...status }) {
1069
+ return { ...plan, ...status };
1070
+ }
1071
+ function applyStatus(tasks, updates) {
1072
+ return tasks.map((task) => {
1073
+ const subtasks = applyStatus(task.subtasks, updates);
1074
+ const update = updates.get(task.id);
1075
+ return update ? { ...withStatus(split(task).plan, update), subtasks } : { ...task, subtasks };
1076
+ });
1077
+ }
1078
+ function changeTask(tasks, taskId, change) {
1079
+ let changed = false;
1080
+ const next = tasks.map((task) => {
1081
+ const updated = task.id === taskId ? change(task) : task;
1082
+ const subtasks = changeTask(updated.subtasks, taskId, change);
1083
+ if (updated === task && subtasks === task.subtasks) return task;
1084
+ changed = true;
1085
+ return { ...updated, subtasks };
1086
+ });
1087
+ return changed ? next : tasks;
1088
+ }
1089
+ var PATCHABLE = /* @__PURE__ */ new Set([
1090
+ "order",
1091
+ "title",
1092
+ "type",
1093
+ "status",
1094
+ "description",
1095
+ "dependencies",
1096
+ "assignedRunner",
1097
+ "assignedModel",
1098
+ "taskMode",
1099
+ "prompt",
1100
+ "userSteps",
1101
+ "autonomy",
1102
+ "sliceType",
1103
+ "userStoriesCovered",
1104
+ "awaitingReason",
1105
+ "resumeAt",
1106
+ "skipReason",
1107
+ "ops",
1108
+ "skills",
1109
+ "addedBy",
1110
+ "attemptUsage"
1111
+ ]);
1112
+ function patchTask(task, changes) {
1113
+ const fields = Object.entries(changes).filter(([key]) => PATCHABLE.has(key));
1114
+ if (fields.length === 0) return task;
1115
+ return { ...task, ...Object.fromEntries(fields) };
1116
+ }
1117
+ function startTask(task) {
1118
+ return task.status === "in_progress" ? task : running(task);
1119
+ }
1120
+ function running({ awaitingReason: _reason, resumeAt: _resumeAt, skipReason: _skipped, checkpoint: _checkpoint, ...task }) {
1121
+ return { ...task, status: "in_progress" };
1122
+ }
1123
+ function handoffOf({ type: _type, ...handoff }) {
1124
+ return handoff;
1125
+ }
1126
+ function planTasks(plan) {
1127
+ if ("tasks" in plan) return plan.tasks;
1128
+ return rejoinPlanTree(plan.pendingTasks, plan.phase === "executing" ? plan.executionLog : []);
1129
+ }
1130
+ function reduceRun(view, input) {
1131
+ switch (input.type) {
1132
+ case "plan_generated":
1133
+ return { ...view, tasks: refresh(view.tasks, input.plan.tasks), runSettings: input.plan.runSettings };
1134
+ case "plan_snapshot":
1135
+ return {
1136
+ ...view,
1137
+ tasks: refresh(view.tasks, planTasks(input.plan).map(serializeTask)),
1138
+ // A saved plan's phase-tagged form has no run settings to say.
1139
+ runSettings: "tasks" in input.plan ? input.plan.runSettings : view.runSettings
1140
+ };
1141
+ case "status_update":
1142
+ return {
1143
+ ...view,
1144
+ tasks: applyStatus(view.tasks, new Map(input.tasks.map((t) => [t.id, t]))),
1145
+ runMerge: input.runMerge ?? null,
1146
+ usage: input.usage
1147
+ };
1148
+ case "task_started":
1149
+ case "task_updated": {
1150
+ const tasks = changeTask(view.tasks, input.taskId, (task) => input.type === "task_started" ? startTask(task) : patchTask(task, input.changes));
1151
+ return tasks === view.tasks ? view : { ...view, tasks };
1152
+ }
1153
+ case "isolation_handoff":
1154
+ return { ...view, handoff: handoffOf(input) };
1155
+ case "isolation_merge":
1156
+ return input.result.outcome === "merged" ? { ...view, mergeResult: input.result, handoff: null, runMerge: null } : { ...view, mergeResult: input.result };
1157
+ // A checkpoint's whole question and every status `review_needed` implies
1158
+ // arrive in the status update that follows; `review_needed`'s own list is
1159
+ // flat, so as a plan it would lift subtasks to the top level.
1160
+ case "checkpoint":
1161
+ case "review_needed":
1162
+ case "review_approved":
1163
+ case "execution_complete":
1164
+ case "execution_stopped":
1165
+ case "execution_action":
1166
+ case "task_output":
1167
+ case "task_log":
1168
+ case "isolation_blocked":
1169
+ case "planner_message":
1170
+ case "planner_turn_started":
1171
+ case "planner_skill_loaded":
1172
+ case "planner_turn_ended":
1173
+ case "planner_text_delta":
1174
+ case "planner_thinking_delta":
1175
+ case "planner_text_retracted":
1176
+ case "planner_usage":
1177
+ case "planner_liveness":
1178
+ case "subagent_started":
1179
+ case "subagent_finished":
1180
+ case "research_step":
1181
+ case "research_step_done":
1182
+ case "plan_token":
1183
+ case "approval_request":
1184
+ case "approval_settled":
1185
+ case "approval_decided":
1186
+ return view;
1187
+ default: {
1188
+ const unhandled = input;
1189
+ return unhandled;
1190
+ }
1191
+ }
1192
+ }
1193
+
1693
1194
  // src/conversation/reduce.ts
1694
1195
  var EMPTY_CONVERSATION = { blocks: [], nextId: 1 };
1695
1196
  function isUnsettledSegment(block, turnId) {
@@ -1702,16 +1203,55 @@ function finalSegmentIndex(blocks, turnId, pastPlan = false) {
1702
1203
  for (let i = blocks.length - 1; i >= 0; i--) {
1703
1204
  const block = blocks[i];
1704
1205
  if (block.type === "usage" || block.type === "thinking" || block.type === "approval" || pastPlan && block.type === "plan") continue;
1206
+ if (block.type === "command" || block.type === "compaction" || block.type === "runner_loaded") continue;
1705
1207
  if (block.turnId !== turnId) continue;
1706
1208
  return isUnsettledSegment(block, turnId) ? i : -1;
1707
1209
  }
1708
1210
  return -1;
1709
1211
  }
1710
- function openTurn(view, { turnId, prompt = "", skills = [] }) {
1212
+ function openTurn(view, { turnId, prompt = "", skills = [], wakeUps = [], forced }) {
1213
+ const adopted = takePendingInputs(view, prompt, wakeUps, turnId);
1214
+ view = adopted.view;
1215
+ wakeUps = wakeUps.slice(adopted.wakeUps);
1216
+ if (wakeUps.length === 0) return adopted.prompt || prompt === "" && adopted.wakeUps > 0 ? view : promptLine(view, turnId, prompt, skills, forced);
1217
+ const said = prompt === "" || adopted.prompt ? view : promptLine(view, turnId, prompt, skills, forced);
1218
+ const at = prompt === "" ? said.blocks.length : findLastIndex(said.blocks, (b) => b.type === "message" && b.role === "user" && b.turnId === turnId);
1219
+ const updates = wakeUps.map((text2, n) => settledMessage(`b${said.nextId + n}`, "wake_up", text2, turnId));
1220
+ return { ...said, blocks: [...said.blocks.slice(0, at), ...updates, ...said.blocks.slice(at)], nextId: said.nextId + updates.length };
1221
+ }
1222
+ function takePendingInputs(view, prompt, wakeUps, turnId) {
1223
+ const pending = view.pendingInputs ?? [];
1224
+ const users = pending.filter(({ input }) => input.source === "user");
1225
+ const updates = pending.filter(({ input }) => input.source === "supervisor");
1226
+ const taken = /* @__PURE__ */ new Set();
1227
+ let matchedPrompt = false;
1228
+ for (let n = 1; prompt !== void 0 && n <= users.length; n++) {
1229
+ if (users.slice(0, n).map(({ input }) => input.text).join("\n\n") !== prompt) continue;
1230
+ for (const { input } of users.slice(0, n)) taken.add(input.id);
1231
+ matchedPrompt = true;
1232
+ break;
1233
+ }
1234
+ let matchedUpdates = 0;
1235
+ while (matchedUpdates < wakeUps.length && updates[matchedUpdates]?.input.text === wakeUps[matchedUpdates]) {
1236
+ taken.add(updates[matchedUpdates++].input.id);
1237
+ }
1238
+ if (taken.size === 0) return { view, prompt: false, wakeUps: 0 };
1239
+ const blockIds = new Set(pending.filter(({ input }) => taken.has(input.id)).flatMap(({ blockIds: blockIds2 }) => blockIds2));
1240
+ return {
1241
+ view: {
1242
+ ...view,
1243
+ blocks: turnId === void 0 ? view.blocks : view.blocks.map((block) => blockIds.has(block.id) ? { ...block, turnId } : block),
1244
+ pendingInputs: pending.filter(({ input }) => !taken.has(input.id))
1245
+ },
1246
+ prompt: matchedPrompt,
1247
+ wakeUps: matchedUpdates
1248
+ };
1249
+ }
1250
+ function promptLine(view, turnId, prompt, skills, forced) {
1711
1251
  const i = findLastIndex(view.blocks, (b) => b.type === "message" && b.role === "user");
1712
1252
  const sent = view.blocks[i];
1713
1253
  const adopted = sent?.type === "message" && sent.turnId === void 0 && sent.text === prompt;
1714
- const said = adopted ? { ...view, blocks: replaceAt(view.blocks, i, userMessage(sent.id, prompt, skills, turnId)) } : append(view, (id) => userMessage(id, prompt, skills, turnId));
1254
+ const said = adopted ? { ...view, blocks: replaceAt(view.blocks, i, userMessage(sent.id, prompt, skills, { turnId, forced })) } : append(view, (id) => userMessage(id, prompt, skills, { turnId, forced }));
1715
1255
  if (skills.length === 0) return said;
1716
1256
  const at = adopted ? i + 1 : findLastIndex(said.blocks, (b) => b.type === "message" && b.role === "user") + 1;
1717
1257
  const loads = skills.map((skill, n) => skillLoadBlock(`b${said.nextId + n}`, skill, turnId));
@@ -1729,14 +1269,15 @@ function dropBuildingPlan(view, turnId, segmentId) {
1729
1269
  const blocks = view.blocks.filter((b) => !(isBuildingPlan(b, turnId) && (segmentId === void 0 || b.segmentId === segmentId)));
1730
1270
  return blocks.length === view.blocks.length ? view : { ...view, blocks };
1731
1271
  }
1732
- function settleReply(view, { content, turnId }) {
1272
+ function settleReply(view, { content, turnId, supervisor }) {
1273
+ const role = supervisor ? "supervisor" : "planner";
1733
1274
  if (turnId !== void 0) {
1734
1275
  const i = finalSegmentIndex(view.blocks, turnId);
1735
1276
  if (i >= 0) {
1736
- return dropBuildingPlan({ ...view, blocks: replaceAt(view.blocks, i, settledMessage(view.blocks[i].id, "planner", content, turnId)) }, turnId);
1277
+ return dropBuildingPlan({ ...view, blocks: replaceAt(view.blocks, i, settledMessage(view.blocks[i].id, role, content, turnId)) }, turnId);
1737
1278
  }
1738
1279
  }
1739
- return append(dropBuildingPlan(view, turnId), (id) => settledMessage(id, "planner", content, turnId));
1280
+ return append(dropBuildingPlan(view, turnId), (id) => settledMessage(id, role, content, turnId));
1740
1281
  }
1741
1282
  function retractText(view, { turnId, segmentId }) {
1742
1283
  const blocks = view.blocks.filter((b) => !(isUnsettledSegment(b, turnId) && (segmentId === void 0 || b.segmentId === segmentId)));
@@ -1850,6 +1391,8 @@ function syncTranscript(view, { plan, turnId }) {
1850
1391
  let latest = view.transcriptAt;
1851
1392
  for (const [i, entry] of history.entries()) {
1852
1393
  const isNew = view.transcriptAt === void 0 || entry.timestamp > view.transcriptAt;
1394
+ if (isNew && entry.role === "user" && !entry.kind) next = takePendingInputs(next, entry.content, []).view;
1395
+ if (isNew && entry.kind === "wake_up") next = takePendingInputs(next, void 0, [entry.content]).view;
1853
1396
  if (isNew && entry.kind === "plan_generated") next = markPlan(next, entry.content, i === committed ? turnId : void 0);
1854
1397
  if (isNew && entry.kind === "system") next = append(next, (id) => settledMessage(id, "system", entry.content));
1855
1398
  if (isNew && entry.kind === "compaction") next = markCompaction(next, entry.content);
@@ -1893,6 +1436,8 @@ function reduceConversation(view, input) {
1893
1436
  return settleApproval2(view, input);
1894
1437
  case "approval_decided":
1895
1438
  return decideApproval2(view, input);
1439
+ case "execution_action":
1440
+ return append(view, (id) => settledMessage(id, "system", input.text));
1896
1441
  case "planner_usage":
1897
1442
  return reportUsage2(view, input);
1898
1443
  default:
@@ -1910,9 +1455,22 @@ function fromEntry(entry, loaded = []) {
1910
1455
  const { skill } = entry;
1911
1456
  return (id) => skillLoadBlock(id(), skillLoadNotice(skill));
1912
1457
  }
1913
- if (entry.role === "user" && !entry.kind) return (id) => userMessage(id(), entry.content, loaded.map(skillLoadNotice));
1914
- const role = entry.kind === "system" || entry.kind === "compaction" ? "system" : entry.role === "user" ? "user" : "planner";
1915
- return (id) => settledMessage(id(), role, entry.content);
1458
+ if (entry.role === "user" && !entry.kind) return (id) => userMessage(id(), entry.content, loaded.map(skillLoadNotice), { forced: entry.forced });
1459
+ return (id) => settledMessage(id(), roleOf(entry), entry.content);
1460
+ }
1461
+ function roleOf(entry) {
1462
+ switch (entry.kind) {
1463
+ case "system":
1464
+ case "compaction":
1465
+ case "execution":
1466
+ return "system";
1467
+ case "wake_up":
1468
+ return "wake_up";
1469
+ case "supervisor":
1470
+ return "supervisor";
1471
+ default:
1472
+ return entry.role === "user" ? "user" : "planner";
1473
+ }
1916
1474
  }
1917
1475
  function loadsAfter(entries, index) {
1918
1476
  const loads = [];
@@ -1923,7 +1481,7 @@ function loadsAfter(entries, index) {
1923
1481
  return loads;
1924
1482
  }
1925
1483
  function rankOf(entry) {
1926
- return entry.role === "user" && (!entry.kind || entry.kind === "skill_load") ? USER : SETTLED;
1484
+ return entry.role === "user" && (!entry.kind || entry.kind === "skill_load" || entry.kind === "wake_up") ? USER : SETTLED;
1927
1485
  }
1928
1486
  function spawnBrief(step) {
1929
1487
  try {
@@ -1981,7 +1539,7 @@ function research(log) {
1981
1539
  }
1982
1540
  return placed;
1983
1541
  }
1984
- function fromTranscript(conversationHistory, researchLog, plannerUsage) {
1542
+ function fromTranscript(conversationHistory, researchLog, plannerUsage, pendingTurns = []) {
1985
1543
  const history = conversationHistory ?? [];
1986
1544
  const compacted = history[0]?.kind === "compaction" ? history[0] : void 0;
1987
1545
  const kept = compacted ? history.slice(1) : history;
@@ -1995,9 +1553,21 @@ function fromTranscript(conversationHistory, researchLog, plannerUsage) {
1995
1553
  let nextId = 1;
1996
1554
  const id = () => `b${nextId++}`;
1997
1555
  const blocks = placed.map((item) => item.build(id));
1556
+ const pendingInputs = [];
1557
+ for (const input of pendingTurns) {
1558
+ const start = blocks.length;
1559
+ if (input.source === "supervisor") {
1560
+ blocks.push(settledMessage(id(), "wake_up", input.text));
1561
+ } else {
1562
+ const skills = (input.skills ?? []).map(skillLoadNotice);
1563
+ blocks.push(userMessage(id(), input.text, skills, { forced: input.forced }));
1564
+ blocks.push(...skills.map((skill) => skillLoadBlock(id(), skill)));
1565
+ }
1566
+ pendingInputs.push({ input: { ...input, skills: input.skills?.map(skillLoadNotice) }, blockIds: blocks.slice(start).map((block) => block.id) });
1567
+ }
1998
1568
  if (plannerUsage && isMeasured(plannerUsage.totals)) blocks.push(usageBlock(id(), usageLine(plannerUsage)));
1999
1569
  const transcriptAt = history.reduce((latest, e) => latest === void 0 || e.timestamp > latest ? e.timestamp : latest, void 0);
2000
- return { blocks, nextId, ...transcriptAt ? { transcriptAt } : {} };
1570
+ return { blocks, nextId, ...transcriptAt ? { transcriptAt } : {}, ...pendingInputs.length > 0 ? { pendingInputs } : {} };
2001
1571
  }
2002
1572
 
2003
1573
  // src/conversation/promptHold.ts
@@ -2013,6 +1583,10 @@ function unsendLatest(hold) {
2013
1583
  if (hold.length === 0) return void 0;
2014
1584
  return { text: hold[hold.length - 1], rest: hold.slice(0, -1) };
2015
1585
  }
1586
+ function takeAt(hold, index) {
1587
+ if (index < 0 || index >= hold.length) return void 0;
1588
+ return { text: hold[index], rest: [...hold.slice(0, index), ...hold.slice(index + 1)] };
1589
+ }
2016
1590
  function unsendAll(hold) {
2017
1591
  if (hold.length === 0) return void 0;
2018
1592
  return { text: hold.join("\n"), rest: EMPTY_HOLD };
@@ -2066,7 +1640,7 @@ function stopTurn(view, gate) {
2066
1640
 
2067
1641
  // src/conversation/detail.ts
2068
1642
  function hasHiddenDetail(blocks) {
2069
- return blocks.some((b) => b.type === "thinking" || b.type === "tool" || b.type === "subagent");
1643
+ return blocks.some((b) => b.type === "thinking" || b.type === "tool" || b.type === "subagent" || b.type === "runner_loaded");
2070
1644
  }
2071
1645
 
2072
1646
  // src/conversation/notices.ts
@@ -2077,20 +1651,29 @@ function taskStartedNotice(title, runner) {
2077
1651
  // src/taskRow/actions.ts
2078
1652
  var UNSTARTED_AI = (t) => t.type === "ai" && NOT_STARTED.has(t.status);
2079
1653
  var QUEUED = (t) => t.status === "pending" || t.status === "approved";
1654
+ var PAUSED = (t) => t.status === "awaiting_user" && t.awaitingReason === "usage-limit";
1655
+ var RESUMES = (t) => PAUSED(t) && !!t.resumeAt;
1656
+ var RESUMABLE = (t) => PAUSED(t) && t.continuable === true;
1657
+ var HOLDS_UP = (t) => t.status === "failed" || t.status === "blocked" || t.status === "awaiting_user";
2080
1658
  var TASK_RULES = [
2081
1659
  ["cancel", (t) => t.status === "in_progress"],
2082
- ["skip", (t) => t.status === "blocked"],
1660
+ ["resume-now", RESUMABLE],
1661
+ ["cancel-resume", RESUMES],
1662
+ ["skip", HOLDS_UP],
2083
1663
  ["force-start", UNSTARTED_AI],
2084
- // A manual task has no runner to report it done, so it can always be marked.
2085
- ["complete", (t) => t.status === "awaiting_user" || t.type === "user"],
1664
+ // A manual task has no runner to report it done, so it can always be marked;
1665
+ // a skipped one's kept work can still be vouched for.
1666
+ ["complete", (t) => t.status === "awaiting_user" || t.status === "skipped" || t.type === "user"],
2086
1667
  ["uncomplete", (t) => t.status === "completed"]
2087
1668
  ];
2088
1669
  var SUBTASK_RULES = [
2089
1670
  ["cancel", (t) => t.status === "in_progress"],
1671
+ ["resume-now", RESUMABLE],
1672
+ ["cancel-resume", RESUMES],
2090
1673
  ["complete", (t) => t.status === "in_progress" && t.type === "ai"],
2091
1674
  ["force-start", (t) => t.type === "ai" && QUEUED(t)],
2092
- ["skip", QUEUED],
2093
- ["complete", (t) => t.status === "awaiting_user"],
1675
+ ["skip", (t) => QUEUED(t) || t.status === "failed"],
1676
+ ["complete", (t) => t.status === "awaiting_user" || t.status === "skipped"],
2094
1677
  ["uncomplete", (t) => t.status === "completed"]
2095
1678
  ];
2096
1679
  function taskRowActions(task, placement = "task") {
@@ -2101,7 +1684,7 @@ function markAction(task) {
2101
1684
  return task.status === "completed" ? "uncomplete" : "complete";
2102
1685
  }
2103
1686
  function markRequestFor(action) {
2104
- if (action === "complete" || action === "skip") return "complete";
1687
+ if (action === "complete") return "complete";
2105
1688
  if (action === "uncomplete") return "uncomplete";
2106
1689
  return null;
2107
1690
  }
@@ -2110,52 +1693,50 @@ function opsEditable(task) {
2110
1693
  }
2111
1694
 
2112
1695
  // src/taskRow/view.ts
2113
- var refsOf = (ids, orderOf) => ids.map((id) => {
2114
- const order = orderOf?.get(id);
2115
- return order === void 0 ? id : taskRef({ order });
2116
- });
1696
+ var refsOf = (ids, tasks) => ids.map((id) => tasks?.length ? taskRefOf(tasks, id) ?? id : id);
2117
1697
  var namedRepo = (repo) => repo && repo !== "." ? repo : null;
2118
1698
  var namedRepos = (repos) => (repos ?? []).filter((r) => r !== ".");
2119
1699
  var filesOf = (files) => files?.length ? capConflictFiles(files) : null;
1700
+ var asBlocking = (tasks) => tasks.map((t) => ({ id: t.id, status: t.status ?? "", dependencies: t.dependencies ?? [], subtasks: asBlocking(t.subtasks ?? []) }));
1701
+ function blockedBy(task, tasks) {
1702
+ if (!tasks?.length || !NOT_STARTED.has(task.status)) return [];
1703
+ return blockingFailures(asBlocking(tasks)).get(task.id) ?? [];
1704
+ }
2120
1705
  function taskRowView(task, context = {}) {
2121
1706
  const parent = context.parent ?? void 0;
2122
1707
  const isolation = task.isolation && task.isolation.state !== "none" ? task.isolation : null;
1708
+ const latestUsage = task.attemptUsage?.at(-1);
1709
+ const blocked = blockedBy(task, context.tasks);
2123
1710
  return {
1711
+ usage: latestUsage ? { ...usageLine(latestUsage), attempt: latestUsage.attempt, wallTimeMs: latestUsage.wallTimeMs, byModel: latestUsage.byModel } : task.type === "ai" && ["completed", "failed", "in_progress", "awaiting_user"].includes(task.status) ? { totals: {}, attempt: 0, byModel: [] } : null,
2124
1712
  orderLabel: taskOrderLabel(task, parent),
2125
1713
  kind: task.type === "user" ? "user" : task.ops ? "ops" : "ai",
2126
- status: taskStatusKind(task),
1714
+ status: blocked.length > 0 ? "blocked" : taskStatusKind(task),
2127
1715
  running: isTaskRunning(task),
2128
1716
  awaiting: awaitingLabel(task),
1717
+ resumes: resumeLabel(task, context.now),
2129
1718
  approvals: approvalLabel(task.awaitingApproval),
2130
1719
  autonomous: task.type === "ai" && context.modes?.find((m) => m.id === task.taskMode)?.autonomous === true,
2131
1720
  repos: namedRepos(isolation?.repos),
2132
1721
  conflict: isolation?.state === "conflict" ? { repo: namedRepo(isolation.conflictRepo), files: filesOf(isolation.conflictFiles) } : null,
2133
1722
  repairing: isolation?.state === "repairing" ? { files: filesOf(isolation.conflictFiles), attempt: isolation.repair ?? null } : null,
2134
1723
  repaired: isolation?.repairedFiles?.length ? { files: capConflictFiles(isolation.repairedFiles), landed: isolation.state === "integrated" } : null,
2135
- mergeGate: task.mergeGate?.length ? refsOf(task.mergeGate, context.orderOf) : null,
1724
+ mergeGate: task.mergeGate?.length ? refsOf(task.mergeGate, context.tasks) : null,
1725
+ blockedBy: blocked.length > 0 ? refsOf(blocked, context.tasks) : null,
1726
+ skipReason: task.status === "skipped" ? task.skipReason ?? null : null,
2136
1727
  opsChangedFiles: task.status === "awaiting_user" && task.awaitingReason === "files-changed",
2137
1728
  forcedPastGate: task.forcedPastGate?.length ? task.forcedPastGate : null,
2138
- dependencies: refsOf(task.dependencies, context.orderOf),
2139
- actions: taskRowActions(task, context.placement ?? (parent ? "subtask" : "task")),
1729
+ dependencies: refsOf(task.dependencies, context.tasks),
1730
+ // A block read off the plan offers what a stored one does: Skip among them.
1731
+ actions: taskRowActions(blocked.length > 0 ? { ...task, status: "blocked" } : task, context.placement ?? (parent ? "subtask" : "task")),
2140
1732
  mark: markAction(task),
2141
1733
  opsEditable: opsEditable(task)
2142
1734
  };
2143
1735
  }
2144
1736
 
2145
1737
  // src/taskRow/texts.ts
2146
- function findNamed(tasks, id) {
2147
- for (const task of tasks) {
2148
- if (task.id === id) return task;
2149
- const nested = findNamed(task.subtasks ?? [], id);
2150
- if (nested) return nested;
2151
- }
2152
- return void 0;
2153
- }
2154
1738
  function titledRefs(ids, tasks) {
2155
- return ids.map((id) => {
2156
- const task = findNamed(tasks, id);
2157
- return task ? titledTaskRef(task) : id;
2158
- });
1739
+ return ids.map((id) => titledTaskRefOf(tasks, id) ?? id);
2159
1740
  }
2160
1741
  function dependentsNotice(dependents) {
2161
1742
  if (dependents.length === 0) return null;
@@ -2178,6 +1759,13 @@ export {
2178
1759
  isMeasured,
2179
1760
  usageLine,
2180
1761
  plannerContextFill,
1762
+ addModelUsage,
1763
+ startAttemptUsage,
1764
+ addAttemptUsage,
1765
+ endAttemptUsage,
1766
+ addUsageTotals,
1767
+ planUsage,
1768
+ sameMessage,
2181
1769
  mapAgentTool,
2182
1770
  normalizeAgentArgs,
2183
1771
  toolHeadline,
@@ -2193,31 +1781,19 @@ export {
2193
1781
  reduceTaskLog,
2194
1782
  replayTaskLog,
2195
1783
  capConflictFiles,
2196
- effectiveAllowlist,
2197
- filterModelsForPrompt,
2198
- clampThinkingEffort,
2199
- coerceAssignments,
2200
- isTaskRunning,
2201
- NOT_STARTED,
2202
- taskStatusKind,
2203
- awaitingLabel,
2204
- approvalLabel,
2205
- validateTaskEdit,
2206
- taskOpsProtocol,
2207
- textHasTaskOps,
2208
- parseTaskOpsJson,
2209
- taskOpRefs,
2210
- refMatchesTask,
2211
- canMergeTasks,
2212
- dependentsOf,
2213
- dependencyCandidates,
2214
- canSetDependencies,
2215
- canSplitTask,
2216
- applyTaskOps,
2217
1784
  summarizeToolCall,
2218
1785
  classifyOutcome,
1786
+ blockingFailures,
2219
1787
  continuability,
2220
1788
  canContinue,
1789
+ SUPERVISOR_LEVEL_LABEL,
1790
+ supervisorSummary,
1791
+ automergeSummary,
1792
+ autorunSummary,
1793
+ ACT_RISK,
1794
+ AUTORUN_RISK,
1795
+ parseSwitch,
1796
+ parseCount,
2221
1797
  serializeTask,
2222
1798
  serializeTaskStatus,
2223
1799
  surfacePlan,
@@ -2226,6 +1802,8 @@ export {
2226
1802
  executionSummary,
2227
1803
  CHECKPOINT_TRUNCATE_LENGTH,
2228
1804
  truncateCheckpointSummary,
1805
+ EMPTY_RUN,
1806
+ reduceRun,
2229
1807
  EMPTY_CONVERSATION,
2230
1808
  reduceConversation,
2231
1809
  fromTranscript,
@@ -2233,6 +1811,7 @@ export {
2233
1811
  holdPrompt,
2234
1812
  drainNext,
2235
1813
  unsendLatest,
1814
+ takeAt,
2236
1815
  unsendAll,
2237
1816
  aheadOfDraft,
2238
1817
  NO_TURN,
@@ -2251,4 +1830,4 @@ export {
2251
1830
  pastGateConfirmation,
2252
1831
  newTaskFields
2253
1832
  };
2254
- //# sourceMappingURL=chunk-QUARDKVT.mjs.map
1833
+ //# sourceMappingURL=chunk-CH5IGWFS.mjs.map