@ordewell/core 0.10.4 → 0.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/{IRunner-D_UkkMbJ.d.ts → IFileSystem-C4_mpfze.d.ts} +159 -15
  2. package/dist/{IRunner-NtTHQgLR.d.mts → IFileSystem-DUgeWWQN.d.mts} +159 -15
  3. package/dist/{ModeResolver-wnTFsDnF.d.mts → ModeResolver-4qNjxcDs.d.mts} +1 -1
  4. package/dist/{ModeResolver-Dy3Ef6p-.d.ts → ModeResolver-DGK2rWHE.d.ts} +1 -1
  5. package/dist/{Task-DZs9eKgg.d.mts → Task-C0o4i3Nn.d.mts} +114 -13
  6. package/dist/{Task-DZs9eKgg.d.ts → Task-C0o4i3Nn.d.ts} +114 -13
  7. package/dist/{chunk-FH6M5QZG.mjs → chunk-DMG4ISUG.mjs} +1008 -49
  8. package/dist/chunk-DMG4ISUG.mjs.map +1 -0
  9. package/dist/{chunk-SM6BV6XS.mjs → chunk-EEE2GBLG.mjs} +116 -27
  10. package/dist/chunk-EEE2GBLG.mjs.map +1 -0
  11. package/dist/chunk-H7QX32BH.mjs +647 -0
  12. package/dist/chunk-H7QX32BH.mjs.map +1 -0
  13. package/dist/{chunk-645FALZX.mjs → chunk-LTHKFBKM.mjs} +4 -4
  14. package/dist/chunk-LTHKFBKM.mjs.map +1 -0
  15. package/dist/{chunk-OYUSADFM.mjs → chunk-NJGID6BG.mjs} +3 -1
  16. package/dist/chunk-NJGID6BG.mjs.map +1 -0
  17. package/dist/index.d.mts +498 -1689
  18. package/dist/index.d.ts +498 -1689
  19. package/dist/index.js +4108 -2082
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +3380 -1639
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/parsing.d.mts +2 -2
  24. package/dist/parsing.d.ts +2 -2
  25. package/dist/parsing.js +231 -75
  26. package/dist/parsing.js.map +1 -1
  27. package/dist/parsing.mjs +3 -3
  28. package/dist/plan-utils.d.mts +20 -4
  29. package/dist/plan-utils.d.ts +20 -4
  30. package/dist/plan-utils.js +335 -88
  31. package/dist/plan-utils.js.map +1 -1
  32. package/dist/plan-utils.mjs +43 -19
  33. package/dist/{plan-utils-CtKceSsQ.d.mts → runSettingsText-BgOmLxdw.d.mts} +2242 -224
  34. package/dist/{plan-utils-POIz5d8z.d.ts → runSettingsText-BgXvajj2.d.ts} +2242 -224
  35. package/dist/testing.d.mts +5 -3
  36. package/dist/testing.d.ts +5 -3
  37. package/dist/testing.js +7 -0
  38. package/dist/testing.js.map +1 -1
  39. package/dist/testing.mjs +7 -2
  40. package/dist/testing.mjs.map +1 -1
  41. package/package.json +1 -1
  42. package/dist/IFileSystem-_effm8-b.d.ts +0 -78
  43. package/dist/IFileSystem-vyqMdL-x.d.mts +0 -78
  44. package/dist/chunk-645FALZX.mjs.map +0 -1
  45. package/dist/chunk-A57ITQE7.mjs +0 -1378
  46. package/dist/chunk-A57ITQE7.mjs.map +0 -1
  47. package/dist/chunk-FH6M5QZG.mjs.map +0 -1
  48. package/dist/chunk-OYUSADFM.mjs.map +0 -1
  49. package/dist/chunk-SM6BV6XS.mjs.map +0 -1
@@ -1,20 +1,42 @@
1
1
  import {
2
- NOT_STARTED,
3
- WAITS_FOR_APPROVAL_LABEL,
4
- approvalLabel,
5
- awaitingLabel,
6
- isTaskRunning,
7
- resumeLabel,
8
- taskStatusKind
9
- } from "./chunk-A57ITQE7.mjs";
2
+ MANUAL_NOT_READ_ONLY,
3
+ MODE_NOT_THE_PLANNERS,
4
+ OPS_NOT_READ_ONLY,
5
+ PlanParseError,
6
+ TASK_OPS_ENVELOPE_KEY,
7
+ checkPlanInvariants,
8
+ detectCycle,
9
+ effectiveAllowlist,
10
+ escapeControlCharsInStrings,
11
+ extractObjectsWithKey,
12
+ opsPlacementRefusal,
13
+ plannedTask,
14
+ resolveAssignment,
15
+ stripTrailingCommas,
16
+ taggedReadOnlyMode,
17
+ taskWhere
18
+ } from "./chunk-H7QX32BH.mjs";
10
19
  import {
20
+ addTaskToPlan,
21
+ canSetDependencies,
22
+ flattenTasks,
23
+ flattenTasksWithParents,
24
+ inheritedOps,
25
+ opsFlag,
11
26
  pausedOnLimit,
12
27
  rejoinPlanTree,
28
+ removeTaskFromPlan,
29
+ renumberTasks,
30
+ resolveTaskRef,
31
+ skillNames,
13
32
  taskOrderLabel,
33
+ taskPlacement,
14
34
  taskRefOf,
15
35
  titledTaskRef,
16
- titledTaskRefOf
17
- } from "./chunk-SM6BV6XS.mjs";
36
+ titledTaskRefOf,
37
+ updateTaskInPlan,
38
+ walkPlan
39
+ } from "./chunk-EEE2GBLG.mjs";
18
40
 
19
41
  // src/models/Usage.ts
