@ordewell/core 0.10.5 → 0.10.7

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 (44) hide show
  1. package/dist/{IRunner-Crkt3tHD.d.ts → IFileSystem-C4_mpfze.d.ts} +97 -14
  2. package/dist/{IRunner-lMQIa5Wc.d.mts → IFileSystem-DUgeWWQN.d.mts} +97 -14
  3. package/dist/{ModeResolver-B4hhABNf.d.mts → ModeResolver-4qNjxcDs.d.mts} +1 -1
  4. package/dist/{ModeResolver-CUR5jCSU.d.ts → ModeResolver-DGK2rWHE.d.ts} +1 -1
  5. package/dist/{Task-D4QHjdGv.d.mts → Task-C0o4i3Nn.d.mts} +23 -4
  6. package/dist/{Task-D4QHjdGv.d.ts → Task-C0o4i3Nn.d.ts} +23 -4
  7. package/dist/{chunk-6LEA6WAM.mjs → chunk-DMG4ISUG.mjs} +923 -45
  8. package/dist/chunk-DMG4ISUG.mjs.map +1 -0
  9. package/dist/{chunk-RT4NQ2BZ.mjs → chunk-EEE2GBLG.mjs} +114 -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-F3KIX7WR.mjs → chunk-LTHKFBKM.mjs} +4 -4
  14. package/dist/chunk-LTHKFBKM.mjs.map +1 -0
  15. package/dist/index.d.mts +278 -1701
  16. package/dist/index.d.ts +278 -1701
  17. package/dist/index.js +2744 -1915
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +2186 -1535
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/parsing.d.mts +2 -2
  22. package/dist/parsing.d.ts +2 -2
  23. package/dist/parsing.js +231 -75
  24. package/dist/parsing.js.map +1 -1
  25. package/dist/parsing.mjs +3 -3
  26. package/dist/plan-utils.d.mts +20 -4
  27. package/dist/plan-utils.d.ts +20 -4
  28. package/dist/plan-utils.js +255 -97
  29. package/dist/plan-utils.js.map +1 -1
  30. package/dist/plan-utils.mjs +33 -19
  31. package/dist/{plan-utils-wI8MuDBC.d.mts → runSettingsText-BgOmLxdw.d.mts} +1941 -108
  32. package/dist/{plan-utils-BVJCzNvm.d.ts → runSettingsText-BgXvajj2.d.ts} +1941 -108
  33. package/dist/testing.d.mts +2 -3
  34. package/dist/testing.d.ts +2 -3
  35. package/dist/testing.js.map +1 -1
  36. package/dist/testing.mjs +1 -1
  37. package/package.json +1 -1
  38. package/dist/IFileSystem-ClBdp59f.d.ts +0 -78
  39. package/dist/IFileSystem-Db34maxC.d.mts +0 -78
  40. package/dist/chunk-4QLFM73A.mjs +0 -1407
  41. package/dist/chunk-4QLFM73A.mjs.map +0 -1
  42. package/dist/chunk-6LEA6WAM.mjs.map +0 -1
  43. package/dist/chunk-F3KIX7WR.mjs.map +0 -1
  44. package/dist/chunk-RT4NQ2BZ.mjs.map +0 -1
@@ -1,21 +1,42 @@
1
1
  import {
2
- NOT_STARTED,
3
- WAITS_FOR_APPROVAL_LABEL,
4
- approvalLabel,
5
- awaitingLabel,
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,
6
9
  effectiveAllowlist,
7
- isTaskRunning,
8
- resumeLabel,
9
- taskStatusKind
10
- } from "./chunk-4QLFM73A.mjs";
10
+ escapeControlCharsInStrings,
11
+ extractObjectsWithKey,
12
+ opsPlacementRefusal,
13
+ plannedTask,
14
+ resolveAssignment,
15
+ stripTrailingCommas,
16
+ taggedReadOnlyMode,
17
+ taskWhere
18
+ } from "./chunk-H7QX32BH.mjs";
11
19
  import {
20
+ addTaskToPlan,
21
+ canSetDependencies,
22
+ flattenTasks,
23
+ flattenTasksWithParents,
24
+ inheritedOps,
25
+ opsFlag,
12
26
  pausedOnLimit,
13
27
  rejoinPlanTree,
28
+ removeTaskFromPlan,
29
+ renumberTasks,
30
+ resolveTaskRef,
31
+ skillNames,
14
32
  taskOrderLabel,
33
+ taskPlacement,
15
34
  taskRefOf,
16
35
  titledTaskRef,
17
- titledTaskRefOf
18
- } from "./chunk-RT4NQ2BZ.mjs";
36
+ titledTaskRefOf,
37
+ updateTaskInPlan,
38
+ walkPlan
39
+ } from "./chunk-EEE2GBLG.mjs";
19
40
 
