@ordewell/core 0.11.3 → 0.12.1

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 (49) hide show
  1. package/AUTHORS.md +3 -1
  2. package/dist/{ModeResolver-DyXeZ9ib.d.ts → ModeResolver-B64aUZXJ.d.ts} +1 -1
  3. package/dist/{ModeResolver-BDAgouuE.d.mts → ModeResolver-CA2fUU9h.d.mts} +1 -1
  4. package/dist/{Task-BL4Idm9r.d.mts → Task-CxGgN6I0.d.mts} +801 -39
  5. package/dist/{Task-BL4Idm9r.d.ts → Task-CxGgN6I0.d.ts} +801 -39
  6. package/dist/{chunk-YAFNYNFX.mjs → chunk-2IFKPYPO.mjs} +453 -17
  7. package/dist/chunk-2IFKPYPO.mjs.map +1 -0
  8. package/dist/{chunk-MYXMIVWA.mjs → chunk-FX33UKAD.mjs} +8 -6
  9. package/dist/chunk-FX33UKAD.mjs.map +1 -0
  10. package/dist/{chunk-QTZ5XTBY.mjs → chunk-PKJTN2PD.mjs} +458 -13
  11. package/dist/chunk-PKJTN2PD.mjs.map +1 -0
  12. package/dist/{chunk-7WNLN63J.mjs → chunk-RZ23BMVR.mjs} +599 -124
  13. package/dist/chunk-RZ23BMVR.mjs.map +1 -0
  14. package/dist/chunk-UDUATSCJ.mjs +48 -0
  15. package/dist/chunk-UDUATSCJ.mjs.map +1 -0
  16. package/dist/{IRunner-DOMtDTVt.d.ts → eventStore-CTCSKS9E.d.ts} +976 -145
  17. package/dist/{IRunner-ihalYfTo.d.mts → eventStore-CcB8yELZ.d.mts} +976 -145
  18. package/dist/index.d.mts +905 -164
  19. package/dist/index.d.ts +905 -164
  20. package/dist/index.js +12496 -3308
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +10830 -2919
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/parsing.d.mts +2 -2
  25. package/dist/parsing.d.ts +2 -2
  26. package/dist/parsing.js +277 -21
  27. package/dist/parsing.js.map +1 -1
  28. package/dist/parsing.mjs +3 -3
  29. package/dist/plan-utils.d.mts +8 -5
  30. package/dist/plan-utils.d.ts +8 -5
  31. package/dist/plan-utils.js +262 -33
  32. package/dist/plan-utils.js.map +1 -1
  33. package/dist/plan-utils.mjs +39 -3
  34. package/dist/{runSettingsText-DVOd7vKC.d.ts → runSettingsText-DFVpe6aU.d.mts} +1996 -895
  35. package/dist/{runSettingsText-BuLVBlXS.d.mts → runSettingsText-lKLDsyli.d.ts} +1996 -895
  36. package/dist/testing.d.mts +99 -4
  37. package/dist/testing.d.ts +99 -4
  38. package/dist/testing.js +213 -8
  39. package/dist/testing.js.map +1 -1
  40. package/dist/testing.mjs +193 -8
  41. package/dist/testing.mjs.map +1 -1
  42. package/docker/task.Dockerfile +22 -0
  43. package/package.json +3 -1
  44. package/skills/diagnose/SKILL.md +2 -2
  45. package/skills/explore/SKILL.md +1 -1
  46. package/dist/chunk-7WNLN63J.mjs.map +0 -1
  47. package/dist/chunk-MYXMIVWA.mjs.map +0 -1
  48. package/dist/chunk-QTZ5XTBY.mjs.map +0 -1
  49. package/dist/chunk-YAFNYNFX.mjs.map +0 -1