20
42
  function partedPromptUsage(parts) {
@@ -84,8 +106,21 @@ function addAttemptUsage(usage, record, now) {
84
106
  const folded = addPlannerUsage(usage, record);
85
107
  return { ...usage, ...folded, lastPromptTokens: folded.lastPromptTokens, contextWindow: folded.contextWindow, wallTimeMs: Math.max(0, Date.parse(now) - Date.parse(usage.startedAt)) };
86
108
  }
87
- function endAttemptUsage(usage, now) {
88
- return { ...usage, endedAt: now, wallTimeMs: Math.max(0, Date.parse(now) - Date.parse(usage.startedAt)) };
109
+ function endAttemptUsage(usage, now, runnerSessionCost, sessionId) {
110
+ return {
111
+ ...usage,
112
+ endedAt: now,
113
+ wallTimeMs: Math.max(0, Date.parse(now) - Date.parse(usage.startedAt)),
114
+ ...sessionId ? { sessionId } : {},
115
+ ...runnerSessionCost ? { runnerSessionCost } : {}
116
+ };
117
+ }
118
+ function countedSessionCost(attempts, sessionId) {
119
+ return [...attempts ?? []].reverse().find((usage) => usage.runnerSessionCost?.sessionId === sessionId)?.runnerSessionCost;
120
+ }
121
+ function resumedSessionWindow(attempts, runner, attempt, sessionId) {
122
+ const last = [...attempts ?? []].reverse().find((usage) => usage.attempt !== attempt && usage.runner === runner && usage.sessionId === sessionId && usage.contextWindow !== void 0);
123
+ return last ? { source: last.source, model: last.model, contextWindow: last.contextWindow } : void 0;
89
124
  }
90
125
  function addUsageTotals(totals, addition) {
91
126
  let next = addUsage(totals, { source: "", inputTokens: addition.inputTokens, outputTokens: addition.outputTokens, cachedInputTokens: addition.cachedInputTokens });
@@ -114,6 +149,16 @@ function planUsage(tasks) {
114
149
  }
115
150
  return result;
116
151
  }
152
+ function taskUsageSummary(task) {
153
+ const attempts = task.attemptUsage ?? [];
154
+ const latest = attempts.at(-1);
155
+ const contextFill = latest ? plannerContextFill(latest) : void 0;
156
+ return {
157
+ totals: attempts.reduce((sum, attempt) => addUsageTotals(sum, attempt.totals), {}),
158
+ attempts: attempts.length,
159
+ ...contextFill ? { contextFill } : {}
160
+ };
161
+ }
117
162
 
118
163
  // src/services/harness/agentTools.ts
119
164
  var KNOWN_TOOLS = {
@@ -507,7 +552,7 @@ function closeOpenBlocks(view, cut) {
507
552
 
508
553
  // src/conversation/taskLog.ts
509
554
  var EMPTY_TASK_LOG = { blocks: [], nextId: 1, queued: [], working: false, streams: {} };
510
- var TEXT_STREAM = "text";
555
+ var textStream = (runId) => runId === void 0 ? "text" : `text:${runId}`;
511
556
  var thinkingStream = (subagentId) => `thinking:${subagentId ?? ""}`;
512
557
  function withoutStream(view, key) {
513
558
  if (!(key in view.streams)) return view;
@@ -524,35 +569,43 @@ function openStream(view, lane, key) {
524
569
  const block = blocks[i];
525
570
  return block?.id === id && (block.type === "message" || block.type === "thinking") && block.streaming ? i : -1;
526
571
  }
527
- function streamDelta(view, lane, key, text2, make) {
528
- const i = openStream(view, lane, key);
572
+ function streamBlock(view, lane, key) {
573
+ const id = view.streams[key];
574
+ return id === void 0 ? -1 : findLastIndex(laneBlocks(view, lane), (b) => b.id === id);
575
+ }
576
+ function streamDelta(view, lane, key, text2, make, anywhere = false) {
577
+ const i = anywhere ? streamBlock(view, lane, key) : openStream(view, lane, key);
529
578
  const open = laneBlocks(view, lane)[i];
530
579
  if (open?.type === "message" || open?.type === "thinking") return laneReplace(view, lane, i, { ...open, text: open.text + text2 });
531
580
  const next = appendOutput(view, lane, (id) => make(id, blockText(text2)));
532
581
  return { ...next, streams: { ...next.streams, [key]: `b${view.nextId}` } };
533
582
  }
534
583
  function streamWhole(view, lane, key, text2, make) {
535
- const id = view.streams[key];
536
- const blocks = laneBlocks(view, lane);
537
- const i = id === void 0 ? -1 : findLastIndex(blocks, (b) => b.id === id);
538
- const streamed = blocks[i];
584
+ const i = streamBlock(view, lane, key);
585
+ const streamed = laneBlocks(view, lane)[i];
539
586
  const next = withoutStream(view, key);
540
587
  if (streamed?.type === "message" || streamed?.type === "thinking") {
541
588
  return laneReplace(next, lane, i, { ...streamed, text: blockText(text2), streaming: false });
542
589
  }
543
590
  return appendOutput(next, lane, make);
544
591
  }
545
- function agentText(view, text2) {
546
- const next = streamWhole(view, null, TEXT_STREAM, text2, (id) => settledMessage(id, "agent", blockText(text2)));
547
- return { ...next, latestText: next.nextId > view.nextId ? `b${view.nextId}` : view.streams[TEXT_STREAM] };
592
+ function openedText(view, next) {
593
+ return next.nextId > view.nextId ? { ...next, latestText: `b${view.nextId}` } : next;
548
594
  }
549
- function agentTextDelta(view, text2) {
550
- const next = streamDelta(view, null, TEXT_STREAM, text2, (id, t) => ({ type: "message", id, role: "agent", text: t, streaming: true }));
551
- return { ...next, latestText: next.streams[TEXT_STREAM] };
595
+ function agentText(view, { runId, text: text2 }) {
596
+ return openedText(view, streamWhole(view, null, textStream(runId), text2, (id) => settledMessage(id, "agent", blockText(text2))));
597
+ }
598
+ function agentTextDelta(view, { runId, text: text2 }) {
599
+ const make = (id, t) => ({ type: "message", id, role: "agent", text: t, streaming: true });
600
+ return openedText(view, streamDelta(view, null, textStream(runId), text2, make, runId !== void 0));
552
601
  }
553
602
  function failTurn(view, message) {
554
603
  const repeated = view.blocks.find((b) => b.id === view.latestText);
555
- const settled = repeated?.type === "message" && repeated.role === "agent" && sameMessage(repeated.text, message) ? withoutStream({ ...view, blocks: view.blocks.filter((b) => b !== repeated) }, TEXT_STREAM) : view;
604
+ const settled = repeated?.type === "message" && repeated.role === "agent" && sameMessage(repeated.text, message) ? {
605
+ ...view,
606
+ blocks: view.blocks.filter((b) => b !== repeated),
607
+ streams: Object.fromEntries(Object.entries(view.streams).filter(([, id]) => id !== repeated.id))
608
+ } : view;
556
609
  return appendOutput({ ...settled, latestText: void 0 }, null, (id) => settledMessage(id, "error", message));
557
610
  }
558
611
  function thinkingBlock(id, text2, streaming, subagentId) {
@@ -682,6 +735,12 @@ function commandOutput(view, { text: text2 }) {
682
735
  return { ...view, blocks: replaceAt(view.blocks, i, { ...block, output: block.output ? `${block.output}
683
736
  ${text2}` : text2 }) };
684
737
  }
738
+ function modelSwitchNotice({ from, to, plan, kept }) {
739
+ if (!plan) return `Model changed in the runner: ${from.label ?? from.model} \u2192 ${to.label ?? to.model}${kept ? ` (${kept})` : ""}`;
740
+ const planned = plan.model ? `${plan.model.label ?? plan.model.model}${plan.effort ? `, ${plan.effort}` : ""}` : void 0;
741
+ if (!kept) return `Model changed in the runner: ${from.label ?? from.model} \u2192 ${to.label ?? to.model}; the plan now says ${planned ?? to.label ?? to.model}`;
742
+ return `The runner now answers on ${to.label ?? to.model}; ${planned ? `the plan still says ${planned}` : "the plan names no model"} (${kept})`;
743
+ }
685
744
  function appendStep(view, make) {
686
745
  const next = append(view, make);
687
746
  return sealLane(next, null, findLastIndex(next.blocks, (b) => b.type !== "usage"));
@@ -727,7 +786,7 @@ function endCall(block) {
727
786
  function endOpenCalls(view) {
728
787
  let changed = false;
729
788
  const blocks = view.blocks.map((block) => {
730
- 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;
789
+ 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.type === "command" && block.status === "running" ? { ...block, status: "stopped" } : block;
731
790
  if (ended !== block) changed = true;
732
791
  return ended;
733
792
  });
@@ -743,6 +802,8 @@ function reduceTaskLog(view, event) {
743
802
  return startCommand(view, event);
744
803
  case "command_output":
745
804
  return commandOutput(view, event);
805
+ case "model_switched":
806
+ return append(view, (id) => settledMessage(id, "system", modelSwitchNotice(event)));
746
807
  case "compaction":
747
808
  return appendStep(view, (id) => ({
748
809
  type: "compaction",
@@ -756,9 +817,9 @@ function reduceTaskLog(view, event) {
756
817
  case "turn_end":
757
818
  return endTurn(view, event);
758
819
  case "text_delta":
759
- return agentTextDelta(view, event.text);
820
+ return agentTextDelta(view, event);
760
821
  case "text":
761
- return agentText(view, event.text);
822
+ return agentText(view, event);
762
823
  case "thinking_delta":
763
824
  return thinkingDelta(view, event);
764
825
  case "thinking":
@@ -792,7 +853,7 @@ function reduceTaskLog(view, event) {
792
853
  case "approval_withdrawn":
793
854
  return settleApproval(view, event.approvalId, (block) => ({ ...block, status: "withdrawn" }));
794
855
  case "runner_ended":
795
- return endOpenCalls(view);
856
+ return closeOpenBlocks({ ...endOpenCalls(view), working: false, streams: {}, latestText: void 0 }, "stopped");
796
857
  default:
797
858
  return view;
798
859
  }
@@ -801,7 +862,765 @@ function replayTaskLog(events, from = EMPTY_TASK_LOG) {
801
862
  return events.reduce(reduceTaskLog, from);
802
863
  }
803
864
 
865
+ // src/taskRow/status.ts
866
+ var isTaskRunning = (task) => task.status === "in_progress" || task.status === "running";
867
+ var NOT_STARTED = /* @__PURE__ */ new Set(["pending", "approved", "blocked"]);
868
+ function taskStatusKind(task) {
869
+ switch (task.status) {
870
+ case "completed":
871
+ return "done";
872
+ case "failed":
873
+ return "failed";
874
+ case "skipped":
875
+ return "skipped";
876
+ case "blocked":
877
+ return "blocked";
878
+ case "awaiting_user":
879
+ return "awaiting";
880
+ }
881
+ if (!isTaskRunning(task)) return "todo";
882
+ return task.status === "in_progress" && task.idleSince ? "quiet" : "running";
883
+ }
884
+ var AWAITING_LABELS = {
885
+ input: "Waiting for your input",
886
+ checkpoint: "Checkpoint",
887
+ conflict: "Merge conflict",
888
+ "files-changed": "Changed tracked files",
889
+ "usage-limit": "Paused on a usage limit"
890
+ };
891
+ function awaitingLabel(task) {
892
+ return task.status === "awaiting_user" && task.awaitingReason ? AWAITING_LABELS[task.awaitingReason] : null;
893
+ }
894
+ function blockedByLabel(refs) {
895
+ return `Blocked by ${refs.join(", ")}`;
896
+ }
897
+ function skippedLabel(reason) {
898
+ return reason ? `Skipped: ${reason}` : "Skipped";
899
+ }
900
+ var WAITS_FOR_APPROVAL_LABEL = "Waits for your approval";
901
+ function approvalLabel(count) {
902
+ if (!count) return null;
903
+ return count > 1 ? `Waiting for approval (${count})` : "Waiting for approval";
904
+ }
905
+ function resumeTime(at, now = /* @__PURE__ */ new Date()) {
906
+ const clock = at.toLocaleTimeString("en-US", { hour: "numeric", minute: at.getMinutes() === 0 ? void 0 : "2-digit", hour12: true }).replace(/[ \u202f]([AP]M)$/i, (_m, meridiem) => meridiem.toLowerCase());
907
+ if (at.toDateString() === now.toDateString()) return clock;
908
+ return `${at.toLocaleDateString("en-US", { month: "short", day: "numeric" })}, ${clock}`;
909
+ }
910
+ function resumeLabel(task, now = /* @__PURE__ */ new Date()) {
911
+ if (task.status !== "awaiting_user" || task.awaitingReason !== "usage-limit" || !task.resumeAt) return null;
912
+ const at = new Date(task.resumeAt);
913
+ return Number.isNaN(at.getTime()) ? null : `Resumes at ${resumeTime(at, now)}`;
914
+ }
915
+
916
+ // src/services/TaskOps.ts
917
+ import { v4 as uuidv4 } from "uuid";
918
+
919
+ // src/services/TaskEditValidator.ts
920
+ function lockedTaskRefusal(task, action = "changed") {
921
+ if (task.status === "in_progress") return `"${task.title}" is running and cannot be ${action} now: the user can stop it first, or change it after it finishes`;
922
+ if (task.status === "completed") return `"${task.title}" is completed and cannot be ${action}: rearm it to run it again, or add a new task for further work`;
923
+ return null;
924
+ }
925
+ var AI_ONLY_FIELDS = ["assignedModel", "taskMode", "autonomy", "ops"];
926
+ function typeCoherenceCheck(target, changes) {
927
+ if (!changes.type || changes.type === target.type) return null;
928
+ if (changes.type === "user") {
929
+ if (!Array.isArray(changes.userSteps) || changes.userSteps.length === 0) {
930
+ return { ok: false, error: `"${target.title}" cannot become a manual task without user steps: include "userSteps" in this edit` };
931
+ }
932
+ return {
933
+ ok: true,
934
+ clear: AI_ONLY_FIELDS.filter((f) => target[f] !== void 0),
935
+ // User tasks are held to HITL, so a flip that kept the AI task's slice would be refused by the plan's own rules.
936
+ ...target.sliceType !== "HITL" ? { set: { sliceType: "HITL" } } : {}
937
+ };
938
+ }
939
+ if (!changes.prompt || !changes.prompt.trim()) {
940
+ return { ok: false, error: `"${target.title}" cannot become an AI task without a prompt: include "prompt" in this edit` };
941
+ }
942
+ return { ok: true, clear: target.userSteps && target.userSteps.length > 0 ? ["userSteps"] : [] };
943
+ }
944
+ function opsCheck(target, isSubtask, changes) {
945
+ if (!("ops" in changes) || !!changes.ops === !!target.ops) return null;
946
+ if (!NOT_STARTED.has(target.status)) {
947
+ return { ok: false, error: `"${target.title}" has started, so whether it is an ops task is fixed` };
948
+ }
949
+ if (!changes.ops) return null;
950
+ const refused = opsPlacementRefusal(changes.type ?? target.type, isSubtask);
951
+ return refused ? { ok: false, error: `"${target.title}" ${refused}` } : null;
952
+ }
953
+ function validateTaskEdit(actor, tasks, taskId, changes) {
954
+ const found = flattenTasksWithParents(tasks).find(({ task }) => task.id === taskId);
955
+ if (!found) return { ok: false, error: "Task not found" };
956
+ const { task: target, parent } = found;
957
+ const locked = actor === "planner" ? lockedTaskRefusal(target, "modified") : null;
958
+ if (locked) return { ok: false, error: locked };
959
+ if (changes.dependencies) {
960
+ const depCheck = canSetDependencies(tasks, taskId, changes.dependencies);
961
+ if (!depCheck.ok) return depCheck;
962
+ }
963
+ const coherence = typeCoherenceCheck(target, changes);
964
+ if (coherence && !coherence.ok) return coherence;
965
+ const ops = opsCheck(target, parent !== null, changes);
966
+ if (ops) return ops;
967
+ return coherence ?? { ok: true };
968
+ }
969
+
970
+ // src/services/TaskOps.ts
971
+ var UPDATABLE_FIELDS = [
972
+ "title",
973
+ "description",
974
+ "prompt",
975
+ "dependencies",
976
+ "assignedRunner",
977
+ "assignedModel",
978
+ "taskMode",
979
+ "type",
980
+ "userSteps",
981
+ "autonomy",
982
+ "sliceType",
983
+ "ops",
984
+ "skills"
985
+ ];
986
+ function taskOpsProtocol(executionNote = "", tools = false) {
987
+ const updateChanges = [...UPDATABLE_FIELDS.filter((f) => f !== "taskMode"), "thinkingEffort", "readOnly"].map((f) => f === "dependencies" ? '"dependencies"?:["<id or #order>"]' : `"${f}"?`).join(",");
988
+ return [
989
+ 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:",
990
+ tools ? ' {"ops": [' : ' {"taskOps": [',
991
+ ` {"op":"update","taskId":"<id or #order>","changes":{${updateChanges}}},`,
992
+ ' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?,"readOnly"?,"ops"?},"handle"?:"<name>"},',
993
+ ' {"op":"remove","taskId":"<id or #order>"},',
994
+ ' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]}, // only to re-prioritise INDEPENDENT tasks',
995
+ ' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...},"handle"?:"<name>"},',
996
+ ' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...],"handle"?:"<name>"},',
997
+ ` {"op":"rearm","taskId":"<id or #order>","changes"?:{${updateChanges}}}`,
998
+ " ]}",
999
+ 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.',
1000
+ '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. "#N" is top-level task N; a subtask is named by its dotted label ("#2.1"), as the plan lists it.',
1001
+ '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.',
1002
+ `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>"}.`,
1003
+ 'Set "readOnly": true on a task that must not change any file, never on one that must run commands (tests, scripts, builds) or an ops task, and "readOnly": false to lift it; you never set "taskMode". A task you move, split or edit keeps being read-only or not unless you say otherwise, and so do the subtasks of a read-only task you add.',
1004
+ 'Keep dependencies consistent: no cycles, no references to removed tasks, and never touch running or completed tasks \u2014 "rearm" is the one exception, below.',
1005
+ ...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."] : [],
1006
+ '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,
1007
+ '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", which also makes the task HITL) or the userSteps (flipping to "ai") are cleared automatically.',
1008
+ `"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.`,
1009
+ '"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.'
1010
+ ];
1011
+ }
1012
+ function textHasTaskOps(text2) {
1013
+ return text2.includes(`"${TASK_OPS_ENVELOPE_KEY}"`);
1014
+ }
1015
+ function parseTaskOpsJson(text2) {
1016
+ const { matches, fallback } = extractObjectsWithKey(text2, TASK_OPS_ENVELOPE_KEY);
1017
+ const candidates = matches.length > 0 ? [...matches].reverse() : [fallback.json];
1018
+ let lastError = null;
1019
+ for (const json of candidates) {
1020
+ try {
1021
+ return parseTaskOpsObject(json, text2);
1022
+ } catch (err) {
1023
+ if (!(err instanceof PlanParseError)) throw err;
1024
+ if (!lastError) lastError = err;
1025
+ }
1026
+ }
1027
+ throw lastError ?? new PlanParseError("No JSON object found in task-ops reply", text2);
1028
+ }
1029
+ function parseTaskOpsObject(json, text2) {
1030
+ let parsed;
1031
+ try {
1032
+ parsed = JSON.parse(escapeControlCharsInStrings(stripTrailingCommas(json)));
1033
+ } catch (err) {
1034
+ throw new PlanParseError(`Task-ops JSON is invalid: ${err instanceof Error ? err.message : String(err)}`, text2);
1035
+ }
1036
+ const ops = parsed.taskOps;
1037
+ if (!Array.isArray(ops) || ops.length === 0) {
1038
+ throw new PlanParseError('"taskOps" must be a non-empty array', text2);
1039
+ }
1040
+ for (const op of ops) {
1041
+ if (typeof op !== "object" || op === null || typeof op.op !== "string") {
1042
+ throw new PlanParseError('Every task op needs an "op" field (update | add | remove | reorder | merge | split)', text2);
1043
+ }
1044
+ }
1045
+ return ops;
1046
+ }
1047
+ function resolveRef(ref, originalTree, liveTree, handleOwner, handleId, opIndex) {
1048
+ if (typeof ref !== "string" && typeof ref !== "number") return { error: `invalid reference "${String(ref)}"` };
1049
+ const s = String(ref).trim();
1050
+ const ownerIdx = handleOwner.get(s);
1051
+ if (ownerIdx !== void 0) {
1052
+ if (ownerIdx >= opIndex) {
1053
+ return { error: `handle "${s}" is defined by a later op in this batch and cannot be referenced yet` };
1054
+ }
1055
+ const id = handleId.get(s);
1056
+ if (!id) return { error: `handle "${s}" was never resolved` };
1057
+ return { id };
1058
+ }
1059
+ if (findInTasks(liveTree, s)) return { id: s };
1060
+ const task = resolveTaskRef(originalTree, s);
1061
+ return task ? { id: task.id } : { error: `task "${s}" not found` };
1062
+ }
1063
+ function updatableChanges(raw) {
1064
+ const changes = {};
1065
+ for (const key of UPDATABLE_FIELDS) {
1066
+ if (raw && key in raw) changes[key] = raw[key];
1067
+ }
1068
+ if ("ops" in changes) changes.ops = opsFlag(changes.ops);
1069
+ if ("skills" in changes) changes.skills = skillNames(changes.skills);
1070
+ return changes;
1071
+ }
1072
+ function findInTasks(tasks, id) {
1073
+ return taskPlacement(tasks, id)?.task;
1074
+ }
1075
+ function findTopLevel(tasks, id) {
1076
+ return tasks.find((t) => t.id === id);
1077
+ }
1078
+ function isPinned(t) {
1079
+ return t.status === "in_progress" || t.status === "completed";
1080
+ }
1081
+ function lockedInSubtree(parent) {
1082
+ for (const sub of parent.subtasks ?? []) {
1083
+ const locked = lockedTaskRefusal(sub, "removed") ?? lockedInSubtree(sub);
1084
+ if (locked) return locked;
1085
+ }
1086
+ return null;
1087
+ }
1088
+ function repairOrder(tasks) {
1089
+ const byId = new Map(tasks.map((t) => [t.id, t]));
1090
+ const placed = /* @__PURE__ */ new Set();
1091
+ const movable = tasks.filter((t) => !isPinned(t));
1092
+ const result = [];
1093
+ const blockerOf = (t) => {
1094
+ const id = t.dependencies.find((d) => byId.has(d) && !placed.has(d));
1095
+ return id ? byId.get(id) : void 0;
1096
+ };
1097
+ const place = (t) => {
1098
+ result.push(t);
1099
+ placed.add(t.id);
1100
+ };
1101
+ for (const occupant of tasks) {
1102
+ if (isPinned(occupant)) {
1103
+ const blocker = blockerOf(occupant);
1104
+ if (blocker) return { error: pinnedRefusal(blocker, occupant, "before") };
1105
+ place(occupant);
1106
+ continue;
1107
+ }
1108
+ const next = movable.findIndex((t) => !blockerOf(t));
1109
+ if (next < 0) {
1110
+ const [stuck, pin] = trailToPin(movable[0], blockerOf);
1111
+ return { error: pinnedRefusal(stuck, pin, "after") };
1112
+ }
1113
+ place(movable.splice(next, 1)[0]);
1114
+ }
1115
+ return { tasks: result };
1116
+ }
1117
+ function trailToPin(task, blockerOf) {
1118
+ let at = task;
1119
+ for (; ; ) {
1120
+ const blocker = blockerOf(at);
1121
+ if (!blocker || isPinned(blocker)) return [at, blocker ?? at];
1122
+ at = blocker;
1123
+ }
1124
+ }
1125
+ function describeMoves(before, after) {
1126
+ const wasAt = new Map(before.map((t, i) => [t.id, i + 1]));
1127
+ const moved = after.map((t, i) => ({ t, from: wasAt.get(t.id), to: i + 1 })).filter((m) => m.from !== m.to);
1128
+ return moved.length ? moved.map((m) => `"${m.t.title}" #${m.from}\u2192#${m.to}`).join(", ") : null;
1129
+ }
1130
+ function pinnedRefusal(other, pin, side) {
1131
+ const word = pin.status === "in_progress" ? "running" : "completed";
1132
+ return `"${other.title}" would have to run ${side} "${pin.title}", which is ${word} and cannot be moved`;
1133
+ }
1134
+ function canMergeTasks(tasks, selectedIds) {
1135
+ if (selectedIds.length < 2) return { ok: false, error: "Select at least two tasks to merge" };
1136
+ const idSet = new Set(selectedIds);
1137
+ const toMerge = tasks.filter((t) => idSet.has(t.id));
1138
+ if (toMerge.length < 2) return { ok: false, error: "At least two valid tasks are required to merge" };
1139
+ for (const t of toMerge) {
1140
+ if (t.status === "in_progress") return { ok: false, error: `"${t.title}" is running and cannot be merged` };
1141
+ if (t.status === "completed") return { ok: false, error: `"${t.title}" is completed and cannot be merged` };
1142
+ }
1143
+ const ordered = [...toMerge].sort((a, b) => a.order - b.order);
1144
+ const survivorId = "__merge_sim__";
1145
+ 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) }));
1146
+ const unionDeps = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));
1147
+ sim.push({ ...ordered[0], id: survivorId, title: "__merge_sim__", order: ordered[0].order, dependencies: unionDeps });
1148
+ const cycle = detectCycle(sim);
1149
+ if (cycle) return { ok: false, error: `Merging would create a dependency cycle involving "${cycle}"` };
1150
+ return { ok: true };
1151
+ }
1152
+ function canSplitTask(tasks, taskId) {
1153
+ const target = tasks.find((t) => t.id === taskId);
1154
+ if (!target) return { ok: false, error: "Task not found" };
1155
+ if (target.status === "in_progress") return { ok: false, error: `"${target.title}" is running and cannot be split` };
1156
+ if (target.status === "completed") return { ok: false, error: `"${target.title}" is completed and cannot be split` };
1157
+ return { ok: true };
1158
+ }
1159
+ function askedReadOnly(spec) {
1160
+ return typeof spec?.readOnly === "boolean" ? spec.readOnly : void 0;
1161
+ }
1162
+ function isReadOnly(task, catalog) {
1163
+ return task.taskMode !== void 0 && task.taskMode === taggedReadOnlyMode(catalog?.runnerModes?.[task.assignedRunner]);
1164
+ }
1165
+ function changedFields(target, written, landed, catalog) {
1166
+ const fields = Object.keys(written).filter((f) => f !== "taskMode");
1167
+ if (isReadOnly({ ...target, ...landed }, catalog) !== isReadOnly(target, catalog)) fields.push("readOnly");
1168
+ return fields;
1169
+ }
1170
+ var NO_CATALOG = { modelsByRunner: {} };
1171
+ var keptIds = /* @__PURE__ */ new WeakMap();
1172
+ function idsOf(op) {
1173
+ if (typeof op !== "object" || op === null) return uuidv4;
1174
+ const ids = keptIds.get(op) ?? [];
1175
+ keptIds.set(op, ids);
1176
+ let next = 0;
1177
+ return () => {
1178
+ if (next === ids.length) ids.push(uuidv4());
1179
+ return ids[next++];
1180
+ };
1181
+ }
1182
+ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
1183
+ const ops = [...held, ...newOps];
1184
+ const opLabel = (i) => i < held.length ? `earlier op ${i + 1} (${ops[i].op})` : `op ${i - held.length + 1} (${ops[i].op})`;
1185
+ let tasks = JSON.parse(JSON.stringify(currentTasks));
1186
+ const errors = [];
1187
+ const summary = [];
1188
+ const adjustments = [];
1189
+ const handleOwner = /* @__PURE__ */ new Map();
1190
+ const handleId = /* @__PURE__ */ new Map();
1191
+ for (const [i, op] of ops.entries()) {
1192
+ const handle = op.handle;
1193
+ if (handle === void 0) continue;
1194
+ if (typeof handle !== "string" || !handle.trim()) {
1195
+ errors.push(`${opLabel(i)}: handle must be a non-empty string`);
1196
+ continue;
1197
+ }
1198
+ if (handleOwner.has(handle)) {
1199
+ errors.push(`${opLabel(i)}: handle "${handle}" is already used earlier in this batch`);
1200
+ continue;
1201
+ }
1202
+ if (resolveTaskRef(currentTasks, handle)) {
1203
+ errors.push(`${opLabel(i)}: handle "${handle}" collides with an existing task reference`);
1204
+ continue;
1205
+ }
1206
+ handleOwner.set(handle, i);
1207
+ }
1208
+ if (errors.length) return { ok: false, tasks: currentTasks, errors, summary: [] };
1209
+ const resolveDeps = (deps, opIndex) => {
1210
+ const ids = [];
1211
+ const bad = [];
1212
+ const liveFlat = flattenTasks(tasks);
1213
+ for (const d of Array.isArray(deps) ? deps : []) {
1214
+ const r = resolveRef(d, currentTasks, tasks, handleOwner, handleId, opIndex);
1215
+ if ("error" in r) {
1216
+ bad.push(`${String(d)} (${r.error})`);
1217
+ continue;
1218
+ }
1219
+ if (!liveFlat.some((t) => t.id === r.id)) {
1220
+ bad.push(`${String(d)} (no longer exists in this batch)`);
1221
+ continue;
1222
+ }
1223
+ ids.push(r.id);
1224
+ }
1225
+ return { ids, bad };
1226
+ };
1227
+ const intake = { runners, runnerModes: catalog?.runnerModes, autonomousDefault: catalog?.autonomousDefault, models: catalog };
1228
+ const resolveTree = (planned, opIndex) => {
1229
+ const walk = (t, parentTitle) => {
1230
+ const ids = [];
1231
+ const bad = [];
1232
+ for (const d of t.dependencies) {
1233
+ const own = planned.renamed.get(d);
1234
+ if (own) {
1235
+ ids.push(own);
1236
+ continue;
1237
+ }
1238
+ const r = resolveDeps([d], opIndex);
1239
+ ids.push(...r.ids);
1240
+ bad.push(...r.bad);
1241
+ }
1242
+ if (bad.length) return `unknown dependencies${parentTitle !== void 0 ? ` of ${taskWhere(t.title, parentTitle)}` : ""}: ${bad.join(", ")}`;
1243
+ const subtasks = [];
1244
+ for (const sub of t.subtasks) {
1245
+ const done2 = walk(sub, t.title);
1246
+ if (typeof done2 === "string") return done2;
1247
+ subtasks.push(done2);
1248
+ }
1249
+ return { ...t, dependencies: ids, subtasks };
1250
+ };
1251
+ const done = walk(planned.task);
1252
+ return typeof done === "string" ? { error: done } : { task: done };
1253
+ };
1254
+ const refusalOf = (planned) => [...planned.ignored, ...planned.errors][0]?.message;
1255
+ const prepareEdit = (op, opIndex, label, audience, refuseRunning = false) => {
1256
+ const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, opIndex);
1257
+ if ("error" in ref) return { error: `${label}: ${ref.error}` };
1258
+ const target = findInTasks(tasks, ref.id);
1259
+ if (!target) return { error: `${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)` };
1260
+ if (refuseRunning && target.status === "in_progress") return { error: `${label}: ${lockedTaskRefusal(target, "re-armed")}` };
1261
+ if (op.changes && "taskMode" in op.changes) return { error: `${label}: ${MODE_NOT_THE_PLANNERS}` };
1262
+ const changes = updatableChanges(op.changes);
1263
+ if (op.changes && "thinkingEffort" in op.changes) {
1264
+ const model = changes.assignedModel ?? target.assignedModel;
1265
+ if (!model) return { error: `${label}: "thinkingEffort" is the assigned model's \u2014 set "assignedModel" with it` };
1266
+ changes.assignedModel = { ...model, thinkingEffort: op.changes.thinkingEffort ?? void 0 };
1267
+ }
1268
+ const changesForCheck = { ...changes };
1269
+ delete changesForCheck.dependencies;
1270
+ const check = validateTaskEdit(audience, tasks, target.id, changesForCheck);
1271
+ if (!check.ok) return { error: `${label}: ${check.error}` };
1272
+ for (const field of check.clear ?? []) changes[field] = void 0;
1273
+ Object.assign(changes, check.set);
1274
+ const readOnly = askedReadOnly(op.changes);
1275
+ if (readOnly === true && (changes.type ?? target.type) === "user") return { error: `${label}: ${MANUAL_NOT_READ_ONLY}` };
1276
+ if ((changes.ops ?? target.ops) && (readOnly ?? isReadOnly(target, catalog))) return { error: `${label}: ${OPS_NOT_READ_ONLY}` };
1277
+ const landed2 = { ...changes };
1278
+ const written = { ...target, ...changes };
1279
+ if (written.type === "user" || runners.includes(written.assignedRunner)) {
1280
+ const resolved = resolveAssignment({ writer: "edit", task: written, before: target, readOnly, catalog: catalog ?? NO_CATALOG });
1281
+ if (!resolved.ok) return { error: `${label}: ${resolved.error}` };
1282
+ if (resolved.task.assignedModel !== written.assignedModel) landed2.assignedModel = resolved.task.assignedModel;
1283
+ if (resolved.task.taskMode !== written.taskMode) landed2.taskMode = resolved.task.taskMode;
1284
+ adjustments.push(...resolved.adjustments);
1285
+ }
1286
+ if ("dependencies" in changes) {
1287
+ const { ids, bad } = resolveDeps(changes.dependencies, opIndex);
1288
+ if (bad.length) return { error: `${label}: unknown dependencies: ${bad.join(", ")}` };
1289
+ landed2.dependencies = ids;
1290
+ }
1291
+ return { target, changes: landed2, fields: changedFields(target, changes, landed2, catalog), check };
1292
+ };
1293
+ const breachKey = (e) => `${e.taskId ?? ""}\0${e.field}`;
1294
+ const standing = new Set(checkPlanInvariants(currentTasks, runners, catalog?.runnerModes).map(breachKey));
1295
+ const brokenBy = /* @__PURE__ */ new Map();
1296
+ let lastLabel = opLabel(0);
1297
+ for (const [i, op] of ops.entries()) {
1298
+ const label = opLabel(i);
1299
+ const newId = idsOf(op);
1300
+ switch (op.op) {
1301
+ case "update": {
1302
+ const prep = prepareEdit(op, i, label, "planner");
1303
+ if ("error" in prep) {
1304
+ errors.push(prep.error);
1305
+ break;
1306
+ }
1307
+ const { target, changes, fields, check } = prep;
1308
+ if (fields.length === 0) {
1309
+ const readOnly = askedReadOnly(op.changes);
1310
+ if (readOnly !== void 0 && isReadOnly(target, catalog) === readOnly) {
1311
+ summary.push(`"${target.title}" is already ${readOnly ? "" : "not "}read-only`);
1312
+ break;
1313
+ }
1314
+ errors.push(`${label}: no valid changes provided`);
1315
+ break;
1316
+ }
1317
+ tasks = updateTaskInPlan(tasks, target.id, changes);
1318
+ const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
1319
+ summary.push(`Updated "${target.title}"${fields.length ? ` (${fields.join(", ")})` : ""}${clearedNote}`);
1320
+ break;
1321
+ }
1322
+ case "add": {
1323
+ const spec = op.task ?? {};
1324
+ if (!spec.title) {
1325
+ errors.push(`${label}: new task needs a title`);
1326
+ break;
1327
+ }
1328
+ const planned = plannedTask(spec, intake, { freshIds: true, newId });
1329
+ const refused = refusalOf(planned);
1330
+ if (refused) {
1331
+ errors.push(`${label}: ${refused}`);
1332
+ break;
1333
+ }
1334
+ adjustments.push(...planned.adjustments);
1335
+ const resolved = resolveTree(planned, i);
1336
+ if ("error" in resolved) {
1337
+ errors.push(`${label}: ${resolved.error}`);
1338
+ break;
1339
+ }
1340
+ tasks = addTaskToPlan(tasks, resolved.task);
1341
+ if (op.handle) handleId.set(op.handle, resolved.task.id);
1342
+ summary.push(`Added "${spec.title}"`);
1343
+ break;
1344
+ }
1345
+ case "remove": {
1346
+ const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, i);
1347
+ if ("error" in ref) {
1348
+ errors.push(`${label}: ${ref.error}`);
1349
+ break;
1350
+ }
1351
+ const target = findInTasks(tasks, ref.id);
1352
+ if (!target) {
1353
+ errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
1354
+ break;
1355
+ }
1356
+ const locked = lockedTaskRefusal(target, "removed") ?? lockedInSubtree(target);
1357
+ if (locked) {
1358
+ errors.push(`${label}: ${locked}`);
1359
+ break;
1360
+ }
1361
+ tasks = removeTaskFromPlan(tasks, target.id);
1362
+ summary.push(`Removed "${target.title}"`);
1363
+ break;
1364
+ }
1365
+ case "reorder": {
1366
+ const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
1367
+ const badRef = refs.find((r) => "error" in r);
1368
+ if (badRef) {
1369
+ errors.push(`${label}: ${badRef.error}`);
1370
+ break;
1371
+ }
1372
+ const resolved = refs.map((r) => findTopLevel(tasks, r.id));
1373
+ if (resolved.some((t) => !t)) {
1374
+ errors.push(`${label}: unknown task in reorder list`);
1375
+ break;
1376
+ }
1377
+ const orderIds = resolved.map((t) => t.id);
1378
+ if (new Set(orderIds).size !== tasks.length) {
1379
+ errors.push(`${label}: reorder must list every top-level task exactly once (${tasks.length} tasks)`);
1380
+ break;
1381
+ }
1382
+ const byId = new Map(tasks.map((t) => [t.id, t]));
1383
+ tasks = renumberTasks(orderIds.map((id) => byId.get(id)));
1384
+ summary.push("Reordered tasks");
1385
+ break;
1386
+ }
1387
+ case "merge": {
1388
+ const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
1389
+ const badRef = refs.find((r) => "error" in r);
1390
+ if (badRef) {
1391
+ errors.push(`${label}: ${badRef.error}`);
1392
+ break;
1393
+ }
1394
+ const resolved = refs.map((r) => findTopLevel(tasks, r.id));
1395
+ if (resolved.some((t) => !t)) {
1396
+ errors.push(`${label}: unknown task in merge list (already removed, merged, or split by an earlier op)`);
1397
+ break;
1398
+ }
1399
+ const toMerge = [...new Map(resolved.map((t) => [t.id, t])).values()].sort((a, b) => a.order - b.order);
1400
+ if (toMerge.length < 2) {
1401
+ errors.push(`${label}: merge needs at least two tasks`);
1402
+ break;
1403
+ }
1404
+ const locked = toMerge.map((t) => lockedTaskRefusal(t, "merged")).find((refusal) => refusal !== null);
1405
+ if (locked) {
1406
+ errors.push(`${label}: ${locked}`);
1407
+ break;
1408
+ }
1409
+ const idSet = new Set(toMerge.map((t) => t.id));
1410
+ const unionDepsRaw = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));
1411
+ const { ids, bad } = resolveDeps(unionDepsRaw, i);
1412
+ if (bad.length) {
1413
+ errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
1414
+ break;
1415
+ }
1416
+ const spec = op.merged ?? {};
1417
+ if (!spec.title) {
1418
+ errors.push(`${label}: merged task needs a title`);
1419
+ break;
1420
+ }
1421
+ const writes = toMerge.find((t) => !isReadOnly(t, catalog));
1422
+ const stories = [...new Set(toMerge.flatMap((t) => t.userStoriesCovered ?? []))];
1423
+ const planned = plannedTask(spec, intake, {
1424
+ freshIds: true,
1425
+ newId,
1426
+ from: {
1427
+ readOnly: !writes,
1428
+ fields: {
1429
+ description: toMerge.map((t) => t.description).filter(Boolean).join("\n\n"),
1430
+ prompt: toMerge.map((t) => t.prompt).filter(Boolean).join("\n\n---\n\n") || void 0,
1431
+ type: toMerge.some((t) => t.type === "user") ? "user" : "ai",
1432
+ assignedRunner: toMerge[0].assignedRunner,
1433
+ assignedModel: toMerge.find((t) => t.assignedModel)?.assignedModel,
1434
+ taskMode: (writes ?? toMerge[0]).taskMode,
1435
+ autonomy: toMerge.find((t) => t.autonomy)?.autonomy,
1436
+ sliceType: toMerge.find((t) => t.sliceType)?.sliceType,
1437
+ ops: inheritedOps(toMerge),
1438
+ skills: toMerge.flatMap((t) => t.skills ?? []),
1439
+ userStoriesCovered: stories.length ? stories : void 0
1440
+ }
1441
+ }
1442
+ });
1443
+ const refused = refusalOf(planned);
1444
+ if (refused) {
1445
+ errors.push(`${label}: ${refused}`);
1446
+ break;
1447
+ }
1448
+ adjustments.push(...planned.adjustments);
1449
+ const built = resolveTree({ ...planned, task: { ...planned.task, dependencies: [] } }, i);
1450
+ if ("error" in built) {
1451
+ errors.push(`${label}: ${built.error}`);
1452
+ break;
1453
+ }
1454
+ const mergedTask = { ...built.task, dependencies: ids };
1455
+ const mergedId = mergedTask.id;
1456
+ const firstId = toMerge[0].id;
1457
+ const result = [];
1458
+ let inserted = false;
1459
+ for (const t of tasks) {
1460
+ if (idSet.has(t.id)) {
1461
+ if (!inserted && t.id === firstId) {
1462
+ result.push(mergedTask);
1463
+ inserted = true;
1464
+ }
1465
+ continue;
1466
+ }
1467
+ result.push({ ...t, dependencies: t.dependencies.map((d) => idSet.has(d) ? mergedId : d) });
1468
+ }
1469
+ if (!inserted) result.push(mergedTask);
1470
+ tasks = renumberTasks(result);
1471
+ if (op.handle) handleId.set(op.handle, mergedId);
1472
+ summary.push(`Merged ${toMerge.length} tasks into "${spec.title}"`);
1473
+ break;
1474
+ }
1475
+ case "split": {
1476
+ const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, i);
1477
+ if ("error" in ref) {
1478
+ errors.push(`${label}: ${ref.error}`);
1479
+ break;
1480
+ }
1481
+ const target = findTopLevel(tasks, ref.id);
1482
+ if (!target) {
1483
+ errors.push(`${label}: task "${op.taskId}" not found`);
1484
+ break;
1485
+ }
1486
+ const locked = lockedTaskRefusal(target, "split");
1487
+ if (locked) {
1488
+ errors.push(`${label}: ${locked}`);
1489
+ break;
1490
+ }
1491
+ const parts = op.parts ?? [];
1492
+ if (parts.length < 2) {
1493
+ errors.push(`${label}: split needs at least two parts`);
1494
+ break;
1495
+ }
1496
+ const newTasks = [];
1497
+ let prevId;
1498
+ let buildError = false;
1499
+ for (let p = 0; p < parts.length; p++) {
1500
+ const spec = parts[p];
1501
+ if (!spec || !spec.title) {
1502
+ errors.push(`${label}: split part ${p + 1} needs a title`);
1503
+ buildError = true;
1504
+ break;
1505
+ }
1506
+ let deps;
1507
+ if (p === 0) {
1508
+ const { ids, bad } = resolveDeps(target.dependencies, i);
1509
+ if (bad.length) {
1510
+ errors.push(`${label}: split part 1 unknown dependencies: ${bad.join(", ")}`);
1511
+ buildError = true;
1512
+ break;
1513
+ }
1514
+ deps = ids;
1515
+ } else {
1516
+ deps = prevId ? [prevId] : [];
1517
+ }
1518
+ const planned = plannedTask(spec, intake, {
1519
+ freshIds: true,
1520
+ newId,
1521
+ from: {
1522
+ readOnly: isReadOnly(target, catalog),
1523
+ fields: {
1524
+ type: target.type,
1525
+ assignedRunner: target.assignedRunner,
1526
+ assignedModel: target.assignedModel,
1527
+ taskMode: target.taskMode,
1528
+ autonomy: target.autonomy,
1529
+ sliceType: target.sliceType,
1530
+ ops: inheritedOps([target]),
1531
+ skills: target.skills,
1532
+ userStoriesCovered: target.userStoriesCovered
1533
+ }
1534
+ }
1535
+ });
1536
+ const refused = refusalOf(planned);
1537
+ if (refused) {
1538
+ errors.push(`${label}: split part ${p + 1}: ${refused}`);
1539
+ buildError = true;
1540
+ break;
1541
+ }
1542
+ adjustments.push(...planned.adjustments);
1543
+ const resolved = resolveTree({ ...planned, task: { ...planned.task, dependencies: [] } }, i);
1544
+ if ("error" in resolved) {
1545
+ errors.push(`${label}: split part ${p + 1}: ${resolved.error}`);
1546
+ buildError = true;
1547
+ break;
1548
+ }
1549
+ const nt = { ...resolved.task, dependencies: deps };
1550
+ newTasks.push(nt);
1551
+ prevId = nt.id;
1552
+ }
1553
+ if (buildError) break;
1554
+ const tailId = newTasks[newTasks.length - 1].id;
1555
+ const result = [];
1556
+ for (const t of tasks) {
1557
+ if (t.id === target.id) {
1558
+ result.push(...newTasks);
1559
+ continue;
1560
+ }
1561
+ result.push({ ...t, dependencies: t.dependencies.map((d) => d === target.id ? tailId : d) });
1562
+ }
1563
+ tasks = renumberTasks(result);
1564
+ if (op.handle) handleId.set(op.handle, tailId);
1565
+ summary.push(`Split "${target.title}" into ${newTasks.length} tasks`);
1566
+ break;
1567
+ }
1568
+ case "rearm": {
1569
+ const prep = prepareEdit(op, i, label, "direct", true);
1570
+ if ("error" in prep) {
1571
+ errors.push(prep.error);
1572
+ break;
1573
+ }
1574
+ const { target, changes, fields, check } = prep;
1575
+ tasks = updateTaskInPlan(tasks, target.id, { ...changes, status: "pending", verdict: void 0, outputSummary: void 0, awaitingReason: void 0, resumeAt: void 0, skipReason: void 0 });
1576
+ const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
1577
+ const changedNote = fields.length ? ` (${fields.join(", ")})` : "";
1578
+ summary.push(`Re-armed "${target.title}"${changedNote}${clearedNote}`);
1579
+ break;
1580
+ }
1581
+ default:
1582
+ errors.push(`${label}: unknown op "${op.op}"`);
1583
+ }
1584
+ if (errors.length) break;
1585
+ lastLabel = label;
1586
+ const now = new Set(checkPlanInvariants(tasks, runners, catalog?.runnerModes).map(breachKey));
1587
+ for (const key of brokenBy.keys()) if (!now.has(key)) brokenBy.delete(key);
1588
+ for (const key of now) if (!standing.has(key) && !brokenBy.has(key)) brokenBy.set(key, label);
1589
+ }
1590
+ if (!errors.length) {
1591
+ for (const e of checkPlanInvariants(tasks, runners, catalog?.runnerModes)) {
1592
+ const key = breachKey(e);
1593
+ if (!standing.has(key)) errors.push(`${brokenBy.get(key) ?? lastLabel}: ${e.message}`);
1594
+ }
1595
+ }
1596
+ if (!errors.length) {
1597
+ const repaired = repairOrder(tasks);
1598
+ if ("error" in repaired) {
1599
+ errors.push(repaired.error);
1600
+ } else {
1601
+ const moves = describeMoves(tasks, repaired.tasks);
1602
+ if (moves) summary.push(`Reordered to keep dependencies first: ${moves}`);
1603
+ tasks = renumberTasks(repaired.tasks);
1604
+ }
1605
+ }
1606
+ if (errors.length) return { ok: false, tasks: currentTasks, errors, summary: [] };
1607
+ const landed = new Map(flattenTasks(tasks).map((t) => [t.id, t]));
1608
+ const told = /* @__PURE__ */ new Set();
1609
+ const standingAdjustments = adjustments.filter((a) => {
1610
+ const task = landed.get(a.taskId);
1611
+ const now = a.field === "assignedModel" ? task?.assignedModel?.modelId : task?.assignedModel?.thinkingEffort;
1612
+ const key = `${a.taskId}\0${a.field}`;
1613
+ if (task === void 0 || (now ?? null) !== a.to || told.has(key)) return false;
1614
+ told.add(key);
1615
+ return true;
1616
+ });
1617
+ return { ok: true, tasks, errors: [], summary, handles: handleId, adjustments: standingAdjustments };
1618
+ }
1619
+
804
1620
  // src/services/continuation.ts
1621
+ function sessionOwner(task) {
1622
+ return task.runnerSessionOwner ?? task.attemptUsage?.at(-1)?.runner;
1623
+ }
805
1624
  function continuability(task) {
806
1625
  const refuse = (reason) => ({ ok: false, reason: `Task "${task.title}" cannot be continued: ${reason}` });
807
1626
  if (task.type !== "ai") return refuse("it is not run by an agent.");
@@ -811,6 +1630,10 @@ function continuability(task) {
811
1630
  const finished = task.status === "completed" || task.status === "failed" || task.status === "skipped";
812
1631
  if (!finished && !pausedOnLimit(task)) return refuse("only a completed, failed or skipped task, or one paused on a usage limit, can be.");
813
1632
  if (!task.runnerSessionId) return refuse("its runner left no saved session to resume. Use Retry instead.");
1633
+ const owner = sessionOwner(task);
1634
+ if (owner && owner !== task.assignedRunner) {
1635
+ return refuse(`its saved session belongs to ${owner}; Retry starts fresh on ${task.assignedRunner} and carries over its files.`);
1636
+ }
814
1637
  return { ok: true, sessionId: task.runnerSessionId };
815
1638
  }
816
1639
  function canContinue(task) {
@@ -818,7 +1641,7 @@ function canContinue(task) {
818
1641
  }
819
1642
 
820
1643
  // src/services/blocking.ts
821
- var UNSTARTED = /* @__PURE__ */ new Set(["pending", "approved", "blocked"]);
1644
+ var UNSTARTED = /* @__PURE__ */ new Set(["pending", "approved"]);
822
1645
  function blockingFailures(tasks) {
823
1646
  const byId = /* @__PURE__ */ new Map();
824
1647
  const index = (list) => {
@@ -934,6 +1757,7 @@ function serializePlan(plan) {
934
1757
  runners: plan.runners,
935
1758
  generatedAt: plan.generatedAt,
936
1759
  runSettings: plan.runSettings,
1760
+ ...plan.plannerResend ? { plannerResend: plan.plannerResend } : {},
937
1761
  conversationHistory: surfaceConversation(plan.conversationHistory),
938
1762
  prdMarkdown: plan.prdMarkdown
939
1763
  };
@@ -944,7 +1768,7 @@ function executionSummary(tasks) {
944
1768
  total: tasks.length,
945
1769
  completed: tasks.filter((t) => t.status === "completed").length,
946
1770
  failed: tasks.filter((t) => t.status === "failed").length,
947
- blocked: tasks.filter((t) => t.status === "blocked" || blocked.has(t.id)).length,
1771
+ blocked: tasks.filter((t) => blocked.has(t.id)).length,
948
1772
  skipped: tasks.filter((t) => t.status === "skipped").length
949
1773
  };
950
1774
  }
@@ -955,6 +1779,31 @@ function truncateCheckpointSummary(summary) {
955
1779
  return firstLine.slice(0, CHECKPOINT_TRUNCATE_LENGTH - 1) + "\u2026";
956
1780
  }
957
1781
 
1782
+ // src/services/AssignmentScope.ts
1783
+ var OVER = /* @__PURE__ */ new Set(["completed", "skipped"]);
1784
+ function assignmentIssue(task, scope) {
1785
+ if (task.type !== "ai" || OVER.has(task.status)) return null;
1786
+ if (!scope.enabledRunners.includes(task.assignedRunner)) return { kind: "runner-off", runner: task.assignedRunner };
1787
+ const model = task.assignedModel;
1788
+ const allowed = effectiveAllowlist(scope.allowlist[task.assignedRunner], task.assignedRunner, scope.models);
1789
+ if (model && allowed && !allowed.includes(model.modelId)) {
1790
+ return { kind: "model-off-list", runner: task.assignedRunner, modelId: model.modelId, modelLabel: model.modelLabel || model.modelId };
1791
+ }
1792
+ return null;
1793
+ }
1794
+ function assignmentIssueText(issue) {
1795
+ return issue.kind === "runner-off" ? `${issue.runner} is switched off` : `${issue.modelLabel} is not in the allowlist`;
1796
+ }
1797
+ function assignmentConsequence({ status, awaitingReason, resumeAt }) {
1798
+ if (status === "failed") return "failed; a retry would run it there unless moved";
1799
+ if (status === "awaiting_user" && awaitingReason === "usage-limit") {
1800
+ return resumeAt ? "paused on a usage limit; resumes there at the reset unless moved" : "paused on a usage limit; a retry would run it there unless moved";
1801
+ }
1802
+ if (status === "awaiting_user" && (awaitingReason === "conflict" || awaitingReason === "files-changed")) return "waiting for the user; what runs next runs there unless moved";
1803
+ if (status === "in_progress" || status === "awaiting_user") return "already running as assigned";
1804
+ return "runs as assigned unless moved";
1805
+ }
1806
+
958
1807
  // src/services/conflictFiles.ts
959
1808
  function capConflictFiles(files, max = 5) {
960
1809
  return files.length <= max ? files.join(", ") : `${files.slice(0, max).join(", ")}, +${files.length - max} more`;
@@ -1053,12 +1902,7 @@ function split({ verdict, idleSince, isolation, checkpoint, queued, continuable,
1053
1902
  return { plan, status: { verdict, idleSince, isolation, checkpoint, queued, continuable, awaitingApproval, mergeGate, forcedPastGate, waitsForApproval } };
1054
1903
  }
1055
1904
  function refresh(previous, tasks) {
1056
- const held = /* @__PURE__ */ new Map();
1057
- const index = (list) => list.forEach((t) => {
1058
- held.set(t.id, t);
1059
- index(t.subtasks);
1060
- });
1061
- index(previous);
1905
+ const held = new Map([...walkPlan(previous)].map(({ task }) => [task.id, task]));
1062
1906
  const fold = ({ subtasks, ...task }) => {
1063
1907
  const was = held.get(task.id);
1064
1908
  const { checkpoint, ...status } = was ? split(was).status : {};
@@ -1122,6 +1966,9 @@ function startTask(task) {
1122
1966
  function running({ awaitingReason: _reason, resumeAt: _resumeAt, skipReason: _skipped, checkpoint: _checkpoint, ...task }) {
1123
1967
  return { ...task, status: "in_progress" };
1124
1968
  }
1969
+ function withoutIsolation(tasks) {
1970
+ return tasks.map(({ isolation: _isolation, ...task }) => ({ ...task, subtasks: withoutIsolation(task.subtasks) }));
1971
+ }
1125
1972
  function handoffOf({ type: _type, ...handoff }) {
1126
1973
  return handoff;
1127
1974
  }
@@ -1158,6 +2005,8 @@ function reduceRun(view, input) {
1158
2005
  return { ...view, handoff: handoffOf(input) };
1159
2006
  case "isolation_merge":
1160
2007
  return input.result.outcome === "merged" ? { ...view, mergeResult: input.result, handoff: null, runMerge: null } : { ...view, mergeResult: input.result };
2008
+ case "run_closed":
2009
+ return { ...view, tasks: withoutIsolation(view.tasks), handoff: null, runMerge: null, mergeResult: null };
1161
2010
  // A checkpoint's whole question and every status `review_needed` implies
1162
2011
  // arrive in the status update that follows; `review_needed`'s own list is
1163
2012
  // flat, so as a plan it would lift subtasks to the top level.
@@ -1273,8 +2122,9 @@ function dropBuildingPlan(view, turnId, segmentId) {
1273
2122
  const blocks = view.blocks.filter((b) => !(isBuildingPlan(b, turnId) && (segmentId === void 0 || b.segmentId === segmentId)));
1274
2123
  return blocks.length === view.blocks.length ? view : { ...view, blocks };
1275
2124
  }
1276
- function settleReply(view, { content, turnId, supervisor }) {
2125
+ function settleReply(view, { content, turnId, supervisor, runs }) {
1277
2126
  const role = supervisor ? "supervisor" : "planner";
2127
+ if (runs) return settleRuns(view, runs, role, turnId);
1278
2128
  if (turnId !== void 0) {
1279
2129
  const i = finalSegmentIndex(view.blocks, turnId);
1280
2130
  if (i >= 0) {
@@ -1283,6 +2133,25 @@ function settleReply(view, { content, turnId, supervisor }) {
1283
2133
  }
1284
2134
  return append(dropBuildingPlan(view, turnId), (id) => settledMessage(id, role, content, turnId));
1285
2135
  }
2136
+ function settleRuns(view, runs, role, turnId) {
2137
+ const settled = /* @__PURE__ */ new Map();
2138
+ let turn = turnId;
2139
+ for (const run of runs) {
2140
+ const i = view.blocks.findIndex((b) => b.type === "message" && b.segmentId === run.segmentId && b.turnId !== void 0 && (turnId === void 0 || b.turnId === turnId));
2141
+ const segment = view.blocks[i];
2142
+ if (segment?.type !== "message") continue;
2143
+ settled.set(i, run);
2144
+ turn ??= segment.turnId;
2145
+ }
2146
+ const contained = new Set(runs.map((run) => run.segmentId));
2147
+ const blocks = view.blocks.flatMap((block, i) => {
2148
+ const run = settled.get(i);
2149
+ if (run) return [settledMessage(block.id, role, run.text, turn)];
2150
+ return turn !== void 0 && isUnsettledSegment(block, turn) && !contained.has(block.segmentId) ? [] : [block];
2151
+ });
2152
+ const streamed = new Set(settled.values());
2153
+ return runs.filter((run) => !streamed.has(run)).reduce((next, run) => append(next, (id) => settledMessage(id, role, run.text, turn)), dropBuildingPlan({ ...view, blocks }, turn));
2154
+ }
1286
2155
  function retractText(view, { turnId, segmentId }) {
1287
2156
  const blocks = view.blocks.filter((b) => !(isUnsettledSegment(b, turnId) && (segmentId === void 0 || b.segmentId === segmentId)));
1288
2157
  return dropBuildingPlan(blocks.length === view.blocks.length ? view : { ...view, blocks }, turnId, segmentId);
@@ -1399,6 +2268,7 @@ function syncTranscript(view, { plan, turnId }) {
1399
2268
  if (isNew && entry.kind === "wake_up") next = takePendingInputs(next, void 0, [entry.content]).view;
1400
2269
  if (isNew && entry.kind === "plan_generated") next = markPlan(next, entry.content, i === committed ? turnId : void 0);
1401
2270
  if (isNew && entry.kind === "system") next = append(next, (id) => settledMessage(id, "system", entry.content));
2271
+ if (isNew && entry.kind === "error") next = append(next, (id) => settledMessage(id, "error", entry.content));
1402
2272
  if (isNew && entry.kind === "compaction") next = markCompaction(next, entry.content);
1403
2273
  if (latest === void 0 || entry.timestamp > latest) latest = entry.timestamp;
1404
2274
  }
@@ -1479,6 +2349,8 @@ function roleOf(entry) {
1479
2349
  return "wake_up";
1480
2350
  case "supervisor":
1481
2351
  return "supervisor";
2352
+ case "error":
2353
+ return "error";
1482
2354
  default:
1483
2355
  return entry.role === "user" ? "user" : "planner";
1484
2356
  }
@@ -1556,7 +2428,7 @@ function fromTranscript(conversationHistory, researchLog, plannerUsage, pendingT
1556
2428
  const kept = compacted ? history.slice(1) : history;
1557
2429
  const cutoff = compacted ? kept[0]?.timestamp ?? compacted.timestamp : "";
1558
2430
  const placed = [
1559
- ...kept.map((entry, i) => ({ at: entry.timestamp, rank: rankOf(entry), build: fromEntry(entry, loadsAfter(kept, i)) })),
2431
+ ...kept.flatMap((entry, i) => entry.runs ? entry.runs.map((run) => ({ at: run.startedAt, rank: SETTLED, build: (id2) => settledMessage(id2(), roleOf(entry), run.text) })) : [{ at: entry.timestamp, rank: rankOf(entry), build: fromEntry(entry, loadsAfter(kept, i)) }]),
1560
2432
  ...research(researchLog ?? []).filter((item) => item.at >= cutoff)
1561
2433
  ];
1562
2434
  placed.sort((a, b) => a.at < b.at ? -1 : a.at > b.at ? 1 : a.rank - b.rank);
@@ -1627,16 +2499,16 @@ function cutOff(gate, input) {
1627
2499
  const turnId = "turnId" in input ? input.turnId : void 0;
1628
2500
  return turnId === void 0 || turnId === gate.stopped;
1629
2501
  }
1630
- function withGate(live, gate) {
1631
- return gate.open === live.gate.open && gate.stopped === live.gate.stopped ? live : { ...live, gate };
2502
+ function withGate(live2, gate) {
2503
+ return gate.open === live2.gate.open && gate.stopped === live2.gate.stopped ? live2 : { ...live2, gate };
1632
2504
  }
1633
2505
  function followTurn(view, gate, input) {
1634
- const live = { view, gate };
2506
+ const live2 = { view, gate };
1635
2507
  if (input.type === "planner_turn_started") {
1636
2508
  return withGate({ view: reduceConversation(view, input), gate }, { open: input.turnId, stopped: null });
1637
2509
  }
1638
2510
  if (cutOff(gate, input)) {
1639
- return input.type === "planner_turn_ended" ? withGate(live, { ...gate, stopped: null }) : live;
2511
+ return input.type === "planner_turn_ended" ? withGate(live2, { ...gate, stopped: null }) : live2;
1640
2512
  }
1641
2513
  const next = { view: reduceConversation(view, input), gate };
1642
2514
  return input.type === "planner_turn_ended" && input.turnId === gate.open ? withGate(next, { ...gate, open: null }) : next;
@@ -1689,6 +2561,60 @@ function markdownHtml(text2) {
1689
2561
  return md.render(text2);
1690
2562
  }
1691
2563
 
2564
+ // src/conversation/taskLogFollow.ts
2565
+ var EMPTY_TASK_LOG_FOLLOW = {
2566
+ view: EMPTY_TASK_LOG,
2567
+ attempts: [],
2568
+ attempt: 0,
2569
+ pending: [],
2570
+ loaded: false,
2571
+ followLatest: true
2572
+ };
2573
+ function covered(position, batch) {
2574
+ return !!position && !!batch.position && position.stream === batch.position.stream && batch.position.seq <= position.seq;
2575
+ }
2576
+ function live(state, batch) {
2577
+ if (batch.attempt <= 0 || batch.attempt < state.attempt) return state;
2578
+ const attempts = state.attempts.includes(batch.attempt) ? state.attempts : [...state.attempts, batch.attempt].sort((a, b) => a - b);
2579
+ if (batch.attempt > state.attempt) {
2580
+ if (!state.followLatest) return attempts === state.attempts ? state : { ...state, attempts };
2581
+ return { ...state, attempts, attempt: batch.attempt, position: void 0, view: batch.events.reduce(reduceTaskLog, EMPTY_TASK_LOG) };
2582
+ }
2583
+ if (covered(state.position, batch)) return state;
2584
+ return { ...state, attempts, view: batch.events.reduce(reduceTaskLog, state.view) };
2585
+ }
2586
+ function followTaskLog(state, input) {
2587
+ switch (input.type) {
2588
+ case "opened":
2589
+ return EMPTY_TASK_LOG_FOLLOW;
2590
+ case "picked":
2591
+ return { ...state, loaded: false, loadingAttempt: input.attempt, followLatest: input.attempt === state.attempts.at(-1) };
2592
+ case "live":
2593
+ return state.loaded ? live(state, input) : { ...state, pending: [...state.pending, input] };
2594
+ case "loaded": {
2595
+ if (state.loaded) return state;
2596
+ if (!input.abandoned && state.loadingAttempt !== void 0 && input.attempt !== state.loadingAttempt) return state;
2597
+ const attempts = [.../* @__PURE__ */ new Set([...input.attempts ?? state.attempts, ...state.pending.map((batch) => batch.attempt)])].filter((attempt) => attempt > 0).sort((a, b) => a - b);
2598
+ let next = {
2599
+ ...state,
2600
+ attempts,
2601
+ loaded: true,
2602
+ pending: [],
2603
+ loadingAttempt: void 0,
2604
+ ...input.abandoned ? {} : { attempt: input.attempt, view: replayTaskLog(input.events), position: input.position }
2605
+ };
2606
+ for (const batch of state.pending) {
2607
+ if (!input.abandoned && batch.attempt === input.attempt) {
2608
+ if (covered(input.position, batch)) continue;
2609
+ if (!input.position && !batch.position) continue;
2610
+ }
2611
+ next = live(next, batch);
2612
+ }
2613
+ return next;
2614
+ }
2615
+ }
2616
+ }
2617
+
1692
2618
  // src/taskRow/actions.ts
1693
2619
  var UNSTARTED_AI = (t) => t.type === "ai" && NOT_STARTED.has(t.status);
1694
2620
  var QUEUED = (t) => t.status === "pending" || t.status === "approved";
@@ -1743,13 +2669,18 @@ function blockedBy(task, tasks) {
1743
2669
  if (!tasks?.length || !NOT_STARTED.has(task.status)) return [];
1744
2670
  return blockingFailures(asBlocking(tasks)).get(task.id) ?? [];
1745
2671
  }
2672
+ function flagOf(task, scope) {
2673
+ const issue = scope && task.assignedRunner !== void 0 ? assignmentIssue({ ...task, assignedRunner: task.assignedRunner }, scope) : null;
2674
+ return issue ? { assignmentFlag: assignmentIssueText(issue), assignmentConsequence: assignmentConsequence(task) } : { assignmentFlag: null, assignmentConsequence: null };
2675
+ }
1746
2676
  function taskRowView(task, context = {}) {
1747
2677
  const parent = context.parent ?? void 0;
1748
2678
  const isolation = task.isolation && task.isolation.state !== "none" ? task.isolation : null;
1749
- const latestUsage = task.attemptUsage?.at(-1);
2679
+ const spend = taskUsageSummary(task);
2680
+ const latestAttempt = task.attemptUsage?.at(-1);
1750
2681
  const blocked = blockedBy(task, context.tasks);
1751
2682
  return {
1752
- 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,
2683
+ usage: latestAttempt ? { totals: spend.totals, ...spend.contextFill ? { contextFill: spend.contextFill } : {}, attempt: latestAttempt.attempt } : task.type === "ai" && ["completed", "failed", "in_progress", "awaiting_user"].includes(task.status) ? { totals: {}, attempt: 0 } : null,
1753
2684
  orderLabel: taskOrderLabel(task, parent),
1754
2685
  kind: task.type === "user" ? "user" : task.ops ? "ops" : "ai",
1755
2686
  status: blocked.length > 0 ? "blocked" : taskStatusKind(task),
@@ -1758,6 +2689,7 @@ function taskRowView(task, context = {}) {
1758
2689
  resumes: resumeLabel(task, context.now),
1759
2690
  approvals: approvalLabel(task.awaitingApproval),
1760
2691
  waitsForApproval: task.waitsForApproval && NOT_STARTED.has(task.status) ? WAITS_FOR_APPROVAL_LABEL : null,
2692
+ ...flagOf(task, context.runnerScope),
1761
2693
  autonomous: task.type === "ai" && context.modes?.find((m) => m.id === task.taskMode)?.autonomous === true,
1762
2694
  repos: namedRepos(isolation?.repos),
1763
2695
  conflict: isolation?.state === "conflict" ? { repo: namedRepo(isolation.conflictRepo), files: filesOf(isolation.conflictFiles) } : null,
@@ -1780,10 +2712,10 @@ function taskRowView(task, context = {}) {
1780
2712
  function titledRefs(ids, tasks) {
1781
2713
  return ids.map((id) => titledTaskRefOf(tasks, id) ?? id);
1782
2714
  }
1783
- function dependentsNotice(dependents) {
2715
+ function dependentsNotice(dependents, tasks = []) {
1784
2716
  if (dependents.length === 0) return null;
1785
2717
  const subject = dependents.length === 1 ? "1 task depends" : `${dependents.length} tasks depend`;
1786
- return `${subject} on it and will lose that dependency: ${dependents.map((t) => titledTaskRef(t)).join(", ")}.`;
2718
+ return `${subject} on it and will lose that dependency: ${dependents.map((t) => (t.id ? titledTaskRefOf(tasks, t.id) : void 0) ?? titledTaskRef(t)).join(", ")}.`;
1787
2719
  }
1788
2720
  function pastGateConfirmation(unmerged, subject = "This task") {
1789
2721
  return `${subject} waits for Merge all: the work of ${unmerged.join(", ")} is not merged into your branch yet, so it would act without it. Starting it now is kept on the task.`;
@@ -1805,8 +2737,11 @@ export {
1805
2737
  startAttemptUsage,
1806
2738
  addAttemptUsage,
1807
2739
  endAttemptUsage,
2740
+ countedSessionCost,
2741
+ resumedSessionWindow,
1808
2742
  addUsageTotals,
1809
2743
  planUsage,
2744
+ taskUsageSummary,
1810
2745
  sameMessage,
1811
2746
  mapAgentTool,
1812
2747
  normalizeAgentArgs,
@@ -1822,6 +2757,25 @@ export {
1822
2757
  runnerToolSubject,
1823
2758
  reduceTaskLog,
1824
2759
  replayTaskLog,
2760
+ isTaskRunning,
2761
+ NOT_STARTED,
2762
+ taskStatusKind,
2763
+ awaitingLabel,
2764
+ blockedByLabel,
2765
+ skippedLabel,
2766
+ WAITS_FOR_APPROVAL_LABEL,
2767
+ approvalLabel,
2768
+ resumeTime,
2769
+ resumeLabel,
2770
+ validateTaskEdit,
2771
+ UPDATABLE_FIELDS,
2772
+ taskOpsProtocol,
2773
+ textHasTaskOps,
2774
+ parseTaskOpsJson,
2775
+ canMergeTasks,
2776
+ canSplitTask,
2777
+ applyTaskOps,
2778
+ sessionOwner,
1825
2779
  continuability,
1826
2780
  canContinue,
1827
2781
  blockingFailures,
@@ -1834,6 +2788,9 @@ export {
1834
2788
  executionSummary,
1835
2789
  CHECKPOINT_TRUNCATE_LENGTH,
1836
2790
  truncateCheckpointSummary,
2791
+ assignmentIssue,
2792
+ assignmentIssueText,
2793
+ assignmentConsequence,
1837
2794
  capConflictFiles,
1838
2795
  summarizeToolCall,
1839
2796
  classifyOutcome,
@@ -1865,6 +2822,8 @@ export {
1865
2822
  taskStartedNotice,
1866
2823
  markdownTokens,
1867
2824
  markdownHtml,
2825
+ EMPTY_TASK_LOG_FOLLOW,
2826
+ followTaskLog,
1868
2827
  taskRowActions,
1869
2828
  markAction,
1870
2829
  markRequestFor,
@@ -1876,4 +2835,4 @@ export {
1876
2835
  pastGateConfirmation,
1877
2836
  newTaskFields
1878
2837
  };
1879
- //# sourceMappingURL=chunk-FH6M5QZG.mjs.map
2838
+ //# sourceMappingURL=chunk-DMG4ISUG.mjs.map