@ordewell/core 0.5.4 → 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.
Files changed (56) hide show
  1. package/dist/IFileSystem-BkPX7mLD.d.mts +76 -0
  2. package/dist/IFileSystem-C0l-4MGT.d.ts +76 -0
  3. package/dist/{ModeResolver-Dkig8ghQ.d.ts → ModeResolver--16lh7dS.d.mts} +1 -1
  4. package/dist/{ModeResolver-DVJ7HV3k.d.mts → ModeResolver-CjpG5Wli.d.ts} +1 -1
  5. package/dist/Task-Dyxp67s2.d.mts +2086 -0
  6. package/dist/Task-Dyxp67s2.d.ts +2086 -0
  7. package/dist/{chunk-T2S5O36I.mjs → chunk-C44UWIAD.mjs} +6 -6
  8. package/dist/chunk-C44UWIAD.mjs.map +1 -0
  9. package/dist/{chunk-JVMDEHRQ.mjs → chunk-EDGUFCIR.mjs} +77 -37
  10. package/dist/chunk-EDGUFCIR.mjs.map +1 -0
  11. package/dist/{chunk-XWOUIA6A.mjs → chunk-JBEFAJ2W.mjs} +2 -2
  12. package/dist/chunk-ROVYWEBI.mjs +2115 -0
  13. package/dist/chunk-ROVYWEBI.mjs.map +1 -0
  14. package/dist/index.d.mts +1647 -758
  15. package/dist/index.d.ts +1647 -758
  16. package/dist/index.js +9301 -4857
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +6222 -2968
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/order-labels.d.mts +3 -1
  21. package/dist/order-labels.d.ts +3 -1
  22. package/dist/{parsing-DRp4dPC0.d.mts → parsing-DPEpAszP.d.mts} +11 -3
  23. package/dist/{parsing-CF_grC29.d.ts → parsing-EcmCsF1y.d.ts} +11 -3
  24. package/dist/parsing.d.mts +5 -3
  25. package/dist/parsing.d.ts +5 -3
  26. package/dist/parsing.js.map +1 -1
  27. package/dist/parsing.mjs +2 -2
  28. package/dist/plan-utils-BAvW3hvl.d.mts +877 -0
  29. package/dist/plan-utils-BMyEiDKv.d.ts +877 -0
  30. package/dist/plan-utils.d.mts +5 -4
  31. package/dist/plan-utils.d.ts +5 -4
  32. package/dist/plan-utils.js +956 -8
  33. package/dist/plan-utils.js.map +1 -1
  34. package/dist/plan-utils.mjs +47 -7
  35. package/dist/testing.d.mts +57 -5
  36. package/dist/testing.d.ts +57 -5
  37. package/dist/testing.js +96 -2
  38. package/dist/testing.js.map +1 -1
  39. package/dist/testing.mjs +94 -2
  40. package/dist/testing.mjs.map +1 -1
  41. package/package.json +2 -1
  42. package/skills/grilling/SKILL.md +6 -16
  43. package/skills/improve-codebase-architecture/SKILL.md +1 -1
  44. package/dist/ApprovalPolicy-BVhGdECT.d.mts +0 -79
  45. package/dist/ApprovalPolicy-BVhGdECT.d.ts +0 -79
  46. package/dist/ITerminalRunner-Bd-vAJnw.d.ts +0 -561
  47. package/dist/ITerminalRunner-ByeoLF57.d.mts +0 -561
  48. package/dist/Task-BxQkPlXO.d.mts +0 -664
  49. package/dist/Task-BxQkPlXO.d.ts +0 -664
  50. package/dist/chunk-GWPIYDQW.mjs +0 -945
  51. package/dist/chunk-GWPIYDQW.mjs.map +0 -1
  52. package/dist/chunk-JVMDEHRQ.mjs.map +0 -1
  53. package/dist/chunk-T2S5O36I.mjs.map +0 -1
  54. package/dist/plan-utils-CkNbqAmS.d.ts +0 -329
  55. package/dist/plan-utils-CtB3_Ovf.d.mts +0 -329
  56. /package/dist/{chunk-XWOUIA6A.mjs.map → chunk-JBEFAJ2W.mjs.map} +0 -0