@@ -1,5 +1,5 @@
1
- import { az as RunnerId, aS as Task } from './Task-BL4Idm9r.mjs';
2
- import { R as RunnerModeInfo } from './ModeResolver-BDAgouuE.mjs';
1
+ import { b3 as RunnerId, bp as Task } from './Task-CxGgN6I0.mjs';
2
+ import { R as RunnerModeInfo } from './ModeResolver-CA2fUU9h.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 { az as RunnerId, aS as Task } from './Task-BL4Idm9r.js';
2
- import { R as RunnerModeInfo } from './ModeResolver-DyXeZ9ib.js';
1
+ import { b3 as RunnerId, bp as Task } from './Task-CxGgN6I0.js';
2
+ import { R as RunnerModeInfo } from './ModeResolver-B64aUZXJ.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
@@ -121,7 +121,7 @@ function createTask(overrides = {}) {
121
121
  assignedModel: overrides.assignedModel,
122
122
  assignedRunner: overrides.assignedRunner ?? DEFAULT_RUNNERS[0],
123
123
  // A script task runs in no mode (ADR-0035): one given it would read as a mode it runs in.
124
- taskMode: overrides.taskMode ?? (type === "script" ? void 0 : "build"),
124
+ taskMode: overrides.taskMode ?? (type === "script" || type === "delegated" ? void 0 : "build"),
125
125
  autonomy: overrides.autonomy,
126
126
  sliceType: overrides.sliceType,
127
127
  userStoriesCovered: overrides.userStoriesCovered,
@@ -131,6 +131,12 @@ function createTask(overrides = {}) {
131
131
  ...overrides.commands !== void 0 ? { commands: overrides.commands.map((argv) => [...argv]) } : {},
132
132
  ...overrides.script !== void 0 ? { script: overrides.script } : {},
133
133
  ...overrides.timeoutMs !== void 0 ? { timeoutMs: overrides.timeoutMs } : {},
134
+ ...overrides.goal !== void 0 ? { goal: overrides.goal } : {},
135
+ ...overrides.budget !== void 0 ? { budget: { ...overrides.budget } } : {},
136
+ ...overrides.grants !== void 0 ? { grants: [...overrides.grants] } : {},
137
+ ...overrides.protectedPaths !== void 0 ? { protectedPaths: [...overrides.protectedPaths] } : {},
138
+ ...overrides.scope !== void 0 ? { scope: structuredClone(overrides.scope) } : {},
139
+ ...overrides.readOnly ? { readOnly: true } : {},
134
140
  ...overrides.loop ? { loop: { body: [...overrides.loop.body], maxRounds: overrides.loop.maxRounds } } : {},
135
141
  ...overrides.loopRound !== void 0 ? { loopRound: overrides.loopRound } : {},
136
142
  ...overrides.research !== void 0 ? { research: overrides.research } : {}
@@ -299,8 +305,82 @@ function escapeControlCharsInStrings(json) {
299
305
  return out;
300
306
  }
301
307
 
302
- // src/services/PlannedTask.ts
303
- var import_uuid2 = require("uuid");
308
+ // src/services/Mandate.ts
309
+ var GRANTS = ["observe", "steer", "control", "replan", "approve", "delegate", "effect", "research", "merge"];
310
+ var FLOOR_GRANTS = ["observe", "steer", "control", "replan", "approve", "merge"];
311
+ var SHAPE_LIMITS = {
312
+ depth: { default: 2, hard: 4 },
313
+ delegatedTasks: { default: 8, hard: 30 },
314
+ tasksPerPlan: { default: 30, hard: 100 }
315
+ };
316
+ var DEFAULT_CHILD_WALL_CLOCK_MS = 24 * 60 * 60 * 1e3;
317
+ var DELEGATED_GRANTS = [...FLOOR_GRANTS, "research", "delegate"];
318
+ var lesser = (limit) => (parent, request) => {
319
+ const { hard } = SHAPE_LIMITS[limit];
320
+ const asked = request[limit];
321
+ if (asked !== void 0 && asked > hard) return { refused: { field: `limits.${limit}`, asked: String(asked), there: `a hard limit of ${hard}` } };
322
+ return { value: Math.min(parent[limit], asked ?? parent[limit]) };
323
+ };
324
+ function intersectModels(parent, asked = {}) {
325
+ const models = { ...asked, ...parent };
326
+ for (const runner of Object.keys(parent)) {
327
+ if (asked[runner]) models[runner] = parent[runner].filter((model) => asked[runner].includes(model));
328
+ }
329
+ return models;
330
+ }
331
+ var LIMIT_RULES = {
332
+ tokens: ({ tokens }, { tokens: asked }) => {
333
+ if (asked === void 0 || asked <= 0 || asked > tokens) return { refused: { field: "limits.tokens", asked: asked === void 0 ? "none" : String(asked), there: String(tokens) } };
334
+ return { value: asked };
335
+ },
336
+ wallClockMs: ({ wallClockMs: left }, { wallClockMs: asked }) => {
337
+ if (asked === void 0) return { value: left === null ? DEFAULT_CHILD_WALL_CLOCK_MS : Math.min(left, DEFAULT_CHILD_WALL_CLOCK_MS) };
338
+ if (asked <= 0 || left !== null && asked > left) return { refused: { field: "limits.wallClockMs", asked: String(asked), there: left === null ? "no limit" : String(left) } };
339
+ return { value: asked };
340
+ },
341
+ depth: ({ depth }, { depth: asked }) => {
342
+ if (depth <= 0) return { refused: { field: "limits.depth", asked: "a child", there: "no depth left" } };
343
+ const { hard } = SHAPE_LIMITS.depth;
344
+ if (asked !== void 0 && asked > hard) return { refused: { field: "limits.depth", asked: String(asked), there: `a hard limit of ${hard}` } };
345
+ return { value: Math.min(depth - 1, asked ?? depth - 1) };
346
+ },
347
+ delegatedTasks: lesser("delegatedTasks"),
348
+ tasksPerPlan: lesser("tasksPerPlan"),
349
+ runners: ({ runners }, { runners: asked }) => ({ value: asked ? runners.filter((runner) => asked.includes(runner)) : runners }),
350
+ models: ({ models }, { models: asked }) => ({ value: intersectModels(models, asked) })
351
+ };
352
+ var COMMAND_GRANTS = {
353
+ run_plan: "control",
354
+ run_task: "control",
355
+ stop_task: "control",
356
+ stop_run: "control",
357
+ retry_task: "control",
358
+ message_task: "steer",
359
+ answer_approval: "steer",
360
+ merge_all: "merge",
361
+ skip_task: "control",
362
+ force_start: null,
363
+ mark_complete: null,
364
+ mark_not_done: null,
365
+ revert_task: null,
366
+ interrupt_task: null,
367
+ force_send_queued: null,
368
+ remove_queued: null,
369
+ cancel_resume: null,
370
+ answer_checkpoint: "steer",
371
+ approve_review: null,
372
+ run_script: "control"
373
+ };
374
+ var TOOL_GRANTS = [
375
+ ...Object.entries(COMMAND_GRANTS).flatMap(([tool, grant]) => grant ? [[tool, grant]] : []),
376
+ ["submit_plan", "replan"],
377
+ ["edit_plan", "replan"]
378
+ ];
379
+
380
+ // src/models/lineBreaks.ts
381
+ var BREAKS = "\\n\\v\\f\\r\\u0085\\u2028\\u2029";
382
+ var LINE_BREAK = new RegExp(`\\r\\n|[${BREAKS}]`);
383
+ var ONE_LINE = new RegExp(`^[^${BREAKS}]*$`);
304
384
 
305
385
  // src/services/ModeResolver.ts
306
386
  function resolveDefaultMode(modes, autonomousDefault = false) {
@@ -349,6 +429,142 @@ function readOnlyRunners(runnerModes) {
349
429
  return Object.entries(runnerModes).filter(([, ms]) => readOnlyMode(ms)).map(([id]) => id);
350
430
  }
351
431
 
432
+ // src/models/Delegation.ts
433
+ var SCOPE_LIMITS = { item: 200, items: 50 };
434
+ var NOT_A_DELEGATED_TASKS_FIELDS = ["prompt", "taskMode", "skills", "userSteps", "ops", "commands", "script", "timeoutMs", "research"];
435
+ var DELEGATION_UNAVAILABLE = "delegation is not available in this session";
436
+ function notOnADelegatedMessage(fields) {
437
+ return `is a delegated task, which holds a goal for a child plan: leave out ${fields.map((f) => `"${f}"`).join(", ")}`;
438
+ }
439
+ function isBudget(value) {
440
+ if (typeof value !== "object" || value === null) return false;
441
+ const { tokens } = value;
442
+ return typeof tokens === "number" && Number.isInteger(tokens) && tokens > 0;
443
+ }
444
+ function wallClockOk(budget) {
445
+ const { wallClockMs } = budget;
446
+ return wallClockMs === void 0 || Number.isInteger(wallClockMs) && wallClockMs > 0;
447
+ }
448
+ function readsOnly(task, runnerModes) {
449
+ if (task.type !== "ai") return false;
450
+ const mode = runnerModes ? taggedReadOnlyMode(runnerModes[task.assignedRunner]) : "plan";
451
+ return mode !== void 0 && task.taskMode === mode;
452
+ }
453
+ function atAGate(task) {
454
+ return task.type === "script" && !task.readOnly || task.ops === true;
455
+ }
456
+ function reaches(tasks, from, target) {
457
+ const byId = new Map(tasks.map((t) => [t.id, t]));
458
+ const seen = /* @__PURE__ */ new Set();
459
+ const walk = (task) => task.dependencies.some((id) => {
460
+ if (seen.has(id)) return false;
461
+ seen.add(id);
462
+ const dep = byId.get(id);
463
+ return dep !== void 0 && (target(dep) || walk(dep));
464
+ });
465
+ return walk(from);
466
+ }
467
+ function delegationErrors(tasks, rules = {}) {
468
+ const errors = [];
469
+ const delegated = tasks.filter((t) => t.type === "delegated");
470
+ const fail = (task, field, message) => errors.push({ taskId: task.id, field, message });
471
+ for (const task of delegated) {
472
+ const where = `Task "${task.title}"`;
473
+ if (typeof task.goal !== "string" || !task.goal.trim()) fail(task, "goal", `${where} is a delegated task with no "goal": say what its child plan must achieve`);
474
+ if (!isBudget(task.budget)) fail(task, "budget", `${where} is a delegated task with no "budget": give it {"tokens": <a whole number>}, and "wallClockMs" if it has a time limit`);
475
+ else if (!wallClockOk(task.budget)) fail(task, "budget", `${where} has a "wallClockMs" that is not a whole number of milliseconds above 0`);
476
+ const paths = task.protectedPaths;
477
+ if (!task.readOnly && (!Array.isArray(paths) || paths.length === 0 || !paths.every((p) => typeof p === "string" && p.trim()))) {
478
+ fail(task, "protectedPaths", `${where} is a delegated task that lands, with no "protectedPaths": name the paths its child may not change, what its check runs and the configuration that runs it`);
479
+ }
480
+ if (task.scope !== void 0 && !scopeOk(task.scope)) fail(task, "scope", `${where} has a "scope" that is not lists of text: "paths", "provides" and "consumes", at most ${SCOPE_LIMITS.items} items of one line and ${SCOPE_LIMITS.item} characters each`);
481
+ const unknown = (task.grants ?? []).filter((grant) => !GRANTS.includes(grant));
482
+ if (unknown.length > 0) fail(task, "grants", `${where} asks in "grants" for ${unknown.map((g) => `"${g}"`).join(", ")}, which no grant is: one of ${GRANTS.join(", ")}`);
483
+ const dependents = tasks.filter((t) => t.dependencies.includes(task.id));
484
+ if (task.readOnly) {
485
+ for (const script of dependents.filter((t) => t.type === "script")) {
486
+ fail(script, "dependencies", `Task "${script.title}" depends on "${task.title}", which is a read-only delegated task, which lands nothing, so a script has nothing to run on: make the check a read-only AI task, which reads its handoff`);
487
+ }
488
+ }
489
+ if (!dependents.some((t) => judges(t, task, rules.runnerModes))) {
490
+ fail(task, "dependencies", `${where} is a delegated task with no check: add a read-only AI task${task.readOnly ? ", which reads its handoff," : ', or a script task marked "readOnly",'} that depends on it directly`);
491
+ }
492
+ }
493
+ for (const task of tasks.filter((t) => t.type === "script" && t.readOnly)) {
494
+ if (rules.canSeal === false) fail(task, "readOnly", `Task "${task.title}" is a read-only script task, and this machine has no sandbox to seal one in: make the check a read-only AI task`);
495
+ }
496
+ for (const check of tasks.filter((t) => t.loop)) {
497
+ if (!reaches(tasks, check, (t) => t.type === "delegated")) continue;
498
+ for (const id of check.loop?.body ?? []) {
499
+ const body = tasks.find((t) => t.id === id);
500
+ if (body && atAGate(body)) {
501
+ fail(body, "loop", `Task "${check.title}" names "${body.title}" in its loop's "body", which waits at a merge gate for work a delegated task's check holds back: make the fix a change task, cut from the integration tip`);
502
+ }
503
+ }
504
+ }
505
+ return errors;
506
+ }
507
+ function scopeOk(scope) {
508
+ if (typeof scope !== "object" || scope === null) return false;
509
+ const { paths, provides, consumes } = scope;
510
+ const itemOk = (item) => typeof item === "string" && item.trim().length > 0 && item.length <= SCOPE_LIMITS.item && ONE_LINE.test(item);
511
+ return [paths, provides, consumes].every((list) => list === void 0 || Array.isArray(list) && list.length <= SCOPE_LIMITS.items && list.every(itemOk));
512
+ }
513
+ function judges(task, delegated, runnerModes) {
514
+ return readsOnly(task, runnerModes) || !delegated.readOnly && task.type === "script" && task.readOnly === true;
515
+ }
516
+ function underProtectedPath(file, protectedPath) {
517
+ const rule = protectedPath.trim().replace(/^\.\//, "").replace(/\\/g, "/");
518
+ if (!rule) return false;
519
+ if (rule.includes("*")) return globRegExp(rule).test(file);
520
+ const bare = rule.replace(/\/+$/, "");
521
+ return file === bare || file.startsWith(`${bare}/`);
522
+ }
523
+ function fixedStart(rule) {
524
+ const bare = rule.trim().replace(/^\.\//, "").replace(/\\/g, "/");
525
+ const star = bare.indexOf("*");
526
+ return star === -1 ? bare : bare.slice(0, star);
527
+ }
528
+ function pathsMeet(a, b) {
529
+ if (underProtectedPath(a.replace(/\/+$/, ""), b) || underProtectedPath(b.replace(/\/+$/, ""), a)) return true;
530
+ if (!a.includes("*") || !b.includes("*")) return false;
531
+ const [one, other] = [fixedStart(a), fixedStart(b)];
532
+ return one.startsWith(other) || other.startsWith(one);
533
+ }
534
+ function narrower(a, b) {
535
+ return b.includes("*") || !a.includes("*") && a.length >= b.length ? a : b;
536
+ }
537
+ function ordered(tasks, a, b) {
538
+ return reaches(tasks, a, (t) => t.id === b.id) || reaches(tasks, b, (t) => t.id === a.id);
539
+ }
540
+ function scopeOverlaps(tasks) {
541
+ const scoped = tasks.filter((t) => t.type === "delegated" && t.scope);
542
+ const said = [];
543
+ scoped.forEach((a, i) => {
544
+ for (const b of scoped.slice(i + 1)) {
545
+ if (ordered(tasks, a, b)) continue;
546
+ const declared = (a.scope?.paths ?? []).flatMap((one) => (b.scope?.paths ?? []).filter((other) => pathsMeet(one, other)).map((other) => narrower(one, other)));
547
+ if (declared.length > 0) said.push(`Tasks "${a.title}" and "${b.title}" both declare ${[...new Set(declared)].join(", ")}, and nothing orders them: give one a dependency on the other, or name an owner.`);
548
+ for (const name of a.scope?.provides ?? []) {
549
+ if (b.scope?.provides?.includes(name)) said.push(`Tasks "${a.title}" and "${b.title}" both provide "${name}", and nothing orders them: name one owner.`);
550
+ }
551
+ for (const [producer, consumer] of [[a, b], [b, a]]) {
552
+ for (const name of producer.scope?.provides ?? []) {
553
+ if (consumer.scope?.consumes?.includes(name)) said.push(`Task "${consumer.title}" consumes "${name}", which "${producer.title}" provides, and nothing orders them: a dependency puts the producer first; without one it gets a line when "${producer.title}" lands.`);
554
+ }
555
+ }
556
+ }
557
+ });
558
+ return said;
559
+ }
560
+ function globRegExp(rule) {
561
+ const pattern = rule.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\/?|\*/g, (star) => star === "*" ? "[^/]*" : "(?:.*/)?");
562
+ return new RegExp(`^${pattern}(?:/.*)?$`);
563
+ }
564
+
565
+ // src/services/PlannedTask.ts
566
+ var import_uuid2 = require("uuid");
567
+
352
568
  // src/services/ModelAllowlistResolver.ts
353
569
  function effectiveAllowlist(allowlist, runner, modelsByRunner) {
354
570
  if (!allowlist || allowlist.length === 0) return void 0;
@@ -433,8 +649,8 @@ function resolveAssignment(write) {
433
649
  const live = writer !== "runner" && !!before && !!catalog.liveRunner?.(before.id);
434
650
  if (live && moved) return { ok: false, field: "assignedRunner", error: liveRunnerRefusal({ title }) };
435
651
  if (live && !sameAssignment(before?.assignedModel, written.assignedModel)) return { ok: false, field: "assignedModel", error: liveModelRefusal({ title }) };
436
- if (written.type !== "ai") return { ok: true, task: written, adjustments: [] };
437
- const mode = modeFor(write, written, moved);
652
+ if (written.type !== "ai" && written.type !== "delegated") return { ok: true, task: written, adjustments: [] };
653
+ const mode = written.type === "delegated" ? { ok: true, mode: void 0 } : modeFor(write, written, moved);
438
654
  if (!mode.ok) return { ok: false, field: "taskMode", error: mode.error };
439
655
  const model = live ? { ok: true, assignment: written.assignedModel } : modelFor(write, moved);
440
656
  if (!model.ok) return { ok: false, field: "assignedModel", error: model.error };
@@ -578,6 +794,7 @@ function loopErrors(tasks) {
578
794
  const fail = (message) => errors.push({ taskId: check.id, field: "loop", message: `${where} ${message}` });
579
795
  if (parent) fail(`has a "loop", but only a top-level task can be a loop's check`);
580
796
  if (check.type === "user") fail("is a manual task, so it cannot be a loop's check: a check is an AI task, which reports through task_complete, or a script task, whose exit status decides");
797
+ if (check.type === "delegated") fail("is a delegated task, so it cannot be a loop's check: a round would start a new child on what is left of its budget. Add a check that depends on it, and put the loop on that");
581
798
  if (check.subtasks?.length) fail("is a loop's check, which has no subtasks: each round runs it again whole");
582
799
  if (!isRoundCount(loop.maxRounds)) fail(roundsMessage(loop.maxRounds));
583
800
  if (!Array.isArray(loop.body) || loop.body.length === 0) fail('has a "loop" with an empty "body": name the tasks that fix what it finds');
@@ -610,6 +827,7 @@ function loopErrors(tasks) {
610
827
  bodyOwner.set(id, check);
611
828
  if (body.loop) fail(`names "${body.title}" in its loop's "body", which has a loop of its own: a loop has no loop inside its body`);
612
829
  if (body.type === "user") fail(`names manual task "${body.title}" in its loop's "body": a body is AI or script tasks, which Ordewell runs again each round`);
830
+ if (body.type === "delegated") fail(`names delegated task "${body.title}" in its loop's "body": a round would start a new child on what is left of its budget`);
613
831
  if (body.subtasks?.length) fail(`names "${body.title}" in its loop's "body", which has subtasks: each round runs a body task again whole, so make each step a task of the body`);
614
832
  }
615
833
  }
@@ -653,6 +871,28 @@ function notOnAScript(raw) {
653
871
  return value !== void 0 && value !== null && value !== false && !(Array.isArray(value) && value.length === 0);
654
872
  });
655
873
  }
874
+ function notOnADelegated(raw) {
875
+ return NOT_A_DELEGATED_TASKS_FIELDS.filter((f) => {
876
+ const value = raw[f];
877
+ return value !== void 0 && value !== null && value !== false && !(Array.isArray(value) && value.length === 0);
878
+ });
879
+ }
880
+ function delegatedFields(raw, base) {
881
+ const list = (value) => Array.isArray(value) ? value.map(String) : void 0;
882
+ const goal = text(raw.goal) ?? base.goal;
883
+ const budget = (isRecord(raw.budget) ? raw.budget : void 0) ?? base.budget;
884
+ const grants = list(raw.grants) ?? base.grants;
885
+ const protectedPaths = list(raw.protectedPaths) ?? base.protectedPaths;
886
+ const scope = (isRecord(raw.scope) ? raw.scope : void 0) ?? base.scope;
887
+ return {
888
+ ...goal !== void 0 ? { goal } : {},
889
+ ...budget ? { budget } : {},
890
+ ...grants ? { grants } : {},
891
+ ...protectedPaths ? { protectedPaths } : {},
892
+ ...scope ? { scope } : {},
893
+ ...raw.readOnly === true || raw.readOnly === void 0 && base.readOnly ? { readOnly: true } : {}
894
+ };
895
+ }
656
896
  function notOnAScriptMessage(fields) {
657
897
  return `is a script task, which runs its "commands" or "script" with no model: leave out ${fields.map((f) => `"${f}"`).join(", ")}`;
658
898
  }
@@ -710,27 +950,35 @@ function plannedTask(spec, catalog, at = {}) {
710
950
  const plannerId = text(raw.id);
711
951
  const id = freshIds || !plannerId ? newId() : plannerId;
712
952
  if (freshIds && plannerId) renamed.set(plannerId, id);
713
- const type = raw.type === "user" || raw.type === "ai" || raw.type === "script" ? raw.type : base.type ?? "ai";
953
+ const delegating = catalog.delegationRefusal === null;
954
+ const type = raw.type === "user" || raw.type === "ai" || raw.type === "script" || raw.type === "delegated" ? raw.type : base.type ?? "ai";
714
955
  const ai = type === "ai";
715
956
  const script = type === "script";
957
+ const delegated = type === "delegated";
958
+ const sealedScript = script && (catalog.sealsScripts?.({ ...Array.isArray(raw.commands) ? { commands: raw.commands } : {}, ...typeof raw.script === "string" ? { script: raw.script } : {}, ...typeof raw.timeoutMs === "number" ? { timeoutMs: raw.timeoutMs } : {} }) === true || delegating && raw.readOnly === true);
716
959
  const assignedRunner = text(raw.assignedRunner) ?? base.assignedRunner ?? catalog.runners[0] ?? DEFAULT_RUNNERS[0];
717
960
  const description = text(raw.description) ?? base.description ?? title;
718
- const notScripts = script ? notOnAScript(raw) : [];
961
+ const notScripts = script ? notOnAScript(raw).filter((f) => !(sealedScript && f === "readOnly")) : [];
719
962
  if (notScripts.length > 0) errors.push({ taskId: id, field: notScripts[0], message: `${where} ${notOnAScriptMessage(notScripts)}` });
963
+ if (delegated) {
964
+ if (!delegating) errors.push({ taskId: id, field: "type", message: catalog.delegationRefusal ?? `${where} is a delegated task, and ${DELEGATION_UNAVAILABLE}.` });
965
+ const extra = notOnADelegated(raw);
966
+ if (extra.length > 0) errors.push({ taskId: id, field: extra[0], message: `${where} ${notOnADelegatedMessage(extra)}` });
967
+ }
720
968
  const body = bodyFields(raw, base);
721
969
  const shapeErrors = scriptBodyErrors({ type, ...body.raw }).filter((e) => body.malformed.has(e.field));
722
970
  for (const e of shapeErrors) errors.push({ taskId: id, field: e.field, message: `${where} ${e.message}` });
723
971
  const loop = loopSpec(raw.loop);
724
972
  if ("error" in loop) errors.push({ taskId: id, field: "loop", message: `${where} ${loop.error}` });
725
973
  if (raw.ops === true && !ai && !script) errors.push({ taskId: id, field: "ops", message: `${where} ${opsPlacementRefusal(type, !!parent)}` });
726
- if (raw.taskMode !== void 0 && !script) ignored.push({ taskId: id, field: "taskMode", message: `${where}: ${MODE_NOT_THE_PLANNERS}` });
974
+ if (raw.taskMode !== void 0 && !script && !delegated) ignored.push({ taskId: id, field: "taskMode", message: `${where}: ${MODE_NOT_THE_PLANNERS}` });
727
975
  if (type === "user" && raw.readOnly === true) ignored.push({ taskId: id, field: "readOnly", message: `${where}: ${MANUAL_NOT_READ_ONLY}` });
728
976
  const ops = raw.ops !== void 0 ? raw.ops === true : base.ops;
729
977
  if (ai && ops && raw.readOnly === true) ignored.push({ taskId: id, field: "readOnly", message: `${where}: ${OPS_NOT_READ_ONLY}` });
730
978
  const asked = typeof raw.readOnly === "boolean" ? raw.readOnly : void 0;
731
979
  const readOnly = ai && !ops && (asked ?? from?.readOnly ?? parent?.readOnly ?? false);
732
980
  if (raw.research !== void 0 && typeof raw.research !== "boolean") errors.push({ taskId: id, field: "research", message: `${where}: "research" must be a boolean` });
733
- const assignedModel = ai ? modelOf(raw, base.assignedModel) : void 0;
981
+ const assignedModel = ai || delegated ? modelOf(raw, base.assignedModel) : void 0;
734
982
  const slice = (value) => value === "AFK" || value === "HITL" ? value : void 0;
735
983
  const autonomy = ai ? slice(raw.autonomy) ?? base.autonomy ?? parent?.autonomy : void 0;
736
984
  const sliceType = slice(raw.sliceType) ?? base.sliceType ?? (ai ? parent?.sliceType : void 0);
@@ -753,7 +1001,7 @@ function plannedTask(spec, catalog, at = {}) {
753
1001
  type,
754
1002
  dependencies: Array.isArray(raw.dependencies) ? raw.dependencies.map(String) : base.dependencies ?? [],
755
1003
  // What a merge or split carries over for a runner is nothing a script runs.
756
- prompt: script ? void 0 : text(raw.prompt) || base.prompt || (ai ? description || title || void 0 : void 0),
1004
+ prompt: script || delegated ? void 0 : text(raw.prompt) || base.prompt || (ai ? description || title || void 0 : void 0),
757
1005
  userSteps: Array.isArray(raw.userSteps) ? raw.userSteps.map((s) => isRecord(s) ? s : {}).map((s) => ({ order: Number(s.order ?? 0), instruction: String(s.instruction ?? ""), completed: false })) : base.userSteps,
758
1006
  subtasks,
759
1007
  assignedModel,
@@ -761,15 +1009,17 @@ function plannedTask(spec, catalog, at = {}) {
761
1009
  autonomy,
762
1010
  sliceType,
763
1011
  userStoriesCovered: Array.isArray(raw.userStoriesCovered) ? raw.userStoriesCovered.map(String) : base.userStoriesCovered,
764
- ops: script ? void 0 : ops,
1012
+ ops: script || delegated ? void 0 : ops,
765
1013
  research: typeof raw.research === "boolean" ? raw.research : base.research,
766
- skills: script ? void 0 : from ? inheritedSkills(raw.skills, base.skills) : skillNames(raw.skills),
1014
+ skills: script || delegated ? void 0 : from ? inheritedSkills(raw.skills, base.skills) : skillNames(raw.skills),
767
1015
  ...body.fields,
768
- ..."loop" in loop && loop.loop ? { loop: loop.loop } : {}
1016
+ ..."loop" in loop && loop.loop ? { loop: loop.loop } : {},
1017
+ ...delegated ? delegatedFields(raw, base) : {},
1018
+ ...sealedScript ? { readOnly: true } : {}
769
1019
  };
770
- let taskMode = script ? void 0 : base.taskMode;
1020
+ let taskMode = script || delegated ? void 0 : base.taskMode;
771
1021
  let landedModel = assignedModel;
772
- if (ai && catalog.runners.includes(assignedRunner)) {
1022
+ if ((ai || delegated) && catalog.runners.includes(assignedRunner)) {
773
1023
  const resolved = resolveAssignment({
774
1024
  writer: catalog.models ? "edit" : "plan",
775
1025
  task: { ...createTask(fields), taskMode },
@@ -780,7 +1030,7 @@ function plannedTask(spec, catalog, at = {}) {
780
1030
  if (!resolved.ok) {
781
1031
  errors.push({ taskId: id, field: resolved.field === "taskMode" ? "readOnly" : resolved.field, message: `${where}: ${resolved.error}` });
782
1032
  } else {
783
- taskMode = resolved.task.taskMode;
1033
+ taskMode = delegated ? void 0 : resolved.task.taskMode;
784
1034
  landedModel = resolved.task.assignedModel;
785
1035
  adjustments.push(...resolved.adjustments);
786
1036
  }
@@ -792,13 +1042,13 @@ function plannedTask(spec, catalog, at = {}) {
792
1042
  }
793
1043
  function checkPlanRunners(tasks, runners) {
794
1044
  if (!runners) return [];
795
- return flattenTasksWithParents(tasks).filter(({ task: t }) => t.type === "ai" && !runners.includes(t.assignedRunner)).map(({ task: t, parent }) => ({
1045
+ return flattenTasksWithParents(tasks).filter(({ task: t }) => (t.type === "ai" || t.type === "delegated") && !runners.includes(t.assignedRunner)).map(({ task: t, parent }) => ({
796
1046
  taskId: t.id,
797
1047
  field: "assignedRunner",
798
1048
  message: `${taskWhere(t.title, parent?.title)} has invalid assignedRunner "${t.assignedRunner}". Expected one of: ${runners.join(", ")}`
799
1049
  }));
800
1050
  }
801
- function checkPlanInvariants(tasks, runners, runnerModes) {
1051
+ function checkPlanInvariants(tasks, runners, runnerModes, delegation = {}) {
802
1052
  const rows = flattenTasksWithParents(tasks);
803
1053
  const errors = [];
804
1054
  const seen = /* @__PURE__ */ new Set();
@@ -827,6 +1077,10 @@ function checkPlanInvariants(tasks, runners, runnerModes) {
827
1077
  const extra = notAScriptsFields(t);
828
1078
  if (extra.length > 0) fail(extra[0], `${where} ${notOnAScriptMessage(extra)}`);
829
1079
  }
1080
+ if (t.type === "delegated") {
1081
+ if (parent !== null) fail("type", `${where} is a delegated task: only a top-level task can be delegated`);
1082
+ if (t.subtasks?.length) fail("subtasks", `${where} is a delegated task, which has no subtasks: its child plan's tasks are its child's own`);
1083
+ }
830
1084
  for (const e of scriptBodyErrors(t)) fail(e.field, `${where} ${e.message}`);
831
1085
  if (t.ops && t.taskMode !== void 0 && t.taskMode === taggedReadOnlyMode(runnerModes?.[t.assignedRunner])) {
832
1086
  fail("taskMode", `${where} is an ops task, which acts in the workspace, so it cannot use the read-only profile`);
@@ -834,6 +1088,7 @@ function checkPlanInvariants(tasks, runners, runnerModes) {
834
1088
  }
835
1089
  errors.push(...checkPlanRunners(tasks, runners));
836
1090
  errors.push(...loopErrors(tasks));
1091
+ errors.push(...delegationErrors(tasks, { ...runnerModes ? { runnerModes } : {}, ...delegation }));
837
1092
  for (const { task: t, parent } of rows) {
838
1093
  for (const error of dependencyTargetErrors(tasks, t.id, t.dependencies.filter((id) => id !== t.id))) {
839
1094
  errors.push({ taskId: t.id, field: "dependencies", message: `${taskWhere(t.title, parent?.title)}: ${error}` });
@@ -883,7 +1138,7 @@ function parsePlanObject(json, raw, runners, runnerModes, autonomousDefault = fa
883
1138
  const [first] = result.errors;
884
1139
  throw new PlanParseError(first.message, raw, { semantic: first.taskId !== void 0 });
885
1140
  }
886
- function validatePlanTasks(obj, runners, runnerModes, autonomousDefault = false) {
1141
+ function validatePlanTasks(obj, runners, runnerModes, autonomousDefault = false, delegation = {}) {
887
1142
  const rawTasks = isRecord2(obj) ? obj.tasks : void 0;
888
1143
  if (!Array.isArray(rawTasks)) {
889
1144
  return { ok: false, errors: [{ field: "tasks", message: "Invalid plan: missing tasks array" }] };
@@ -891,7 +1146,7 @@ function validatePlanTasks(obj, runners, runnerModes, autonomousDefault = false)
891
1146
  if (rawTasks.length === 0) {
892
1147
  return { ok: false, errors: [{ field: "tasks", message: "Plan contained no tasks" }] };
893
1148
  }
894
- const planned = rawTasks.map((t) => plannedTask(t, { runners, runnerModes, autonomousDefault }));
1149
+ const planned = rawTasks.map((t) => plannedTask(t, { runners, runnerModes, autonomousDefault, ...delegation }));
895
1150
  const tasks = planned.map((p) => p.task);
896
1151
  const errors = [
897
1152
  ...sliceRequired(tasks),
@@ -902,7 +1157,8 @@ function validatePlanTasks(obj, runners, runnerModes, autonomousDefault = false)
902
1157
  const ignored = planned.flatMap((p) => p.ignored);
903
1158
  const warnings = [
904
1159
  ...ignored.some((i) => i.field === "taskMode") ? [`"taskMode" is not yours to set and was ignored: each task runs in its runner's mode for the user's autonomy level, or its read-only mode when you set "readOnly": true.`] : [],
905
- ...ignored.filter((i) => i.field === "readOnly").map((i) => `${i.message}: it was ignored.`)
1160
+ ...ignored.filter((i) => i.field === "readOnly").map((i) => `${i.message}: it was ignored.`),
1161
+ ...scopeOverlaps(tasks)
906
1162
  ];
907
1163
  return warnings.length > 0 ? { ok: true, tasks, warnings } : { ok: true, tasks };
908
1164
  }