@ordewell/core 0.10.5 → 0.10.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 (44) hide show
  1. package/dist/{IRunner-Crkt3tHD.d.ts → IFileSystem-C4_mpfze.d.ts} +97 -14
  2. package/dist/{IRunner-lMQIa5Wc.d.mts → IFileSystem-DUgeWWQN.d.mts} +97 -14
  3. package/dist/{ModeResolver-B4hhABNf.d.mts → ModeResolver-4qNjxcDs.d.mts} +1 -1
  4. package/dist/{ModeResolver-CUR5jCSU.d.ts → ModeResolver-DGK2rWHE.d.ts} +1 -1
  5. package/dist/{Task-D4QHjdGv.d.mts → Task-C0o4i3Nn.d.mts} +23 -4
  6. package/dist/{Task-D4QHjdGv.d.ts → Task-C0o4i3Nn.d.ts} +23 -4
  7. package/dist/{chunk-6LEA6WAM.mjs → chunk-DMG4ISUG.mjs} +923 -45
  8. package/dist/chunk-DMG4ISUG.mjs.map +1 -0
  9. package/dist/{chunk-RT4NQ2BZ.mjs → chunk-EEE2GBLG.mjs} +114 -27
  10. package/dist/chunk-EEE2GBLG.mjs.map +1 -0
  11. package/dist/chunk-H7QX32BH.mjs +647 -0
  12. package/dist/chunk-H7QX32BH.mjs.map +1 -0
  13. package/dist/{chunk-F3KIX7WR.mjs → chunk-LTHKFBKM.mjs} +4 -4
  14. package/dist/chunk-LTHKFBKM.mjs.map +1 -0
  15. package/dist/index.d.mts +278 -1701
  16. package/dist/index.d.ts +278 -1701
  17. package/dist/index.js +2729 -1903
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +2171 -1523
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/parsing.d.mts +2 -2
  22. package/dist/parsing.d.ts +2 -2
  23. package/dist/parsing.js +231 -75
  24. package/dist/parsing.js.map +1 -1
  25. package/dist/parsing.mjs +3 -3
  26. package/dist/plan-utils.d.mts +20 -4
  27. package/dist/plan-utils.d.ts +20 -4
  28. package/dist/plan-utils.js +255 -97
  29. package/dist/plan-utils.js.map +1 -1
  30. package/dist/plan-utils.mjs +33 -19
  31. package/dist/{plan-utils-wI8MuDBC.d.mts → runSettingsText-BgOmLxdw.d.mts} +1941 -108
  32. package/dist/{plan-utils-BVJCzNvm.d.ts → runSettingsText-BgXvajj2.d.ts} +1941 -108
  33. package/dist/testing.d.mts +2 -3
  34. package/dist/testing.d.ts +2 -3
  35. package/dist/testing.js.map +1 -1
  36. package/dist/testing.mjs +1 -1
  37. package/package.json +1 -1
  38. package/dist/IFileSystem-ClBdp59f.d.ts +0 -78
  39. package/dist/IFileSystem-Db34maxC.d.mts +0 -78
  40. package/dist/chunk-4QLFM73A.mjs +0 -1407
  41. package/dist/chunk-4QLFM73A.mjs.map +0 -1
  42. package/dist/chunk-6LEA6WAM.mjs.map +0 -1
  43. package/dist/chunk-F3KIX7WR.mjs.map +0 -1
  44. package/dist/chunk-RT4NQ2BZ.mjs.map +0 -1
@@ -1,5 +1,5 @@
1
- import { a3 as RunnerId, ah as Task } from './Task-D4QHjdGv.mjs';
2
- import { R as RunnerModeInfo } from './ModeResolver-B4hhABNf.mjs';
1
+ import { a4 as RunnerId, ai as Task } from './Task-C0o4i3Nn.mjs';
2
+ import { R as RunnerModeInfo } from './ModeResolver-4qNjxcDs.mjs';
3
3
 
4
4
  declare function parsePlanJson(raw: string, runners: RunnerId[], runnerModes?: Record<RunnerId, RunnerModeInfo[]>, autonomousDefault?: boolean): Task[];
5
5
 