20
41
  // src/models/Usage.ts
21
42
  function partedPromptUsage(parts) {
@@ -531,7 +552,7 @@ function closeOpenBlocks(view, cut) {
531
552
 
532
553
  // src/conversation/taskLog.ts
533
554
  var EMPTY_TASK_LOG = { blocks: [], nextId: 1, queued: [], working: false, streams: {} };
534
- var TEXT_STREAM = "text";
555
+ var textStream = (runId) => runId === void 0 ? "text" : `text:${runId}`;
535
556
  var thinkingStream = (subagentId) => `thinking:${subagentId ?? ""}`;
536
557
  function withoutStream(view, key) {
537
558
  if (!(key in view.streams)) return view;
@@ -548,35 +569,43 @@ function openStream(view, lane, key) {
548
569
  const block = blocks[i];
549
570
  return block?.id === id && (block.type === "message" || block.type === "thinking") && block.streaming ? i : -1;
550
571
  }
551
- function streamDelta(view, lane, key, text2, make) {
552
- 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);
553
578
  const open = laneBlocks(view, lane)[i];
554
579
  if (open?.type === "message" || open?.type === "thinking") return laneReplace(view, lane, i, { ...open, text: open.text + text2 });
555
580
  const next = appendOutput(view, lane, (id) => make(id, blockText(text2)));
556
581
  return { ...next, streams: { ...next.streams, [key]: `b${view.nextId}` } };
557
582
  }
558
583
  function streamWhole(view, lane, key, text2, make) {
559
- const id = view.streams[key];
560
- const blocks = laneBlocks(view, lane);
561
- const i = id === void 0 ? -1 : findLastIndex(blocks, (b) => b.id === id);
562
- const streamed = blocks[i];
584
+ const i = streamBlock(view, lane, key);
585
+ const streamed = laneBlocks(view, lane)[i];
563
586
  const next = withoutStream(view, key);
564
587
  if (streamed?.type === "message" || streamed?.type === "thinking") {
565
588
  return laneReplace(next, lane, i, { ...streamed, text: blockText(text2), streaming: false });
566
589
  }
567
590
  return appendOutput(next, lane, make);
568
591
  }
569
- function agentText(view, text2) {
570
- const next = streamWhole(view, null, TEXT_STREAM, text2, (id) => settledMessage(id, "agent", blockText(text2)));
571
- 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;
572
594
  }
573
- function agentTextDelta(view, text2) {
574
- const next = streamDelta(view, null, TEXT_STREAM, text2, (id, t) => ({ type: "message", id, role: "agent", text: t, streaming: true }));
575
- 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));
576
601
  }
577
602
  function failTurn(view, message) {
578
603
  const repeated = view.blocks.find((b) => b.id === view.latestText);
579
- 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;
580
609
  return appendOutput({ ...settled, latestText: void 0 }, null, (id) => settledMessage(id, "error", message));
581
610
  }
