@ordewell/core 0.5.5 → 0.5.6

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