package/dist/parsing.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a3 as RunnerId, ah as Task } from './Task-D4QHjdGv.js';
2
- import { R as RunnerModeInfo } from './ModeResolver-CUR5jCSU.js';
1
+ import { a4 as RunnerId, ai as Task } from './Task-C0o4i3Nn.js';
2
+ import { R as RunnerModeInfo } from './ModeResolver-DGK2rWHE.js';
3
3
 
4
4
  declare function parsePlanJson(raw: string, runners: RunnerId[], runnerModes?: Record<RunnerId, RunnerModeInfo[]>, autonomousDefault?: boolean): Task[];
5
5
 
package/dist/parsing.js CHANGED
@@ -34,9 +34,68 @@ var import_uuid = require("uuid");
34
34
  var SKILL_NAME_PATTERN = "[a-z0-9][a-z0-9_-]*";
35
35
  var SKILL_TOKEN = new RegExp(`(^|\\s)\\/(${SKILL_NAME_PATTERN})([,.!?;:]*)(?=\\s|$)`, "gi");
36
36
 
37
- // src/services/TaskRefs.ts
38
- function taskOrderLabel(task, parent) {
39
- return parent ? `${parent.label ?? parent.order}.${task.order}` : String(task.order);
37
+ // src/services/PlanTree.ts
38
+ function* walkPlan(tasks) {
39
+ function* walk(level, ancestors, parentLabel) {
40
+ for (const task of level) {
41
+ const label = parentLabel ? `${parentLabel}.${task.order}` : String(task.order);
42
+ yield { task, parent: ancestors.at(-1), ancestors, siblings: level, label };
43
+ yield* walk(task.subtasks ?? [], [...ancestors, task], label);
44
+ }
45
+ }
46
+ yield* walk(tasks, []);
47
+ }
48
+ function taskPlacement(tasks, id) {
49
+ for (const row of walkPlan(tasks)) if (row.task.id === id) return row;
50
+ return void 0;
51
+ }
52
+ function subtreeIds(tasks, id) {
53
+ const found = taskPlacement(tasks, id);
54
+ return new Set(found ? [...walkPlan([found.task])].map(({ task }) => task.id) : []);
55
+ }
56
+ function dependencyTargetErrors(tasks, taskId, dependencies) {
57
+ const found = taskPlacement(tasks, taskId);
58
+ if (!found) return ["Task not found"];
59
+ const descendants = subtreeIds(tasks, taskId);
60
+ const errors = [];
61
+ for (const dep of dependencies) {
62
+ if (dep === taskId) {
63
+ errors.push(`"${found.task.title}" cannot depend on itself`);
64
+ continue;
65
+ }
66
+ const target = taskPlacement(tasks, dep)?.task;
67
+ if (!target) {
68
+ errors.push(`Unknown dependency "${dep}"`);
69
+ continue;
70
+ }
71
+ if (found.ancestors.some((t) => t.id === dep) || descendants.has(dep)) {
72
+ errors.push(`"${found.task.title}" cannot depend on "${target.title}", its ancestor or descendant`);
73
+ }
74
+ }
75
+ return errors;
76
+ }
77
+ function findDependencyCycle(tasks) {
78
+ const byId = new Map([...walkPlan(tasks)].map(({ task }) => [task.id, task]));
79
+ const path = [];
80
+ const done = /* @__PURE__ */ new Set();
81
+ const visit = (id) => {
82
+ if (done.has(id)) return null;
83
+ const at = path.indexOf(id);
84
+ if (at >= 0) return path.slice(at);
85
+ path.push(id);
86
+ for (const dep of byId.get(id)?.dependencies ?? []) {
87
+ const hit = visit(dep);
88
+ if (hit) return hit;
89
+ }
90
+ path.pop();
91
+ done.add(id);
92
+ return null;
93
+ };
94
+ for (const id of byId.keys()) {
95
+ const hit = visit(id);
96
+ if (hit) return hit.map((key) => byId.get(key));
97
+ }
98
+ return null;
40
99
  }
41
100
 
42
101
  // src/models/Task.ts
@@ -85,14 +144,7 @@ function opsFlag(value, type = "ai") {
85
144
  }
86
145
  var DEFAULT_RUNNERS = ["claude-code"];
87
146
  function flattenTasksWithParents(tasks) {
88
- const rows = [];
89
- const walk = (t, parent, parentLabel) => {
90
- const label = taskOrderLabel(t, parent ? { order: parent.order, label: parentLabel } : void 0);
91
- rows.push({ task: t, parent, label, ref: `#${label}` });
92
- for (const sub of t.subtasks ?? []) walk(sub, t, label);
93
- };
94
- for (const t of tasks) walk(t, null);
95
- return rows;
147
+ return [...walkPlan(tasks)].map(({ task, parent, label }) => ({ task, parent: parent ?? null, label, ref: `#${label}` }));
96
148
  }
97
149
 
98
150
  // src/services/JsonExtractor.ts
@@ -300,32 +352,144 @@ function effectiveAllowlist(allowlist, runner, modelsByRunner) {
300
352
  const kept = allowlist.filter((id) => own.has(id) || !elsewhere.has(id));
301
353
  return kept.length > 0 ? kept : void 0;
302
354
  }
355
+ var EFFORT_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
356
+ var EFFORT_ALIASES = { disabled: "none", adaptive: "medium" };
357
+ function clampThinkingEffort(effort, variants) {
358
+ if (!effort) return void 0;
359
+ if (variants.length === 0) return void 0;
360
+ const ids = new Set(variants.map((v) => v.id));
361
+ if (ids.has(effort)) return effort;
362
+ const canon = EFFORT_ALIASES[effort] ?? effort;
363
+ if (ids.has(canon)) return canon;
364
+ const idx = EFFORT_LADDER.indexOf(canon);
365
+ if (idx < 0) return void 0;
366
+ for (let d = 1; d < EFFORT_LADDER.length; d++) {
367
+ const lower = EFFORT_LADDER[idx - d];
368
+ if (lower && ids.has(lower)) return lower;
369
+ const higher = EFFORT_LADDER[idx + d];
370
+ if (higher && ids.has(higher)) return higher;
371
+ }
372
+ return void 0;
373
+ }
303
374
 
304
- // src/services/TaskEditValidator.ts
305
- function checkModelAndModeValidity(runner, assignedModel, taskMode, catalog) {
306
- if (!catalog) return { ok: true };
307
- if (assignedModel) {
308
- const models = catalog.modelsByRunner[runner];
309
- if (models) {
310
- const modelId = assignedModel.modelId;
311
- const allowed = effectiveAllowlist(catalog.perRunnerAllowlist?.[runner], runner, catalog.modelsByRunner);
312
- if (!models.some((m) => m.modelId === modelId) && !allowed?.includes(modelId)) {
313
- return { ok: false, error: `Runner "${runner}" does not offer model "${modelId}"` };
314
- }
315
- if (allowed && !allowed.includes(modelId)) {
316
- return { ok: false, error: `Model "${modelId}" is excluded by the allowlist for runner "${runner}"` };
317
- }
375
+ // src/services/TaskAssignment.ts
376
+ function liveModelRefusal(task) {
377
+ return `"${task.title}" has a live runner, so its model and effort cannot change now: stop the task first, or use the runner's own /model`;
378
+ }
379
+ function liveRunnerRefusal(task) {
380
+ return `"${task.title}" has a live runner, so its runner cannot change now: stop the task first`;
381
+ }
382
+ function sameAssignment(a, b) {
383
+ return a?.modelId === b?.modelId && a?.thinkingEffort === b?.thinkingEffort;
384
+ }
385
+ function readOnlyOn(mode, runner, catalog) {
386
+ return mode !== void 0 && mode === taggedReadOnlyMode(catalog.runnerModes?.[runner]);
387
+ }
388
+ function modeFor(write, task, moved) {
389
+ const { writer, before, catalog } = write;
390
+ const prior = before ?? write.from;
391
+ const runner = task.assignedRunner;
392
+ const modes = catalog.runnerModes?.[runner];
393
+ const picked = (writer === "user" || writer === "userAdd") && task.taskMode !== void 0 && task.taskMode !== prior?.taskMode;
394
+ if (picked) {
395
+ if (modes && modes.length > 0 && !modes.some((m) => m.id === task.taskMode)) {
396
+ return { ok: false, error: `Runner "${runner}" has no task mode "${task.taskMode}": valid modes: ${modes.map((m) => m.id).join(", ")}` };
318
397
  }
398
+ const unavailable = unavailableModeReason(modes, task.taskMode);
399
+ if (unavailable) return { ok: false, error: `Mode "${task.taskMode}" of runner "${runner}" is unavailable on this host: ${unavailable}` };
400
+ return { ok: true, mode: task.taskMode };
319
401
  }
320
- if (taskMode !== void 0) {
321
- const modes = catalog.runnerModes[runner];
322
- if (modes && modes.length > 0 && !modes.some((m) => m.id === taskMode)) {
323
- return { ok: false, error: `Runner "${runner}" has no task mode "${taskMode}": valid modes: ${modes.map((m) => m.id).join(", ")}` };
324
- }
325
- const unavailable = unavailableModeReason(modes, taskMode);
326
- if (unavailable) return { ok: false, error: `Mode "${taskMode}" of runner "${runner}" is unavailable on this host: ${unavailable}` };
402
+ const becameAi = before?.type === "user";
403
+ if (writer === "runner" || !catalog.runnerModes || before && !moved && !becameAi && write.readOnly === void 0) return { ok: true, mode: task.taskMode };
404
+ const carried = prior !== void 0 && task.taskMode === prior.taskMode && readOnlyOn(prior.taskMode, prior.assignedRunner, catalog);
405
+ const readOnly = write.readOnly ?? (carried || readOnlyOn(task.taskMode, runner, catalog));
406
+ if (readOnly && !readOnlyMode(modes) && writer !== "plan" && writer !== "edit") {
407
+ const reason = unavailableModeReason(modes, taggedReadOnlyMode(modes));
408
+ const able = readOnlyRunners(catalog.runnerModes).filter((r) => r !== runner);
409
+ return {
410
+ ok: false,
411
+ error: `"${task.title}" is read-only, and runner "${runner}" has no read-only mode${reason ? " available on this host" : ""}: give it another mode first${able.length > 0 ? `, or move it to one of: ${able.join(", ")}` : ""}${reason ? `. ${reason}` : ""}`
412
+ };
413
+ }
414
+ const mode = plannedMode(runner, catalog.runnerModes, { readOnly, current: task.taskMode }, catalog.autonomousDefault ?? true);
415
+ return mode.ok ? { ok: true, mode: mode.mode ?? task.taskMode } : mode;
416
+ }
417
+ function resolveAssignment(write) {
418
+ const { writer, before, catalog } = write;
419
+ const written = write.task;
420
+ const prior = before ?? write.from;
421
+ const moved = prior !== void 0 && prior.assignedRunner !== written.assignedRunner;
422
+ const title = (before ?? written).title;
423
+ const live = writer !== "runner" && !!before && !!catalog.liveRunner?.(before.id);
424
+ if (live && moved) return { ok: false, field: "assignedRunner", error: liveRunnerRefusal({ title }) };
425
+ if (live && !sameAssignment(before?.assignedModel, written.assignedModel)) return { ok: false, field: "assignedModel", error: liveModelRefusal({ title }) };
426
+ if (written.type === "user") return { ok: true, task: written, adjustments: [] };
427
+ const mode = modeFor(write, written, moved);
428
+ if (!mode.ok) return { ok: false, field: "taskMode", error: mode.error };
429
+ const model = live ? { ok: true, assignment: written.assignedModel } : modelFor(write, moved);
430
+ if (!model.ok) return { ok: false, field: "assignedModel", error: model.error };
431
+ const task = model.assignment === written.assignedModel && mode.mode === written.taskMode ? written : { ...written, assignedModel: model.assignment, taskMode: mode.mode };
432
+ const adjustments = [];
433
+ const note = (field, from, to) => {
434
+ if ((from ?? null) !== (to ?? null)) adjustments.push({ taskId: written.id, title: written.title, field, from: from ?? null, to: to ?? null });
435
+ };
436
+ note("assignedModel", written.assignedModel?.modelId, task.assignedModel?.modelId);
437
+ note("thinkingEffort", written.assignedModel?.thinkingEffort, task.assignedModel?.thinkingEffort);
438
+ return { ok: true, task, adjustments };
439
+ }
440
+ function modelFor(write, moved) {
441
+ const { writer, before, catalog } = write;
442
+ const prior = before ?? write.from;
443
+ const written = write.task.assignedModel;
444
+ const runner = write.task.assignedRunner;
445
+ const models = catalog.modelsByRunner;
446
+ const own = models[runner] ?? [];
447
+ const listing = (id2) => own.find((m) => m.modelId === id2);
448
+ const carriedFrom = moved && written && prior?.assignedModel?.modelId === written.modelId ? prior.assignedRunner : void 0;
449
+ const seen = carriedFrom && written && !models[carriedFrom]?.some((m) => m.modelId === written.modelId) ? { ...models, [carriedFrom]: [...models[carriedFrom] ?? [], { modelId: written.modelId, modelLabel: written.modelLabel, variants: [] }] } : models;
450
+ const allowed = effectiveAllowlist(catalog.perRunnerAllowlist?.[runner], runner, seen);
451
+ const allows = (id2) => !allowed || allowed.includes(id2);
452
+ const settled = (assignment) => {
453
+ const known = listing(assignment.modelId);
454
+ if (!known) return assignment;
455
+ const thinkingEffort = clampThinkingEffort(assignment.thinkingEffort, known.variants);
456
+ const availableVariants = known.variants.map((v) => v.id);
457
+ if (thinkingEffort === assignment.thinkingEffort && assignment.availableVariants?.join() === availableVariants.join()) return assignment;
458
+ const next = { ...assignment, availableVariants };
459
+ if (thinkingEffort === void 0) delete next.thinkingEffort;
460
+ else next.thinkingEffort = thinkingEffort;
461
+ return next;
462
+ };
463
+ const refill = (effort) => {
464
+ const id2 = allowed?.find((a) => listing(a)) ?? allowed?.[0] ?? own[0]?.modelId;
465
+ if (id2 === void 0) return void 0;
466
+ const known = listing(id2);
467
+ const thinkingEffort = clampThinkingEffort(effort, known?.variants ?? []);
468
+ return {
469
+ modelId: id2,
470
+ modelLabel: known?.modelLabel ?? id2,
471
+ ...thinkingEffort !== void 0 ? { thinkingEffort } : {},
472
+ ...known ? { availableVariants: known.variants.map((v) => v.id) } : {}
473
+ };
474
+ };
475
+ if (!written) return { ok: true, assignment: writer === "runner" ? void 0 : refill(void 0) };
476
+ if (writer === "runner") return { ok: true, assignment: settled(written) };
477
+ const id = written.modelId;
478
+ const same = prior?.assignedModel?.modelId === id;
479
+ if (before && same && !moved) return { ok: true, assignment: settled(written) };
480
+ const listedElsewhere = Object.entries(models).some(([other, list]) => other !== runner && list?.some((m) => m.modelId === id));
481
+ if (same) {
482
+ const fits = !moved || !!listing(id) || own.length === 0 && !listedElsewhere;
483
+ return { ok: true, assignment: fits && allows(id) ? settled(written) : refill(written.thinkingEffort) };
327
484
  }
328
- return { ok: true };
485
+ if (writer === "edit" || writer === "user") {
486
+ const offered = own.length === 0 || !!listing(id) || !!allowed?.includes(id);
487
+ if (!offered) return { ok: false, error: `Runner "${runner}" does not offer model "${id}"` };
488
+ if (!allows(id)) return { ok: false, error: `Model "${id}" is excluded by the allowlist for runner "${runner}"` };
489
+ return { ok: true, assignment: settled(written) };
490
+ }
491
+ const disowned = !listing(id) && listedElsewhere;
492
+ return { ok: true, assignment: disowned || !allows(id) ? refill(written.thinkingEffort) : settled(written) };
329
493
  }
330
494
 
331
495
  // src/services/PlannedTask.ts
@@ -366,6 +530,7 @@ function plannedTask(spec, catalog, at = {}) {
366
530
  const errors = [];
367
531
  const ignored = [];
368
532
  const renamed = /* @__PURE__ */ new Map();
533
+ const adjustments = [];
369
534
  const title = text(raw.title) ?? "";
370
535
  const where = taskWhere(title, parent?.title);
371
536
  const plannerId = text(raw.id);
@@ -382,18 +547,7 @@ function plannedTask(spec, catalog, at = {}) {
382
547
  if (ai && ops && raw.readOnly === true) ignored.push({ taskId: id, field: "readOnly", message: `${where}: ${OPS_NOT_READ_ONLY}` });
383
548
  const asked = typeof raw.readOnly === "boolean" ? raw.readOnly : void 0;
384
549
  const readOnly = ai && !ops && (asked ?? from?.readOnly ?? parent?.readOnly ?? false);
385
- let taskMode = base.taskMode;
386
- if (ai && catalog.runners.includes(assignedRunner)) {
387
- const mode = plannedMode(assignedRunner, catalog.runnerModes, { readOnly, current: base.taskMode }, catalog.autonomousDefault ?? true);
388
- if (!mode.ok) errors.push({ taskId: id, field: "readOnly", message: `${where}: ${mode.error}` });
389
- else if (mode.mode !== void 0) taskMode = mode.mode;
390
- }
391
- if (taskMode === void 0 && readOnly && !catalog.runnerModes) taskMode = "plan";
392
550
  const assignedModel = ai ? modelOf(raw, base.assignedModel) : void 0;
393
- if (assignedModel && isRecord(raw.assignedModel) && catalog.models && catalog.runners.includes(assignedRunner)) {
394
- const validity = checkModelAndModeValidity(assignedRunner, assignedModel, void 0, catalog.models);
395
- if (!validity.ok) errors.push({ taskId: id, field: "assignedModel", message: `${where}: ${validity.error}` });
396
- }
397
551
  const slice = (value) => value === "AFK" || value === "HITL" ? value : void 0;
398
552
  const autonomy = ai ? slice(raw.autonomy) ?? base.autonomy ?? parent?.autonomy : void 0;
399
553
  const sliceType = slice(raw.sliceType) ?? base.sliceType ?? (ai ? parent?.sliceType : void 0);
@@ -404,10 +558,11 @@ function plannedTask(spec, catalog, at = {}) {
404
558
  subtasks.push(child.task);
405
559
  errors.push(...child.errors);
406
560
  ignored.push(...child.ignored);
561
+ adjustments.push(...child.adjustments);
407
562
  for (const [was, now] of child.renamed) renamed.set(was, now);
408
563
  }
409
564
  }
410
- const task = createTask({
565
+ const fields = {
411
566
  id,
412
567
  order: Number(raw.order ?? 0),
413
568
  title,
@@ -419,17 +574,36 @@ function plannedTask(spec, catalog, at = {}) {
419
574
  subtasks,
420
575
  assignedModel,
421
576
  assignedRunner,
422
- taskMode,
423
577
  autonomy,
424
578
  sliceType,
425
579
  userStoriesCovered: Array.isArray(raw.userStoriesCovered) ? raw.userStoriesCovered.map(String) : base.userStoriesCovered,
426
580
  ops,
427
581
  skills: from ? inheritedSkills(raw.skills, base.skills) : skillNames(raw.skills)
428
- });
582
+ };
583
+ let taskMode = base.taskMode;
584
+ let landedModel = assignedModel;
585
+ if (ai && catalog.runners.includes(assignedRunner)) {
586
+ const resolved = resolveAssignment({
587
+ writer: catalog.models ? "edit" : "plan",
588
+ task: { ...createTask(fields), taskMode },
589
+ from: from ? { assignedRunner: base.assignedRunner ?? assignedRunner, assignedModel: base.assignedModel, taskMode: base.taskMode } : void 0,
590
+ readOnly,
591
+ catalog: { modelsByRunner: {}, ...catalog.models, runnerModes: catalog.runnerModes, autonomousDefault: catalog.autonomousDefault }
592
+ });
593
+ if (!resolved.ok) {
594
+ errors.push({ taskId: id, field: resolved.field === "taskMode" ? "readOnly" : resolved.field, message: `${where}: ${resolved.error}` });
595
+ } else {
596
+ taskMode = resolved.task.taskMode;
597
+ landedModel = resolved.task.assignedModel;
598
+ adjustments.push(...resolved.adjustments);
599
+ }
600
+ }
601
+ if (taskMode === void 0 && readOnly && !catalog.runnerModes) taskMode = "plan";
602
+ const task = createTask({ ...fields, assignedModel: landedModel, taskMode });
429
603
  if (Array.isArray(raw.skills)) skillsAsked.set(task, raw.skills);
430
- return { task, errors, ignored, renamed };
604
+ return { task, errors, ignored, renamed, adjustments };
431
605
  }
432
- function checkPlanInvariants(tasks, runners) {
606
+ function checkPlanInvariants(tasks, runners, runnerModes) {
433
607
  const rows = flattenTasksWithParents(tasks);
434
608
  const errors = [];
435
609
  const seen = /* @__PURE__ */ new Set();
@@ -446,14 +620,16 @@ function checkPlanInvariants(tasks, runners) {
446
620
  }
447
621
  const opsRefused = t.ops ? opsPlacementRefusal(t.type, parent !== null) : null;
448
622
  if (opsRefused) fail("ops", `${where} ${opsRefused}`);
449
- if (t.type !== "user" && !runners.includes(t.assignedRunner)) {
623
+ if (t.ops && t.taskMode !== void 0 && t.taskMode === taggedReadOnlyMode(runnerModes?.[t.assignedRunner])) {
624
+ fail("taskMode", `${where} is an ops task, which runs commands, so it cannot run in its runner's read-only mode`);
625
+ }
626
+ if (runners && t.type !== "user" && !runners.includes(t.assignedRunner)) {
450
627
  fail("assignedRunner", `${where} has invalid assignedRunner "${t.assignedRunner}". Expected one of: ${runners.join(", ")}`);
451
628
  }
452
629
  }
453
- const ids = new Set(rows.map((r) => r.task.id));
454
630
  for (const { task: t, parent } of rows) {
455
- for (const dep of t.dependencies) {
456
- if (!ids.has(dep)) errors.push({ taskId: t.id, field: "dependencies", message: `${taskWhere(t.title, parent?.title)} depends on "${dep}", which is not a task in the plan` });
631
+ for (const error of dependencyTargetErrors(tasks, t.id, t.dependencies.filter((id) => id !== t.id))) {
632
+ errors.push({ taskId: t.id, field: "dependencies", message: `${taskWhere(t.title, parent?.title)}: ${error}` });
457
633
  }
458
634
  }
459
635
  const cycle = findCycle(rows.map((r) => r.task));
@@ -461,27 +637,7 @@ function checkPlanInvariants(tasks, runners) {
461
637
  return errors;
462
638
  }
463
639
  function findCycle(tasks) {
464
- const byId = new Map(tasks.map((t) => [t.id, t]));
465
- const path = [];
466
- const done = /* @__PURE__ */ new Set();
467
- const visit = (id) => {
468
- if (done.has(id)) return null;
469
- const at = path.indexOf(id);
470
- if (at >= 0) return path.slice(at);
471
- path.push(id);
472
- for (const dep of byId.get(id)?.dependencies ?? []) {
473
- const hit = visit(dep);
474
- if (hit) return hit;
475
- }
476
- path.pop();
477
- done.add(id);
478
- return null;
479
- };
480
- for (const t of tasks) {
481
- const hit = visit(t.id);
482
- if (hit) return hit.map((id) => byId.get(id));
483
- }
484
- return null;
640
+ return findDependencyCycle(tasks);
485
641
  }
486
642
 
487
643
  // src/services/PlanValidator.ts
@@ -533,7 +689,7 @@ function validatePlanTasks(obj, runners, runnerModes, autonomousDefault = true)
533
689
  const errors = [
534
690
  ...sliceRequired(tasks),
535
691
  ...planned.flatMap((p) => p.errors),
536
- ...checkPlanInvariants(tasks, runners)
692
+ ...checkPlanInvariants(tasks, runners, runnerModes)
537
693
  ];
538
694
  if (errors.length > 0) return { ok: false, errors };
539
695
  const ignored = planned.flatMap((p) => p.ignored);