582
611
  function thinkingBlock(id, text2, streaming, subagentId) {
@@ -757,7 +786,7 @@ function endCall(block) {
757
786
  function endOpenCalls(view) {
758
787
  let changed = false;
759
788
  const blocks = view.blocks.map((block) => {
760
- 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;
761
790
  if (ended !== block) changed = true;
762
791
  return ended;
763
792
  });
@@ -788,9 +817,9 @@ function reduceTaskLog(view, event) {
788
817
  case "turn_end":
789
818
  return endTurn(view, event);
790
819
  case "text_delta":
791
- return agentTextDelta(view, event.text);
820
+ return agentTextDelta(view, event);
792
821
  case "text":
793
- return agentText(view, event.text);
822
+ return agentText(view, event);
794
823
  case "thinking_delta":
795
824
  return thinkingDelta(view, event);
796
825
  case "thinking":
@@ -824,7 +853,7 @@ function reduceTaskLog(view, event) {
824
853
  case "approval_withdrawn":
825
854
  return settleApproval(view, event.approvalId, (block) => ({ ...block, status: "withdrawn" }));
826
855
  case "runner_ended":
827
- return endOpenCalls(view);
856
+ return closeOpenBlocks({ ...endOpenCalls(view), working: false, streams: {}, latestText: void 0 }, "stopped");
828
857
  default:
829
858
  return view;
830
859
  }
@@ -833,6 +862,761 @@ function replayTaskLog(events, from = EMPTY_TASK_LOG) {
833
862
  return events.reduce(reduceTaskLog, from);
834
863
  }
835
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
+
836
1620
  // src/services/continuation.ts
837
1621
  function sessionOwner(task) {
838
1622
  return task.runnerSessionOwner ?? task.attemptUsage?.at(-1)?.runner;
@@ -857,7 +1641,7 @@ function canContinue(task) {
857
1641
  }
858
1642
 
859
1643
  // src/services/blocking.ts
860
- var UNSTARTED = /* @__PURE__ */ new Set(["pending", "approved", "blocked"]);
1644
+ var UNSTARTED = /* @__PURE__ */ new Set(["pending", "approved"]);
861
1645
  function blockingFailures(tasks) {
862
1646
  const byId = /* @__PURE__ */ new Map();
863
1647
  const index = (list) => {
@@ -984,7 +1768,7 @@ function executionSummary(tasks) {
984
1768
  total: tasks.length,
985
1769
  completed: tasks.filter((t) => t.status === "completed").length,
986
1770
  failed: tasks.filter((t) => t.status === "failed").length,
987
- blocked: tasks.filter((t) => t.status === "blocked" || blocked.has(t.id)).length,
1771
+ blocked: tasks.filter((t) => blocked.has(t.id)).length,
988
1772
  skipped: tasks.filter((t) => t.status === "skipped").length
989
1773
  };
990
1774
  }
@@ -1118,12 +1902,7 @@ function split({ verdict, idleSince, isolation, checkpoint, queued, continuable,
1118
1902
  return { plan, status: { verdict, idleSince, isolation, checkpoint, queued, continuable, awaitingApproval, mergeGate, forcedPastGate, waitsForApproval } };
1119
1903
  }
1120
1904
  function refresh(previous, tasks) {
1121
- const held = /* @__PURE__ */ new Map();
1122
- const index = (list) => list.forEach((t) => {
1123
- held.set(t.id, t);
1124
- index(t.subtasks);
1125
- });
1126
- index(previous);
1905
+ const held = new Map([...walkPlan(previous)].map(({ task }) => [task.id, task]));
1127
1906
  const fold = ({ subtasks, ...task }) => {
1128
1907
  const was = held.get(task.id);
1129
1908
  const { checkpoint, ...status } = was ? split(was).status : {};
@@ -1187,6 +1966,9 @@ function startTask(task) {
1187
1966
  function running({ awaitingReason: _reason, resumeAt: _resumeAt, skipReason: _skipped, checkpoint: _checkpoint, ...task }) {
1188
1967
  return { ...task, status: "in_progress" };
1189
1968
  }
1969
+ function withoutIsolation(tasks) {
1970
+ return tasks.map(({ isolation: _isolation, ...task }) => ({ ...task, subtasks: withoutIsolation(task.subtasks) }));
1971
+ }
1190
1972
  function handoffOf({ type: _type, ...handoff }) {
1191
1973
  return handoff;
1192
1974
  }
@@ -1223,6 +2005,8 @@ function reduceRun(view, input) {
1223
2005
  return { ...view, handoff: handoffOf(input) };
1224
2006
  case "isolation_merge":
1225
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 };
1226
2010
  // A checkpoint's whole question and every status `review_needed` implies
1227
2011
  // arrive in the status update that follows; `review_needed`'s own list is
1228
2012
  // flat, so as a plan it would lift subtasks to the top level.
@@ -1338,8 +2122,9 @@ function dropBuildingPlan(view, turnId, segmentId) {
1338
2122
  const blocks = view.blocks.filter((b) => !(isBuildingPlan(b, turnId) && (segmentId === void 0 || b.segmentId === segmentId)));
1339
2123
  return blocks.length === view.blocks.length ? view : { ...view, blocks };
1340
2124
  }
1341
- function settleReply(view, { content, turnId, supervisor }) {
2125
+ function settleReply(view, { content, turnId, supervisor, runs }) {
1342
2126
  const role = supervisor ? "supervisor" : "planner";
2127
+ if (runs) return settleRuns(view, runs, role, turnId);
1343
2128
  if (turnId !== void 0) {
1344
2129
  const i = finalSegmentIndex(view.blocks, turnId);
1345
2130
  if (i >= 0) {
@@ -1348,6 +2133,25 @@ function settleReply(view, { content, turnId, supervisor }) {
1348
2133
  }
1349
2134
  return append(dropBuildingPlan(view, turnId), (id) => settledMessage(id, role, content, turnId));
1350
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
+ }
1351
2155
  function retractText(view, { turnId, segmentId }) {
1352
2156
  const blocks = view.blocks.filter((b) => !(isUnsettledSegment(b, turnId) && (segmentId === void 0 || b.segmentId === segmentId)));
1353
2157
  return dropBuildingPlan(blocks.length === view.blocks.length ? view : { ...view, blocks }, turnId, segmentId);
@@ -1624,7 +2428,7 @@ function fromTranscript(conversationHistory, researchLog, plannerUsage, pendingT
1624
2428
  const kept = compacted ? history.slice(1) : history;
1625
2429
  const cutoff = compacted ? kept[0]?.timestamp ?? compacted.timestamp : "";
1626
2430
  const placed = [
1627
- ...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)) }]),
1628
2432
  ...research(researchLog ?? []).filter((item) => item.at >= cutoff)
1629
2433
  ];
1630
2434
  placed.sort((a, b) => a.at < b.at ? -1 : a.at > b.at ? 1 : a.rank - b.rank);
@@ -1695,16 +2499,16 @@ function cutOff(gate, input) {
1695
2499
  const turnId = "turnId" in input ? input.turnId : void 0;
1696
2500
  return turnId === void 0 || turnId === gate.stopped;
1697
2501
  }
1698
- function withGate(live, gate) {
1699
- 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 };
1700
2504
  }
1701
2505
  function followTurn(view, gate, input) {
1702
- const live = { view, gate };
2506
+ const live2 = { view, gate };
1703
2507
  if (input.type === "planner_turn_started") {
1704
2508
  return withGate({ view: reduceConversation(view, input), gate }, { open: input.turnId, stopped: null });
1705
2509
  }
1706
2510
  if (cutOff(gate, input)) {
1707
- 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;
1708
2512
  }
1709
2513
  const next = { view: reduceConversation(view, input), gate };
1710
2514
  return input.type === "planner_turn_ended" && input.turnId === gate.open ? withGate(next, { ...gate, open: null }) : next;
@@ -1757,6 +2561,60 @@ function markdownHtml(text2) {
1757
2561
  return md.render(text2);
1758
2562
  }
1759
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
+
1760
2618
  // src/taskRow/actions.ts
1761
2619
  var UNSTARTED_AI = (t) => t.type === "ai" && NOT_STARTED.has(t.status);
1762
2620
  var QUEUED = (t) => t.status === "pending" || t.status === "approved";
@@ -1854,10 +2712,10 @@ function taskRowView(task, context = {}) {
1854
2712
  function titledRefs(ids, tasks) {
1855
2713
  return ids.map((id) => titledTaskRefOf(tasks, id) ?? id);
1856
2714
  }
1857
- function dependentsNotice(dependents) {
2715
+ function dependentsNotice(dependents, tasks = []) {
1858
2716
  if (dependents.length === 0) return null;
1859
2717
  const subject = dependents.length === 1 ? "1 task depends" : `${dependents.length} tasks depend`;
1860
- 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(", ")}.`;
1861
2719
  }
1862
2720
  function pastGateConfirmation(unmerged, subject = "This task") {
1863
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.`;
@@ -1899,6 +2757,24 @@ export {
1899
2757
  runnerToolSubject,
1900
2758
  reduceTaskLog,
1901
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,
1902
2778
  sessionOwner,
1903
2779
  continuability,
1904
2780
  canContinue,
@@ -1946,6 +2822,8 @@ export {
1946
2822
  taskStartedNotice,
1947
2823
  markdownTokens,
1948
2824
  markdownHtml,
2825
+ EMPTY_TASK_LOG_FOLLOW,
2826
+ followTaskLog,
1949
2827
  taskRowActions,
1950
2828
  markAction,
1951
2829
  markRequestFor,
@@ -1957,4 +2835,4 @@ export {
1957
2835
  pastGateConfirmation,
1958
2836
  newTaskFields
1959
2837
  };
1960
- //# sourceMappingURL=chunk-6LEA6WAM.mjs.map
2838
+ //# sourceMappingURL=chunk-DMG4ISUG.mjs.map