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