@ordewell/core 0.4.9 → 0.4.10

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.
@@ -0,0 +1,862 @@
1
+ import {
2
+ PlanParseError,
3
+ TASK_OPS_ENVELOPE_KEY,
4
+ addTaskToPlan,
5
+ createTask,
6
+ escapeControlCharsInStrings,
7
+ extractObjectsWithKey,
8
+ flattenTasks,
9
+ removeTaskFromPlan,
10
+ renumberTasks,
11
+ stripTrailingCommas,
12
+ updateTaskInPlan
13
+ } from "./chunk-IO6PEEN6.mjs";
14
+
15
+ // src/services/ModelAllowlistResolver.ts
16
+ function effectiveAllowlist(allowlist, runner, modelsByRunner) {
17
+ if (!allowlist || allowlist.length === 0) return void 0;
18
+ if (!modelsByRunner) return allowlist;
19
+ const own = new Set((modelsByRunner[runner] ?? []).map((m) => m.modelId));
20
+ const elsewhere = /* @__PURE__ */ new Set();
21
+ for (const [other, models] of Object.entries(modelsByRunner)) {
22
+ if (other === runner) continue;
23
+ for (const m of models ?? []) elsewhere.add(m.modelId);
24
+ }
25
+ const kept = allowlist.filter((id) => own.has(id) || !elsewhere.has(id));
26
+ return kept.length > 0 ? kept : void 0;
27
+ }
28
+ function filterModelsForPrompt(modelsByRunner, perRunnerAllowlist) {
29
+ const result = {};
30
+ for (const [runner, models] of Object.entries(modelsByRunner)) {
31
+ if (!models) continue;
32
+ const allowed = effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
33
+ if (!allowed) {
34
+ result[runner] = models;
35
+ continue;
36
+ }
37
+ const allowedSet = new Set(allowed);
38
+ const intersection = models.filter((m) => allowedSet.has(m.modelId));
39
+ const covered = new Set(intersection.map((m) => m.modelId));
40
+ const missing = allowed.filter((id) => !covered.has(id)).map((id) => ({ modelId: id, modelLabel: id.split("/").pop() || id, variants: [] }));
41
+ result[runner] = [...intersection, ...missing];
42
+ }
43
+ return result;
44
+ }
45
+ var EFFORT_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
46
+ var EFFORT_ALIASES = { disabled: "none", adaptive: "medium" };
47
+ function clampThinkingEffort(effort, variants) {
48
+ if (!effort) return void 0;
49
+ if (variants.length === 0) return void 0;
50
+ const ids = new Set(variants.map((v) => v.id));
51
+ if (ids.has(effort)) return effort;
52
+ const canon = EFFORT_ALIASES[effort] ?? effort;
53
+ if (ids.has(canon)) return canon;
54
+ const idx = EFFORT_LADDER.indexOf(canon);
55
+ if (idx < 0) return void 0;
56
+ for (let d = 1; d < EFFORT_LADDER.length; d++) {
57
+ const lower = EFFORT_LADDER[idx - d];
58
+ if (lower && ids.has(lower)) return lower;
59
+ const higher = EFFORT_LADDER[idx + d];
60
+ if (higher && ids.has(higher)) return higher;
61
+ }
62
+ return void 0;
63
+ }
64
+ function coerceAssignments(tasks, perRunnerAllowlist, allowedRunners, modelsByRunner) {
65
+ const clampVariant = (task) => {
66
+ if (!task.assignedModel || !modelsByRunner) return task;
67
+ const model = modelsByRunner[task.assignedRunner]?.find(
68
+ (m) => m.modelId === task.assignedModel.modelId
69
+ );
70
+ if (!model) return task;
71
+ const clamped = clampThinkingEffort(task.assignedModel.thinkingEffort, model.variants);
72
+ return {
73
+ ...task,
74
+ assignedModel: {
75
+ ...task.assignedModel,
76
+ thinkingEffort: clamped,
77
+ availableVariants: model.variants.map((v) => v.id)
78
+ }
79
+ };
80
+ };
81
+ const allowedFor = (runner) => effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
82
+ const assignmentFor = (runner, modelId) => {
83
+ const known = modelsByRunner?.[runner]?.find((m) => m.modelId === modelId);
84
+ return {
85
+ modelId,
86
+ modelLabel: known?.modelLabel ?? modelId,
87
+ thinkingEffort: void 0,
88
+ ...known ? { availableVariants: known.variants.map((v) => v.id) } : {}
89
+ };
90
+ };
91
+ return tasks.map((task) => {
92
+ if (task.type === "user") return task;
93
+ if (allowedRunners && !allowedRunners.includes(task.assignedRunner)) {
94
+ const fallbackRunner = allowedRunners[0];
95
+ const fallbackModels = allowedFor(fallbackRunner);
96
+ const fallbackModel = fallbackModels?.length ? assignmentFor(fallbackRunner, fallbackModels[0]) : modelsByRunner?.[fallbackRunner]?.[0] ? assignmentFor(fallbackRunner, modelsByRunner[fallbackRunner][0].modelId) : task.assignedModel;
97
+ return clampVariant({
98
+ ...task,
99
+ assignedRunner: fallbackRunner,
100
+ assignedModel: fallbackModel
101
+ });
102
+ }
103
+ const allowlist = allowedFor(task.assignedRunner);
104
+ if (!task.assignedModel) {
105
+ if (allowlist && allowlist.length > 0) {
106
+ return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) });
107
+ }
108
+ const discovered = modelsByRunner?.[task.assignedRunner]?.[0];
109
+ if (discovered) {
110
+ return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, discovered.modelId) });
111
+ }
112
+ return task;
113
+ }
114
+ if (!allowlist || allowlist.length === 0) return clampVariant(task);
115
+ if (allowlist.includes(task.assignedModel.modelId)) return clampVariant(task);
116
+ return { ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) };
117
+ });
118
+ }
119
+
120
+ // src/services/TaskOps.ts
121
+ import { v4 as uuidv4 } from "uuid";
122
+
123
+ // src/services/TaskEditValidator.ts
124
+ var AI_ONLY_FIELDS = ["assignedModel", "thinkingEffort", "taskMode", "autonomy"];
125
+ function checkModelAndModeValidity(runner, assignedModel, taskMode, catalog) {
126
+ if (!catalog) return { ok: true };
127
+ if (assignedModel) {
128
+ const models = catalog.modelsByRunner[runner];
129
+ if (models) {
130
+ const modelId = assignedModel.modelId;
131
+ if (!models.some((m) => m.modelId === modelId)) {
132
+ return { ok: false, error: `Runner "${runner}" does not offer model "${modelId}"` };
133
+ }
134
+ const allowed = effectiveAllowlist(catalog.perRunnerAllowlist?.[runner], runner, catalog.modelsByRunner);
135
+ if (allowed && !allowed.includes(modelId)) {
136
+ return { ok: false, error: `Model "${modelId}" is excluded by the allowlist for runner "${runner}"` };
137
+ }
138
+ }
139
+ }
140
+ if (taskMode !== void 0) {
141
+ const modes = catalog.runnerModes[runner];
142
+ if (modes && modes.length > 0 && !modes.some((m) => m.id === taskMode)) {
143
+ return { ok: false, error: `Runner "${runner}" has no task mode "${taskMode}" \u2014 valid modes: ${modes.map((m) => m.id).join(", ")}` };
144
+ }
145
+ }
146
+ return { ok: true };
147
+ }
148
+ function typeCoherenceCheck(target, changes) {
149
+ if (!changes.type || changes.type === target.type) return null;
150
+ if (changes.type === "user") {
151
+ if (!Array.isArray(changes.userSteps) || changes.userSteps.length === 0) {
152
+ return { ok: false, error: `"${target.title}" cannot become a manual task without user steps \u2014 include "userSteps" in this edit` };
153
+ }
154
+ return { ok: true, clear: AI_ONLY_FIELDS.filter((f) => target[f] !== void 0) };
155
+ }
156
+ if (!changes.prompt || !changes.prompt.trim()) {
157
+ return { ok: false, error: `"${target.title}" cannot become an AI task without a prompt \u2014 include "prompt" in this edit` };
158
+ }
159
+ return { ok: true, clear: target.userSteps && target.userSteps.length > 0 ? ["userSteps"] : [] };
160
+ }
161
+ function validateTaskEdit(actor, tasks, taskId, changes, catalog) {
162
+ const target = tasks.find((t) => t.id === taskId);
163
+ if (!target) return { ok: false, error: "Task not found" };
164
+ if (actor === "planner") {
165
+ if (target.status === "in_progress") return { ok: false, error: `"${target.title}" is running and cannot be modified` };
166
+ if (target.status === "completed") return { ok: false, error: `"${target.title}" is completed and cannot be modified` };
167
+ }
168
+ if (changes.dependencies) {
169
+ const depCheck = canSetDependencies(tasks, taskId, changes.dependencies);
170
+ if (!depCheck.ok) return depCheck;
171
+ }
172
+ const coherence = typeCoherenceCheck(target, changes);
173
+ if (coherence && !coherence.ok) return coherence;
174
+ const runner = changes.assignedRunner ?? target.assignedRunner;
175
+ const validity = checkModelAndModeValidity(runner, changes.assignedModel, changes.taskMode, catalog);
176
+ if (!validity.ok) return validity;
177
+ return coherence ?? { ok: true };
178
+ }
179
+
180
+ // src/services/TaskOps.ts
181
+ var UPDATABLE_FIELDS = [
182
+ "title",
183
+ "description",
184
+ "prompt",
185
+ "dependencies",
186
+ "assignedRunner",
187
+ "assignedModel",
188
+ "taskMode",
189
+ "thinkingEffort",
190
+ "type",
191
+ "userSteps",
192
+ "autonomy",
193
+ "sliceType"
194
+ ];
195
+ function textHasTaskOps(text) {
196
+ return text.includes(`"${TASK_OPS_ENVELOPE_KEY}"`);
197
+ }
198
+ function parseTaskOpsJson(text) {
199
+ const { matches, fallback } = extractObjectsWithKey(text, TASK_OPS_ENVELOPE_KEY);
200
+ const candidates = matches.length > 0 ? [...matches].reverse() : [fallback.json];
201
+ let lastError = null;
202
+ for (const json of candidates) {
203
+ try {
204
+ return parseTaskOpsObject(json, text);
205
+ } catch (err) {
206
+ if (!(err instanceof PlanParseError)) throw err;
207
+ if (!lastError) lastError = err;
208
+ }
209
+ }
210
+ throw lastError ?? new PlanParseError("No JSON object found in task-ops reply", text);
211
+ }
212
+ function parseTaskOpsObject(json, text) {
213
+ let parsed;
214
+ try {
215
+ parsed = JSON.parse(escapeControlCharsInStrings(stripTrailingCommas(json)));
216
+ } catch (err) {
217
+ throw new PlanParseError(`Task-ops JSON is invalid: ${err instanceof Error ? err.message : String(err)}`, text);
218
+ }
219
+ const ops = parsed.taskOps;
220
+ if (!Array.isArray(ops) || ops.length === 0) {
221
+ throw new PlanParseError('"taskOps" must be a non-empty array', text);
222
+ }
223
+ for (const op of ops) {
224
+ if (typeof op !== "object" || op === null || typeof op.op !== "string") {
225
+ throw new PlanParseError('Every task op needs an "op" field (update | add | remove | reorder | merge | split)', text);
226
+ }
227
+ }
228
+ return ops;
229
+ }
230
+ function resolveRef(ref, originalScope, liveScope, handleOwner, handleId, opIndex) {
231
+ if (typeof ref !== "string" && typeof ref !== "number") return { error: `invalid reference "${String(ref)}"` };
232
+ const s = String(ref).trim();
233
+ const ownerIdx = handleOwner.get(s);
234
+ if (ownerIdx !== void 0) {
235
+ if (ownerIdx >= opIndex) {
236
+ return { error: `handle "${s}" is defined later in this batch (op ${ownerIdx + 1}) and cannot be referenced yet` };
237
+ }
238
+ const id = handleId.get(s);
239
+ if (!id) return { error: `handle "${s}" was never resolved` };
240
+ return { id };
241
+ }
242
+ const byLiveId = liveScope.find((t) => t.id === s);
243
+ if (byLiveId) return { id: byLiveId.id };
244
+ const orderStr = s.startsWith("#") ? s.slice(1) : s;
245
+ if (/^\d+$/.test(orderStr)) {
246
+ const order = Number(orderStr);
247
+ const byOrder = originalScope.filter((t) => t.order === order);
248
+ if (byOrder.length === 1) return { id: byOrder[0].id };
249
+ }
250
+ const byTitle = originalScope.find((t) => t.title === s);
251
+ if (byTitle) return { id: byTitle.id };
252
+ return { error: `task "${s}" not found` };
253
+ }
254
+ function refCollidesWithExisting(name, originalFlatAll) {
255
+ if (originalFlatAll.some((t) => t.id === name)) return true;
256
+ const orderStr = name.startsWith("#") ? name.slice(1) : name;
257
+ if (/^\d+$/.test(orderStr) && originalFlatAll.some((t) => t.order === Number(orderStr))) return true;
258
+ return originalFlatAll.some((t) => t.title === name);
259
+ }
260
+ function findInTasks(tasks, id) {
261
+ return flattenTasks(tasks).find((t) => t.id === id);
262
+ }
263
+ function findTopLevel(tasks, id) {
264
+ return tasks.find((t) => t.id === id);
265
+ }
266
+ function detectCycle(tasks) {
267
+ const byId = new Map(tasks.map((t) => [t.id, t]));
268
+ const visiting = /* @__PURE__ */ new Set();
269
+ const done = /* @__PURE__ */ new Set();
270
+ const visit = (id) => {
271
+ if (done.has(id)) return null;
272
+ if (visiting.has(id)) return id;
273
+ visiting.add(id);
274
+ for (const dep of byId.get(id)?.dependencies ?? []) {
275
+ const hit = visit(dep);
276
+ if (hit) return hit;
277
+ }
278
+ visiting.delete(id);
279
+ done.add(id);
280
+ return null;
281
+ };
282
+ for (const t of tasks) {
283
+ const hit = visit(t.id);
284
+ if (hit) return byId.get(hit)?.title ?? hit;
285
+ }
286
+ return null;
287
+ }
288
+ function isPinned(t) {
289
+ return t.status === "in_progress" || t.status === "completed";
290
+ }
291
+ function repairOrder(tasks) {
292
+ const byId = new Map(tasks.map((t) => [t.id, t]));
293
+ const placed = /* @__PURE__ */ new Set();
294
+ const movable = tasks.filter((t) => !isPinned(t));
295
+ const result = [];
296
+ const blockerOf = (t) => {
297
+ const id = t.dependencies.find((d) => byId.has(d) && !placed.has(d));
298
+ return id ? byId.get(id) : void 0;
299
+ };
300
+ const place = (t) => {
301
+ result.push(t);
302
+ placed.add(t.id);
303
+ };
304
+ for (const occupant of tasks) {
305
+ if (isPinned(occupant)) {
306
+ const blocker = blockerOf(occupant);
307
+ if (blocker) return { error: pinnedRefusal(blocker, occupant, "before") };
308
+ place(occupant);
309
+ continue;
310
+ }
311
+ const next = movable.findIndex((t) => !blockerOf(t));
312
+ if (next < 0) {
313
+ const [stuck, pin] = trailToPin(movable[0], blockerOf);
314
+ return { error: pinnedRefusal(stuck, pin, "after") };
315
+ }
316
+ place(movable.splice(next, 1)[0]);
317
+ }
318
+ return { tasks: result };
319
+ }
320
+ function trailToPin(task, blockerOf) {
321
+ let at = task;
322
+ for (; ; ) {
323
+ const blocker = blockerOf(at);
324
+ if (!blocker || isPinned(blocker)) return [at, blocker ?? at];
325
+ at = blocker;
326
+ }
327
+ }
328
+ function describeMoves(before, after) {
329
+ const wasAt = new Map(before.map((t, i) => [t.id, i + 1]));
330
+ const moved = after.map((t, i) => ({ t, from: wasAt.get(t.id), to: i + 1 })).filter((m) => m.from !== m.to);
331
+ return moved.length ? moved.map((m) => `"${m.t.title}" #${m.from}\u2192#${m.to}`).join(", ") : null;
332
+ }
333
+ function pinnedRefusal(other, pin, side) {
334
+ const word = pin.status === "in_progress" ? "running" : "completed";
335
+ return `"${other.title}" would have to run ${side} "${pin.title}", which is ${word} and cannot be moved`;
336
+ }
337
+ function canMergeTasks(tasks, selectedIds) {
338
+ if (selectedIds.length < 2) return { ok: false, error: "Select at least two tasks to merge" };
339
+ const idSet = new Set(selectedIds);
340
+ const toMerge = tasks.filter((t) => idSet.has(t.id));
341
+ if (toMerge.length < 2) return { ok: false, error: "At least two valid tasks are required to merge" };
342
+ for (const t of toMerge) {
343
+ if (t.status === "in_progress") return { ok: false, error: `"${t.title}" is running and cannot be merged` };
344
+ if (t.status === "completed") return { ok: false, error: `"${t.title}" is completed and cannot be merged` };
345
+ }
346
+ const ordered = [...toMerge].sort((a, b) => a.order - b.order);
347
+ const survivorId = "__merge_sim__";
348
+ 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) }));
349
+ const unionDeps = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));
350
+ sim.push({ ...ordered[0], id: survivorId, title: "__merge_sim__", order: ordered[0].order, dependencies: unionDeps });
351
+ const cycle = detectCycle(sim);
352
+ if (cycle) return { ok: false, error: `Merging would create a dependency cycle involving "${cycle}"` };
353
+ return { ok: true };
354
+ }
355
+ function dependentsOf(tasks, taskId) {
356
+ return tasks.filter((t) => t.id !== taskId && t.dependencies.includes(taskId));
357
+ }
358
+ function dependencyCandidates(tasks, taskId) {
359
+ if (!taskId) return [...tasks];
360
+ const target = tasks.find((t) => t.id === taskId);
361
+ return target ? tasks.filter((t) => t.order < target.order) : [];
362
+ }
363
+ function canSetDependencies(tasks, taskId, dependencies) {
364
+ const target = tasks.find((t) => t.id === taskId);
365
+ if (!target) return { ok: false, error: "Task not found" };
366
+ const allowed = new Set(dependencyCandidates(tasks, taskId).map((t) => t.id));
367
+ for (const dep of dependencies) {
368
+ if (dep === taskId) return { ok: false, error: `"${target.title}" cannot depend on itself` };
369
+ const found = tasks.find((t) => t.id === dep);
370
+ if (!found) return { ok: false, error: `Unknown dependency "${dep}"` };
371
+ if (!allowed.has(dep)) {
372
+ return { ok: false, error: `"${target.title}" cannot depend on "${found.title}", which comes after it in the plan` };
373
+ }
374
+ }
375
+ return { ok: true };
376
+ }
377
+ function canSplitTask(tasks, taskId) {
378
+ const target = tasks.find((t) => t.id === taskId);
379
+ if (!target) return { ok: false, error: "Task not found" };
380
+ if (target.status === "in_progress") return { ok: false, error: `"${target.title}" is running and cannot be split` };
381
+ if (target.status === "completed") return { ok: false, error: `"${target.title}" is completed and cannot be split` };
382
+ return { ok: true };
383
+ }
384
+ function applyTaskOps(currentTasks, ops, runners, catalog) {
385
+ let tasks = JSON.parse(JSON.stringify(currentTasks));
386
+ const errors = [];
387
+ const summary = [];
388
+ const originalFlatAll = flattenTasks(currentTasks);
389
+ const handleOwner = /* @__PURE__ */ new Map();
390
+ const handleId = /* @__PURE__ */ new Map();
391
+ for (const [i, op] of ops.entries()) {
392
+ const handle = op.handle;
393
+ if (handle === void 0) continue;
394
+ if (typeof handle !== "string" || !handle.trim()) {
395
+ errors.push(`op ${i + 1} (${op.op}): handle must be a non-empty string`);
396
+ continue;
397
+ }
398
+ if (handleOwner.has(handle)) {
399
+ errors.push(`op ${i + 1} (${op.op}): handle "${handle}" is already used earlier in this batch`);
400
+ continue;
401
+ }
402
+ if (refCollidesWithExisting(handle, originalFlatAll)) {
403
+ errors.push(`op ${i + 1} (${op.op}): handle "${handle}" collides with an existing task reference`);
404
+ continue;
405
+ }
406
+ handleOwner.set(handle, i);
407
+ }
408
+ if (errors.length) return { ok: false, tasks: currentTasks, errors, summary: [] };
409
+ const resolveDeps = (deps, opIndex) => {
410
+ const ids = [];
411
+ const bad = [];
412
+ const liveFlat = flattenTasks(tasks);
413
+ for (const d of Array.isArray(deps) ? deps : []) {
414
+ const r = resolveRef(d, originalFlatAll, liveFlat, handleOwner, handleId, opIndex);
415
+ if ("error" in r) {
416
+ bad.push(`${String(d)} (${r.error})`);
417
+ continue;
418
+ }
419
+ if (!liveFlat.some((t) => t.id === r.id)) {
420
+ bad.push(`${String(d)} (no longer exists in this batch)`);
421
+ continue;
422
+ }
423
+ ids.push(r.id);
424
+ }
425
+ return { ids, bad };
426
+ };
427
+ for (const [i, op] of ops.entries()) {
428
+ const label = `op ${i + 1} (${op.op})`;
429
+ switch (op.op) {
430
+ case "update": {
431
+ const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
432
+ if ("error" in ref) {
433
+ errors.push(`${label}: ${ref.error}`);
434
+ break;
435
+ }
436
+ const target = findInTasks(tasks, ref.id);
437
+ if (!target) {
438
+ errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
439
+ break;
440
+ }
441
+ const changes = {};
442
+ for (const key of UPDATABLE_FIELDS) {
443
+ if (op.changes && key in op.changes) changes[key] = op.changes[key];
444
+ }
445
+ if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
446
+ errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
447
+ break;
448
+ }
449
+ const changesForCheck = { ...changes };
450
+ delete changesForCheck.dependencies;
451
+ const check = validateTaskEdit("planner", flattenTasks(tasks), target.id, changesForCheck, catalog);
452
+ if (!check.ok) {
453
+ errors.push(`${label}: ${check.error}`);
454
+ break;
455
+ }
456
+ for (const field of check.clear ?? []) changes[field] = void 0;
457
+ if ("dependencies" in changes) {
458
+ const { ids, bad } = resolveDeps(changes.dependencies, i);
459
+ if (bad.length) {
460
+ errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
461
+ break;
462
+ }
463
+ changes.dependencies = ids.filter((id) => id !== target.id);
464
+ }
465
+ if (Object.keys(changes).length === 0) {
466
+ errors.push(`${label}: no valid changes provided`);
467
+ break;
468
+ }
469
+ tasks = updateTaskInPlan(tasks, target.id, changes);
470
+ const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
471
+ summary.push(`Updated "${target.title}" (${Object.keys(changes).join(", ")})${clearedNote}`);
472
+ break;
473
+ }
474
+ case "add": {
475
+ const spec = op.task ?? {};
476
+ if (!spec.title) {
477
+ errors.push(`${label}: new task needs a title`);
478
+ break;
479
+ }
480
+ const { ids, bad } = resolveDeps(spec.dependencies, i);
481
+ if (bad.length) {
482
+ errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
483
+ break;
484
+ }
485
+ const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : runners[0];
486
+ const validity = checkModelAndModeValidity(runner, spec.assignedModel, spec.taskMode, catalog);
487
+ if (!validity.ok) {
488
+ errors.push(`${label}: ${validity.error}`);
489
+ break;
490
+ }
491
+ const newId = uuidv4();
492
+ tasks = addTaskToPlan(tasks, {
493
+ ...spec,
494
+ id: newId,
495
+ dependencies: ids,
496
+ assignedRunner: runner,
497
+ description: spec.description ?? spec.title,
498
+ prompt: spec.prompt ?? spec.description ?? spec.title
499
+ });
500
+ if (op.handle) handleId.set(op.handle, newId);
501
+ summary.push(`Added "${spec.title}"`);
502
+ break;
503
+ }
504
+ case "remove": {
505
+ const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
506
+ if ("error" in ref) {
507
+ errors.push(`${label}: ${ref.error}`);
508
+ break;
509
+ }
510
+ const target = findInTasks(tasks, ref.id);
511
+ if (!target) {
512
+ errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
513
+ break;
514
+ }
515
+ if (target.status === "in_progress") {
516
+ errors.push(`${label}: "${target.title}" is running and cannot be removed`);
517
+ break;
518
+ }
519
+ if (target.status === "completed") {
520
+ errors.push(`${label}: "${target.title}" is completed and cannot be removed`);
521
+ break;
522
+ }
523
+ tasks = removeTaskFromPlan(tasks, target.id);
524
+ summary.push(`Removed "${target.title}"`);
525
+ break;
526
+ }
527
+ case "reorder": {
528
+ const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
529
+ const badRef = refs.find((r) => "error" in r);
530
+ if (badRef) {
531
+ errors.push(`${label}: ${badRef.error}`);
532
+ break;
533
+ }
534
+ const resolved = refs.map((r) => findTopLevel(tasks, r.id));
535
+ if (resolved.some((t) => !t)) {
536
+ errors.push(`${label}: unknown task in reorder list`);
537
+ break;
538
+ }
539
+ const orderIds = resolved.map((t) => t.id);
540
+ if (new Set(orderIds).size !== tasks.length) {
541
+ errors.push(`${label}: reorder must list every top-level task exactly once (${tasks.length} tasks)`);
542
+ break;
543
+ }
544
+ const byId = new Map(tasks.map((t) => [t.id, t]));
545
+ tasks = renumberTasks(orderIds.map((id) => byId.get(id)));
546
+ summary.push("Reordered tasks");
547
+ break;
548
+ }
549
+ case "merge": {
550
+ const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
551
+ const badRef = refs.find((r) => "error" in r);
552
+ if (badRef) {
553
+ errors.push(`${label}: ${badRef.error}`);
554
+ break;
555
+ }
556
+ const resolved = refs.map((r) => findTopLevel(tasks, r.id));
557
+ if (resolved.some((t) => !t)) {
558
+ errors.push(`${label}: unknown task in merge list (already removed, merged, or split by an earlier op)`);
559
+ break;
560
+ }
561
+ const toMerge = resolved.map((t) => t).sort((a, b) => a.order - b.order);
562
+ if (toMerge.length < 2) {
563
+ errors.push(`${label}: merge needs at least two tasks`);
564
+ break;
565
+ }
566
+ const locked = toMerge.find((t) => t.status === "in_progress" || t.status === "completed");
567
+ if (locked) {
568
+ errors.push(`${label}: "${locked.title}" is ${locked.status === "in_progress" ? "running" : "completed"} and cannot be merged`);
569
+ break;
570
+ }
571
+ const idSet = new Set(toMerge.map((t) => t.id));
572
+ const unionDepsRaw = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));
573
+ const { ids, bad } = resolveDeps(unionDepsRaw, i);
574
+ if (bad.length) {
575
+ errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
576
+ break;
577
+ }
578
+ const spec = op.merged ?? {};
579
+ if (!spec.title) {
580
+ errors.push(`${label}: merged task needs a title`);
581
+ break;
582
+ }
583
+ const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : toMerge[0].assignedRunner;
584
+ const mergedTask = createTask({
585
+ title: spec.title,
586
+ description: spec.description ?? toMerge.map((t) => t.description).filter(Boolean).join("\n\n"),
587
+ prompt: spec.prompt ?? (toMerge.map((t) => t.prompt).filter(Boolean).join("\n\n---\n\n") || void 0),
588
+ type: spec.type ?? (toMerge.some((t) => t.type === "user") ? "user" : "ai"),
589
+ dependencies: ids,
590
+ assignedRunner: runner,
591
+ assignedModel: spec.assignedModel ?? toMerge.find((t) => t.assignedModel)?.assignedModel,
592
+ thinkingEffort: spec.thinkingEffort ?? toMerge.find((t) => t.thinkingEffort)?.thinkingEffort,
593
+ taskMode: spec.taskMode ?? toMerge[0].taskMode,
594
+ autonomy: spec.autonomy ?? toMerge.find((t) => t.autonomy)?.autonomy,
595
+ sliceType: spec.sliceType ?? toMerge.find((t) => t.sliceType)?.sliceType,
596
+ userStoriesCovered: spec.userStoriesCovered ?? (toMerge.flatMap((t) => t.userStoriesCovered ?? []).length ? [...new Set(toMerge.flatMap((t) => t.userStoriesCovered ?? []))] : void 0)
597
+ });
598
+ const mergedId = mergedTask.id;
599
+ const firstId = toMerge[0].id;
600
+ const result = [];
601
+ let inserted = false;
602
+ for (const t of tasks) {
603
+ if (idSet.has(t.id)) {
604
+ if (!inserted && t.id === firstId) {
605
+ result.push(mergedTask);
606
+ inserted = true;
607
+ }
608
+ continue;
609
+ }
610
+ result.push({ ...t, dependencies: t.dependencies.map((d) => idSet.has(d) ? mergedId : d) });
611
+ }
612
+ if (!inserted) result.push(mergedTask);
613
+ tasks = renumberTasks(result);
614
+ if (op.handle) handleId.set(op.handle, mergedId);
615
+ summary.push(`Merged ${toMerge.length} tasks into "${spec.title}"`);
616
+ break;
617
+ }
618
+ case "split": {
619
+ const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, i);
620
+ if ("error" in ref) {
621
+ errors.push(`${label}: ${ref.error}`);
622
+ break;
623
+ }
624
+ const target = findTopLevel(tasks, ref.id);
625
+ if (!target) {
626
+ errors.push(`${label}: task "${op.taskId}" not found`);
627
+ break;
628
+ }
629
+ if (target.status === "in_progress") {
630
+ errors.push(`${label}: "${target.title}" is running and cannot be split`);
631
+ break;
632
+ }
633
+ if (target.status === "completed") {
634
+ errors.push(`${label}: "${target.title}" is completed and cannot be split`);
635
+ break;
636
+ }
637
+ const parts = op.parts ?? [];
638
+ if (parts.length < 2) {
639
+ errors.push(`${label}: split needs at least two parts`);
640
+ break;
641
+ }
642
+ const newTasks = [];
643
+ let prevId;
644
+ let buildError = false;
645
+ for (let p = 0; p < parts.length; p++) {
646
+ const spec = parts[p];
647
+ if (!spec || !spec.title) {
648
+ errors.push(`${label}: split part ${p + 1} needs a title`);
649
+ buildError = true;
650
+ break;
651
+ }
652
+ let deps;
653
+ if (p === 0) {
654
+ const { ids, bad } = resolveDeps(target.dependencies, i);
655
+ if (bad.length) {
656
+ errors.push(`${label}: split part 1 unknown dependencies: ${bad.join(", ")}`);
657
+ buildError = true;
658
+ break;
659
+ }
660
+ deps = ids;
661
+ } else {
662
+ deps = prevId ? [prevId] : [];
663
+ }
664
+ const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : target.assignedRunner;
665
+ const nt = createTask({
666
+ title: spec.title,
667
+ description: spec.description ?? spec.title,
668
+ prompt: spec.prompt ?? spec.description ?? spec.title,
669
+ type: spec.type ?? target.type,
670
+ dependencies: deps,
671
+ assignedRunner: runner,
672
+ assignedModel: spec.assignedModel ?? target.assignedModel,
673
+ taskMode: spec.taskMode ?? target.taskMode,
674
+ autonomy: spec.autonomy ?? target.autonomy,
675
+ sliceType: spec.sliceType ?? target.sliceType,
676
+ userStoriesCovered: spec.userStoriesCovered ?? target.userStoriesCovered
677
+ });
678
+ newTasks.push(nt);
679
+ prevId = nt.id;
680
+ }
681
+ if (buildError) break;
682
+ const tailId = newTasks[newTasks.length - 1].id;
683
+ const result = [];
684
+ for (const t of tasks) {
685
+ if (t.id === target.id) {
686
+ result.push(...newTasks);
687
+ continue;
688
+ }
689
+ result.push({ ...t, dependencies: t.dependencies.map((d) => d === target.id ? tailId : d) });
690
+ }
691
+ tasks = renumberTasks(result);
692
+ if (op.handle) handleId.set(op.handle, tailId);
693
+ summary.push(`Split "${target.title}" into ${newTasks.length} tasks`);
694
+ break;
695
+ }
696
+ case "rearm": {
697
+ const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
698
+ if ("error" in ref) {
699
+ errors.push(`${label}: ${ref.error}`);
700
+ break;
701
+ }
702
+ const target = findInTasks(tasks, ref.id);
703
+ if (!target) {
704
+ errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
705
+ break;
706
+ }
707
+ if (target.status === "in_progress") {
708
+ errors.push(`${label}: "${target.title}" is running and cannot be re-armed`);
709
+ break;
710
+ }
711
+ const changes = {};
712
+ for (const key of UPDATABLE_FIELDS) {
713
+ if (op.changes && key in op.changes) changes[key] = op.changes[key];
714
+ }
715
+ if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
716
+ errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
717
+ break;
718
+ }
719
+ const changesForCheck = { ...changes };
720
+ delete changesForCheck.dependencies;
721
+ const check = validateTaskEdit("direct", flattenTasks(tasks), target.id, changesForCheck, catalog);
722
+ if (!check.ok) {
723
+ errors.push(`${label}: ${check.error}`);
724
+ break;
725
+ }
726
+ for (const field of check.clear ?? []) changes[field] = void 0;
727
+ if ("dependencies" in changes) {
728
+ const { ids, bad } = resolveDeps(changes.dependencies, i);
729
+ if (bad.length) {
730
+ errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
731
+ break;
732
+ }
733
+ changes.dependencies = ids.filter((id) => id !== target.id);
734
+ }
735
+ tasks = updateTaskInPlan(tasks, target.id, { ...changes, status: "pending", verdict: void 0, outputSummary: void 0 });
736
+ for (const dep of flattenTasks(tasks)) {
737
+ if (dep.status === "blocked" && dep.dependencies.includes(target.id)) {
738
+ tasks = updateTaskInPlan(tasks, dep.id, { status: "pending" });
739
+ }
740
+ }
741
+ const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
742
+ const changedNote = Object.keys(changes).length ? ` (${Object.keys(changes).join(", ")})` : "";
743
+ summary.push(`Re-armed "${target.title}"${changedNote}${clearedNote}`);
744
+ break;
745
+ }
746
+ default:
747
+ errors.push(`${label}: unknown op "${op.op}"`);
748
+ }
749
+ if (errors.length) break;
750
+ }
751
+ if (!errors.length) {
752
+ const flat = flattenTasks(tasks);
753
+ const idSet = new Set(flat.map((t) => t.id));
754
+ for (const t of flat) {
755
+ for (const dep of t.dependencies) {
756
+ if (!idSet.has(dep)) errors.push(`"${t.title}" depends on a task that no longer exists (${dep})`);
757
+ }
758
+ }
759
+ const cycleAt = detectCycle(flat);
760
+ if (cycleAt) errors.push(`Dependency cycle detected involving "${cycleAt}"`);
761
+ if (!errors.length) {
762
+ const repaired = repairOrder(tasks);
763
+ if ("error" in repaired) {
764
+ errors.push(repaired.error);
765
+ } else {
766
+ const moves = describeMoves(tasks, repaired.tasks);
767
+ if (moves) summary.push(`Reordered to keep dependencies first: ${moves}`);
768
+ tasks = renumberTasks(repaired.tasks);
769
+ }
770
+ }
771
+ }
772
+ return errors.length ? { ok: false, tasks: currentTasks, errors, summary: [] } : { ok: true, tasks, errors: [], summary };
773
+ }
774
+
775
+ // src/services/researchStepSummary.ts
776
+ function ellipsize(text, max) {
777
+ const oneLine = text.replace(/\s+/g, " ").trim();
778
+ return oneLine.length > max ? `${oneLine.slice(0, max)}\u2026` : oneLine;
779
+ }
780
+ function summarizeToolCall(tool, argsJson, toolLabel) {
781
+ const display = toolLabel?.trim() || tool;
782
+ if (tool === "agent_tool") {
783
+ try {
784
+ const args = JSON.parse(argsJson);
785
+ const hint = args.path ?? args.file_path ?? args.filePath ?? args.pattern ?? args.query ?? args.url ?? args.command;
786
+ if (hint) return `${display} ${ellipsize(String(hint), 50)}`;
787
+ } catch {
788
+ }
789
+ return display;
790
+ }
791
+ if (tool === "spawn_research_agent") {
792
+ try {
793
+ const args = JSON.parse(argsJson);
794
+ const prompt = typeof args.prompt === "string" ? args.prompt : "";
795
+ const oneLine = prompt.replace(/\s+/g, " ").trim();
796
+ return `spawn: "${oneLine.slice(0, 60)}${oneLine.length > 60 ? "\u2026" : ""}"`;
797
+ } catch {
798
+ return "spawn_research_agent";
799
+ }
800
+ }
801
+ try {
802
+ const args = JSON.parse(argsJson);
803
+ if (tool === "find_symbol" && args.symbol) {
804
+ return `find_symbol ${args.symbol}${args.language ? ` [${args.language}]` : ""}`;
805
+ }
806
+ if (tool === "web_search" && args.query) {
807
+ return `web_search "${ellipsize(String(args.query), 50)}"`;
808
+ }
809
+ if (tool === "fetch" && args.url) {
810
+ return `fetch ${ellipsize(String(args.url), 60)}`;
811
+ }
812
+ if (tool === "glob" && args.pattern) {
813
+ return `glob ${ellipsize(String(args.pattern), 50)}`;
814
+ }
815
+ if (tool === "grep" && args.pattern) {
816
+ const mode = args.output_mode && args.output_mode !== "content" ? ` (${args.output_mode})` : "";
817
+ const scope = args.include ? ` in ${args.include}` : "";
818
+ return `grep ${ellipsize(String(args.pattern), 40)}${scope}${mode}`;
819
+ }
820
+ const path = args.filePath || args.file_path || args.path;
821
+ if (path) {
822
+ const basename = String(path).split("/").pop() || String(path);
823
+ return `${display} ${basename}`;
824
+ }
825
+ if (args.pattern) {
826
+ return `${display} ${args.pattern}`;
827
+ }
828
+ if (args.command) {
829
+ return `bash: ${String(args.command).slice(0, 40)}`;
830
+ }
831
+ } catch {
832
+ }
833
+ return display;
834
+ }
835
+ function classifyOutcome(success, output) {
836
+ if (!success) {
837
+ if (/^Command refused:/.test(output) || /is not available\. You are a read-only research subagent/.test(output)) return "refused";
838
+ if (/not approved/i.test(output) || /^Access denied:/.test(output) || /needs user approval/i.test(output)) return "denied";
839
+ return "failure";
840
+ }
841
+ return "success";
842
+ }
843
+
844
+ export {
845
+ effectiveAllowlist,
846
+ filterModelsForPrompt,
847
+ clampThinkingEffort,
848
+ coerceAssignments,
849
+ validateTaskEdit,
850
+ UPDATABLE_FIELDS,
851
+ textHasTaskOps,
852
+ parseTaskOpsJson,
853
+ canMergeTasks,
854
+ dependentsOf,
855
+ dependencyCandidates,
856
+ canSetDependencies,
857
+ canSplitTask,
858
+ applyTaskOps,
859
+ summarizeToolCall,
860
+ classifyOutcome
861
+ };
862
+ //# sourceMappingURL=chunk-YI4ZDZEE.mjs.map