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