@ordewell/core 0.5.5 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/IFileSystem-BkPX7mLD.d.mts +76 -0
- package/dist/IFileSystem-C0l-4MGT.d.ts +76 -0
- package/dist/{ModeResolver-D3XO0fT9.d.ts → ModeResolver--16lh7dS.d.mts} +1 -1
- package/dist/{ModeResolver-D-SUFRNF.d.mts → ModeResolver-CjpG5Wli.d.ts} +1 -1
- package/dist/Task-Dyxp67s2.d.mts +2086 -0
- package/dist/Task-Dyxp67s2.d.ts +2086 -0
- package/dist/{chunk-T2S5O36I.mjs → chunk-C44UWIAD.mjs} +6 -6
- package/dist/chunk-C44UWIAD.mjs.map +1 -0
- package/dist/{chunk-HD2FWPRV.mjs → chunk-EDGUFCIR.mjs} +6 -1
- package/dist/chunk-EDGUFCIR.mjs.map +1 -0
- package/dist/{chunk-UUBGVCGJ.mjs → chunk-JBEFAJ2W.mjs} +2 -2
- package/dist/{chunk-KLN7ELXO.mjs → chunk-ROVYWEBI.mjs} +1560 -1269
- package/dist/chunk-ROVYWEBI.mjs.map +1 -0
- package/dist/index.d.mts +1414 -785
- package/dist/index.d.ts +1414 -785
- package/dist/index.js +7008 -4389
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6075 -3767
- package/dist/index.mjs.map +1 -1
- package/dist/order-labels.d.mts +3 -1
- package/dist/order-labels.d.ts +3 -1
- package/dist/{parsing-CDtRSxBY.d.mts → parsing-DPEpAszP.d.mts} +2 -2
- package/dist/{parsing-BTP4bwkk.d.ts → parsing-EcmCsF1y.d.ts} +2 -2
- package/dist/parsing.d.mts +5 -3
- package/dist/parsing.d.ts +5 -3
- package/dist/parsing.js.map +1 -1
- package/dist/parsing.mjs +2 -2
- package/dist/{plan-utils-pE4TBwxl.d.mts → plan-utils-BAvW3hvl.d.mts} +212 -43
- package/dist/{plan-utils-BFaPo-IT.d.ts → plan-utils-BMyEiDKv.d.ts} +212 -43
- package/dist/plan-utils.d.mts +5 -4
- package/dist/plan-utils.d.ts +5 -4
- package/dist/plan-utils.js +351 -64
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +11 -5
- package/dist/testing.d.mts +54 -4
- package/dist/testing.d.ts +54 -4
- package/dist/testing.js +93 -2
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +91 -2
- package/dist/testing.mjs.map +1 -1
- package/package.json +2 -1
- package/skills/grilling/SKILL.md +6 -16
- package/skills/improve-codebase-architecture/SKILL.md +1 -1
- package/dist/ApprovalPolicy-BVhGdECT.d.mts +0 -79
- package/dist/ApprovalPolicy-BVhGdECT.d.ts +0 -79
- package/dist/ITerminalRunner-BV9Rd2o9.d.ts +0 -563
- package/dist/ITerminalRunner-C77ZNZS9.d.mts +0 -563
- package/dist/Task-Vl5Zq_D-.d.mts +0 -825
- package/dist/Task-Vl5Zq_D-.d.ts +0 -825
- package/dist/chunk-HD2FWPRV.mjs.map +0 -1
- package/dist/chunk-KLN7ELXO.mjs.map +0 -1
- package/dist/chunk-T2S5O36I.mjs.map +0 -1
- /package/dist/{chunk-UUBGVCGJ.mjs.map → chunk-JBEFAJ2W.mjs.map} +0 -0
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
renumberTasks,
|
|
11
11
|
stripTrailingCommas,
|
|
12
12
|
updateTaskInPlan
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-EDGUFCIR.mjs";
|
|
14
14
|
|
|
15
15
|
// src/models/Usage.ts
|
|
16
16
|
function partedPromptUsage(parts) {
|
|
@@ -59,732 +59,1147 @@ function plannerContextFill(usage) {
|
|
|
59
59
|
return { usedTokens: usage.lastPromptTokens, windowTokens: usage.contextWindow };
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
// src/services/
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
// src/services/harness/agentTools.ts
|
|
63
|
+
var KNOWN_TOOLS = {
|
|
64
|
+
// Claude Code
|
|
65
|
+
read: "read_file",
|
|
66
|
+
grep: "grep",
|
|
67
|
+
glob: "glob",
|
|
68
|
+
bash: "bash",
|
|
69
|
+
ls: "list_dir",
|
|
70
|
+
webfetch: "fetch",
|
|
71
|
+
websearch: "web_search",
|
|
72
|
+
// Codex — its sandboxed command tool is a shell by another name
|
|
73
|
+
shell: "bash",
|
|
74
|
+
exec_command: "bash",
|
|
75
|
+
local_shell: "bash",
|
|
76
|
+
// OpenCode
|
|
77
|
+
list: "list_dir",
|
|
78
|
+
fetch: "fetch"
|
|
79
|
+
};
|
|
80
|
+
function mapAgentTool(name) {
|
|
81
|
+
const normalized = name.trim().toLowerCase().replace(/[_-]/g, "");
|
|
82
|
+
const direct = KNOWN_TOOLS[normalized] ?? KNOWN_TOOLS[name.trim().toLowerCase()];
|
|
83
|
+
if (direct) {
|
|
84
|
+
return { tool: direct, toolLabel: name };
|
|
71
85
|
}
|
|
72
|
-
|
|
73
|
-
return kept.length > 0 ? kept : void 0;
|
|
86
|
+
return { tool: "agent_tool", toolLabel: name };
|
|
74
87
|
}
|
|
75
|
-
function
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
function normalizeAgentArgs(tool, args) {
|
|
89
|
+
const out = { ...args };
|
|
90
|
+
const alias = (from, to) => {
|
|
91
|
+
if (out[to] === void 0 && out[from] !== void 0) out[to] = out[from];
|
|
92
|
+
};
|
|
93
|
+
if (tool === "read_file" || tool === "list_dir") {
|
|
94
|
+
alias("filePath", "path");
|
|
95
|
+
alias("file_path", "path");
|
|
96
|
+
alias("target", "path");
|
|
97
|
+
}
|
|
98
|
+
if (tool === "bash") {
|
|
99
|
+
alias("cmd", "command");
|
|
100
|
+
alias("script", "command");
|
|
101
|
+
if (typeof out.command !== "string" && Array.isArray(out.command)) {
|
|
102
|
+
out.command = out.command.join(" ");
|
|
83
103
|
}
|
|
84
|
-
const allowedSet = new Set(allowed);
|
|
85
|
-
const intersection = models.filter((m) => allowedSet.has(m.modelId));
|
|
86
|
-
const covered = new Set(intersection.map((m) => m.modelId));
|
|
87
|
-
const missing = allowed.filter((id) => !covered.has(id)).map((id) => ({ modelId: id, modelLabel: id.split("/").pop() || id, variants: [] }));
|
|
88
|
-
result[runner] = [...intersection, ...missing];
|
|
89
104
|
}
|
|
90
|
-
|
|
105
|
+
if (tool === "fetch") alias("uri", "url");
|
|
106
|
+
if (tool === "web_search") alias("q", "query");
|
|
107
|
+
return out;
|
|
91
108
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const ids = new Set(variants.map((v) => v.id));
|
|
98
|
-
if (ids.has(effort)) return effort;
|
|
99
|
-
const canon = EFFORT_ALIASES[effort] ?? effort;
|
|
100
|
-
if (ids.has(canon)) return canon;
|
|
101
|
-
const idx = EFFORT_LADDER.indexOf(canon);
|
|
102
|
-
if (idx < 0) return void 0;
|
|
103
|
-
for (let d = 1; d < EFFORT_LADDER.length; d++) {
|
|
104
|
-
const lower = EFFORT_LADDER[idx - d];
|
|
105
|
-
if (lower && ids.has(lower)) return lower;
|
|
106
|
-
const higher = EFFORT_LADDER[idx + d];
|
|
107
|
-
if (higher && ids.has(higher)) return higher;
|
|
108
|
-
}
|
|
109
|
-
return void 0;
|
|
109
|
+
|
|
110
|
+
// src/services/terminalRender.ts
|
|
111
|
+
var ANSI_OR_CTRL_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?|\x1b[()][AB012]|\x1b[=>]|[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
112
|
+
function flattenTerminalOutput(raw) {
|
|
113
|
+
return raw.replace(ANSI_OR_CTRL_RE, "").replace(/[─-▟]/g, "").replace(/\s+/g, "");
|
|
110
114
|
}
|
|
111
|
-
function
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
availableVariants: model.variants.map((v) => v.id)
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
};
|
|
128
|
-
const allowedFor = (runner) => effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
129
|
-
const assignmentFor = (runner, modelId) => {
|
|
130
|
-
const known = modelsByRunner?.[runner]?.find((m) => m.modelId === modelId);
|
|
131
|
-
return {
|
|
132
|
-
modelId,
|
|
133
|
-
modelLabel: known?.modelLabel ?? modelId,
|
|
134
|
-
thinkingEffort: void 0,
|
|
135
|
-
...known ? { availableVariants: known.variants.map((v) => v.id) } : {}
|
|
136
|
-
};
|
|
115
|
+
function renderTerminalOutput(raw) {
|
|
116
|
+
const rows = /* @__PURE__ */ new Map();
|
|
117
|
+
let row = 1;
|
|
118
|
+
let col = 1;
|
|
119
|
+
let savedRow = 1;
|
|
120
|
+
let savedCol = 1;
|
|
121
|
+
const cells = (r) => {
|
|
122
|
+
let line = rows.get(r);
|
|
123
|
+
if (!line) {
|
|
124
|
+
line = [];
|
|
125
|
+
rows.set(r, line);
|
|
126
|
+
}
|
|
127
|
+
return line;
|
|
137
128
|
};
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
assignedModel: fallbackModel
|
|
148
|
-
});
|
|
129
|
+
const firstParam = (params, fallback = 1) => params[0] || fallback;
|
|
130
|
+
const eraseLine = (mode) => {
|
|
131
|
+
const line = cells(row);
|
|
132
|
+
if (mode === 2) {
|
|
133
|
+
rows.set(row, []);
|
|
134
|
+
} else if (mode === 1) {
|
|
135
|
+
for (let c = 0; c < col; c++) line[c] = " ";
|
|
136
|
+
} else {
|
|
137
|
+
line.length = Math.max(0, col - 1);
|
|
149
138
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
139
|
+
};
|
|
140
|
+
for (let i = 0; i < raw.length; ) {
|
|
141
|
+
const ch = raw[i];
|
|
142
|
+
if (ch === "\x1B") {
|
|
143
|
+
const kind = raw[i + 1];
|
|
144
|
+
if (kind === "[") {
|
|
145
|
+
let end = i + 2;
|
|
146
|
+
while (end < raw.length) {
|
|
147
|
+
const code = raw.charCodeAt(end);
|
|
148
|
+
if (code >= 64 && code <= 126) break;
|
|
149
|
+
end++;
|
|
150
|
+
}
|
|
151
|
+
if (end >= raw.length) break;
|
|
152
|
+
const final = raw[end];
|
|
153
|
+
const body = raw.slice(i + 2, end).replace(/^[?<>=!]+/, "");
|
|
154
|
+
const params = body.split(";").map((p) => Number.parseInt(p, 10) || 0);
|
|
155
|
+
switch (final) {
|
|
156
|
+
case "H":
|
|
157
|
+
case "f":
|
|
158
|
+
row = params[0] || 1;
|
|
159
|
+
col = params[1] || 1;
|
|
160
|
+
break;
|
|
161
|
+
case "G":
|
|
162
|
+
col = firstParam(params);
|
|
163
|
+
break;
|
|
164
|
+
case "d":
|
|
165
|
+
row = firstParam(params);
|
|
166
|
+
break;
|
|
167
|
+
case "A":
|
|
168
|
+
row = Math.max(1, row - firstParam(params));
|
|
169
|
+
break;
|
|
170
|
+
case "B":
|
|
171
|
+
row += firstParam(params);
|
|
172
|
+
break;
|
|
173
|
+
case "C":
|
|
174
|
+
col += firstParam(params);
|
|
175
|
+
break;
|
|
176
|
+
case "D":
|
|
177
|
+
col = Math.max(1, col - firstParam(params));
|
|
178
|
+
break;
|
|
179
|
+
case "E":
|
|
180
|
+
row += firstParam(params);
|
|
181
|
+
col = 1;
|
|
182
|
+
break;
|
|
183
|
+
case "F":
|
|
184
|
+
row = Math.max(1, row - firstParam(params));
|
|
185
|
+
col = 1;
|
|
186
|
+
break;
|
|
187
|
+
case "s":
|
|
188
|
+
savedRow = row;
|
|
189
|
+
savedCol = col;
|
|
190
|
+
break;
|
|
191
|
+
case "u":
|
|
192
|
+
row = savedRow;
|
|
193
|
+
col = savedCol;
|
|
194
|
+
break;
|
|
195
|
+
case "K":
|
|
196
|
+
eraseLine(params[0] || 0);
|
|
197
|
+
break;
|
|
198
|
+
case "J":
|
|
199
|
+
if ((params[0] || 0) === 2 || (params[0] || 0) === 3) rows.clear();
|
|
200
|
+
break;
|
|
201
|
+
case "X": {
|
|
202
|
+
const line = cells(row);
|
|
203
|
+
for (let c = 0; c < firstParam(params); c++) line[col - 1 + c] = " ";
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
case "P": {
|
|
207
|
+
cells(row).splice(col - 1, firstParam(params));
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case "@": {
|
|
211
|
+
cells(row).splice(col - 1, 0, ...Array(firstParam(params)).fill(" "));
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
default:
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
i = end + 1;
|
|
218
|
+
continue;
|
|
158
219
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// src/services/TaskOps.ts
|
|
168
|
-
import { v4 as uuidv4 } from "uuid";
|
|
169
|
-
|
|
170
|
-
// src/services/TaskEditValidator.ts
|
|
171
|
-
var AI_ONLY_FIELDS = ["assignedModel", "thinkingEffort", "taskMode", "autonomy"];
|
|
172
|
-
function checkModelAndModeValidity(runner, assignedModel, taskMode, catalog) {
|
|
173
|
-
if (!catalog) return { ok: true };
|
|
174
|
-
if (assignedModel) {
|
|
175
|
-
const models = catalog.modelsByRunner[runner];
|
|
176
|
-
if (models) {
|
|
177
|
-
const modelId = assignedModel.modelId;
|
|
178
|
-
const allowed = effectiveAllowlist(catalog.perRunnerAllowlist?.[runner], runner, catalog.modelsByRunner);
|
|
179
|
-
if (!models.some((m) => m.modelId === modelId) && !allowed?.includes(modelId)) {
|
|
180
|
-
return { ok: false, error: `Runner "${runner}" does not offer model "${modelId}"` };
|
|
220
|
+
if (kind === "]" || kind === "P" || kind === "^" || kind === "_") {
|
|
221
|
+
let end = i + 2;
|
|
222
|
+
while (end < raw.length && raw[end] !== "\x07" && !(raw[end] === "\x1B" && raw[end + 1] === "\\")) end++;
|
|
223
|
+
if (end >= raw.length) break;
|
|
224
|
+
i = raw[end] === "\x07" ? end + 1 : end + 2;
|
|
225
|
+
continue;
|
|
181
226
|
}
|
|
182
|
-
if (
|
|
183
|
-
|
|
227
|
+
if (kind === "7") {
|
|
228
|
+
savedRow = row;
|
|
229
|
+
savedCol = col;
|
|
230
|
+
} else if (kind === "8") {
|
|
231
|
+
row = savedRow;
|
|
232
|
+
col = savedCol;
|
|
233
|
+
} else if (kind === "c") {
|
|
234
|
+
rows.clear();
|
|
235
|
+
row = 1;
|
|
236
|
+
col = 1;
|
|
184
237
|
}
|
|
238
|
+
i += kind === "(" || kind === ")" ? 3 : 2;
|
|
239
|
+
continue;
|
|
185
240
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return { ok: false, error: `Runner "${runner}" has no task mode "${taskMode}" \u2014 valid modes: ${modes.map((m) => m.id).join(", ")}` };
|
|
241
|
+
if (ch === "\r") {
|
|
242
|
+
col = 1;
|
|
243
|
+
i++;
|
|
244
|
+
continue;
|
|
191
245
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (!changes.type || changes.type === target.type) return null;
|
|
197
|
-
if (changes.type === "user") {
|
|
198
|
-
if (!Array.isArray(changes.userSteps) || changes.userSteps.length === 0) {
|
|
199
|
-
return { ok: false, error: `"${target.title}" cannot become a manual task without user steps \u2014 include "userSteps" in this edit` };
|
|
246
|
+
if (ch === "\n") {
|
|
247
|
+
row++;
|
|
248
|
+
i++;
|
|
249
|
+
continue;
|
|
200
250
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
251
|
+
if (ch === "\b") {
|
|
252
|
+
col = Math.max(1, col - 1);
|
|
253
|
+
i++;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (ch === " ") {
|
|
257
|
+
col += 8 - (col - 1) % 8;
|
|
258
|
+
i++;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (ch < " " || ch === "\x7F") {
|
|
262
|
+
i++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
cells(row)[col - 1] = ch;
|
|
266
|
+
col++;
|
|
267
|
+
i++;
|
|
205
268
|
}
|
|
206
|
-
|
|
269
|
+
const populated = [...rows.keys()].sort((a, b) => a - b);
|
|
270
|
+
return populated.map((r) => cells(r).join("")).join("\n");
|
|
207
271
|
}
|
|
208
|
-
function
|
|
209
|
-
const
|
|
210
|
-
if (!
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
if (
|
|
272
|
+
function renderCleanCapture(raw, doneToken) {
|
|
273
|
+
const rendered = renderTerminalOutput(raw).replace(/[\s─-▟]+$/gm, "");
|
|
274
|
+
if (!doneToken) return rendered.trim();
|
|
275
|
+
const lines = rendered.split("\n");
|
|
276
|
+
const flat = (s) => flattenTerminalOutput(s);
|
|
277
|
+
for (let i = 0; i < lines.length; i++) {
|
|
278
|
+
const own = flat(lines[i]).includes(doneToken);
|
|
279
|
+
if (own) return lines.slice(0, i).join("\n").trim();
|
|
280
|
+
const window = flat(lines[i]) + (i + 1 < lines.length ? flat(lines[i + 1]) : "");
|
|
281
|
+
if (window.includes(doneToken)) return lines.slice(0, i + 1).join("\n").trim();
|
|
218
282
|
}
|
|
219
|
-
|
|
220
|
-
if (coherence && !coherence.ok) return coherence;
|
|
221
|
-
const runner = changes.assignedRunner ?? target.assignedRunner;
|
|
222
|
-
const validity = checkModelAndModeValidity(runner, changes.assignedModel, changes.taskMode, catalog);
|
|
223
|
-
if (!validity.ok) return validity;
|
|
224
|
-
return coherence ?? { ok: true };
|
|
283
|
+
return rendered.trim();
|
|
225
284
|
}
|
|
226
285
|
|
|
227
|
-
// src/
|
|
228
|
-
var
|
|
229
|
-
"
|
|
230
|
-
"
|
|
231
|
-
"
|
|
232
|
-
"
|
|
233
|
-
"
|
|
234
|
-
"
|
|
235
|
-
"
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
"
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?},"handle"?:"<name>"},',
|
|
249
|
-
' {"op":"remove","taskId":"<id or #order>"},',
|
|
250
|
-
' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]}, // only to re-prioritise INDEPENDENT tasks',
|
|
251
|
-
' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...},"handle"?:"<name>"},',
|
|
252
|
-
' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...],"handle"?:"<name>"},',
|
|
253
|
-
` {"op":"rearm","taskId":"<id or #order>","changes"?:{${updateChanges}}}`,
|
|
254
|
-
" ]}",
|
|
255
|
-
'- For sweeping changes, you may instead emit a full {"tasks":[...]} plan JSON.',
|
|
256
|
-
'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.',
|
|
257
|
-
'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.',
|
|
258
|
-
'When creating or changing tasks, set "assignedRunner" and "assignedModel" ({"modelId","modelLabel","thinkingEffort"?}) using only the runners and models listed in <available_models>.',
|
|
259
|
-
'Keep dependencies consistent: no cycles, no references to removed tasks, and never touch running or completed tasks \u2014 "rearm" is the one exception, below.',
|
|
260
|
-
'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,
|
|
261
|
-
'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.',
|
|
262
|
-
'"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.'
|
|
263
|
-
];
|
|
286
|
+
// src/conversation/format.ts
|
|
287
|
+
var DISPLAY_NAMES = {
|
|
288
|
+
read_file: "Read",
|
|
289
|
+
read_files: "Read",
|
|
290
|
+
list_dir: "List",
|
|
291
|
+
glob: "Glob",
|
|
292
|
+
grep: "Grep",
|
|
293
|
+
find_symbol: "Symbol",
|
|
294
|
+
bash: "Bash",
|
|
295
|
+
fetch: "Fetch",
|
|
296
|
+
web_search: "WebSearch",
|
|
297
|
+
spawn_research_agent: "Agent"
|
|
298
|
+
};
|
|
299
|
+
var HINT_FIELDS = ["file_path", "filePath", "path", "url", "pattern", "query", "command", "description", "prompt"];
|
|
300
|
+
function parseArgs(args) {
|
|
301
|
+
try {
|
|
302
|
+
const parsed = JSON.parse(args);
|
|
303
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
264
307
|
}
|
|
265
|
-
function
|
|
266
|
-
return text2.
|
|
308
|
+
function oneLine(text2) {
|
|
309
|
+
return text2.replace(/\s+/g, " ").trim();
|
|
267
310
|
}
|
|
268
|
-
function
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
for (const json of candidates) {
|
|
273
|
-
try {
|
|
274
|
-
return parseTaskOpsObject(json, text2);
|
|
275
|
-
} catch (err) {
|
|
276
|
-
if (!(err instanceof PlanParseError)) throw err;
|
|
277
|
-
if (!lastError) lastError = err;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
throw lastError ?? new PlanParseError("No JSON object found in task-ops reply", text2);
|
|
311
|
+
function firstLineOf(command) {
|
|
312
|
+
const lines = command.trim().split(/\r?\n/);
|
|
313
|
+
const more = lines.length - 1;
|
|
314
|
+
return more > 0 ? `${lines[0]} \u2026 +${more} line${more === 1 ? "" : "s"}` : lines[0];
|
|
281
315
|
}
|
|
282
|
-
function
|
|
283
|
-
|
|
284
|
-
try {
|
|
285
|
-
parsed = JSON.parse(escapeControlCharsInStrings(stripTrailingCommas(json)));
|
|
286
|
-
} catch (err) {
|
|
287
|
-
throw new PlanParseError(`Task-ops JSON is invalid: ${err instanceof Error ? err.message : String(err)}`, text2);
|
|
288
|
-
}
|
|
289
|
-
const ops = parsed.taskOps;
|
|
290
|
-
if (!Array.isArray(ops) || ops.length === 0) {
|
|
291
|
-
throw new PlanParseError('"taskOps" must be a non-empty array', text2);
|
|
292
|
-
}
|
|
293
|
-
for (const op of ops) {
|
|
294
|
-
if (typeof op !== "object" || op === null || typeof op.op !== "string") {
|
|
295
|
-
throw new PlanParseError('Every task op needs an "op" field (update | add | remove | reorder | merge | split)', text2);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
return ops;
|
|
316
|
+
function text(value) {
|
|
317
|
+
return typeof value === "string" ? value : "";
|
|
299
318
|
}
|
|
300
|
-
function
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
const id = handleId.get(s);
|
|
309
|
-
if (!id) return { error: `handle "${s}" was never resolved` };
|
|
310
|
-
return { id };
|
|
311
|
-
}
|
|
312
|
-
const byLiveId = liveScope.find((t) => t.id === s);
|
|
313
|
-
if (byLiveId) return { id: byLiveId.id };
|
|
314
|
-
const orderStr = s.startsWith("#") ? s.slice(1) : s;
|
|
315
|
-
if (/^\d+$/.test(orderStr)) {
|
|
316
|
-
const order = Number(orderStr);
|
|
317
|
-
const byOrder = originalScope.filter((t) => t.order === order);
|
|
318
|
-
if (byOrder.length === 1) return { id: byOrder[0].id };
|
|
319
|
+
function searchArg(pattern, path, include = "") {
|
|
320
|
+
return `${pattern}${path ? ` in ${path}` : ""}${include ? ` (${include})` : ""}`;
|
|
321
|
+
}
|
|
322
|
+
function hintArg(args) {
|
|
323
|
+
for (const field of HINT_FIELDS) {
|
|
324
|
+
const value = text(args[field]);
|
|
325
|
+
if (value) return field === "command" ? firstLineOf(value) : oneLine(value);
|
|
319
326
|
}
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
return { error: `task "${s}" not found` };
|
|
327
|
+
const firstText = Object.values(args).find((v) => typeof v === "string" && v.trim() !== "");
|
|
328
|
+
return firstText ? oneLine(firstText) : "";
|
|
323
329
|
}
|
|
324
|
-
function
|
|
325
|
-
switch (
|
|
326
|
-
case "
|
|
327
|
-
|
|
328
|
-
case "
|
|
329
|
-
case "
|
|
330
|
-
return
|
|
331
|
-
case "
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
330
|
+
function keyArgOf(tool, args) {
|
|
331
|
+
switch (tool) {
|
|
332
|
+
case "bash":
|
|
333
|
+
return firstLineOf(text(args.command));
|
|
334
|
+
case "read_file":
|
|
335
|
+
case "list_dir":
|
|
336
|
+
return text(args.path);
|
|
337
|
+
case "read_files":
|
|
338
|
+
return Array.isArray(args.paths) ? args.paths.map(String).join(", ") : "";
|
|
339
|
+
case "glob":
|
|
340
|
+
return searchArg(text(args.pattern), text(args.path));
|
|
341
|
+
case "grep":
|
|
342
|
+
return searchArg(text(args.pattern), text(args.path), text(args.include));
|
|
343
|
+
case "find_symbol":
|
|
344
|
+
return `${text(args.symbol)}${text(args.language) ? ` [${text(args.language)}]` : ""}`;
|
|
345
|
+
case "fetch":
|
|
346
|
+
return text(args.url);
|
|
347
|
+
case "web_search":
|
|
348
|
+
return oneLine(text(args.query));
|
|
349
|
+
case "spawn_research_agent":
|
|
350
|
+
return oneLine(text(args.prompt));
|
|
351
|
+
default:
|
|
352
|
+
return hintArg(args);
|
|
336
353
|
}
|
|
337
354
|
}
|
|
338
|
-
function
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
const orderStr = s.startsWith("#") ? s.slice(1) : s;
|
|
343
|
-
return /^\d+$/.test(orderStr) && task.order === Number(orderStr) || task.title === s;
|
|
355
|
+
function toolHeadline(tool, args, toolLabel) {
|
|
356
|
+
const name = toolLabel?.trim() || DISPLAY_NAMES[tool] || tool;
|
|
357
|
+
const parsed = parseArgs(args);
|
|
358
|
+
return { name, keyArg: parsed ? keyArgOf(tool, parsed) : oneLine(args) };
|
|
344
359
|
}
|
|
345
|
-
function
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
return originalFlatAll.some((t) => t.title === name);
|
|
360
|
+
function outputLines(output) {
|
|
361
|
+
const lines = output.replace(/\r\n?/g, "\n").replace(ANSI_OR_CTRL_RE, "").split("\n");
|
|
362
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
363
|
+
return lines;
|
|
350
364
|
}
|
|
351
|
-
function
|
|
352
|
-
|
|
365
|
+
function outputPreview(output, maxLines) {
|
|
366
|
+
const lines = outputLines(output);
|
|
367
|
+
const shown = lines.slice(0, Math.max(0, maxLines));
|
|
368
|
+
return { lines: shown, hiddenLineCount: lines.length - shown.length };
|
|
353
369
|
}
|
|
354
|
-
|
|
355
|
-
|
|
370
|
+
|
|
371
|
+
// src/conversation/records.ts
|
|
372
|
+
function settledMessage(id, role, text2, turnId) {
|
|
373
|
+
return { type: "message", id, role, text: text2, streaming: false, ...turnId ? { turnId } : {} };
|
|
356
374
|
}
|
|
357
|
-
function
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const hit = visit(dep);
|
|
367
|
-
if (hit) return hit;
|
|
368
|
-
}
|
|
369
|
-
visiting.delete(id);
|
|
370
|
-
done.add(id);
|
|
371
|
-
return null;
|
|
375
|
+
function subagentBlock(id, { toolCallId, model, usage, turnId, ...fields }) {
|
|
376
|
+
return {
|
|
377
|
+
type: "subagent",
|
|
378
|
+
id,
|
|
379
|
+
...fields,
|
|
380
|
+
...toolCallId ? { toolCallId } : {},
|
|
381
|
+
...model ? { model } : {},
|
|
382
|
+
...usage ? { usage } : {},
|
|
383
|
+
...turnId ? { turnId } : {}
|
|
372
384
|
};
|
|
373
|
-
for (const t of tasks) {
|
|
374
|
-
const hit = visit(t.id);
|
|
375
|
-
if (hit) return byId.get(hit)?.title ?? hit;
|
|
376
|
-
}
|
|
377
|
-
return null;
|
|
378
385
|
}
|
|
379
|
-
function
|
|
380
|
-
return
|
|
386
|
+
function usageBlock(id, { totals, bySubagent, contextFill }) {
|
|
387
|
+
return { type: "usage", id, totals, ...bySubagent ? { bySubagent } : {}, ...contextFill ? { contextFill } : {} };
|
|
381
388
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
389
|
+
var STATUS_OF_OUTCOME = {
|
|
390
|
+
success: "ok",
|
|
391
|
+
failure: "error",
|
|
392
|
+
refused: "denied",
|
|
393
|
+
denied: "denied",
|
|
394
|
+
not_executed: "interrupted"
|
|
395
|
+
};
|
|
396
|
+
function finishedTool(call, output, outcome) {
|
|
397
|
+
return { ...call, status: STATUS_OF_OUTCOME[outcome], outcome, output, outputLineCount: outputLines(output).length };
|
|
398
|
+
}
|
|
399
|
+
function settledTool(call, step) {
|
|
400
|
+
return finishedTool(call, step.result, step.outcome);
|
|
401
|
+
}
|
|
402
|
+
function pendingTool(id, { tool, toolLabel, args, toolCallId, turnId }) {
|
|
403
|
+
return {
|
|
404
|
+
type: "tool",
|
|
405
|
+
id,
|
|
406
|
+
tool,
|
|
407
|
+
headline: toolHeadline(tool, args, toolLabel),
|
|
408
|
+
args,
|
|
409
|
+
status: "pending",
|
|
410
|
+
output: "",
|
|
411
|
+
outputLineCount: 0,
|
|
412
|
+
...toolCallId ? { toolCallId } : {},
|
|
413
|
+
...toolLabel ? { toolLabel } : {},
|
|
414
|
+
...turnId ? { turnId } : {}
|
|
394
415
|
};
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
416
|
+
}
|
|
417
|
+
function toolFromStep(id, step, turnId) {
|
|
418
|
+
return settledTool(pendingTool(id, { ...step, turnId }), step);
|
|
419
|
+
}
|
|
420
|
+
function planMarker(content) {
|
|
421
|
+
const count = /(\d+) tasks?\b/.exec(content);
|
|
422
|
+
return { status: content.startsWith("Plan updated") ? "updated" : "generated", ...count ? { taskCount: Number(count[1]) } : {} };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/conversation/lanes.ts
|
|
426
|
+
function findLastIndex(items, match) {
|
|
427
|
+
for (let i = items.length - 1; i >= 0; i--) if (match(items[i])) return i;
|
|
428
|
+
return -1;
|
|
429
|
+
}
|
|
430
|
+
function replaceAt(items, index, item) {
|
|
431
|
+
const next = items.slice();
|
|
432
|
+
next[index] = item;
|
|
433
|
+
return next;
|
|
434
|
+
}
|
|
435
|
+
function insertTop(blocks, block) {
|
|
436
|
+
const last = blocks[blocks.length - 1];
|
|
437
|
+
return last?.type === "usage" ? [...blocks.slice(0, -1), block, last] : [...blocks, block];
|
|
438
|
+
}
|
|
439
|
+
function append(view, make) {
|
|
440
|
+
return { ...view, blocks: insertTop(view.blocks, make(`b${view.nextId}`)), nextId: view.nextId + 1 };
|
|
441
|
+
}
|
|
442
|
+
function subagentAt(view, lane) {
|
|
443
|
+
const block = view.blocks[lane];
|
|
444
|
+
if (block.type !== "subagent") throw new Error(`block ${lane} is not a subagent`);
|
|
445
|
+
return block;
|
|
446
|
+
}
|
|
447
|
+
function laneBlocks(view, lane) {
|
|
448
|
+
return lane === null ? view.blocks : subagentAt(view, lane).children;
|
|
449
|
+
}
|
|
450
|
+
function setChildren(view, lane, children) {
|
|
451
|
+
return { ...view, blocks: replaceAt(view.blocks, lane, { ...subagentAt(view, lane), children }) };
|
|
452
|
+
}
|
|
453
|
+
function laneReplace(view, lane, index, block) {
|
|
454
|
+
if (lane === null) return { ...view, blocks: replaceAt(view.blocks, index, block) };
|
|
455
|
+
return setChildren(view, lane, replaceAt(subagentAt(view, lane).children, index, block));
|
|
456
|
+
}
|
|
457
|
+
function laneAppend(view, lane, make) {
|
|
458
|
+
if (lane === null) return append(view, make);
|
|
459
|
+
const children = [...subagentAt(view, lane).children, make(`b${view.nextId}`)];
|
|
460
|
+
return { ...setChildren(view, lane, children), nextId: view.nextId + 1 };
|
|
461
|
+
}
|
|
462
|
+
function isStreaming(block) {
|
|
463
|
+
return (block.type === "message" || block.type === "thinking") && block.streaming;
|
|
464
|
+
}
|
|
465
|
+
function sealed(block) {
|
|
466
|
+
return isStreaming(block) ? { ...block, streaming: false } : block;
|
|
467
|
+
}
|
|
468
|
+
function sealLane(view, lane, keep) {
|
|
469
|
+
const blocks = laneBlocks(view, lane);
|
|
470
|
+
if (!blocks.some((b, i) => i !== keep && isStreaming(b))) return view;
|
|
471
|
+
if (lane === null) return { ...view, blocks: view.blocks.map((b, i) => i === keep ? b : sealed(b)) };
|
|
472
|
+
return setChildren(view, lane, subagentAt(view, lane).children.map((b, i) => i === keep ? b : sealed(b)));
|
|
473
|
+
}
|
|
474
|
+
function appendOutput(view, lane, make) {
|
|
475
|
+
const next = laneAppend(view, lane, make);
|
|
476
|
+
return sealLane(next, lane, findLastIndex(laneBlocks(next, lane), (b) => b.type !== "usage"));
|
|
477
|
+
}
|
|
478
|
+
function subagentLane(view, subagentId, turnId) {
|
|
479
|
+
const existing = findLastIndex(view.blocks, (b) => b.type === "subagent" && b.subagentId === subagentId);
|
|
480
|
+
if (existing >= 0) return [view, existing];
|
|
481
|
+
const spawn = findLastIndex(view.blocks, (b) => b.type === "tool" && (b.spawns === subagentId || b.toolCallId === subagentId));
|
|
482
|
+
const call = view.blocks[spawn];
|
|
483
|
+
if (call?.type === "tool") {
|
|
484
|
+
const block = subagentBlock(call.id, {
|
|
485
|
+
subagentId,
|
|
486
|
+
brief: call.headline.keyArg,
|
|
487
|
+
status: "running",
|
|
488
|
+
children: [],
|
|
489
|
+
digest: "",
|
|
490
|
+
toolCallId: call.toolCallId,
|
|
491
|
+
turnId: call.turnId
|
|
492
|
+
});
|
|
493
|
+
return [{ ...view, blocks: replaceAt(view.blocks, spawn, block) }, spawn];
|
|
408
494
|
}
|
|
409
|
-
|
|
495
|
+
const next = append(view, (id) => subagentBlock(id, { subagentId, brief: "", status: "running", children: [], digest: "", turnId }));
|
|
496
|
+
return [next, findLastIndex(next.blocks, (b) => b.type === "subagent" && b.subagentId === subagentId)];
|
|
410
497
|
}
|
|
411
|
-
function
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
498
|
+
function updateSubagent(view, subagentId, turnId, update) {
|
|
499
|
+
const [next, lane] = subagentLane(view, subagentId, turnId);
|
|
500
|
+
return { ...next, blocks: replaceAt(next.blocks, lane, update(subagentAt(next, lane))) };
|
|
501
|
+
}
|
|
502
|
+
function laneOf(view, subagentId, turnId) {
|
|
503
|
+
return subagentId ? subagentLane(view, subagentId, turnId) : [view, null];
|
|
504
|
+
}
|
|
505
|
+
function think(view, text2, turnId, segmentId, subagentId) {
|
|
506
|
+
const [placed, lane] = laneOf(view, subagentId, turnId);
|
|
507
|
+
const blocks = laneBlocks(placed, lane);
|
|
508
|
+
const lastIndex = findLastIndex(blocks, (b) => b.type !== "usage");
|
|
509
|
+
const last = blocks[lastIndex];
|
|
510
|
+
if (last?.type === "thinking" && last.streaming && last.segmentId === segmentId) {
|
|
511
|
+
return laneReplace(placed, lane, lastIndex, { ...last, text: last.text + text2 });
|
|
417
512
|
}
|
|
513
|
+
return appendOutput(placed, lane, (id) => ({
|
|
514
|
+
type: "thinking",
|
|
515
|
+
id,
|
|
516
|
+
text: text2,
|
|
517
|
+
streaming: true,
|
|
518
|
+
...turnId ? { turnId } : {},
|
|
519
|
+
...segmentId ? { segmentId } : {},
|
|
520
|
+
...subagentId ? { subagentId } : {}
|
|
521
|
+
}));
|
|
418
522
|
}
|
|
419
|
-
function
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
|
|
523
|
+
function setUsageLine(view, line) {
|
|
524
|
+
const last = view.blocks[view.blocks.length - 1];
|
|
525
|
+
const block = usageBlock(last?.type === "usage" ? last.id : `b${view.nextId}`, line);
|
|
526
|
+
if (last?.type === "usage") return { ...view, blocks: replaceAt(view.blocks, view.blocks.length - 1, block) };
|
|
527
|
+
return { ...view, blocks: [...view.blocks, block], nextId: view.nextId + 1 };
|
|
423
528
|
}
|
|
424
|
-
function
|
|
425
|
-
|
|
426
|
-
return `"${other.title}" would have to run ${side} "${pin.title}", which is ${word} and cannot be moved`;
|
|
529
|
+
function closeBlock(block, interrupted) {
|
|
530
|
+
return interrupted && block.type === "tool" && block.status === "pending" ? { ...block, status: "interrupted" } : sealed(block);
|
|
427
531
|
}
|
|
428
|
-
function
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
532
|
+
function closeSubagent(block, cut) {
|
|
533
|
+
const children = block.children.map((c) => closeBlock(c, cut !== null));
|
|
534
|
+
const status = cut && block.status === "running" ? cut : block.status;
|
|
535
|
+
return status === block.status && children.every((c, i) => c === block.children[i]) ? block : { ...block, status, children };
|
|
536
|
+
}
|
|
537
|
+
function closeOpenBlocks(view, cut) {
|
|
538
|
+
const blocks = view.blocks.map((b) => b.type === "subagent" ? closeSubagent(b, cut) : closeBlock(b, cut !== null));
|
|
539
|
+
return blocks.every((b, i) => b === view.blocks[i]) ? view : { ...view, blocks };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/conversation/taskLog.ts
|
|
543
|
+
var EMPTY_TASK_LOG = { blocks: [], nextId: 1, queued: [], working: false, streams: {} };
|
|
544
|
+
var TEXT_STREAM = "text";
|
|
545
|
+
var thinkingStream = (subagentId) => `thinking:${subagentId ?? ""}`;
|
|
546
|
+
function withoutStream(view, key) {
|
|
547
|
+
if (!(key in view.streams)) return view;
|
|
548
|
+
return { ...view, streams: Object.fromEntries(Object.entries(view.streams).filter(([k]) => k !== key)) };
|
|
549
|
+
}
|
|
550
|
+
function blockText(text2) {
|
|
551
|
+
return text2.replace(/^\n+/, "");
|
|
552
|
+
}
|
|
553
|
+
function openStream(view, lane, key) {
|
|
554
|
+
const id = view.streams[key];
|
|
555
|
+
if (id === void 0) return -1;
|
|
556
|
+
const blocks = laneBlocks(view, lane);
|
|
557
|
+
const i = findLastIndex(blocks, (b) => b.type !== "usage");
|
|
558
|
+
const block = blocks[i];
|
|
559
|
+
return block?.id === id && (block.type === "message" || block.type === "thinking") && block.streaming ? i : -1;
|
|
560
|
+
}
|
|
561
|
+
function streamDelta(view, lane, key, text2, make) {
|
|
562
|
+
const i = openStream(view, lane, key);
|
|
563
|
+
const open = laneBlocks(view, lane)[i];
|
|
564
|
+
if (open?.type === "message" || open?.type === "thinking") return laneReplace(view, lane, i, { ...open, text: open.text + text2 });
|
|
565
|
+
const next = appendOutput(view, lane, (id) => make(id, blockText(text2)));
|
|
566
|
+
return { ...next, streams: { ...next.streams, [key]: `b${view.nextId}` } };
|
|
567
|
+
}
|
|
568
|
+
function streamWhole(view, lane, key, text2, make) {
|
|
569
|
+
const id = view.streams[key];
|
|
570
|
+
const blocks = laneBlocks(view, lane);
|
|
571
|
+
const i = id === void 0 ? -1 : findLastIndex(blocks, (b) => b.id === id);
|
|
572
|
+
const streamed = blocks[i];
|
|
573
|
+
const next = withoutStream(view, key);
|
|
574
|
+
if (streamed?.type === "message" || streamed?.type === "thinking") {
|
|
575
|
+
return laneReplace(next, lane, i, { ...streamed, text: blockText(text2), streaming: false });
|
|
436
576
|
}
|
|
437
|
-
|
|
438
|
-
const survivorId = "__merge_sim__";
|
|
439
|
-
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) }));
|
|
440
|
-
const unionDeps = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));
|
|
441
|
-
sim.push({ ...ordered[0], id: survivorId, title: "__merge_sim__", order: ordered[0].order, dependencies: unionDeps });
|
|
442
|
-
const cycle = detectCycle(sim);
|
|
443
|
-
if (cycle) return { ok: false, error: `Merging would create a dependency cycle involving "${cycle}"` };
|
|
444
|
-
return { ok: true };
|
|
577
|
+
return appendOutput(next, lane, make);
|
|
445
578
|
}
|
|
446
|
-
function
|
|
447
|
-
return
|
|
579
|
+
function agentText(view, text2) {
|
|
580
|
+
return streamWhole(view, null, TEXT_STREAM, text2, (id) => settledMessage(id, "agent", blockText(text2)));
|
|
448
581
|
}
|
|
449
|
-
function
|
|
450
|
-
|
|
451
|
-
const target = tasks.find((t) => t.id === taskId);
|
|
452
|
-
return target ? tasks.filter((t) => t.order < target.order) : [];
|
|
582
|
+
function agentTextDelta(view, text2) {
|
|
583
|
+
return streamDelta(view, null, TEXT_STREAM, text2, (id, t) => ({ type: "message", id, role: "agent", text: t, streaming: true }));
|
|
453
584
|
}
|
|
454
|
-
function
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
585
|
+
function thinkingBlock(id, text2, streaming, subagentId) {
|
|
586
|
+
return { type: "thinking", id, text: text2, streaming, ...subagentId ? { subagentId } : {} };
|
|
587
|
+
}
|
|
588
|
+
function thinkingDelta(view, { text: text2, subagentId }) {
|
|
589
|
+
const [placed, lane] = laneOf(view, subagentId);
|
|
590
|
+
return streamDelta(placed, lane, thinkingStream(subagentId), text2, (id, t) => thinkingBlock(id, t, true, subagentId));
|
|
591
|
+
}
|
|
592
|
+
function thinkingWhole(view, { text: text2, subagentId }) {
|
|
593
|
+
const [placed, lane] = laneOf(view, subagentId);
|
|
594
|
+
return streamWhole(placed, lane, thinkingStream(subagentId), text2, (id) => thinkingBlock(id, blockText(text2), false, subagentId));
|
|
595
|
+
}
|
|
596
|
+
function announceTool(view, { id: toolCallId, name, args, subagentId }) {
|
|
597
|
+
const { tool, toolLabel } = mapAgentTool(name);
|
|
598
|
+
const [placed, lane] = laneOf(view, subagentId);
|
|
599
|
+
return appendOutput(placed, lane, (id) => {
|
|
600
|
+
const call = pendingTool(id, { tool, toolLabel, args, toolCallId });
|
|
601
|
+
const parsed = parseArgs2(args);
|
|
602
|
+
return parsed ? { ...call, headline: toolHeadline(tool, JSON.stringify(normalizeAgentArgs(tool, parsed)), toolLabel) } : call;
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
function parseArgs2(args) {
|
|
606
|
+
try {
|
|
607
|
+
const parsed = JSON.parse(args);
|
|
608
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
609
|
+
} catch {
|
|
610
|
+
return null;
|
|
465
611
|
}
|
|
466
|
-
return { ok: true };
|
|
467
612
|
}
|
|
468
|
-
function
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
613
|
+
function settleTool(view, { id: toolCallId, output, success, subagentId }) {
|
|
614
|
+
const [placed, lane] = laneOf(view, subagentId);
|
|
615
|
+
const blocks = laneBlocks(placed, lane);
|
|
616
|
+
const i = findLastIndex(blocks, (b) => b.type === "tool" && b.status === "pending" && b.toolCallId === toolCallId);
|
|
617
|
+
const pending = blocks[i];
|
|
618
|
+
const outcome = success ? "success" : "failure";
|
|
619
|
+
if (pending?.type === "tool") return laneReplace(placed, lane, i, finishedTool(pending, output, outcome));
|
|
620
|
+
if (lane === null && blocks.some((b) => b.type === "subagent" && b.toolCallId === toolCallId)) return view;
|
|
621
|
+
return laneAppend(placed, lane, (id) => finishedTool(pendingTool(id, { tool: "agent_tool", toolLabel: "Tool", args: "", toolCallId }), output, outcome));
|
|
474
622
|
}
|
|
475
|
-
function
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
623
|
+
function startSubagent(view, { subagentId, brief, model }) {
|
|
624
|
+
return updateSubagent(view, subagentId, void 0, (block) => ({ ...block, ...brief ? { brief } : {}, ...model ? { model } : {} }));
|
|
625
|
+
}
|
|
626
|
+
function finishSubagent(view, { subagentId, outcome, digest }) {
|
|
627
|
+
const usage = view.usage?.bySubagent?.[subagentId];
|
|
628
|
+
const next = updateSubagent(view, subagentId, void 0, (block) => ({
|
|
629
|
+
...block,
|
|
630
|
+
status: outcome,
|
|
631
|
+
digest,
|
|
632
|
+
children: block.children.map(sealed),
|
|
633
|
+
...usage ? { usage } : {}
|
|
634
|
+
}));
|
|
635
|
+
return withoutStream(next, thinkingStream(subagentId));
|
|
636
|
+
}
|
|
637
|
+
function reportUsage(view, { record }) {
|
|
638
|
+
const usage = addPlannerUsage(view.usage ?? { totals: {} }, record);
|
|
639
|
+
const next = { ...view, usage };
|
|
640
|
+
return isMeasured(usage.totals) ? setUsageLine(next, usageLine(usage)) : next;
|
|
641
|
+
}
|
|
642
|
+
function runnerToolSubject(tool, args) {
|
|
643
|
+
const { tool: mapped, toolLabel } = mapAgentTool(tool);
|
|
644
|
+
const parsed = parseArgs2(args);
|
|
645
|
+
const headline = toolHeadline(mapped, parsed ? JSON.stringify(normalizeAgentArgs(mapped, parsed)) : args, toolLabel);
|
|
646
|
+
return headline.keyArg ? `${headline.name}(${headline.keyArg})` : headline.name;
|
|
647
|
+
}
|
|
648
|
+
function requestApproval(view, { approvalId, tool, args, allowForTask, toolCallId }) {
|
|
649
|
+
if (view.blocks.some((b) => b.type === "approval" && b.approvalId === approvalId)) return view;
|
|
650
|
+
return append(view, (id) => ({
|
|
651
|
+
type: "approval",
|
|
652
|
+
id,
|
|
653
|
+
approvalId,
|
|
654
|
+
kind: "runner_tool",
|
|
655
|
+
subject: runnerToolSubject(tool, args),
|
|
656
|
+
scope: tool,
|
|
657
|
+
status: "pending",
|
|
658
|
+
...allowForTask ? { allowForTask } : {},
|
|
659
|
+
...toolCallId ? { toolCallId } : {}
|
|
660
|
+
}));
|
|
661
|
+
}
|
|
662
|
+
function settleApproval(view, approvalId, settle) {
|
|
663
|
+
const i = findLastIndex(view.blocks, (b) => b.type === "approval" && b.approvalId === approvalId);
|
|
664
|
+
const block = view.blocks[i];
|
|
665
|
+
if (block?.type !== "approval" || block.status !== "pending") return view;
|
|
666
|
+
return { ...view, blocks: replaceAt(view.blocks, i, settle(block)) };
|
|
667
|
+
}
|
|
668
|
+
function decideApproval(view, { approvalId, decision, note }) {
|
|
669
|
+
return settleApproval(view, approvalId, (block) => ({
|
|
670
|
+
...block,
|
|
671
|
+
status: decision === "deny" ? "denied" : "granted",
|
|
672
|
+
decidedBy: "asked",
|
|
673
|
+
...decision === "allowForTask" ? { forTask: true } : {},
|
|
674
|
+
...note ? { note } : {}
|
|
675
|
+
}));
|
|
676
|
+
}
|
|
677
|
+
function startTurn(view, { message, messageId }) {
|
|
678
|
+
const queued = messageId ? view.queued.filter((m) => m.id !== messageId) : view.queued;
|
|
679
|
+
return append({ ...view, queued, working: true }, (id) => settledMessage(id, "user", message));
|
|
680
|
+
}
|
|
681
|
+
var CUT = { completed: null, interrupted: "stopped", failed: "failed" };
|
|
682
|
+
function endTurn(view, { reason }) {
|
|
683
|
+
const closed = closeOpenBlocks({ ...view, working: false, lastTurnEnd: reason, streams: {} }, CUT[reason]);
|
|
684
|
+
return reason === "interrupted" ? append(closed, (id) => settledMessage(id, "system", "Interrupted.")) : closed;
|
|
685
|
+
}
|
|
686
|
+
function queueMessage(view, { messageId, text: text2 }) {
|
|
687
|
+
if (view.queued.some((m) => m.id === messageId)) return view;
|
|
688
|
+
return { ...view, queued: [...view.queued, { id: messageId, text: text2 }] };
|
|
689
|
+
}
|
|
690
|
+
function unqueueMessage(view, { messageId }) {
|
|
691
|
+
const queued = view.queued.filter((m) => m.id !== messageId);
|
|
692
|
+
return queued.length === view.queued.length ? view : { ...view, queued };
|
|
693
|
+
}
|
|
694
|
+
function reduceTaskLog(view, event) {
|
|
695
|
+
switch (event.type) {
|
|
696
|
+
case "turn_start":
|
|
697
|
+
return startTurn(view, event);
|
|
698
|
+
case "turn_end":
|
|
699
|
+
return endTurn(view, event);
|
|
700
|
+
case "text_delta":
|
|
701
|
+
return agentTextDelta(view, event.text);
|
|
702
|
+
case "text":
|
|
703
|
+
return agentText(view, event.text);
|
|
704
|
+
case "thinking_delta":
|
|
705
|
+
return thinkingDelta(view, event);
|
|
706
|
+
case "thinking":
|
|
707
|
+
return thinkingWhole(view, event);
|
|
708
|
+
case "tool_call":
|
|
709
|
+
return announceTool(view, event);
|
|
710
|
+
case "tool_result":
|
|
711
|
+
return settleTool(view, event);
|
|
712
|
+
case "subagent_started":
|
|
713
|
+
return startSubagent(view, event);
|
|
714
|
+
case "subagent_finished":
|
|
715
|
+
return finishSubagent(view, event);
|
|
716
|
+
case "usage":
|
|
717
|
+
return reportUsage(view, event);
|
|
718
|
+
case "message_queued":
|
|
719
|
+
return queueMessage(view, event);
|
|
720
|
+
case "message_removed":
|
|
721
|
+
return unqueueMessage(view, event);
|
|
722
|
+
case "error":
|
|
723
|
+
return appendOutput(view, null, (id) => settledMessage(id, "error", event.message));
|
|
724
|
+
case "approval_requested":
|
|
725
|
+
return requestApproval(view, event);
|
|
726
|
+
case "approval_decided":
|
|
727
|
+
return decideApproval(view, event);
|
|
728
|
+
case "approval_withdrawn":
|
|
729
|
+
return settleApproval(view, event.approvalId, (block) => ({ ...block, status: "withdrawn" }));
|
|
730
|
+
default:
|
|
731
|
+
return view;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function replayTaskLog(events, from = EMPTY_TASK_LOG) {
|
|
735
|
+
return events.reduce(reduceTaskLog, from);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/services/conflictFiles.ts
|
|
739
|
+
function capConflictFiles(files, max = 5) {
|
|
740
|
+
return files.length <= max ? files.join(", ") : `${files.slice(0, max).join(", ")}, +${files.length - max} more`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// src/services/ModelAllowlistResolver.ts
|
|
744
|
+
function effectiveAllowlist(allowlist, runner, modelsByRunner) {
|
|
745
|
+
if (!allowlist || allowlist.length === 0) return void 0;
|
|
746
|
+
if (!modelsByRunner) return allowlist;
|
|
747
|
+
const own = new Set((modelsByRunner[runner] ?? []).map((m) => m.modelId));
|
|
748
|
+
const elsewhere = /* @__PURE__ */ new Set();
|
|
749
|
+
for (const [other, models] of Object.entries(modelsByRunner)) {
|
|
750
|
+
if (other === runner) continue;
|
|
751
|
+
for (const m of models ?? []) elsewhere.add(m.modelId);
|
|
752
|
+
}
|
|
753
|
+
const kept = allowlist.filter((id) => own.has(id) || !elsewhere.has(id));
|
|
754
|
+
return kept.length > 0 ? kept : void 0;
|
|
755
|
+
}
|
|
756
|
+
function filterModelsForPrompt(modelsByRunner, perRunnerAllowlist) {
|
|
757
|
+
const result = {};
|
|
758
|
+
for (const [runner, models] of Object.entries(modelsByRunner)) {
|
|
759
|
+
if (!models) continue;
|
|
760
|
+
const allowed = effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
761
|
+
if (!allowed) {
|
|
762
|
+
result[runner] = models;
|
|
495
763
|
continue;
|
|
496
764
|
}
|
|
497
|
-
|
|
765
|
+
const allowedSet = new Set(allowed);
|
|
766
|
+
const intersection = models.filter((m) => allowedSet.has(m.modelId));
|
|
767
|
+
const covered = new Set(intersection.map((m) => m.modelId));
|
|
768
|
+
const missing = allowed.filter((id) => !covered.has(id)).map((id) => ({ modelId: id, modelLabel: id.split("/").pop() || id, variants: [] }));
|
|
769
|
+
result[runner] = [...intersection, ...missing];
|
|
498
770
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
771
|
+
return result;
|
|
772
|
+
}
|
|
773
|
+
var EFFORT_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
774
|
+
var EFFORT_ALIASES = { disabled: "none", adaptive: "medium" };
|
|
775
|
+
function clampThinkingEffort(effort, variants) {
|
|
776
|
+
if (!effort) return void 0;
|
|
777
|
+
if (variants.length === 0) return void 0;
|
|
778
|
+
const ids = new Set(variants.map((v) => v.id));
|
|
779
|
+
if (ids.has(effort)) return effort;
|
|
780
|
+
const canon = EFFORT_ALIASES[effort] ?? effort;
|
|
781
|
+
if (ids.has(canon)) return canon;
|
|
782
|
+
const idx = EFFORT_LADDER.indexOf(canon);
|
|
783
|
+
if (idx < 0) return void 0;
|
|
784
|
+
for (let d = 1; d < EFFORT_LADDER.length; d++) {
|
|
785
|
+
const lower = EFFORT_LADDER[idx - d];
|
|
786
|
+
if (lower && ids.has(lower)) return lower;
|
|
787
|
+
const higher = EFFORT_LADDER[idx + d];
|
|
788
|
+
if (higher && ids.has(higher)) return higher;
|
|
789
|
+
}
|
|
790
|
+
return void 0;
|
|
791
|
+
}
|
|
792
|
+
function coerceAssignments(tasks, perRunnerAllowlist, allowedRunners, modelsByRunner) {
|
|
793
|
+
const clampVariant = (task) => {
|
|
794
|
+
if (!task.assignedModel || !modelsByRunner) return task;
|
|
795
|
+
const model = modelsByRunner[task.assignedRunner]?.find(
|
|
796
|
+
(m) => m.modelId === task.assignedModel.modelId
|
|
797
|
+
);
|
|
798
|
+
if (!model) return task;
|
|
799
|
+
const clamped = clampThinkingEffort(task.assignedModel.thinkingEffort, model.variants);
|
|
800
|
+
return {
|
|
801
|
+
...task,
|
|
802
|
+
assignedModel: {
|
|
803
|
+
...task.assignedModel,
|
|
804
|
+
thinkingEffort: clamped,
|
|
805
|
+
availableVariants: model.variants.map((v) => v.id)
|
|
509
806
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
807
|
+
};
|
|
808
|
+
};
|
|
809
|
+
const allowedFor = (runner) => effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
810
|
+
const assignmentFor = (runner, modelId) => {
|
|
811
|
+
const known = modelsByRunner?.[runner]?.find((m) => m.modelId === modelId);
|
|
812
|
+
return {
|
|
813
|
+
modelId,
|
|
814
|
+
modelLabel: known?.modelLabel ?? modelId,
|
|
815
|
+
thinkingEffort: void 0,
|
|
816
|
+
...known ? { availableVariants: known.variants.map((v) => v.id) } : {}
|
|
817
|
+
};
|
|
818
|
+
};
|
|
819
|
+
return tasks.map((task) => {
|
|
820
|
+
if (task.type === "user") return task;
|
|
821
|
+
if (allowedRunners && !allowedRunners.includes(task.assignedRunner)) {
|
|
822
|
+
const fallbackRunner = allowedRunners[0];
|
|
823
|
+
const fallbackModels = allowedFor(fallbackRunner);
|
|
824
|
+
const fallbackModel = fallbackModels?.length ? assignmentFor(fallbackRunner, fallbackModels[0]) : modelsByRunner?.[fallbackRunner]?.[0] ? assignmentFor(fallbackRunner, modelsByRunner[fallbackRunner][0].modelId) : task.assignedModel;
|
|
825
|
+
return clampVariant({
|
|
826
|
+
...task,
|
|
827
|
+
assignedRunner: fallbackRunner,
|
|
828
|
+
assignedModel: fallbackModel
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
const allowlist = allowedFor(task.assignedRunner);
|
|
832
|
+
if (!task.assignedModel) {
|
|
833
|
+
if (allowlist && allowlist.length > 0) {
|
|
834
|
+
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) });
|
|
513
835
|
}
|
|
514
|
-
|
|
836
|
+
const discovered = modelsByRunner?.[task.assignedRunner]?.[0];
|
|
837
|
+
if (discovered) {
|
|
838
|
+
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, discovered.modelId) });
|
|
839
|
+
}
|
|
840
|
+
return task;
|
|
515
841
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
536
|
-
if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
|
|
537
|
-
errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
|
|
538
|
-
break;
|
|
539
|
-
}
|
|
540
|
-
const changesForCheck = { ...changes };
|
|
541
|
-
delete changesForCheck.dependencies;
|
|
542
|
-
const check = validateTaskEdit("planner", flattenTasks(tasks), target.id, changesForCheck, catalog);
|
|
543
|
-
if (!check.ok) {
|
|
544
|
-
errors.push(`${label}: ${check.error}`);
|
|
545
|
-
break;
|
|
546
|
-
}
|
|
547
|
-
for (const field of check.clear ?? []) changes[field] = void 0;
|
|
548
|
-
if ("dependencies" in changes) {
|
|
549
|
-
const { ids, bad } = resolveDeps(changes.dependencies, i);
|
|
550
|
-
if (bad.length) {
|
|
551
|
-
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
552
|
-
break;
|
|
553
|
-
}
|
|
554
|
-
changes.dependencies = ids.filter((id) => id !== target.id);
|
|
555
|
-
}
|
|
556
|
-
if (Object.keys(changes).length === 0) {
|
|
557
|
-
errors.push(`${label}: no valid changes provided`);
|
|
558
|
-
break;
|
|
559
|
-
}
|
|
560
|
-
tasks = updateTaskInPlan(tasks, target.id, changes);
|
|
561
|
-
const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
|
|
562
|
-
summary.push(`Updated "${target.title}" (${Object.keys(changes).join(", ")})${clearedNote}`);
|
|
563
|
-
break;
|
|
842
|
+
if (!allowlist || allowlist.length === 0) return clampVariant(task);
|
|
843
|
+
if (allowlist.includes(task.assignedModel.modelId)) return clampVariant(task);
|
|
844
|
+
return { ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) };
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// src/services/TaskOps.ts
|
|
849
|
+
import { v4 as uuidv4 } from "uuid";
|
|
850
|
+
|
|
851
|
+
// src/services/TaskEditValidator.ts
|
|
852
|
+
var AI_ONLY_FIELDS = ["assignedModel", "thinkingEffort", "taskMode", "autonomy"];
|
|
853
|
+
function checkModelAndModeValidity(runner, assignedModel, taskMode, catalog) {
|
|
854
|
+
if (!catalog) return { ok: true };
|
|
855
|
+
if (assignedModel) {
|
|
856
|
+
const models = catalog.modelsByRunner[runner];
|
|
857
|
+
if (models) {
|
|
858
|
+
const modelId = assignedModel.modelId;
|
|
859
|
+
const allowed = effectiveAllowlist(catalog.perRunnerAllowlist?.[runner], runner, catalog.modelsByRunner);
|
|
860
|
+
if (!models.some((m) => m.modelId === modelId) && !allowed?.includes(modelId)) {
|
|
861
|
+
return { ok: false, error: `Runner "${runner}" does not offer model "${modelId}"` };
|
|
564
862
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (!spec.title) {
|
|
568
|
-
errors.push(`${label}: new task needs a title`);
|
|
569
|
-
break;
|
|
570
|
-
}
|
|
571
|
-
const { ids, bad } = resolveDeps(spec.dependencies, i);
|
|
572
|
-
if (bad.length) {
|
|
573
|
-
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
574
|
-
break;
|
|
575
|
-
}
|
|
576
|
-
const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : runners[0];
|
|
577
|
-
const validity = checkModelAndModeValidity(runner, spec.assignedModel, spec.taskMode, catalog);
|
|
578
|
-
if (!validity.ok) {
|
|
579
|
-
errors.push(`${label}: ${validity.error}`);
|
|
580
|
-
break;
|
|
581
|
-
}
|
|
582
|
-
const newId = uuidv4();
|
|
583
|
-
tasks = addTaskToPlan(tasks, {
|
|
584
|
-
...spec,
|
|
585
|
-
id: newId,
|
|
586
|
-
dependencies: ids,
|
|
587
|
-
assignedRunner: runner,
|
|
588
|
-
description: spec.description ?? spec.title,
|
|
589
|
-
prompt: spec.prompt ?? spec.description ?? spec.title
|
|
590
|
-
});
|
|
591
|
-
if (op.handle) handleId.set(op.handle, newId);
|
|
592
|
-
summary.push(`Added "${spec.title}"`);
|
|
593
|
-
break;
|
|
863
|
+
if (allowed && !allowed.includes(modelId)) {
|
|
864
|
+
return { ok: false, error: `Model "${modelId}" is excluded by the allowlist for runner "${runner}"` };
|
|
594
865
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (taskMode !== void 0) {
|
|
869
|
+
const modes = catalog.runnerModes[runner];
|
|
870
|
+
if (modes && modes.length > 0 && !modes.some((m) => m.id === taskMode)) {
|
|
871
|
+
return { ok: false, error: `Runner "${runner}" has no task mode "${taskMode}" \u2014 valid modes: ${modes.map((m) => m.id).join(", ")}` };
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return { ok: true };
|
|
875
|
+
}
|
|
876
|
+
function typeCoherenceCheck(target, changes) {
|
|
877
|
+
if (!changes.type || changes.type === target.type) return null;
|
|
878
|
+
if (changes.type === "user") {
|
|
879
|
+
if (!Array.isArray(changes.userSteps) || changes.userSteps.length === 0) {
|
|
880
|
+
return { ok: false, error: `"${target.title}" cannot become a manual task without user steps \u2014 include "userSteps" in this edit` };
|
|
881
|
+
}
|
|
882
|
+
return { ok: true, clear: AI_ONLY_FIELDS.filter((f) => target[f] !== void 0) };
|
|
883
|
+
}
|
|
884
|
+
if (!changes.prompt || !changes.prompt.trim()) {
|
|
885
|
+
return { ok: false, error: `"${target.title}" cannot become an AI task without a prompt \u2014 include "prompt" in this edit` };
|
|
886
|
+
}
|
|
887
|
+
return { ok: true, clear: target.userSteps && target.userSteps.length > 0 ? ["userSteps"] : [] };
|
|
888
|
+
}
|
|
889
|
+
function validateTaskEdit(actor, tasks, taskId, changes, catalog) {
|
|
890
|
+
const target = tasks.find((t) => t.id === taskId);
|
|
891
|
+
if (!target) return { ok: false, error: "Task not found" };
|
|
892
|
+
if (actor === "planner") {
|
|
893
|
+
if (target.status === "in_progress") return { ok: false, error: `"${target.title}" is running and cannot be modified` };
|
|
894
|
+
if (target.status === "completed") return { ok: false, error: `"${target.title}" is completed and cannot be modified` };
|
|
895
|
+
}
|
|
896
|
+
if (changes.dependencies) {
|
|
897
|
+
const depCheck = canSetDependencies(tasks, taskId, changes.dependencies);
|
|
898
|
+
if (!depCheck.ok) return depCheck;
|
|
899
|
+
}
|
|
900
|
+
const coherence = typeCoherenceCheck(target, changes);
|
|
901
|
+
if (coherence && !coherence.ok) return coherence;
|
|
902
|
+
const runner = changes.assignedRunner ?? target.assignedRunner;
|
|
903
|
+
const validity = checkModelAndModeValidity(runner, changes.assignedModel, changes.taskMode, catalog);
|
|
904
|
+
if (!validity.ok) return validity;
|
|
905
|
+
return coherence ?? { ok: true };
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/services/TaskOps.ts
|
|
909
|
+
var UPDATABLE_FIELDS = [
|
|
910
|
+
"title",
|
|
911
|
+
"description",
|
|
912
|
+
"prompt",
|
|
913
|
+
"dependencies",
|
|
914
|
+
"assignedRunner",
|
|
915
|
+
"assignedModel",
|
|
916
|
+
"taskMode",
|
|
917
|
+
"thinkingEffort",
|
|
918
|
+
"type",
|
|
919
|
+
"userSteps",
|
|
920
|
+
"autonomy",
|
|
921
|
+
"sliceType"
|
|
922
|
+
];
|
|
923
|
+
function taskOpsProtocol(executionNote = "") {
|
|
924
|
+
const updateChanges = UPDATABLE_FIELDS.map((f) => f === "dependencies" ? '"dependencies"?:["<id or #order>"]' : `"${f}"?`).join(",");
|
|
925
|
+
return [
|
|
926
|
+
"- To modify specific tasks, reply with ONLY this JSON object:",
|
|
927
|
+
' {"taskOps": [',
|
|
928
|
+
` {"op":"update","taskId":"<id or #order>","changes":{${updateChanges}}},`,
|
|
929
|
+
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?},"handle"?:"<name>"},',
|
|
930
|
+
' {"op":"remove","taskId":"<id or #order>"},',
|
|
931
|
+
' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]}, // only to re-prioritise INDEPENDENT tasks',
|
|
932
|
+
' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...},"handle"?:"<name>"},',
|
|
933
|
+
' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...],"handle"?:"<name>"},',
|
|
934
|
+
` {"op":"rearm","taskId":"<id or #order>","changes"?:{${updateChanges}}}`,
|
|
935
|
+
" ]}",
|
|
936
|
+
'- For sweeping changes, you may instead emit a full {"tasks":[...]} plan JSON.',
|
|
937
|
+
'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.',
|
|
938
|
+
'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.',
|
|
939
|
+
'When creating or changing tasks, set "assignedRunner" and "assignedModel" ({"modelId","modelLabel","thinkingEffort"?}) using only the runners and models listed in <available_models>.',
|
|
940
|
+
'Keep dependencies consistent: no cycles, no references to removed tasks, and never touch running or completed tasks \u2014 "rearm" is the one exception, below.',
|
|
941
|
+
'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,
|
|
942
|
+
'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.',
|
|
943
|
+
'"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.'
|
|
944
|
+
];
|
|
945
|
+
}
|
|
946
|
+
function textHasTaskOps(text2) {
|
|
947
|
+
return text2.includes(`"${TASK_OPS_ENVELOPE_KEY}"`);
|
|
948
|
+
}
|
|
949
|
+
function parseTaskOpsJson(text2) {
|
|
950
|
+
const { matches, fallback } = extractObjectsWithKey(text2, TASK_OPS_ENVELOPE_KEY);
|
|
951
|
+
const candidates = matches.length > 0 ? [...matches].reverse() : [fallback.json];
|
|
952
|
+
let lastError = null;
|
|
953
|
+
for (const json of candidates) {
|
|
954
|
+
try {
|
|
955
|
+
return parseTaskOpsObject(json, text2);
|
|
956
|
+
} catch (err) {
|
|
957
|
+
if (!(err instanceof PlanParseError)) throw err;
|
|
958
|
+
if (!lastError) lastError = err;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
throw lastError ?? new PlanParseError("No JSON object found in task-ops reply", text2);
|
|
962
|
+
}
|
|
963
|
+
function parseTaskOpsObject(json, text2) {
|
|
964
|
+
let parsed;
|
|
965
|
+
try {
|
|
966
|
+
parsed = JSON.parse(escapeControlCharsInStrings(stripTrailingCommas(json)));
|
|
967
|
+
} catch (err) {
|
|
968
|
+
throw new PlanParseError(`Task-ops JSON is invalid: ${err instanceof Error ? err.message : String(err)}`, text2);
|
|
969
|
+
}
|
|
970
|
+
const ops = parsed.taskOps;
|
|
971
|
+
if (!Array.isArray(ops) || ops.length === 0) {
|
|
972
|
+
throw new PlanParseError('"taskOps" must be a non-empty array', text2);
|
|
973
|
+
}
|
|
974
|
+
for (const op of ops) {
|
|
975
|
+
if (typeof op !== "object" || op === null || typeof op.op !== "string") {
|
|
976
|
+
throw new PlanParseError('Every task op needs an "op" field (update | add | remove | reorder | merge | split)', text2);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return ops;
|
|
980
|
+
}
|
|
981
|
+
function resolveRef(ref, originalScope, liveScope, handleOwner, handleId, opIndex) {
|
|
982
|
+
if (typeof ref !== "string" && typeof ref !== "number") return { error: `invalid reference "${String(ref)}"` };
|
|
983
|
+
const s = String(ref).trim();
|
|
984
|
+
const ownerIdx = handleOwner.get(s);
|
|
985
|
+
if (ownerIdx !== void 0) {
|
|
986
|
+
if (ownerIdx >= opIndex) {
|
|
987
|
+
return { error: `handle "${s}" is defined later in this batch (op ${ownerIdx + 1}) and cannot be referenced yet` };
|
|
988
|
+
}
|
|
989
|
+
const id = handleId.get(s);
|
|
990
|
+
if (!id) return { error: `handle "${s}" was never resolved` };
|
|
991
|
+
return { id };
|
|
992
|
+
}
|
|
993
|
+
const byLiveId = liveScope.find((t) => t.id === s);
|
|
994
|
+
if (byLiveId) return { id: byLiveId.id };
|
|
995
|
+
const orderStr = s.startsWith("#") ? s.slice(1) : s;
|
|
996
|
+
if (/^\d+$/.test(orderStr)) {
|
|
997
|
+
const order = Number(orderStr);
|
|
998
|
+
const byOrder = originalScope.filter((t) => t.order === order);
|
|
999
|
+
if (byOrder.length === 1) return { id: byOrder[0].id };
|
|
1000
|
+
}
|
|
1001
|
+
const byTitle = originalScope.find((t) => t.title === s);
|
|
1002
|
+
if (byTitle) return { id: byTitle.id };
|
|
1003
|
+
return { error: `task "${s}" not found` };
|
|
1004
|
+
}
|
|
1005
|
+
function taskOpRefs(op) {
|
|
1006
|
+
switch (op.op) {
|
|
1007
|
+
case "update":
|
|
1008
|
+
case "remove":
|
|
1009
|
+
case "split":
|
|
1010
|
+
case "rearm":
|
|
1011
|
+
return [op.taskId];
|
|
1012
|
+
case "merge":
|
|
1013
|
+
case "reorder":
|
|
1014
|
+
return op.taskIds;
|
|
1015
|
+
case "add":
|
|
1016
|
+
return [];
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function refMatchesTask(ref, task) {
|
|
1020
|
+
if (typeof ref !== "string" && typeof ref !== "number") return false;
|
|
1021
|
+
const s = String(ref).trim();
|
|
1022
|
+
if (task.id === s) return true;
|
|
1023
|
+
const orderStr = s.startsWith("#") ? s.slice(1) : s;
|
|
1024
|
+
return /^\d+$/.test(orderStr) && task.order === Number(orderStr) || task.title === s;
|
|
1025
|
+
}
|
|
1026
|
+
function refCollidesWithExisting(name, originalFlatAll) {
|
|
1027
|
+
if (originalFlatAll.some((t) => t.id === name)) return true;
|
|
1028
|
+
const orderStr = name.startsWith("#") ? name.slice(1) : name;
|
|
1029
|
+
if (/^\d+$/.test(orderStr) && originalFlatAll.some((t) => t.order === Number(orderStr))) return true;
|
|
1030
|
+
return originalFlatAll.some((t) => t.title === name);
|
|
1031
|
+
}
|
|
1032
|
+
function findInTasks(tasks, id) {
|
|
1033
|
+
return flattenTasks(tasks).find((t) => t.id === id);
|
|
1034
|
+
}
|
|
1035
|
+
function findTopLevel(tasks, id) {
|
|
1036
|
+
return tasks.find((t) => t.id === id);
|
|
1037
|
+
}
|
|
1038
|
+
function detectCycle(tasks) {
|
|
1039
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
1040
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1041
|
+
const done = /* @__PURE__ */ new Set();
|
|
1042
|
+
const visit = (id) => {
|
|
1043
|
+
if (done.has(id)) return null;
|
|
1044
|
+
if (visiting.has(id)) return id;
|
|
1045
|
+
visiting.add(id);
|
|
1046
|
+
for (const dep of byId.get(id)?.dependencies ?? []) {
|
|
1047
|
+
const hit = visit(dep);
|
|
1048
|
+
if (hit) return hit;
|
|
1049
|
+
}
|
|
1050
|
+
visiting.delete(id);
|
|
1051
|
+
done.add(id);
|
|
1052
|
+
return null;
|
|
1053
|
+
};
|
|
1054
|
+
for (const t of tasks) {
|
|
1055
|
+
const hit = visit(t.id);
|
|
1056
|
+
if (hit) return byId.get(hit)?.title ?? hit;
|
|
1057
|
+
}
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
function isPinned(t) {
|
|
1061
|
+
return t.status === "in_progress" || t.status === "completed";
|
|
1062
|
+
}
|
|
1063
|
+
function repairOrder(tasks) {
|
|
1064
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
1065
|
+
const placed = /* @__PURE__ */ new Set();
|
|
1066
|
+
const movable = tasks.filter((t) => !isPinned(t));
|
|
1067
|
+
const result = [];
|
|
1068
|
+
const blockerOf = (t) => {
|
|
1069
|
+
const id = t.dependencies.find((d) => byId.has(d) && !placed.has(d));
|
|
1070
|
+
return id ? byId.get(id) : void 0;
|
|
1071
|
+
};
|
|
1072
|
+
const place = (t) => {
|
|
1073
|
+
result.push(t);
|
|
1074
|
+
placed.add(t.id);
|
|
1075
|
+
};
|
|
1076
|
+
for (const occupant of tasks) {
|
|
1077
|
+
if (isPinned(occupant)) {
|
|
1078
|
+
const blocker = blockerOf(occupant);
|
|
1079
|
+
if (blocker) return { error: pinnedRefusal(blocker, occupant, "before") };
|
|
1080
|
+
place(occupant);
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
const next = movable.findIndex((t) => !blockerOf(t));
|
|
1084
|
+
if (next < 0) {
|
|
1085
|
+
const [stuck, pin] = trailToPin(movable[0], blockerOf);
|
|
1086
|
+
return { error: pinnedRefusal(stuck, pin, "after") };
|
|
1087
|
+
}
|
|
1088
|
+
place(movable.splice(next, 1)[0]);
|
|
1089
|
+
}
|
|
1090
|
+
return { tasks: result };
|
|
1091
|
+
}
|
|
1092
|
+
function trailToPin(task, blockerOf) {
|
|
1093
|
+
let at = task;
|
|
1094
|
+
for (; ; ) {
|
|
1095
|
+
const blocker = blockerOf(at);
|
|
1096
|
+
if (!blocker || isPinned(blocker)) return [at, blocker ?? at];
|
|
1097
|
+
at = blocker;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
function describeMoves(before, after) {
|
|
1101
|
+
const wasAt = new Map(before.map((t, i) => [t.id, i + 1]));
|
|
1102
|
+
const moved = after.map((t, i) => ({ t, from: wasAt.get(t.id), to: i + 1 })).filter((m) => m.from !== m.to);
|
|
1103
|
+
return moved.length ? moved.map((m) => `"${m.t.title}" #${m.from}\u2192#${m.to}`).join(", ") : null;
|
|
1104
|
+
}
|
|
1105
|
+
function pinnedRefusal(other, pin, side) {
|
|
1106
|
+
const word = pin.status === "in_progress" ? "running" : "completed";
|
|
1107
|
+
return `"${other.title}" would have to run ${side} "${pin.title}", which is ${word} and cannot be moved`;
|
|
1108
|
+
}
|
|
1109
|
+
function canMergeTasks(tasks, selectedIds) {
|
|
1110
|
+
if (selectedIds.length < 2) return { ok: false, error: "Select at least two tasks to merge" };
|
|
1111
|
+
const idSet = new Set(selectedIds);
|
|
1112
|
+
const toMerge = tasks.filter((t) => idSet.has(t.id));
|
|
1113
|
+
if (toMerge.length < 2) return { ok: false, error: "At least two valid tasks are required to merge" };
|
|
1114
|
+
for (const t of toMerge) {
|
|
1115
|
+
if (t.status === "in_progress") return { ok: false, error: `"${t.title}" is running and cannot be merged` };
|
|
1116
|
+
if (t.status === "completed") return { ok: false, error: `"${t.title}" is completed and cannot be merged` };
|
|
1117
|
+
}
|
|
1118
|
+
const ordered = [...toMerge].sort((a, b) => a.order - b.order);
|
|
1119
|
+
const survivorId = "__merge_sim__";
|
|
1120
|
+
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) }));
|
|
1121
|
+
const unionDeps = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));
|
|
1122
|
+
sim.push({ ...ordered[0], id: survivorId, title: "__merge_sim__", order: ordered[0].order, dependencies: unionDeps });
|
|
1123
|
+
const cycle = detectCycle(sim);
|
|
1124
|
+
if (cycle) return { ok: false, error: `Merging would create a dependency cycle involving "${cycle}"` };
|
|
1125
|
+
return { ok: true };
|
|
1126
|
+
}
|
|
1127
|
+
function dependentsOf(tasks, taskId) {
|
|
1128
|
+
return tasks.filter((t) => t.id !== taskId && t.dependencies.includes(taskId));
|
|
1129
|
+
}
|
|
1130
|
+
function dependencyCandidates(tasks, taskId) {
|
|
1131
|
+
if (!taskId) return [...tasks];
|
|
1132
|
+
const target = tasks.find((t) => t.id === taskId);
|
|
1133
|
+
return target ? tasks.filter((t) => t.order < target.order) : [];
|
|
1134
|
+
}
|
|
1135
|
+
function canSetDependencies(tasks, taskId, dependencies) {
|
|
1136
|
+
const target = tasks.find((t) => t.id === taskId);
|
|
1137
|
+
if (!target) return { ok: false, error: "Task not found" };
|
|
1138
|
+
const allowed = new Set(dependencyCandidates(tasks, taskId).map((t) => t.id));
|
|
1139
|
+
for (const dep of dependencies) {
|
|
1140
|
+
if (dep === taskId) return { ok: false, error: `"${target.title}" cannot depend on itself` };
|
|
1141
|
+
const found = tasks.find((t) => t.id === dep);
|
|
1142
|
+
if (!found) return { ok: false, error: `Unknown dependency "${dep}"` };
|
|
1143
|
+
if (!allowed.has(dep)) {
|
|
1144
|
+
return { ok: false, error: `"${target.title}" cannot depend on "${found.title}", which comes after it in the plan` };
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return { ok: true };
|
|
1148
|
+
}
|
|
1149
|
+
function canSplitTask(tasks, taskId) {
|
|
1150
|
+
const target = tasks.find((t) => t.id === taskId);
|
|
1151
|
+
if (!target) return { ok: false, error: "Task not found" };
|
|
1152
|
+
if (target.status === "in_progress") return { ok: false, error: `"${target.title}" is running and cannot be split` };
|
|
1153
|
+
if (target.status === "completed") return { ok: false, error: `"${target.title}" is completed and cannot be split` };
|
|
1154
|
+
return { ok: true };
|
|
1155
|
+
}
|
|
1156
|
+
function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
1157
|
+
let tasks = JSON.parse(JSON.stringify(currentTasks));
|
|
1158
|
+
const errors = [];
|
|
1159
|
+
const summary = [];
|
|
1160
|
+
const originalFlatAll = flattenTasks(currentTasks);
|
|
1161
|
+
const handleOwner = /* @__PURE__ */ new Map();
|
|
1162
|
+
const handleId = /* @__PURE__ */ new Map();
|
|
1163
|
+
for (const [i, op] of ops.entries()) {
|
|
1164
|
+
const handle = op.handle;
|
|
1165
|
+
if (handle === void 0) continue;
|
|
1166
|
+
if (typeof handle !== "string" || !handle.trim()) {
|
|
1167
|
+
errors.push(`op ${i + 1} (${op.op}): handle must be a non-empty string`);
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
if (handleOwner.has(handle)) {
|
|
1171
|
+
errors.push(`op ${i + 1} (${op.op}): handle "${handle}" is already used earlier in this batch`);
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
if (refCollidesWithExisting(handle, originalFlatAll)) {
|
|
1175
|
+
errors.push(`op ${i + 1} (${op.op}): handle "${handle}" collides with an existing task reference`);
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
handleOwner.set(handle, i);
|
|
1179
|
+
}
|
|
1180
|
+
if (errors.length) return { ok: false, tasks: currentTasks, errors, summary: [] };
|
|
1181
|
+
const resolveDeps = (deps, opIndex) => {
|
|
1182
|
+
const ids = [];
|
|
1183
|
+
const bad = [];
|
|
1184
|
+
const liveFlat = flattenTasks(tasks);
|
|
1185
|
+
for (const d of Array.isArray(deps) ? deps : []) {
|
|
1186
|
+
const r = resolveRef(d, originalFlatAll, liveFlat, handleOwner, handleId, opIndex);
|
|
1187
|
+
if ("error" in r) {
|
|
1188
|
+
bad.push(`${String(d)} (${r.error})`);
|
|
1189
|
+
continue;
|
|
786
1190
|
}
|
|
787
|
-
|
|
1191
|
+
if (!liveFlat.some((t) => t.id === r.id)) {
|
|
1192
|
+
bad.push(`${String(d)} (no longer exists in this batch)`);
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
ids.push(r.id);
|
|
1196
|
+
}
|
|
1197
|
+
return { ids, bad };
|
|
1198
|
+
};
|
|
1199
|
+
for (const [i, op] of ops.entries()) {
|
|
1200
|
+
const label = `op ${i + 1} (${op.op})`;
|
|
1201
|
+
switch (op.op) {
|
|
1202
|
+
case "update": {
|
|
788
1203
|
const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
789
1204
|
if ("error" in ref) {
|
|
790
1205
|
errors.push(`${label}: ${ref.error}`);
|
|
@@ -795,10 +1210,6 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
795
1210
|
errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
|
|
796
1211
|
break;
|
|
797
1212
|
}
|
|
798
|
-
if (target.status === "in_progress") {
|
|
799
|
-
errors.push(`${label}: "${target.title}" is running and cannot be re-armed`);
|
|
800
|
-
break;
|
|
801
|
-
}
|
|
802
1213
|
const changes = {};
|
|
803
1214
|
for (const key of UPDATABLE_FIELDS) {
|
|
804
1215
|
if (op.changes && key in op.changes) changes[key] = op.changes[key];
|
|
@@ -807,551 +1218,478 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
807
1218
|
errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
|
|
808
1219
|
break;
|
|
809
1220
|
}
|
|
810
|
-
const changesForCheck = { ...changes };
|
|
811
|
-
delete changesForCheck.dependencies;
|
|
812
|
-
const check = validateTaskEdit("
|
|
813
|
-
if (!check.ok) {
|
|
814
|
-
errors.push(`${label}: ${check.error}`);
|
|
815
|
-
break;
|
|
816
|
-
}
|
|
817
|
-
for (const field of check.clear ?? []) changes[field] = void 0;
|
|
818
|
-
if ("dependencies" in changes) {
|
|
819
|
-
const { ids, bad } = resolveDeps(changes.dependencies, i);
|
|
820
|
-
if (bad.length) {
|
|
821
|
-
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
822
|
-
break;
|
|
823
|
-
}
|
|
824
|
-
changes.dependencies = ids.filter((id) => id !== target.id);
|
|
825
|
-
}
|
|
826
|
-
tasks = updateTaskInPlan(tasks, target.id, { ...changes, status: "pending", verdict: void 0, outputSummary: void 0 });
|
|
827
|
-
for (const dep of flattenTasks(tasks)) {
|
|
828
|
-
if (dep.status === "blocked" && dep.dependencies.includes(target.id)) {
|
|
829
|
-
tasks = updateTaskInPlan(tasks, dep.id, { status: "pending" });
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
|
|
833
|
-
const changedNote = Object.keys(changes).length ? ` (${Object.keys(changes).join(", ")})` : "";
|
|
834
|
-
summary.push(`Re-armed "${target.title}"${changedNote}${clearedNote}`);
|
|
835
|
-
break;
|
|
836
|
-
}
|
|
837
|
-
default:
|
|
838
|
-
errors.push(`${label}: unknown op "${op.op}"`);
|
|
839
|
-
}
|
|
840
|
-
if (errors.length) break;
|
|
841
|
-
}
|
|
842
|
-
if (!errors.length) {
|
|
843
|
-
const flat = flattenTasks(tasks);
|
|
844
|
-
const idSet = new Set(flat.map((t) => t.id));
|
|
845
|
-
for (const t of flat) {
|
|
846
|
-
for (const dep of t.dependencies) {
|
|
847
|
-
if (!idSet.has(dep)) errors.push(`"${t.title}" depends on a task that no longer exists (${dep})`);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
const cycleAt = detectCycle(flat);
|
|
851
|
-
if (cycleAt) errors.push(`Dependency cycle detected involving "${cycleAt}"`);
|
|
852
|
-
if (!errors.length) {
|
|
853
|
-
const repaired = repairOrder(tasks);
|
|
854
|
-
if ("error" in repaired) {
|
|
855
|
-
errors.push(repaired.error);
|
|
856
|
-
} else {
|
|
857
|
-
const moves = describeMoves(tasks, repaired.tasks);
|
|
858
|
-
if (moves) summary.push(`Reordered to keep dependencies first: ${moves}`);
|
|
859
|
-
tasks = renumberTasks(repaired.tasks);
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
return errors.length ? { ok: false, tasks: currentTasks, errors, summary: [] } : { ok: true, tasks, errors: [], summary };
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// src/services/researchStepSummary.ts
|
|
867
|
-
function ellipsize(text2, max) {
|
|
868
|
-
const oneLine2 = text2.replace(/\s+/g, " ").trim();
|
|
869
|
-
return oneLine2.length > max ? `${oneLine2.slice(0, max)}\u2026` : oneLine2;
|
|
870
|
-
}
|
|
871
|
-
function summarizeToolCall(tool, argsJson, toolLabel) {
|
|
872
|
-
const display = toolLabel?.trim() || tool;
|
|
873
|
-
if (tool === "agent_tool") {
|
|
874
|
-
try {
|
|
875
|
-
const args = JSON.parse(argsJson);
|
|
876
|
-
const hint = args.path ?? args.file_path ?? args.filePath ?? args.pattern ?? args.query ?? args.url ?? args.command;
|
|
877
|
-
if (hint) return `${display} ${ellipsize(String(hint), 50)}`;
|
|
878
|
-
} catch {
|
|
879
|
-
}
|
|
880
|
-
return display;
|
|
881
|
-
}
|
|
882
|
-
if (tool === "spawn_research_agent") {
|
|
883
|
-
try {
|
|
884
|
-
const args = JSON.parse(argsJson);
|
|
885
|
-
const prompt = typeof args.prompt === "string" ? args.prompt : "";
|
|
886
|
-
const oneLine2 = prompt.replace(/\s+/g, " ").trim();
|
|
887
|
-
return `spawn: "${oneLine2.slice(0, 60)}${oneLine2.length > 60 ? "\u2026" : ""}"`;
|
|
888
|
-
} catch {
|
|
889
|
-
return "spawn_research_agent";
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
try {
|
|
893
|
-
const args = JSON.parse(argsJson);
|
|
894
|
-
if (tool === "find_symbol" && args.symbol) {
|
|
895
|
-
return `find_symbol ${args.symbol}${args.language ? ` [${args.language}]` : ""}`;
|
|
896
|
-
}
|
|
897
|
-
if (tool === "web_search" && args.query) {
|
|
898
|
-
return `web_search "${ellipsize(String(args.query), 50)}"`;
|
|
899
|
-
}
|
|
900
|
-
if (tool === "fetch" && args.url) {
|
|
901
|
-
return `fetch ${ellipsize(String(args.url), 60)}`;
|
|
902
|
-
}
|
|
903
|
-
if (tool === "glob" && args.pattern) {
|
|
904
|
-
return `glob ${ellipsize(String(args.pattern), 50)}`;
|
|
905
|
-
}
|
|
906
|
-
if (tool === "grep" && args.pattern) {
|
|
907
|
-
const mode = args.output_mode && args.output_mode !== "content" ? ` (${args.output_mode})` : "";
|
|
908
|
-
const scope = args.include ? ` in ${args.include}` : "";
|
|
909
|
-
return `grep ${ellipsize(String(args.pattern), 40)}${scope}${mode}`;
|
|
910
|
-
}
|
|
911
|
-
const path = args.filePath || args.file_path || args.path;
|
|
912
|
-
if (path) {
|
|
913
|
-
const basename = String(path).split("/").pop() || String(path);
|
|
914
|
-
return `${display} ${basename}`;
|
|
915
|
-
}
|
|
916
|
-
if (args.pattern) {
|
|
917
|
-
return `${display} ${args.pattern}`;
|
|
918
|
-
}
|
|
919
|
-
if (args.command) {
|
|
920
|
-
return `bash: ${String(args.command).slice(0, 40)}`;
|
|
921
|
-
}
|
|
922
|
-
} catch {
|
|
923
|
-
}
|
|
924
|
-
return display;
|
|
925
|
-
}
|
|
926
|
-
function classifyOutcome(success, output) {
|
|
927
|
-
if (!success) {
|
|
928
|
-
if (/^Command refused:/.test(output) || /is not available\. You are a read-only research subagent/.test(output)) return "refused";
|
|
929
|
-
if (/not approved/i.test(output) || /^Access denied:/.test(output) || /needs user approval/i.test(output)) return "denied";
|
|
930
|
-
return "failure";
|
|
931
|
-
}
|
|
932
|
-
return "success";
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// src/services/SessionMessage.ts
|
|
936
|
-
function serializeTask(t) {
|
|
937
|
-
return {
|
|
938
|
-
id: t.id,
|
|
939
|
-
order: t.order,
|
|
940
|
-
title: t.title,
|
|
941
|
-
type: t.type,
|
|
942
|
-
description: t.description,
|
|
943
|
-
dependencies: t.dependencies || [],
|
|
944
|
-
assignedRunner: t.assignedRunner,
|
|
945
|
-
assignedModel: t.assignedModel || null,
|
|
946
|
-
taskMode: t.taskMode || "build",
|
|
947
|
-
prompt: t.prompt || null,
|
|
948
|
-
subtasks: (t.subtasks || []).map(serializeTask),
|
|
949
|
-
userSteps: t.userSteps || void 0,
|
|
950
|
-
thinkingEffort: t.thinkingEffort || void 0,
|
|
951
|
-
autonomy: t.autonomy || void 0,
|
|
952
|
-
sliceType: t.sliceType || void 0,
|
|
953
|
-
userStoriesCovered: t.userStoriesCovered || void 0
|
|
954
|
-
};
|
|
955
|
-
}
|
|
956
|
-
function serializeTaskStatus(t, idleSince = null, isolation = null) {
|
|
957
|
-
return {
|
|
958
|
-
id: t.id,
|
|
959
|
-
status: t.status,
|
|
960
|
-
verdict: t.verdict ? { outcome: t.verdict.outcome, reason: t.verdict.reason, checks: t.verdict.checks || [] } : null,
|
|
961
|
-
idleSince,
|
|
962
|
-
...isolation ? { isolation } : {}
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
function serializePlan(plan) {
|
|
966
|
-
return {
|
|
967
|
-
tasks: plan.tasks.map(serializeTask),
|
|
968
|
-
runners: plan.runners,
|
|
969
|
-
generatedAt: plan.generatedAt,
|
|
970
|
-
conversationHistory: plan.conversationHistory,
|
|
971
|
-
prdMarkdown: plan.prdMarkdown,
|
|
972
|
-
queuedMessages: plan.queuedMessages
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
function executionSummary(tasks) {
|
|
976
|
-
return {
|
|
977
|
-
total: tasks.length,
|
|
978
|
-
completed: tasks.filter((t) => t.status === "completed").length,
|
|
979
|
-
failed: tasks.filter((t) => t.status === "failed").length
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
var CHECKPOINT_TRUNCATE_LENGTH = 120;
|
|
983
|
-
function truncateCheckpointSummary(summary) {
|
|
984
|
-
const firstLine = summary.split("\n")[0].trim();
|
|
985
|
-
if (firstLine.length <= CHECKPOINT_TRUNCATE_LENGTH) return firstLine;
|
|
986
|
-
return firstLine.slice(0, CHECKPOINT_TRUNCATE_LENGTH - 1) + "\u2026";
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// src/services/terminalRender.ts
|
|
990
|
-
var ANSI_OR_CTRL_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?|\x1b[()][AB012]|\x1b[=>]|[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
991
|
-
function flattenTerminalOutput(raw) {
|
|
992
|
-
return raw.replace(ANSI_OR_CTRL_RE, "").replace(/[─-▟]/g, "").replace(/\s+/g, "");
|
|
993
|
-
}
|
|
994
|
-
function renderTerminalOutput(raw) {
|
|
995
|
-
const rows = /* @__PURE__ */ new Map();
|
|
996
|
-
let row = 1;
|
|
997
|
-
let col = 1;
|
|
998
|
-
let savedRow = 1;
|
|
999
|
-
let savedCol = 1;
|
|
1000
|
-
const cells = (r) => {
|
|
1001
|
-
let line = rows.get(r);
|
|
1002
|
-
if (!line) {
|
|
1003
|
-
line = [];
|
|
1004
|
-
rows.set(r, line);
|
|
1005
|
-
}
|
|
1006
|
-
return line;
|
|
1007
|
-
};
|
|
1008
|
-
const firstParam = (params, fallback = 1) => params[0] || fallback;
|
|
1009
|
-
const eraseLine = (mode) => {
|
|
1010
|
-
const line = cells(row);
|
|
1011
|
-
if (mode === 2) {
|
|
1012
|
-
rows.set(row, []);
|
|
1013
|
-
} else if (mode === 1) {
|
|
1014
|
-
for (let c = 0; c < col; c++) line[c] = " ";
|
|
1015
|
-
} else {
|
|
1016
|
-
line.length = Math.max(0, col - 1);
|
|
1017
|
-
}
|
|
1018
|
-
};
|
|
1019
|
-
for (let i = 0; i < raw.length; ) {
|
|
1020
|
-
const ch = raw[i];
|
|
1021
|
-
if (ch === "\x1B") {
|
|
1022
|
-
const kind = raw[i + 1];
|
|
1023
|
-
if (kind === "[") {
|
|
1024
|
-
let end = i + 2;
|
|
1025
|
-
while (end < raw.length) {
|
|
1026
|
-
const code = raw.charCodeAt(end);
|
|
1027
|
-
if (code >= 64 && code <= 126) break;
|
|
1028
|
-
end++;
|
|
1029
|
-
}
|
|
1030
|
-
if (end >= raw.length) break;
|
|
1031
|
-
const final = raw[end];
|
|
1032
|
-
const body = raw.slice(i + 2, end).replace(/^[?<>=!]+/, "");
|
|
1033
|
-
const params = body.split(";").map((p) => Number.parseInt(p, 10) || 0);
|
|
1034
|
-
switch (final) {
|
|
1035
|
-
case "H":
|
|
1036
|
-
case "f":
|
|
1037
|
-
row = params[0] || 1;
|
|
1038
|
-
col = params[1] || 1;
|
|
1039
|
-
break;
|
|
1040
|
-
case "G":
|
|
1041
|
-
col = firstParam(params);
|
|
1042
|
-
break;
|
|
1043
|
-
case "d":
|
|
1044
|
-
row = firstParam(params);
|
|
1045
|
-
break;
|
|
1046
|
-
case "A":
|
|
1047
|
-
row = Math.max(1, row - firstParam(params));
|
|
1048
|
-
break;
|
|
1049
|
-
case "B":
|
|
1050
|
-
row += firstParam(params);
|
|
1051
|
-
break;
|
|
1052
|
-
case "C":
|
|
1053
|
-
col += firstParam(params);
|
|
1054
|
-
break;
|
|
1055
|
-
case "D":
|
|
1056
|
-
col = Math.max(1, col - firstParam(params));
|
|
1057
|
-
break;
|
|
1058
|
-
case "E":
|
|
1059
|
-
row += firstParam(params);
|
|
1060
|
-
col = 1;
|
|
1061
|
-
break;
|
|
1062
|
-
case "F":
|
|
1063
|
-
row = Math.max(1, row - firstParam(params));
|
|
1064
|
-
col = 1;
|
|
1065
|
-
break;
|
|
1066
|
-
case "s":
|
|
1067
|
-
savedRow = row;
|
|
1068
|
-
savedCol = col;
|
|
1069
|
-
break;
|
|
1070
|
-
case "u":
|
|
1071
|
-
row = savedRow;
|
|
1072
|
-
col = savedCol;
|
|
1073
|
-
break;
|
|
1074
|
-
case "K":
|
|
1075
|
-
eraseLine(params[0] || 0);
|
|
1076
|
-
break;
|
|
1077
|
-
case "J":
|
|
1078
|
-
if ((params[0] || 0) === 2 || (params[0] || 0) === 3) rows.clear();
|
|
1221
|
+
const changesForCheck = { ...changes };
|
|
1222
|
+
delete changesForCheck.dependencies;
|
|
1223
|
+
const check = validateTaskEdit("planner", flattenTasks(tasks), target.id, changesForCheck, catalog);
|
|
1224
|
+
if (!check.ok) {
|
|
1225
|
+
errors.push(`${label}: ${check.error}`);
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1228
|
+
for (const field of check.clear ?? []) changes[field] = void 0;
|
|
1229
|
+
if ("dependencies" in changes) {
|
|
1230
|
+
const { ids, bad } = resolveDeps(changes.dependencies, i);
|
|
1231
|
+
if (bad.length) {
|
|
1232
|
+
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
1079
1233
|
break;
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1234
|
+
}
|
|
1235
|
+
changes.dependencies = ids.filter((id) => id !== target.id);
|
|
1236
|
+
}
|
|
1237
|
+
if (Object.keys(changes).length === 0) {
|
|
1238
|
+
errors.push(`${label}: no valid changes provided`);
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
tasks = updateTaskInPlan(tasks, target.id, changes);
|
|
1242
|
+
const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
|
|
1243
|
+
summary.push(`Updated "${target.title}" (${Object.keys(changes).join(", ")})${clearedNote}`);
|
|
1244
|
+
break;
|
|
1245
|
+
}
|
|
1246
|
+
case "add": {
|
|
1247
|
+
const spec = op.task ?? {};
|
|
1248
|
+
if (!spec.title) {
|
|
1249
|
+
errors.push(`${label}: new task needs a title`);
|
|
1250
|
+
break;
|
|
1251
|
+
}
|
|
1252
|
+
const { ids, bad } = resolveDeps(spec.dependencies, i);
|
|
1253
|
+
if (bad.length) {
|
|
1254
|
+
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
1255
|
+
break;
|
|
1256
|
+
}
|
|
1257
|
+
const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : runners[0];
|
|
1258
|
+
const validity = checkModelAndModeValidity(runner, spec.assignedModel, spec.taskMode, catalog);
|
|
1259
|
+
if (!validity.ok) {
|
|
1260
|
+
errors.push(`${label}: ${validity.error}`);
|
|
1261
|
+
break;
|
|
1262
|
+
}
|
|
1263
|
+
const newId = uuidv4();
|
|
1264
|
+
tasks = addTaskToPlan(tasks, {
|
|
1265
|
+
...spec,
|
|
1266
|
+
id: newId,
|
|
1267
|
+
dependencies: ids,
|
|
1268
|
+
assignedRunner: runner,
|
|
1269
|
+
description: spec.description ?? spec.title,
|
|
1270
|
+
prompt: spec.prompt ?? spec.description ?? spec.title
|
|
1271
|
+
});
|
|
1272
|
+
if (op.handle) handleId.set(op.handle, newId);
|
|
1273
|
+
summary.push(`Added "${spec.title}"`);
|
|
1274
|
+
break;
|
|
1275
|
+
}
|
|
1276
|
+
case "remove": {
|
|
1277
|
+
const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
1278
|
+
if ("error" in ref) {
|
|
1279
|
+
errors.push(`${label}: ${ref.error}`);
|
|
1280
|
+
break;
|
|
1281
|
+
}
|
|
1282
|
+
const target = findInTasks(tasks, ref.id);
|
|
1283
|
+
if (!target) {
|
|
1284
|
+
errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
|
|
1285
|
+
break;
|
|
1286
|
+
}
|
|
1287
|
+
if (target.status === "in_progress") {
|
|
1288
|
+
errors.push(`${label}: "${target.title}" is running and cannot be removed`);
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
if (target.status === "completed") {
|
|
1292
|
+
errors.push(`${label}: "${target.title}" is completed and cannot be removed`);
|
|
1293
|
+
break;
|
|
1294
|
+
}
|
|
1295
|
+
tasks = removeTaskFromPlan(tasks, target.id);
|
|
1296
|
+
summary.push(`Removed "${target.title}"`);
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
case "reorder": {
|
|
1300
|
+
const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
|
|
1301
|
+
const badRef = refs.find((r) => "error" in r);
|
|
1302
|
+
if (badRef) {
|
|
1303
|
+
errors.push(`${label}: ${badRef.error}`);
|
|
1304
|
+
break;
|
|
1305
|
+
}
|
|
1306
|
+
const resolved = refs.map((r) => findTopLevel(tasks, r.id));
|
|
1307
|
+
if (resolved.some((t) => !t)) {
|
|
1308
|
+
errors.push(`${label}: unknown task in reorder list`);
|
|
1309
|
+
break;
|
|
1310
|
+
}
|
|
1311
|
+
const orderIds = resolved.map((t) => t.id);
|
|
1312
|
+
if (new Set(orderIds).size !== tasks.length) {
|
|
1313
|
+
errors.push(`${label}: reorder must list every top-level task exactly once (${tasks.length} tasks)`);
|
|
1314
|
+
break;
|
|
1315
|
+
}
|
|
1316
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
1317
|
+
tasks = renumberTasks(orderIds.map((id) => byId.get(id)));
|
|
1318
|
+
summary.push("Reordered tasks");
|
|
1319
|
+
break;
|
|
1320
|
+
}
|
|
1321
|
+
case "merge": {
|
|
1322
|
+
const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
|
|
1323
|
+
const badRef = refs.find((r) => "error" in r);
|
|
1324
|
+
if (badRef) {
|
|
1325
|
+
errors.push(`${label}: ${badRef.error}`);
|
|
1326
|
+
break;
|
|
1327
|
+
}
|
|
1328
|
+
const resolved = refs.map((r) => findTopLevel(tasks, r.id));
|
|
1329
|
+
if (resolved.some((t) => !t)) {
|
|
1330
|
+
errors.push(`${label}: unknown task in merge list (already removed, merged, or split by an earlier op)`);
|
|
1331
|
+
break;
|
|
1332
|
+
}
|
|
1333
|
+
const toMerge = resolved.map((t) => t).sort((a, b) => a.order - b.order);
|
|
1334
|
+
if (toMerge.length < 2) {
|
|
1335
|
+
errors.push(`${label}: merge needs at least two tasks`);
|
|
1336
|
+
break;
|
|
1337
|
+
}
|
|
1338
|
+
const locked = toMerge.find((t) => t.status === "in_progress" || t.status === "completed");
|
|
1339
|
+
if (locked) {
|
|
1340
|
+
errors.push(`${label}: "${locked.title}" is ${locked.status === "in_progress" ? "running" : "completed"} and cannot be merged`);
|
|
1341
|
+
break;
|
|
1342
|
+
}
|
|
1343
|
+
const idSet = new Set(toMerge.map((t) => t.id));
|
|
1344
|
+
const unionDepsRaw = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));
|
|
1345
|
+
const { ids, bad } = resolveDeps(unionDepsRaw, i);
|
|
1346
|
+
if (bad.length) {
|
|
1347
|
+
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
const spec = op.merged ?? {};
|
|
1351
|
+
if (!spec.title) {
|
|
1352
|
+
errors.push(`${label}: merged task needs a title`);
|
|
1353
|
+
break;
|
|
1354
|
+
}
|
|
1355
|
+
const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : toMerge[0].assignedRunner;
|
|
1356
|
+
const mergedTask = createTask({
|
|
1357
|
+
title: spec.title,
|
|
1358
|
+
description: spec.description ?? toMerge.map((t) => t.description).filter(Boolean).join("\n\n"),
|
|
1359
|
+
prompt: spec.prompt ?? (toMerge.map((t) => t.prompt).filter(Boolean).join("\n\n---\n\n") || void 0),
|
|
1360
|
+
type: spec.type ?? (toMerge.some((t) => t.type === "user") ? "user" : "ai"),
|
|
1361
|
+
dependencies: ids,
|
|
1362
|
+
assignedRunner: runner,
|
|
1363
|
+
assignedModel: spec.assignedModel ?? toMerge.find((t) => t.assignedModel)?.assignedModel,
|
|
1364
|
+
thinkingEffort: spec.thinkingEffort ?? toMerge.find((t) => t.thinkingEffort)?.thinkingEffort,
|
|
1365
|
+
taskMode: spec.taskMode ?? toMerge[0].taskMode,
|
|
1366
|
+
autonomy: spec.autonomy ?? toMerge.find((t) => t.autonomy)?.autonomy,
|
|
1367
|
+
sliceType: spec.sliceType ?? toMerge.find((t) => t.sliceType)?.sliceType,
|
|
1368
|
+
userStoriesCovered: spec.userStoriesCovered ?? (toMerge.flatMap((t) => t.userStoriesCovered ?? []).length ? [...new Set(toMerge.flatMap((t) => t.userStoriesCovered ?? []))] : void 0)
|
|
1369
|
+
});
|
|
1370
|
+
const mergedId = mergedTask.id;
|
|
1371
|
+
const firstId = toMerge[0].id;
|
|
1372
|
+
const result = [];
|
|
1373
|
+
let inserted = false;
|
|
1374
|
+
for (const t of tasks) {
|
|
1375
|
+
if (idSet.has(t.id)) {
|
|
1376
|
+
if (!inserted && t.id === firstId) {
|
|
1377
|
+
result.push(mergedTask);
|
|
1378
|
+
inserted = true;
|
|
1379
|
+
}
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
result.push({ ...t, dependencies: t.dependencies.map((d) => idSet.has(d) ? mergedId : d) });
|
|
1383
|
+
}
|
|
1384
|
+
if (!inserted) result.push(mergedTask);
|
|
1385
|
+
tasks = renumberTasks(result);
|
|
1386
|
+
if (op.handle) handleId.set(op.handle, mergedId);
|
|
1387
|
+
summary.push(`Merged ${toMerge.length} tasks into "${spec.title}"`);
|
|
1388
|
+
break;
|
|
1389
|
+
}
|
|
1390
|
+
case "split": {
|
|
1391
|
+
const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, i);
|
|
1392
|
+
if ("error" in ref) {
|
|
1393
|
+
errors.push(`${label}: ${ref.error}`);
|
|
1394
|
+
break;
|
|
1395
|
+
}
|
|
1396
|
+
const target = findTopLevel(tasks, ref.id);
|
|
1397
|
+
if (!target) {
|
|
1398
|
+
errors.push(`${label}: task "${op.taskId}" not found`);
|
|
1399
|
+
break;
|
|
1400
|
+
}
|
|
1401
|
+
if (target.status === "in_progress") {
|
|
1402
|
+
errors.push(`${label}: "${target.title}" is running and cannot be split`);
|
|
1403
|
+
break;
|
|
1404
|
+
}
|
|
1405
|
+
if (target.status === "completed") {
|
|
1406
|
+
errors.push(`${label}: "${target.title}" is completed and cannot be split`);
|
|
1407
|
+
break;
|
|
1408
|
+
}
|
|
1409
|
+
const parts = op.parts ?? [];
|
|
1410
|
+
if (parts.length < 2) {
|
|
1411
|
+
errors.push(`${label}: split needs at least two parts`);
|
|
1412
|
+
break;
|
|
1413
|
+
}
|
|
1414
|
+
const newTasks = [];
|
|
1415
|
+
let prevId;
|
|
1416
|
+
let buildError = false;
|
|
1417
|
+
for (let p = 0; p < parts.length; p++) {
|
|
1418
|
+
const spec = parts[p];
|
|
1419
|
+
if (!spec || !spec.title) {
|
|
1420
|
+
errors.push(`${label}: split part ${p + 1} needs a title`);
|
|
1421
|
+
buildError = true;
|
|
1083
1422
|
break;
|
|
1084
1423
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1424
|
+
let deps;
|
|
1425
|
+
if (p === 0) {
|
|
1426
|
+
const { ids, bad } = resolveDeps(target.dependencies, i);
|
|
1427
|
+
if (bad.length) {
|
|
1428
|
+
errors.push(`${label}: split part 1 unknown dependencies: ${bad.join(", ")}`);
|
|
1429
|
+
buildError = true;
|
|
1430
|
+
break;
|
|
1431
|
+
}
|
|
1432
|
+
deps = ids;
|
|
1433
|
+
} else {
|
|
1434
|
+
deps = prevId ? [prevId] : [];
|
|
1435
|
+
}
|
|
1436
|
+
const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : target.assignedRunner;
|
|
1437
|
+
const nt = createTask({
|
|
1438
|
+
title: spec.title,
|
|
1439
|
+
description: spec.description ?? spec.title,
|
|
1440
|
+
prompt: spec.prompt ?? spec.description ?? spec.title,
|
|
1441
|
+
type: spec.type ?? target.type,
|
|
1442
|
+
dependencies: deps,
|
|
1443
|
+
assignedRunner: runner,
|
|
1444
|
+
assignedModel: spec.assignedModel ?? target.assignedModel,
|
|
1445
|
+
taskMode: spec.taskMode ?? target.taskMode,
|
|
1446
|
+
autonomy: spec.autonomy ?? target.autonomy,
|
|
1447
|
+
sliceType: spec.sliceType ?? target.sliceType,
|
|
1448
|
+
userStoriesCovered: spec.userStoriesCovered ?? target.userStoriesCovered
|
|
1449
|
+
});
|
|
1450
|
+
newTasks.push(nt);
|
|
1451
|
+
prevId = nt.id;
|
|
1452
|
+
}
|
|
1453
|
+
if (buildError) break;
|
|
1454
|
+
const tailId = newTasks[newTasks.length - 1].id;
|
|
1455
|
+
const result = [];
|
|
1456
|
+
for (const t of tasks) {
|
|
1457
|
+
if (t.id === target.id) {
|
|
1458
|
+
result.push(...newTasks);
|
|
1459
|
+
continue;
|
|
1088
1460
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1461
|
+
result.push({ ...t, dependencies: t.dependencies.map((d) => d === target.id ? tailId : d) });
|
|
1462
|
+
}
|
|
1463
|
+
tasks = renumberTasks(result);
|
|
1464
|
+
if (op.handle) handleId.set(op.handle, tailId);
|
|
1465
|
+
summary.push(`Split "${target.title}" into ${newTasks.length} tasks`);
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
case "rearm": {
|
|
1469
|
+
const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
1470
|
+
if ("error" in ref) {
|
|
1471
|
+
errors.push(`${label}: ${ref.error}`);
|
|
1472
|
+
break;
|
|
1473
|
+
}
|
|
1474
|
+
const target = findInTasks(tasks, ref.id);
|
|
1475
|
+
if (!target) {
|
|
1476
|
+
errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
|
|
1477
|
+
break;
|
|
1478
|
+
}
|
|
1479
|
+
if (target.status === "in_progress") {
|
|
1480
|
+
errors.push(`${label}: "${target.title}" is running and cannot be re-armed`);
|
|
1481
|
+
break;
|
|
1482
|
+
}
|
|
1483
|
+
const changes = {};
|
|
1484
|
+
for (const key of UPDATABLE_FIELDS) {
|
|
1485
|
+
if (op.changes && key in op.changes) changes[key] = op.changes[key];
|
|
1486
|
+
}
|
|
1487
|
+
if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
|
|
1488
|
+
errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
|
|
1489
|
+
break;
|
|
1490
|
+
}
|
|
1491
|
+
const changesForCheck = { ...changes };
|
|
1492
|
+
delete changesForCheck.dependencies;
|
|
1493
|
+
const check = validateTaskEdit("direct", flattenTasks(tasks), target.id, changesForCheck, catalog);
|
|
1494
|
+
if (!check.ok) {
|
|
1495
|
+
errors.push(`${label}: ${check.error}`);
|
|
1496
|
+
break;
|
|
1497
|
+
}
|
|
1498
|
+
for (const field of check.clear ?? []) changes[field] = void 0;
|
|
1499
|
+
if ("dependencies" in changes) {
|
|
1500
|
+
const { ids, bad } = resolveDeps(changes.dependencies, i);
|
|
1501
|
+
if (bad.length) {
|
|
1502
|
+
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
1091
1503
|
break;
|
|
1092
1504
|
}
|
|
1093
|
-
|
|
1094
|
-
break;
|
|
1505
|
+
changes.dependencies = ids.filter((id) => id !== target.id);
|
|
1095
1506
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1507
|
+
tasks = updateTaskInPlan(tasks, target.id, { ...changes, status: "pending", verdict: void 0, outputSummary: void 0, awaitingReason: void 0 });
|
|
1508
|
+
for (const dep of flattenTasks(tasks)) {
|
|
1509
|
+
if (dep.status === "blocked" && dep.dependencies.includes(target.id)) {
|
|
1510
|
+
tasks = updateTaskInPlan(tasks, dep.id, { status: "pending" });
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
|
|
1514
|
+
const changedNote = Object.keys(changes).length ? ` (${Object.keys(changes).join(", ")})` : "";
|
|
1515
|
+
summary.push(`Re-armed "${target.title}"${changedNote}${clearedNote}`);
|
|
1516
|
+
break;
|
|
1098
1517
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1518
|
+
default:
|
|
1519
|
+
errors.push(`${label}: unknown op "${op.op}"`);
|
|
1520
|
+
}
|
|
1521
|
+
if (errors.length) break;
|
|
1522
|
+
}
|
|
1523
|
+
if (!errors.length) {
|
|
1524
|
+
const flat = flattenTasks(tasks);
|
|
1525
|
+
const idSet = new Set(flat.map((t) => t.id));
|
|
1526
|
+
for (const t of flat) {
|
|
1527
|
+
for (const dep of t.dependencies) {
|
|
1528
|
+
if (!idSet.has(dep)) errors.push(`"${t.title}" depends on a task that no longer exists (${dep})`);
|
|
1105
1529
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1530
|
+
}
|
|
1531
|
+
const cycleAt = detectCycle(flat);
|
|
1532
|
+
if (cycleAt) errors.push(`Dependency cycle detected involving "${cycleAt}"`);
|
|
1533
|
+
if (!errors.length) {
|
|
1534
|
+
const repaired = repairOrder(tasks);
|
|
1535
|
+
if ("error" in repaired) {
|
|
1536
|
+
errors.push(repaired.error);
|
|
1537
|
+
} else {
|
|
1538
|
+
const moves = describeMoves(tasks, repaired.tasks);
|
|
1539
|
+
if (moves) summary.push(`Reordered to keep dependencies first: ${moves}`);
|
|
1540
|
+
tasks = renumberTasks(repaired.tasks);
|
|
1116
1541
|
}
|
|
1117
|
-
i += kind === "(" || kind === ")" ? 3 : 2;
|
|
1118
|
-
continue;
|
|
1119
1542
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1543
|
+
}
|
|
1544
|
+
return errors.length ? { ok: false, tasks: currentTasks, errors, summary: [] } : { ok: true, tasks, errors: [], summary };
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// src/services/researchStepSummary.ts
|
|
1548
|
+
function ellipsize(text2, max) {
|
|
1549
|
+
const oneLine2 = text2.replace(/\s+/g, " ").trim();
|
|
1550
|
+
return oneLine2.length > max ? `${oneLine2.slice(0, max)}\u2026` : oneLine2;
|
|
1551
|
+
}
|
|
1552
|
+
function summarizeToolCall(tool, argsJson, toolLabel) {
|
|
1553
|
+
const display = toolLabel?.trim() || tool;
|
|
1554
|
+
if (tool === "agent_tool") {
|
|
1555
|
+
try {
|
|
1556
|
+
const args = JSON.parse(argsJson);
|
|
1557
|
+
const hint = args.path ?? args.file_path ?? args.filePath ?? args.pattern ?? args.query ?? args.url ?? args.command;
|
|
1558
|
+
if (hint) return `${display} ${ellipsize(String(hint), 50)}`;
|
|
1559
|
+
} catch {
|
|
1124
1560
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1561
|
+
return display;
|
|
1562
|
+
}
|
|
1563
|
+
if (tool === "spawn_research_agent") {
|
|
1564
|
+
try {
|
|
1565
|
+
const args = JSON.parse(argsJson);
|
|
1566
|
+
const prompt = typeof args.prompt === "string" ? args.prompt : "";
|
|
1567
|
+
const oneLine2 = prompt.replace(/\s+/g, " ").trim();
|
|
1568
|
+
return `spawn: "${oneLine2.slice(0, 60)}${oneLine2.length > 60 ? "\u2026" : ""}"`;
|
|
1569
|
+
} catch {
|
|
1570
|
+
return "spawn_research_agent";
|
|
1129
1571
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1572
|
+
}
|
|
1573
|
+
try {
|
|
1574
|
+
const args = JSON.parse(argsJson);
|
|
1575
|
+
if (tool === "find_symbol" && args.symbol) {
|
|
1576
|
+
return `find_symbol ${args.symbol}${args.language ? ` [${args.language}]` : ""}`;
|
|
1134
1577
|
}
|
|
1135
|
-
if (
|
|
1136
|
-
|
|
1137
|
-
i++;
|
|
1138
|
-
continue;
|
|
1578
|
+
if (tool === "web_search" && args.query) {
|
|
1579
|
+
return `web_search "${ellipsize(String(args.query), 50)}"`;
|
|
1139
1580
|
}
|
|
1140
|
-
if (
|
|
1141
|
-
|
|
1142
|
-
continue;
|
|
1581
|
+
if (tool === "fetch" && args.url) {
|
|
1582
|
+
return `fetch ${ellipsize(String(args.url), 60)}`;
|
|
1143
1583
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
if (
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
// src/conversation/format.ts
|
|
1166
|
-
var DISPLAY_NAMES = {
|
|
1167
|
-
read_file: "Read",
|
|
1168
|
-
read_files: "Read",
|
|
1169
|
-
list_dir: "List",
|
|
1170
|
-
glob: "Glob",
|
|
1171
|
-
grep: "Grep",
|
|
1172
|
-
find_symbol: "Symbol",
|
|
1173
|
-
bash: "Bash",
|
|
1174
|
-
fetch: "Fetch",
|
|
1175
|
-
web_search: "WebSearch",
|
|
1176
|
-
spawn_research_agent: "Agent"
|
|
1177
|
-
};
|
|
1178
|
-
var HINT_FIELDS = ["file_path", "filePath", "path", "url", "pattern", "query", "command", "description", "prompt"];
|
|
1179
|
-
function parseArgs(args) {
|
|
1180
|
-
try {
|
|
1181
|
-
const parsed = JSON.parse(args);
|
|
1182
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
1183
|
-
} catch {
|
|
1184
|
-
return null;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
function oneLine(text2) {
|
|
1188
|
-
return text2.replace(/\s+/g, " ").trim();
|
|
1189
|
-
}
|
|
1190
|
-
function firstLineOf(command) {
|
|
1191
|
-
const lines = command.trim().split(/\r?\n/);
|
|
1192
|
-
const more = lines.length - 1;
|
|
1193
|
-
return more > 0 ? `${lines[0]} \u2026 +${more} line${more === 1 ? "" : "s"}` : lines[0];
|
|
1194
|
-
}
|
|
1195
|
-
function text(value) {
|
|
1196
|
-
return typeof value === "string" ? value : "";
|
|
1197
|
-
}
|
|
1198
|
-
function searchArg(pattern, path, include = "") {
|
|
1199
|
-
return `${pattern}${path ? ` in ${path}` : ""}${include ? ` (${include})` : ""}`;
|
|
1200
|
-
}
|
|
1201
|
-
function hintArg(args) {
|
|
1202
|
-
for (const field of HINT_FIELDS) {
|
|
1203
|
-
const value = text(args[field]);
|
|
1204
|
-
if (value) return field === "command" ? firstLineOf(value) : oneLine(value);
|
|
1584
|
+
if (tool === "glob" && args.pattern) {
|
|
1585
|
+
return `glob ${ellipsize(String(args.pattern), 50)}`;
|
|
1586
|
+
}
|
|
1587
|
+
if (tool === "grep" && args.pattern) {
|
|
1588
|
+
const mode = args.output_mode && args.output_mode !== "content" ? ` (${args.output_mode})` : "";
|
|
1589
|
+
const scope = args.include ? ` in ${args.include}` : "";
|
|
1590
|
+
return `grep ${ellipsize(String(args.pattern), 40)}${scope}${mode}`;
|
|
1591
|
+
}
|
|
1592
|
+
const path = args.filePath || args.file_path || args.path;
|
|
1593
|
+
if (path) {
|
|
1594
|
+
const basename = String(path).split("/").pop() || String(path);
|
|
1595
|
+
return `${display} ${basename}`;
|
|
1596
|
+
}
|
|
1597
|
+
if (args.pattern) {
|
|
1598
|
+
return `${display} ${args.pattern}`;
|
|
1599
|
+
}
|
|
1600
|
+
if (args.command) {
|
|
1601
|
+
return `bash: ${String(args.command).slice(0, 40)}`;
|
|
1602
|
+
}
|
|
1603
|
+
} catch {
|
|
1205
1604
|
}
|
|
1206
|
-
|
|
1207
|
-
return firstText ? oneLine(firstText) : "";
|
|
1605
|
+
return display;
|
|
1208
1606
|
}
|
|
1209
|
-
function
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
case "list_dir":
|
|
1215
|
-
return text(args.path);
|
|
1216
|
-
case "read_files":
|
|
1217
|
-
return Array.isArray(args.paths) ? args.paths.map(String).join(", ") : "";
|
|
1218
|
-
case "glob":
|
|
1219
|
-
return searchArg(text(args.pattern), text(args.path));
|
|
1220
|
-
case "grep":
|
|
1221
|
-
return searchArg(text(args.pattern), text(args.path), text(args.include));
|
|
1222
|
-
case "find_symbol":
|
|
1223
|
-
return `${text(args.symbol)}${text(args.language) ? ` [${text(args.language)}]` : ""}`;
|
|
1224
|
-
case "fetch":
|
|
1225
|
-
return text(args.url);
|
|
1226
|
-
case "web_search":
|
|
1227
|
-
return oneLine(text(args.query));
|
|
1228
|
-
case "spawn_research_agent":
|
|
1229
|
-
return oneLine(text(args.prompt));
|
|
1230
|
-
default:
|
|
1231
|
-
return hintArg(args);
|
|
1607
|
+
function classifyOutcome(success, output) {
|
|
1608
|
+
if (!success) {
|
|
1609
|
+
if (/^Command refused:/.test(output) || /is not available\. You are a read-only research subagent/.test(output)) return "refused";
|
|
1610
|
+
if (/not approved/i.test(output) || /^Access denied:/.test(output) || /needs user approval/i.test(output)) return "denied";
|
|
1611
|
+
return "failure";
|
|
1232
1612
|
}
|
|
1613
|
+
return "success";
|
|
1233
1614
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
return
|
|
1615
|
+
|
|
1616
|
+
// src/services/continuation.ts
|
|
1617
|
+
function continuability(task) {
|
|
1618
|
+
const refuse = (reason) => ({ ok: false, reason: `Task "${task.title}" cannot be continued: ${reason}` });
|
|
1619
|
+
if (task.type !== "ai") return refuse("it is not run by an agent.");
|
|
1620
|
+
if (task.status === "awaiting_user" && task.awaitingReason === "conflict") {
|
|
1621
|
+
return refuse("its work is waiting on a merge conflict. Resolve or repair the conflict instead.");
|
|
1622
|
+
}
|
|
1623
|
+
if (task.status !== "completed" && task.status !== "failed") return refuse("only a completed or failed task can be.");
|
|
1624
|
+
if (task.transport?.kind !== "structured") return refuse("it ran in a terminal, so there is no saved session to resume. Use Retry instead.");
|
|
1625
|
+
if (!task.transport.nativeSessionId) return refuse("its runner left no saved session to resume. Use Retry instead.");
|
|
1626
|
+
return { ok: true, sessionId: task.transport.nativeSessionId };
|
|
1243
1627
|
}
|
|
1244
|
-
function
|
|
1245
|
-
|
|
1246
|
-
const shown = lines.slice(0, Math.max(0, maxLines));
|
|
1247
|
-
return { lines: shown, hiddenLineCount: lines.length - shown.length };
|
|
1628
|
+
function canContinue(task) {
|
|
1629
|
+
return continuability(task).ok;
|
|
1248
1630
|
}
|
|
1249
1631
|
|
|
1250
|
-
// src/
|
|
1251
|
-
function
|
|
1252
|
-
return { type: "message", id, role, text: text2, streaming: false, ...turnId ? { turnId } : {} };
|
|
1253
|
-
}
|
|
1254
|
-
function subagentBlock(id, { toolCallId, model, usage, turnId, ...fields }) {
|
|
1632
|
+
// src/services/SessionMessage.ts
|
|
1633
|
+
function serializeTask(t) {
|
|
1255
1634
|
return {
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1635
|
+
id: t.id,
|
|
1636
|
+
order: t.order,
|
|
1637
|
+
title: t.title,
|
|
1638
|
+
type: t.type,
|
|
1639
|
+
description: t.description,
|
|
1640
|
+
dependencies: t.dependencies || [],
|
|
1641
|
+
assignedRunner: t.assignedRunner,
|
|
1642
|
+
assignedModel: t.assignedModel || null,
|
|
1643
|
+
taskMode: t.taskMode || "build",
|
|
1644
|
+
prompt: t.prompt || null,
|
|
1645
|
+
subtasks: (t.subtasks || []).map(serializeTask),
|
|
1646
|
+
userSteps: t.userSteps || void 0,
|
|
1647
|
+
thinkingEffort: t.thinkingEffort || void 0,
|
|
1648
|
+
autonomy: t.autonomy || void 0,
|
|
1649
|
+
sliceType: t.sliceType || void 0,
|
|
1650
|
+
userStoriesCovered: t.userStoriesCovered || void 0
|
|
1263
1651
|
};
|
|
1264
1652
|
}
|
|
1265
|
-
function
|
|
1266
|
-
return {
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1653
|
+
function serializeTaskStatus(t, idleSince = null, isolation = null, queued = [], awaitingApproval = 0) {
|
|
1654
|
+
return {
|
|
1655
|
+
id: t.id,
|
|
1656
|
+
status: t.status,
|
|
1657
|
+
verdict: t.verdict ? { outcome: t.verdict.outcome, reason: t.verdict.reason, checks: t.verdict.checks || [] } : null,
|
|
1658
|
+
idleSince,
|
|
1659
|
+
...isolation ? { isolation } : {},
|
|
1660
|
+
...t.transport ? { transport: t.transport.fallback ? { kind: t.transport.kind, fallback: t.transport.fallback } : { kind: t.transport.kind } } : {},
|
|
1661
|
+
...t.status === "awaiting_user" && t.awaitingReason ? { awaitingReason: t.awaitingReason } : {},
|
|
1662
|
+
...queued.length > 0 ? { queued: queued.map((m) => ({ ...m })) } : {},
|
|
1663
|
+
...canContinue(t) ? { continuable: true } : {},
|
|
1664
|
+
...awaitingApproval > 0 ? { awaitingApproval } : {}
|
|
1665
|
+
};
|
|
1277
1666
|
}
|
|
1278
|
-
function
|
|
1667
|
+
function serializePlan(plan) {
|
|
1279
1668
|
return {
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
output: "",
|
|
1287
|
-
outputLineCount: 0,
|
|
1288
|
-
...toolCallId ? { toolCallId } : {},
|
|
1289
|
-
...toolLabel ? { toolLabel } : {},
|
|
1290
|
-
...turnId ? { turnId } : {}
|
|
1669
|
+
tasks: plan.tasks.map(serializeTask),
|
|
1670
|
+
runners: plan.runners,
|
|
1671
|
+
generatedAt: plan.generatedAt,
|
|
1672
|
+
conversationHistory: plan.conversationHistory,
|
|
1673
|
+
prdMarkdown: plan.prdMarkdown,
|
|
1674
|
+
queuedMessages: plan.queuedMessages
|
|
1291
1675
|
};
|
|
1292
1676
|
}
|
|
1293
|
-
function
|
|
1294
|
-
return
|
|
1677
|
+
function executionSummary(tasks) {
|
|
1678
|
+
return {
|
|
1679
|
+
total: tasks.length,
|
|
1680
|
+
completed: tasks.filter((t) => t.status === "completed").length,
|
|
1681
|
+
failed: tasks.filter((t) => t.status === "failed").length
|
|
1682
|
+
};
|
|
1295
1683
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1684
|
+
var CHECKPOINT_TRUNCATE_LENGTH = 120;
|
|
1685
|
+
function truncateCheckpointSummary(summary) {
|
|
1686
|
+
const firstLine = summary.split("\n")[0].trim();
|
|
1687
|
+
if (firstLine.length <= CHECKPOINT_TRUNCATE_LENGTH) return firstLine;
|
|
1688
|
+
return firstLine.slice(0, CHECKPOINT_TRUNCATE_LENGTH - 1) + "\u2026";
|
|
1299
1689
|
}
|
|
1300
1690
|
|
|
1301
1691
|
// src/conversation/reduce.ts
|
|
1302
1692
|
var EMPTY_CONVERSATION = { blocks: [], nextId: 1 };
|
|
1303
|
-
function findLastIndex(items, match) {
|
|
1304
|
-
for (let i = items.length - 1; i >= 0; i--) if (match(items[i])) return i;
|
|
1305
|
-
return -1;
|
|
1306
|
-
}
|
|
1307
|
-
function replaceAt(items, index, item) {
|
|
1308
|
-
const next = items.slice();
|
|
1309
|
-
next[index] = item;
|
|
1310
|
-
return next;
|
|
1311
|
-
}
|
|
1312
|
-
function insertTop(blocks, block) {
|
|
1313
|
-
const last = blocks[blocks.length - 1];
|
|
1314
|
-
return last?.type === "usage" ? [...blocks.slice(0, -1), block, last] : [...blocks, block];
|
|
1315
|
-
}
|
|
1316
|
-
function append(view, make) {
|
|
1317
|
-
return { ...view, blocks: insertTop(view.blocks, make(`b${view.nextId}`)), nextId: view.nextId + 1 };
|
|
1318
|
-
}
|
|
1319
|
-
function subagentAt(view, lane) {
|
|
1320
|
-
const block = view.blocks[lane];
|
|
1321
|
-
if (block.type !== "subagent") throw new Error(`block ${lane} is not a subagent`);
|
|
1322
|
-
return block;
|
|
1323
|
-
}
|
|
1324
|
-
function laneBlocks(view, lane) {
|
|
1325
|
-
return lane === null ? view.blocks : subagentAt(view, lane).children;
|
|
1326
|
-
}
|
|
1327
|
-
function setChildren(view, lane, children) {
|
|
1328
|
-
return { ...view, blocks: replaceAt(view.blocks, lane, { ...subagentAt(view, lane), children }) };
|
|
1329
|
-
}
|
|
1330
|
-
function laneReplace(view, lane, index, block) {
|
|
1331
|
-
if (lane === null) return { ...view, blocks: replaceAt(view.blocks, index, block) };
|
|
1332
|
-
return setChildren(view, lane, replaceAt(subagentAt(view, lane).children, index, block));
|
|
1333
|
-
}
|
|
1334
|
-
function laneAppend(view, lane, make) {
|
|
1335
|
-
if (lane === null) return append(view, make);
|
|
1336
|
-
const children = [...subagentAt(view, lane).children, make(`b${view.nextId}`)];
|
|
1337
|
-
return { ...setChildren(view, lane, children), nextId: view.nextId + 1 };
|
|
1338
|
-
}
|
|
1339
|
-
function isStreaming(block) {
|
|
1340
|
-
return (block.type === "message" || block.type === "thinking") && block.streaming;
|
|
1341
|
-
}
|
|
1342
|
-
function sealed(block) {
|
|
1343
|
-
return isStreaming(block) ? { ...block, streaming: false } : block;
|
|
1344
|
-
}
|
|
1345
|
-
function sealLane(view, lane, keep) {
|
|
1346
|
-
const blocks = laneBlocks(view, lane);
|
|
1347
|
-
if (!blocks.some((b, i) => i !== keep && isStreaming(b))) return view;
|
|
1348
|
-
if (lane === null) return { ...view, blocks: view.blocks.map((b, i) => i === keep ? b : sealed(b)) };
|
|
1349
|
-
return setChildren(view, lane, subagentAt(view, lane).children.map((b, i) => i === keep ? b : sealed(b)));
|
|
1350
|
-
}
|
|
1351
|
-
function appendOutput(view, lane, make) {
|
|
1352
|
-
const next = laneAppend(view, lane, make);
|
|
1353
|
-
return sealLane(next, lane, findLastIndex(laneBlocks(next, lane), (b) => b.type !== "usage"));
|
|
1354
|
-
}
|
|
1355
1693
|
function isUnsettledSegment(block, turnId) {
|
|
1356
1694
|
return block.type === "message" && block.turnId === turnId && block.segmentId !== void 0;
|
|
1357
1695
|
}
|
|
@@ -1400,34 +1738,10 @@ function retractText(view, { turnId, segmentId }) {
|
|
|
1400
1738
|
const blocks = view.blocks.filter((b) => !(isUnsettledSegment(b, turnId) && (segmentId === void 0 || b.segmentId === segmentId)));
|
|
1401
1739
|
return dropBuildingPlan(blocks.length === view.blocks.length ? view : { ...view, blocks }, turnId, segmentId);
|
|
1402
1740
|
}
|
|
1403
|
-
function
|
|
1404
|
-
const existing = findLastIndex(view.blocks, (b) => b.type === "subagent" && b.subagentId === subagentId);
|
|
1405
|
-
if (existing >= 0) return [view, existing];
|
|
1406
|
-
const spawn = findLastIndex(view.blocks, (b) => b.type === "tool" && (b.spawns === subagentId || b.toolCallId === subagentId));
|
|
1407
|
-
const call = view.blocks[spawn];
|
|
1408
|
-
if (call?.type === "tool") {
|
|
1409
|
-
const block = subagentBlock(call.id, {
|
|
1410
|
-
subagentId,
|
|
1411
|
-
brief: call.headline.keyArg,
|
|
1412
|
-
status: "running",
|
|
1413
|
-
children: [],
|
|
1414
|
-
digest: "",
|
|
1415
|
-
toolCallId: call.toolCallId,
|
|
1416
|
-
turnId: call.turnId
|
|
1417
|
-
});
|
|
1418
|
-
return [{ ...view, blocks: replaceAt(view.blocks, spawn, block) }, spawn];
|
|
1419
|
-
}
|
|
1420
|
-
const next = append(view, (id) => subagentBlock(id, { subagentId, brief: "", status: "running", children: [], digest: "", turnId }));
|
|
1421
|
-
return [next, findLastIndex(next.blocks, (b) => b.type === "subagent" && b.subagentId === subagentId)];
|
|
1422
|
-
}
|
|
1423
|
-
function updateSubagent(view, subagentId, turnId, update) {
|
|
1424
|
-
const [next, lane] = subagentLane(view, subagentId, turnId);
|
|
1425
|
-
return { ...next, blocks: replaceAt(next.blocks, lane, update(subagentAt(next, lane))) };
|
|
1426
|
-
}
|
|
1427
|
-
function startSubagent(view, { subagentId, brief, model, turnId }) {
|
|
1741
|
+
function startSubagent2(view, { subagentId, brief, model, turnId }) {
|
|
1428
1742
|
return updateSubagent(view, subagentId, turnId, (block) => ({ ...block, ...brief ? { brief } : {}, ...model ? { model } : {} }));
|
|
1429
1743
|
}
|
|
1430
|
-
function
|
|
1744
|
+
function finishSubagent2(view, { subagentId, outcome, digest, usage, turnId }) {
|
|
1431
1745
|
return updateSubagent(view, subagentId, turnId, (block) => ({
|
|
1432
1746
|
...block,
|
|
1433
1747
|
status: outcome,
|
|
@@ -1436,35 +1750,17 @@ function finishSubagent(view, { subagentId, outcome, digest, usage, turnId }) {
|
|
|
1436
1750
|
...usage ? { usage } : {}
|
|
1437
1751
|
}));
|
|
1438
1752
|
}
|
|
1439
|
-
function
|
|
1440
|
-
const [placed, lane] = subagentId ? subagentLane(view, subagentId, turnId) : [view, null];
|
|
1441
|
-
const blocks = laneBlocks(placed, lane);
|
|
1442
|
-
const lastIndex = findLastIndex(blocks, (b) => b.type !== "usage");
|
|
1443
|
-
const last = blocks[lastIndex];
|
|
1444
|
-
if (last?.type === "thinking" && last.streaming && last.segmentId === segmentId) {
|
|
1445
|
-
return laneReplace(placed, lane, lastIndex, { ...last, text: last.text + text2 });
|
|
1446
|
-
}
|
|
1447
|
-
return appendOutput(placed, lane, (id) => ({
|
|
1448
|
-
type: "thinking",
|
|
1449
|
-
id,
|
|
1450
|
-
text: text2,
|
|
1451
|
-
streaming: true,
|
|
1452
|
-
...turnId ? { turnId } : {},
|
|
1453
|
-
...segmentId ? { segmentId } : {},
|
|
1454
|
-
...subagentId ? { subagentId } : {}
|
|
1455
|
-
}));
|
|
1456
|
-
}
|
|
1457
|
-
function announceTool(view, call) {
|
|
1753
|
+
function announceTool2(view, call) {
|
|
1458
1754
|
const spawns = call.tool === "spawn_research_agent" ? call.subagentId : void 0;
|
|
1459
|
-
const [placed, lane] =
|
|
1755
|
+
const [placed, lane] = laneOf(view, spawns ? void 0 : call.subagentId, call.turnId);
|
|
1460
1756
|
return appendOutput(placed, lane, (id) => ({ ...pendingTool(id, call), ...spawns ? { spawns } : {} }));
|
|
1461
1757
|
}
|
|
1462
1758
|
function isPendingCallOf(block, step) {
|
|
1463
1759
|
if (block.type !== "tool" || block.status !== "pending") return false;
|
|
1464
1760
|
return step.toolCallId ? block.toolCallId === step.toolCallId : block.toolCallId === void 0 && block.tool === step.tool;
|
|
1465
1761
|
}
|
|
1466
|
-
function
|
|
1467
|
-
const [placed, lane] =
|
|
1762
|
+
function settleTool2(view, { step, turnId }) {
|
|
1763
|
+
const [placed, lane] = laneOf(view, step.subagentId, turnId);
|
|
1468
1764
|
const blocks = laneBlocks(placed, lane);
|
|
1469
1765
|
const i = findLastIndex(blocks, (b) => isPendingCallOf(b, step));
|
|
1470
1766
|
const pending = blocks[i];
|
|
@@ -1497,7 +1793,7 @@ function markPlan(view, content, turnId) {
|
|
|
1497
1793
|
}
|
|
1498
1794
|
return append(view, (id) => ({ type: "plan", id, text: "", ...marker }));
|
|
1499
1795
|
}
|
|
1500
|
-
function
|
|
1796
|
+
function requestApproval2(view, { id: approvalId, kind, subject, scope, detail, turnId }) {
|
|
1501
1797
|
return append(view, (id) => ({
|
|
1502
1798
|
type: "approval",
|
|
1503
1799
|
id,
|
|
@@ -1510,13 +1806,13 @@ function requestApproval(view, { id: approvalId, kind, subject, scope, detail, t
|
|
|
1510
1806
|
...turnId ? { turnId } : {}
|
|
1511
1807
|
}));
|
|
1512
1808
|
}
|
|
1513
|
-
function
|
|
1809
|
+
function settleApproval2(view, { id, granted }) {
|
|
1514
1810
|
const i = findLastIndex(view.blocks, (b) => b.type === "approval" && b.approvalId === id);
|
|
1515
1811
|
const request = view.blocks[i];
|
|
1516
1812
|
if (request?.type !== "approval") return view;
|
|
1517
1813
|
return { ...view, blocks: replaceAt(view.blocks, i, { ...request, status: granted ? "granted" : "denied", decidedBy: "asked" }) };
|
|
1518
1814
|
}
|
|
1519
|
-
function
|
|
1815
|
+
function decideApproval2(view, { kind, subject, scope, detail, granted, source }) {
|
|
1520
1816
|
return append(view, (id) => ({
|
|
1521
1817
|
type: "approval",
|
|
1522
1818
|
id,
|
|
@@ -1528,28 +1824,14 @@ function decideApproval(view, { kind, subject, scope, detail, granted, source })
|
|
|
1528
1824
|
...detail ? { detail } : {}
|
|
1529
1825
|
}));
|
|
1530
1826
|
}
|
|
1531
|
-
function
|
|
1532
|
-
|
|
1533
|
-
const last = view.blocks[view.blocks.length - 1];
|
|
1534
|
-
const line = usageBlock(last?.type === "usage" ? last.id : `b${view.nextId}`, message);
|
|
1535
|
-
if (last?.type === "usage") return { ...view, blocks: replaceAt(view.blocks, view.blocks.length - 1, line) };
|
|
1536
|
-
return { ...view, blocks: [...view.blocks, line], nextId: view.nextId + 1 };
|
|
1537
|
-
}
|
|
1538
|
-
function closeBlock(block, interrupted) {
|
|
1539
|
-
return interrupted && block.type === "tool" && block.status === "pending" ? { ...block, status: "interrupted" } : sealed(block);
|
|
1540
|
-
}
|
|
1541
|
-
function closeSubagent(block, cut) {
|
|
1542
|
-
const children = block.children.map((c) => closeBlock(c, cut !== null));
|
|
1543
|
-
const status = cut && block.status === "running" ? cut : block.status;
|
|
1544
|
-
return status === block.status && children.every((c, i) => c === block.children[i]) ? block : { ...block, status, children };
|
|
1827
|
+
function reportUsage2(view, message) {
|
|
1828
|
+
return isMeasured(message.totals) ? setUsageLine(view, message) : view;
|
|
1545
1829
|
}
|
|
1546
|
-
function
|
|
1830
|
+
function endTurn2(view, { turnId, outcome }) {
|
|
1547
1831
|
let next = dropBuildingPlan(view, turnId);
|
|
1548
1832
|
const planText = outcome === "plan" ? finalSegmentIndex(next.blocks, turnId, true) : -1;
|
|
1549
1833
|
if (planText >= 0) next = { ...next, blocks: next.blocks.filter((_, i) => i !== planText) };
|
|
1550
|
-
|
|
1551
|
-
const blocks = next.blocks.map((b) => b.type === "subagent" ? closeSubagent(b, cut) : closeBlock(b, cut !== null));
|
|
1552
|
-
return blocks.every((b, i) => b === next.blocks[i]) ? next : { ...next, blocks };
|
|
1834
|
+
return closeOpenBlocks(next, outcome === "stopped" ? "stopped" : outcome === "error" ? "failed" : null);
|
|
1553
1835
|
}
|
|
1554
1836
|
function markCompaction(view, summary) {
|
|
1555
1837
|
const i = findLastIndex(view.blocks, (b) => b.type === "message" && b.role === "planner" && b.turnId === void 0 && b.text === summary);
|
|
@@ -1586,27 +1868,27 @@ function reduceConversation(view, input) {
|
|
|
1586
1868
|
case "planner_thinking_delta":
|
|
1587
1869
|
return think(view, input.text, input.turnId, input.segmentId, input.subagentId);
|
|
1588
1870
|
case "research_step":
|
|
1589
|
-
return
|
|
1871
|
+
return announceTool2(view, input);
|
|
1590
1872
|
case "research_step_done":
|
|
1591
|
-
return
|
|
1873
|
+
return settleTool2(view, input);
|
|
1592
1874
|
case "subagent_started":
|
|
1593
|
-
return
|
|
1875
|
+
return startSubagent2(view, input);
|
|
1594
1876
|
case "subagent_finished":
|
|
1595
|
-
return
|
|
1877
|
+
return finishSubagent2(view, input);
|
|
1596
1878
|
case "plan_token":
|
|
1597
1879
|
return streamPlan(view, input);
|
|
1598
1880
|
case "plan_generated":
|
|
1599
1881
|
return syncTranscript(view, input);
|
|
1600
1882
|
case "planner_turn_ended":
|
|
1601
|
-
return
|
|
1883
|
+
return endTurn2(view, input);
|
|
1602
1884
|
case "approval_request":
|
|
1603
|
-
return
|
|
1885
|
+
return requestApproval2(view, input);
|
|
1604
1886
|
case "approval_settled":
|
|
1605
|
-
return
|
|
1887
|
+
return settleApproval2(view, input);
|
|
1606
1888
|
case "approval_decided":
|
|
1607
|
-
return
|
|
1889
|
+
return decideApproval2(view, input);
|
|
1608
1890
|
case "planner_usage":
|
|
1609
|
-
return
|
|
1891
|
+
return reportUsage2(view, input);
|
|
1610
1892
|
default:
|
|
1611
1893
|
return view;
|
|
1612
1894
|
}
|
|
@@ -1776,6 +2058,19 @@ export {
|
|
|
1776
2058
|
isMeasured,
|
|
1777
2059
|
usageLine,
|
|
1778
2060
|
plannerContextFill,
|
|
2061
|
+
mapAgentTool,
|
|
2062
|
+
normalizeAgentArgs,
|
|
2063
|
+
flattenTerminalOutput,
|
|
2064
|
+
renderTerminalOutput,
|
|
2065
|
+
renderCleanCapture,
|
|
2066
|
+
toolHeadline,
|
|
2067
|
+
outputLines,
|
|
2068
|
+
outputPreview,
|
|
2069
|
+
EMPTY_TASK_LOG,
|
|
2070
|
+
runnerToolSubject,
|
|
2071
|
+
reduceTaskLog,
|
|
2072
|
+
replayTaskLog,
|
|
2073
|
+
capConflictFiles,
|
|
1779
2074
|
effectiveAllowlist,
|
|
1780
2075
|
filterModelsForPrompt,
|
|
1781
2076
|
clampThinkingEffort,
|
|
@@ -1794,18 +2089,14 @@ export {
|
|
|
1794
2089
|
applyTaskOps,
|
|
1795
2090
|
summarizeToolCall,
|
|
1796
2091
|
classifyOutcome,
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
renderCleanCapture,
|
|
2092
|
+
continuability,
|
|
2093
|
+
canContinue,
|
|
1800
2094
|
serializeTask,
|
|
1801
2095
|
serializeTaskStatus,
|
|
1802
2096
|
serializePlan,
|
|
1803
2097
|
executionSummary,
|
|
1804
2098
|
CHECKPOINT_TRUNCATE_LENGTH,
|
|
1805
2099
|
truncateCheckpointSummary,
|
|
1806
|
-
toolHeadline,
|
|
1807
|
-
outputLines,
|
|
1808
|
-
outputPreview,
|
|
1809
2100
|
EMPTY_CONVERSATION,
|
|
1810
2101
|
reduceConversation,
|
|
1811
2102
|
fromTranscript,
|
|
@@ -1821,4 +2112,4 @@ export {
|
|
|
1821
2112
|
hasHiddenDetail,
|
|
1822
2113
|
taskStartedNotice
|
|
1823
2114
|
};
|
|
1824
|
-
//# sourceMappingURL=chunk-
|
|
2115
|
+
//# sourceMappingURL=chunk-ROVYWEBI.mjs.map
|