@@ -0,0 +1,2115 @@
1
+ import {
2
+ PlanParseError,
3
+ TASK_OPS_ENVELOPE_KEY,
4
+ addTaskToPlan,
5
+ createTask,
6
+ escapeControlCharsInStrings,
7
+ extractObjectsWithKey,
8
+ flattenTasks,
9
+ removeTaskFromPlan,
10
+ renumberTasks,
11
+ stripTrailingCommas,
12
+ updateTaskInPlan
13
+ } from "./chunk-EDGUFCIR.mjs";
14
+
15
+ // src/models/Usage.ts
16
+ function partedPromptUsage(parts) {
17
+ const reported = [parts.uncached, parts.cacheRead, parts.cacheWrite].filter((n) => n !== void 0);
18
+ return {
19
+ ...reported.length > 0 ? { inputTokens: reported.reduce((a, b) => a + b, 0) } : {},
20
+ ...parts.cacheRead !== void 0 ? { cachedInputTokens: parts.cacheRead } : {}
21
+ };
22
+ }
23
+ var TOKEN_FIELDS = ["inputTokens", "outputTokens", "cachedInputTokens"];
24
+ function addUsage(totals, record) {
25
+ const next = { ...totals };
26
+ for (const field of TOKEN_FIELDS) {
27
+ const reported = record[field];
28
+ if (reported !== void 0) next[field] = (totals[field] ?? 0) + reported;
29
+ }
30
+ if (record.reportedCost) {
31
+ const { amount, currency } = record.reportedCost;
32
+ next.reportedCost = { ...totals.reportedCost, [currency]: (totals.reportedCost?.[currency] ?? 0) + amount };
33
+ }
34
+ return next;
35
+ }
36
+ function addPlannerUsage(usage, record) {
37
+ const next = { ...usage, totals: addUsage(usage.totals, record) };
38
+ if (record.subagentId) {
39
+ next.bySubagent = { ...usage.bySubagent, [record.subagentId]: addUsage(usage.bySubagent?.[record.subagentId] ?? {}, record) };
40
+ return next;
41
+ }
42
+ if (record.inputTokens !== void 0) next.lastPromptTokens = record.inputTokens;
43
+ if (record.contextWindow !== void 0 && record.contextWindow > 0) next.contextWindow = record.contextWindow;
44
+ return next;
45
+ }
46
+ function isMeasured(totals) {
47
+ return totals.inputTokens !== void 0 || totals.outputTokens !== void 0 || totals.cachedInputTokens !== void 0 || Object.keys(totals.reportedCost ?? {}).length > 0;
48
+ }
49
+ function usageLine(usage) {
50
+ const contextFill = plannerContextFill(usage);
51
+ return {
52
+ totals: usage.totals,
53
+ ...usage.bySubagent ? { bySubagent: usage.bySubagent } : {},
54
+ ...contextFill ? { contextFill } : {}
55
+ };
56
+ }
57
+ function plannerContextFill(usage) {
58
+ if (usage.lastPromptTokens === void 0 || usage.contextWindow === void 0 || usage.contextWindow <= 0) return void 0;
59
+ return { usedTokens: usage.lastPromptTokens, windowTokens: usage.contextWindow };
60
+ }
61
+
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 };
85
+ }
86
+ return { tool: "agent_tool", toolLabel: name };
87
+ }
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(" ");
103
+ }
104
+ }
105
+ if (tool === "fetch") alias("uri", "url");
106
+ if (tool === "web_search") alias("q", "query");
107
+ return out;
108
+ }
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, "");
114
+ }
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;
128
+ };
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);
138
+ }
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;
219
+ }
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;
226
+ }
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;
237
+ }
238
+ i += kind === "(" || kind === ")" ? 3 : 2;
239
+ continue;
240
+ }
241
+ if (ch === "\r") {
242
+ col = 1;
243
+ i++;
244
+ continue;
245
+ }
246
+ if (ch === "\n") {
247
+ row++;
248
+ i++;
249
+ continue;
250
+ }
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++;
268
+ }
269
+ const populated = [...rows.keys()].sort((a, b) => a - b);
270
+ return populated.map((r) => cells(r).join("")).join("\n");
271
+ }
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();
282
+ }
283
+ return rendered.trim();
284
+ }
285
+
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
+ }
307
+ }
308
+ function oneLine(text2) {
309
+ return text2.replace(/\s+/g, " ").trim();
310
+ }
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];
315
+ }
316
+ function text(value) {
317
+ return typeof value === "string" ? value : "";
318
+ }
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);
326
+ }
327
+ const firstText = Object.values(args).find((v) => typeof v === "string" && v.trim() !== "");
328
+ return firstText ? oneLine(firstText) : "";
329
+ }
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);
353
+ }
354
+ }
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) };
359
+ }
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;
364
+ }
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 };
369
+ }
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 } : {} };
374
+ }
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 } : {}
384
+ };
385
+ }
386
+ function usageBlock(id, { totals, bySubagent, contextFill }) {
387
+ return { type: "usage", id, totals, ...bySubagent ? { bySubagent } : {}, ...contextFill ? { contextFill } : {} };
388
+ }
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 } : {}
415
+ };
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];
494
+ }
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)];
497
+ }
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 });
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
+ }));
522
+ }
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 };
528
+ }
529
+ function closeBlock(block, interrupted) {
530
+ return interrupted && block.type === "tool" && block.status === "pending" ? { ...block, status: "interrupted" } : sealed(block);
531
+ }
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 });
576
+ }
577
+ return appendOutput(next, lane, make);
578
+ }
579
+ function agentText(view, text2) {
580
+ return streamWhole(view, null, TEXT_STREAM, text2, (id) => settledMessage(id, "agent", blockText(text2)));
581
+ }
582
+ function agentTextDelta(view, text2) {
583
+ return streamDelta(view, null, TEXT_STREAM, text2, (id, t) => ({ type: "message", id, role: "agent", text: t, streaming: true }));
584
+ }
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;
611
+ }
612
+ }
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));
622
+ }
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;
763
+ continue;
764
+ }
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];
770
+ }
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)
806
+ }
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]) });
835
+ }
836
+ const discovered = modelsByRunner?.[task.assignedRunner]?.[0];
837
+ if (discovered) {
838
+ return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, discovered.modelId) });
839
+ }
840
+ return task;
841
+ }
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}"` };
862
+ }
863
+ if (allowed && !allowed.includes(modelId)) {
864
+ return { ok: false, error: `Model "${modelId}" is excluded by the allowlist for runner "${runner}"` };
865
+ }
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;
1190
+ }
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": {
1203
+ const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
1204
+ if ("error" in ref) {
1205
+ errors.push(`${label}: ${ref.error}`);
1206
+ break;
1207
+ }
1208
+ const target = findInTasks(tasks, ref.id);
1209
+ if (!target) {
1210
+ errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
1211
+ break;
1212
+ }
1213
+ const changes = {};
1214
+ for (const key of UPDATABLE_FIELDS) {
1215
+ if (op.changes && key in op.changes) changes[key] = op.changes[key];
1216
+ }
1217
+ if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
1218
+ errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
1219
+ break;
1220
+ }
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(", ")}`);
1233
+ break;
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;
1422
+ break;
1423
+ }
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;
1460
+ }
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(", ")}`);
1503
+ break;
1504
+ }
1505
+ changes.dependencies = ids.filter((id) => id !== target.id);
1506
+ }
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;
1517
+ }
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})`);
1529
+ }
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);
1541
+ }
1542
+ }
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 {
1560
+ }
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";
1571
+ }
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}]` : ""}`;
1577
+ }
1578
+ if (tool === "web_search" && args.query) {
1579
+ return `web_search "${ellipsize(String(args.query), 50)}"`;
1580
+ }
1581
+ if (tool === "fetch" && args.url) {
1582
+ return `fetch ${ellipsize(String(args.url), 60)}`;
1583
+ }
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 {
1604
+ }
1605
+ return display;
1606
+ }
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";
1612
+ }
1613
+ return "success";
1614
+ }
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 };
1627
+ }
1628
+ function canContinue(task) {
1629
+ return continuability(task).ok;
1630
+ }
1631
+
1632
+ // src/services/SessionMessage.ts
1633
+ function serializeTask(t) {
1634
+ return {
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
1651
+ };
1652
+ }
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
+ };
1666
+ }
1667
+ function serializePlan(plan) {
1668
+ return {
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
1675
+ };
1676
+ }
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
+ };
1683
+ }
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";
1689
+ }
1690
+
1691
+ // src/conversation/reduce.ts
1692
+ var EMPTY_CONVERSATION = { blocks: [], nextId: 1 };
1693
+ function isUnsettledSegment(block, turnId) {
1694
+ return block.type === "message" && block.turnId === turnId && block.segmentId !== void 0;
1695
+ }
1696
+ function isBuildingPlan(block, turnId) {
1697
+ return block.type === "plan" && block.status === "building" && block.turnId === turnId;
1698
+ }
1699
+ function finalSegmentIndex(blocks, turnId, pastPlan = false) {
1700
+ for (let i = blocks.length - 1; i >= 0; i--) {
1701
+ const block = blocks[i];
1702
+ if (block.type === "usage" || block.type === "thinking" || block.type === "approval" || pastPlan && block.type === "plan") continue;
1703
+ if (block.turnId !== turnId) continue;
1704
+ return isUnsettledSegment(block, turnId) ? i : -1;
1705
+ }
1706
+ return -1;
1707
+ }
1708
+ function openTurn(view, turnId, prompt) {
1709
+ const i = findLastIndex(view.blocks, (b) => b.type === "message" && b.role === "user");
1710
+ const sent = view.blocks[i];
1711
+ if (sent?.type === "message" && sent.turnId === void 0 && sent.text === prompt) {
1712
+ return { ...view, blocks: replaceAt(view.blocks, i, { ...sent, turnId }) };
1713
+ }
1714
+ return append(view, (id) => settledMessage(id, "user", prompt, turnId));
1715
+ }
1716
+ function streamText(view, { turnId, segmentId, text: text2 }) {
1717
+ const i = findLastIndex(view.blocks, (b) => isUnsettledSegment(b, turnId) && b.segmentId === segmentId);
1718
+ const segment = view.blocks[i];
1719
+ if (segment?.type === "message") {
1720
+ return sealLane(laneReplace(view, null, i, { ...segment, text: segment.text + text2, streaming: true }), null, i);
1721
+ }
1722
+ return appendOutput(view, null, (id) => ({ type: "message", id, role: "planner", text: text2, streaming: true, turnId, segmentId }));
1723
+ }
1724
+ function dropBuildingPlan(view, turnId, segmentId) {
1725
+ const blocks = view.blocks.filter((b) => !(isBuildingPlan(b, turnId) && (segmentId === void 0 || b.segmentId === segmentId)));
1726
+ return blocks.length === view.blocks.length ? view : { ...view, blocks };
1727
+ }
1728
+ function settleReply(view, { content, turnId }) {
1729
+ if (turnId !== void 0) {
1730
+ const i = finalSegmentIndex(view.blocks, turnId);
1731
+ if (i >= 0) {
1732
+ return dropBuildingPlan({ ...view, blocks: replaceAt(view.blocks, i, settledMessage(view.blocks[i].id, "planner", content, turnId)) }, turnId);
1733
+ }
1734
+ }
1735
+ return append(dropBuildingPlan(view, turnId), (id) => settledMessage(id, "planner", content, turnId));
1736
+ }
1737
+ function retractText(view, { turnId, segmentId }) {
1738
+ const blocks = view.blocks.filter((b) => !(isUnsettledSegment(b, turnId) && (segmentId === void 0 || b.segmentId === segmentId)));
1739
+ return dropBuildingPlan(blocks.length === view.blocks.length ? view : { ...view, blocks }, turnId, segmentId);
1740
+ }
1741
+ function startSubagent2(view, { subagentId, brief, model, turnId }) {
1742
+ return updateSubagent(view, subagentId, turnId, (block) => ({ ...block, ...brief ? { brief } : {}, ...model ? { model } : {} }));
1743
+ }
1744
+ function finishSubagent2(view, { subagentId, outcome, digest, usage, turnId }) {
1745
+ return updateSubagent(view, subagentId, turnId, (block) => ({
1746
+ ...block,
1747
+ status: outcome,
1748
+ digest,
1749
+ children: block.children.map(sealed),
1750
+ ...usage ? { usage } : {}
1751
+ }));
1752
+ }
1753
+ function announceTool2(view, call) {
1754
+ const spawns = call.tool === "spawn_research_agent" ? call.subagentId : void 0;
1755
+ const [placed, lane] = laneOf(view, spawns ? void 0 : call.subagentId, call.turnId);
1756
+ return appendOutput(placed, lane, (id) => ({ ...pendingTool(id, call), ...spawns ? { spawns } : {} }));
1757
+ }
1758
+ function isPendingCallOf(block, step) {
1759
+ if (block.type !== "tool" || block.status !== "pending") return false;
1760
+ return step.toolCallId ? block.toolCallId === step.toolCallId : block.toolCallId === void 0 && block.tool === step.tool;
1761
+ }
1762
+ function settleTool2(view, { step, turnId }) {
1763
+ const [placed, lane] = laneOf(view, step.subagentId, turnId);
1764
+ const blocks = laneBlocks(placed, lane);
1765
+ const i = findLastIndex(blocks, (b) => isPendingCallOf(b, step));
1766
+ const pending = blocks[i];
1767
+ if (pending?.type === "tool") return laneReplace(placed, lane, i, settledTool(pending, step));
1768
+ if (lane === null && step.toolCallId && blocks.some((b) => b.type === "subagent" && b.toolCallId === step.toolCallId)) return view;
1769
+ return laneAppend(placed, lane, (id) => toolFromStep(id, step, turnId));
1770
+ }
1771
+ function streamPlan(view, { token, turnId, segmentId }) {
1772
+ const i = findLastIndex(view.blocks, (b) => isBuildingPlan(b, turnId));
1773
+ const building = view.blocks[i];
1774
+ if (building?.type === "plan" && building.segmentId === segmentId) {
1775
+ return { ...view, blocks: replaceAt(view.blocks, i, { ...building, text: building.text + token }) };
1776
+ }
1777
+ return append(dropBuildingPlan(view, turnId), (id) => ({
1778
+ type: "plan",
1779
+ id,
1780
+ status: "building",
1781
+ text: token,
1782
+ ...turnId ? { turnId } : {},
1783
+ ...segmentId ? { segmentId } : {}
1784
+ }));
1785
+ }
1786
+ function markPlan(view, content, turnId) {
1787
+ const marker = planMarker(content);
1788
+ const i = findLastIndex(view.blocks, (b) => isBuildingPlan(b, turnId));
1789
+ const building = view.blocks[i];
1790
+ if (building?.type === "plan") {
1791
+ const settled = { type: "plan", id: building.id, ...marker, text: "", ...building.turnId ? { turnId: building.turnId } : {} };
1792
+ return { ...view, blocks: replaceAt(view.blocks, i, settled) };
1793
+ }
1794
+ return append(view, (id) => ({ type: "plan", id, text: "", ...marker }));
1795
+ }
1796
+ function requestApproval2(view, { id: approvalId, kind, subject, scope, detail, turnId }) {
1797
+ return append(view, (id) => ({
1798
+ type: "approval",
1799
+ id,
1800
+ approvalId,
1801
+ kind,
1802
+ subject,
1803
+ scope,
1804
+ status: "pending",
1805
+ ...detail ? { detail } : {},
1806
+ ...turnId ? { turnId } : {}
1807
+ }));
1808
+ }
1809
+ function settleApproval2(view, { id, granted }) {
1810
+ const i = findLastIndex(view.blocks, (b) => b.type === "approval" && b.approvalId === id);
1811
+ const request = view.blocks[i];
1812
+ if (request?.type !== "approval") return view;
1813
+ return { ...view, blocks: replaceAt(view.blocks, i, { ...request, status: granted ? "granted" : "denied", decidedBy: "asked" }) };
1814
+ }
1815
+ function decideApproval2(view, { kind, subject, scope, detail, granted, source }) {
1816
+ return append(view, (id) => ({
1817
+ type: "approval",
1818
+ id,
1819
+ kind,
1820
+ subject,
1821
+ scope,
1822
+ status: granted ? "granted" : "denied",
1823
+ decidedBy: source,
1824
+ ...detail ? { detail } : {}
1825
+ }));
1826
+ }
1827
+ function reportUsage2(view, message) {
1828
+ return isMeasured(message.totals) ? setUsageLine(view, message) : view;
1829
+ }
1830
+ function endTurn2(view, { turnId, outcome }) {
1831
+ let next = dropBuildingPlan(view, turnId);
1832
+ const planText = outcome === "plan" ? finalSegmentIndex(next.blocks, turnId, true) : -1;
1833
+ if (planText >= 0) next = { ...next, blocks: next.blocks.filter((_, i) => i !== planText) };
1834
+ return closeOpenBlocks(next, outcome === "stopped" ? "stopped" : outcome === "error" ? "failed" : null);
1835
+ }
1836
+ function markCompaction(view, summary) {
1837
+ const i = findLastIndex(view.blocks, (b) => b.type === "message" && b.role === "planner" && b.turnId === void 0 && b.text === summary);
1838
+ const announced = view.blocks[i];
1839
+ if (announced?.type === "message") return { ...view, blocks: replaceAt(view.blocks, i, { ...announced, role: "system" }) };
1840
+ return append(view, (id) => settledMessage(id, "system", summary));
1841
+ }
1842
+ function syncTranscript(view, { plan, turnId }) {
1843
+ const history = plan.conversationHistory ?? [];
1844
+ const committed = findLastIndex(history, (e) => e.kind === "plan_generated");
1845
+ let next = view;
1846
+ let latest = view.transcriptAt;
1847
+ for (const [i, entry] of history.entries()) {
1848
+ const isNew = view.transcriptAt === void 0 || entry.timestamp > view.transcriptAt;
1849
+ if (isNew && entry.kind === "plan_generated") next = markPlan(next, entry.content, i === committed ? turnId : void 0);
1850
+ if (isNew && entry.kind === "system") next = append(next, (id) => settledMessage(id, "system", entry.content));
1851
+ if (isNew && entry.kind === "compaction") next = markCompaction(next, entry.content);
1852
+ if (latest === void 0 || entry.timestamp > latest) latest = entry.timestamp;
1853
+ }
1854
+ return next === view && latest === view.transcriptAt ? view : { ...next, transcriptAt: latest };
1855
+ }
1856
+ function reduceConversation(view, input) {
1857
+ switch (input.type) {
1858
+ case "local_entry":
1859
+ return append(view, (id) => settledMessage(id, input.role, input.text));
1860
+ case "planner_turn_started":
1861
+ return input.prompt === void 0 ? view : openTurn(view, input.turnId, input.prompt);
1862
+ case "planner_text_delta":
1863
+ return streamText(view, input);
1864
+ case "planner_message":
1865
+ return settleReply(view, input);
1866
+ case "planner_text_retracted":
1867
+ return retractText(view, input);
1868
+ case "planner_thinking_delta":
1869
+ return think(view, input.text, input.turnId, input.segmentId, input.subagentId);
1870
+ case "research_step":
1871
+ return announceTool2(view, input);
1872
+ case "research_step_done":
1873
+ return settleTool2(view, input);
1874
+ case "subagent_started":
1875
+ return startSubagent2(view, input);
1876
+ case "subagent_finished":
1877
+ return finishSubagent2(view, input);
1878
+ case "plan_token":
1879
+ return streamPlan(view, input);
1880
+ case "plan_generated":
1881
+ return syncTranscript(view, input);
1882
+ case "planner_turn_ended":
1883
+ return endTurn2(view, input);
1884
+ case "approval_request":
1885
+ return requestApproval2(view, input);
1886
+ case "approval_settled":
1887
+ return settleApproval2(view, input);
1888
+ case "approval_decided":
1889
+ return decideApproval2(view, input);
1890
+ case "planner_usage":
1891
+ return reportUsage2(view, input);
1892
+ default:
1893
+ return view;
1894
+ }
1895
+ }
1896
+
1897
+ // src/conversation/transcript.ts
1898
+ var USER = 0;
1899
+ var RESEARCH = 1;
1900
+ var SETTLED = 2;
1901
+ function fromEntry(entry) {
1902
+ if (entry.kind === "plan_generated") return (id) => ({ type: "plan", id: id(), text: "", ...planMarker(entry.content) });
1903
+ const role = entry.kind === "system" || entry.kind === "compaction" ? "system" : entry.role === "user" ? "user" : "planner";
1904
+ return (id) => settledMessage(id(), role, entry.content);
1905
+ }
1906
+ function spawnBrief(step) {
1907
+ try {
1908
+ const args = JSON.parse(step.args);
1909
+ const prompt = args && typeof args === "object" ? args.prompt : void 0;
1910
+ return typeof prompt === "string" ? prompt.trim() : void 0;
1911
+ } catch {
1912
+ return void 0;
1913
+ }
1914
+ }
1915
+ function spawnedBy(step, unclaimed) {
1916
+ const byId = unclaimed.find((e) => e.subagentId === step.toolCallId);
1917
+ if (byId) return byId;
1918
+ if (step.tool !== "spawn_research_agent") return void 0;
1919
+ const brief = spawnBrief(step);
1920
+ return brief === void 0 ? void 0 : unclaimed.find((e) => e.brief === brief);
1921
+ }
1922
+ function research(log) {
1923
+ const subagents = log.filter((e) => "type" in e && e.type === "subagent");
1924
+ const ownSubagent = new Set(subagents.map((e) => e.subagentId));
1925
+ const unclaimed = [...subagents];
1926
+ const spawnCall = /* @__PURE__ */ new Map();
1927
+ const placed = [];
1928
+ for (const entry of log) {
1929
+ if ("type" in entry) {
1930
+ if (entry.type === "system") placed.push({ at: entry.timestamp, rank: RESEARCH, build: (id) => settledMessage(id(), "system", entry.content) });
1931
+ continue;
1932
+ }
1933
+ if (entry.subagentId && ownSubagent.has(entry.subagentId)) continue;
1934
+ const spawned = spawnedBy(entry, unclaimed);
1935
+ if (spawned) {
1936
+ unclaimed.splice(unclaimed.indexOf(spawned), 1);
1937
+ spawnCall.set(spawned.subagentId, entry.toolCallId);
1938
+ continue;
1939
+ }
1940
+ placed.push({ at: entry.timestamp, rank: RESEARCH, build: (id) => toolFromStep(id(), entry) });
1941
+ }
1942
+ for (const entry of subagents) {
1943
+ const toolCallId = spawnCall.get(entry.subagentId);
1944
+ const steps = log.filter((e) => !("type" in e) && e.subagentId === entry.subagentId);
1945
+ placed.push({
1946
+ at: entry.timestamp,
1947
+ rank: RESEARCH,
1948
+ build: (id) => subagentBlock(id(), {
1949
+ subagentId: entry.subagentId,
1950
+ brief: entry.brief,
1951
+ status: entry.outcome,
1952
+ digest: entry.digest,
1953
+ children: steps.map((step) => toolFromStep(id(), step)),
1954
+ toolCallId,
1955
+ model: entry.model,
1956
+ usage: entry.usage
1957
+ })
1958
+ });
1959
+ }
1960
+ return placed;
1961
+ }
1962
+ function fromTranscript(conversationHistory, researchLog, plannerUsage) {
1963
+ const history = conversationHistory ?? [];
1964
+ const compacted = history[0]?.kind === "compaction" ? history[0] : void 0;
1965
+ const kept = compacted ? history.slice(1) : history;
1966
+ const cutoff = compacted ? kept[0]?.timestamp ?? compacted.timestamp : "";
1967
+ const placed = [
1968
+ ...kept.map((entry) => ({ at: entry.timestamp, rank: entry.role === "user" && !entry.kind ? USER : SETTLED, build: fromEntry(entry) })),
1969
+ ...research(researchLog ?? []).filter((item) => item.at >= cutoff)
1970
+ ];
1971
+ placed.sort((a, b) => a.at < b.at ? -1 : a.at > b.at ? 1 : a.rank - b.rank);
1972
+ if (compacted) placed.unshift({ at: "", rank: SETTLED, build: fromEntry(compacted) });
1973
+ let nextId = 1;
1974
+ const id = () => `b${nextId++}`;
1975
+ const blocks = placed.map((item) => item.build(id));
1976
+ if (plannerUsage && isMeasured(plannerUsage.totals)) blocks.push(usageBlock(id(), usageLine(plannerUsage)));
1977
+ const transcriptAt = history.reduce((latest, e) => latest === void 0 || e.timestamp > latest ? e.timestamp : latest, void 0);
1978
+ return { blocks, nextId, ...transcriptAt ? { transcriptAt } : {} };
1979
+ }
1980
+
1981
+ // src/conversation/promptHold.ts
1982
+ var EMPTY_HOLD = [];
1983
+ function holdPrompt(hold, text2) {
1984
+ return [...hold, text2];
1985
+ }
1986
+ function drainNext(hold) {
1987
+ const [text2, ...rest] = hold;
1988
+ return text2 === void 0 ? void 0 : { text: text2, rest };
1989
+ }
1990
+ function unsendLatest(hold) {
1991
+ if (hold.length === 0) return void 0;
1992
+ return { text: hold[hold.length - 1], rest: hold.slice(0, -1) };
1993
+ }
1994
+ function unsendAll(hold) {
1995
+ if (hold.length === 0) return void 0;
1996
+ return { text: hold.join("\n"), rest: EMPTY_HOLD };
1997
+ }
1998
+ function aheadOfDraft(text2, draft) {
1999
+ return draft ? `${text2}
2000
+ ${draft}` : text2;
2001
+ }
2002
+
2003
+ // src/conversation/turnGate.ts
2004
+ var NO_TURN = { open: null, stopped: null };
2005
+ var TURN_STREAM = /* @__PURE__ */ new Set([
2006
+ "planner_text_delta",
2007
+ "planner_thinking_delta",
2008
+ "planner_text_retracted",
2009
+ "planner_message",
2010
+ "plan_token",
2011
+ "research_step",
2012
+ "research_step_done",
2013
+ "subagent_started",
2014
+ "subagent_finished",
2015
+ "planner_turn_ended"
2016
+ ]);
2017
+ function cutOff(gate, input) {
2018
+ if (gate.stopped === null || !TURN_STREAM.has(input.type)) return false;
2019
+ const turnId = "turnId" in input ? input.turnId : void 0;
2020
+ return turnId === void 0 || turnId === gate.stopped;
2021
+ }
2022
+ function withGate(live, gate) {
2023
+ return gate.open === live.gate.open && gate.stopped === live.gate.stopped ? live : { ...live, gate };
2024
+ }
2025
+ function followTurn(view, gate, input) {
2026
+ const live = { view, gate };
2027
+ if (input.type === "planner_turn_started") {
2028
+ return withGate({ view: reduceConversation(view, input), gate }, { open: input.turnId, stopped: null });
2029
+ }
2030
+ if (cutOff(gate, input)) {
2031
+ return input.type === "planner_turn_ended" ? withGate(live, { ...gate, stopped: null }) : live;
2032
+ }
2033
+ const next = { view: reduceConversation(view, input), gate };
2034
+ return input.type === "planner_turn_ended" && input.turnId === gate.open ? withGate(next, { ...gate, open: null }) : next;
2035
+ }
2036
+ function stopTurn(view, gate) {
2037
+ if (gate.open === null) return { view, gate };
2038
+ return {
2039
+ view: reduceConversation(view, { type: "planner_turn_ended", turnId: gate.open, outcome: "stopped" }),
2040
+ gate: { open: null, stopped: gate.open }
2041
+ };
2042
+ }
2043
+
2044
+ // src/conversation/detail.ts
2045
+ function hasHiddenDetail(blocks) {
2046
+ return blocks.some((b) => b.type === "thinking" || b.type === "tool" || b.type === "subagent");
2047
+ }
2048
+
2049
+ // src/conversation/notices.ts
2050
+ function taskStartedNotice(title, runner) {
2051
+ return runner ? `Started "${title}" \xB7 ${runner}` : `Started "${title}"`;
2052
+ }
2053
+
2054
+ export {
2055
+ partedPromptUsage,
2056
+ addUsage,
2057
+ addPlannerUsage,
2058
+ isMeasured,
2059
+ usageLine,
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,
2074
+ effectiveAllowlist,
2075
+ filterModelsForPrompt,
2076
+ clampThinkingEffort,
2077
+ coerceAssignments,
2078
+ validateTaskEdit,
2079
+ taskOpsProtocol,
2080
+ textHasTaskOps,
2081
+ parseTaskOpsJson,
2082
+ taskOpRefs,
2083
+ refMatchesTask,
2084
+ canMergeTasks,
2085
+ dependentsOf,
2086
+ dependencyCandidates,
2087
+ canSetDependencies,
2088
+ canSplitTask,
2089
+ applyTaskOps,
2090
+ summarizeToolCall,
2091
+ classifyOutcome,
2092
+ continuability,
2093
+ canContinue,
2094
+ serializeTask,
2095
+ serializeTaskStatus,
2096
+ serializePlan,
2097
+ executionSummary,
2098
+ CHECKPOINT_TRUNCATE_LENGTH,
2099
+ truncateCheckpointSummary,
2100
+ EMPTY_CONVERSATION,
2101
+ reduceConversation,
2102
+ fromTranscript,
2103
+ EMPTY_HOLD,
2104
+ holdPrompt,
2105
+ drainNext,
2106
+ unsendLatest,
2107
+ unsendAll,
2108
+ aheadOfDraft,
2109
+ NO_TURN,
2110
+ followTurn,
2111
+ stopTurn,
2112
+ hasHiddenDetail,
2113
+ taskStartedNotice
2114
+ };
2115
+ //# sourceMappingURL=chunk-ROVYWEBI.mjs.map