@ordewell/core 0.10.5 → 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.
- package/dist/{IRunner-Crkt3tHD.d.ts → IFileSystem-C4_mpfze.d.ts} +97 -14
- package/dist/{IRunner-lMQIa5Wc.d.mts → IFileSystem-DUgeWWQN.d.mts} +97 -14
- package/dist/{ModeResolver-B4hhABNf.d.mts → ModeResolver-4qNjxcDs.d.mts} +1 -1
- package/dist/{ModeResolver-CUR5jCSU.d.ts → ModeResolver-DGK2rWHE.d.ts} +1 -1
- package/dist/{Task-D4QHjdGv.d.mts → Task-C0o4i3Nn.d.mts} +23 -4
- package/dist/{Task-D4QHjdGv.d.ts → Task-C0o4i3Nn.d.ts} +23 -4
- package/dist/{chunk-6LEA6WAM.mjs → chunk-DMG4ISUG.mjs} +923 -45
- package/dist/chunk-DMG4ISUG.mjs.map +1 -0
- package/dist/{chunk-RT4NQ2BZ.mjs → chunk-EEE2GBLG.mjs} +114 -27
- package/dist/chunk-EEE2GBLG.mjs.map +1 -0
- package/dist/chunk-H7QX32BH.mjs +647 -0
- package/dist/chunk-H7QX32BH.mjs.map +1 -0
- package/dist/{chunk-F3KIX7WR.mjs → chunk-LTHKFBKM.mjs} +4 -4
- package/dist/chunk-LTHKFBKM.mjs.map +1 -0
- package/dist/index.d.mts +278 -1701
- package/dist/index.d.ts +278 -1701
- package/dist/index.js +2729 -1903
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2171 -1523
- package/dist/index.mjs.map +1 -1
- package/dist/parsing.d.mts +2 -2
- package/dist/parsing.d.ts +2 -2
- package/dist/parsing.js +231 -75
- package/dist/parsing.js.map +1 -1
- package/dist/parsing.mjs +3 -3
- package/dist/plan-utils.d.mts +20 -4
- package/dist/plan-utils.d.ts +20 -4
- package/dist/plan-utils.js +255 -97
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +33 -19
- package/dist/{plan-utils-wI8MuDBC.d.mts → runSettingsText-BgOmLxdw.d.mts} +1941 -108
- package/dist/{plan-utils-BVJCzNvm.d.ts → runSettingsText-BgXvajj2.d.ts} +1941 -108
- package/dist/testing.d.mts +2 -3
- package/dist/testing.d.ts +2 -3
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +1 -1
- package/dist/IFileSystem-ClBdp59f.d.ts +0 -78
- package/dist/IFileSystem-Db34maxC.d.mts +0 -78
- package/dist/chunk-4QLFM73A.mjs +0 -1407
- package/dist/chunk-4QLFM73A.mjs.map +0 -1
- package/dist/chunk-6LEA6WAM.mjs.map +0 -1
- package/dist/chunk-F3KIX7WR.mjs.map +0 -1
- package/dist/chunk-RT4NQ2BZ.mjs.map +0 -1
|
@@ -1,21 +1,42 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
552
|
-
const
|
|
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
|
|
560
|
-
const
|
|
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
|
|
570
|
-
|
|
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
|
|
574
|
-
|
|
575
|
-
|
|
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) ?
|
|
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
|
|
820
|
+
return agentTextDelta(view, event);
|
|
792
821
|
case "text":
|
|
793
|
-
return agentText(view, event
|
|
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"
|
|
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) =>
|
|
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 =
|
|
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.
|
|
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(
|
|
1699
|
-
return gate.open ===
|
|
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
|
|
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(
|
|
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-
|
|
2838
|
+
//# sourceMappingURL=chunk-DMG4ISUG.mjs.map
|