@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
@@ -7,7 +7,7 @@ import {
7
7
  flattenTasksWithParents,
8
8
  inheritedSkills,
9
9
  skillNames
10
- } from "./chunk-QTZ5XTBY.mjs";
10
+ } from "./chunk-PKJTN2PD.mjs";
11
11
 
12
12
  // src/models/ScriptTask.ts
13
13
  var DEFAULT_SCRIPT_TIMEOUT_MS = 10 * 6e4;
@@ -124,9 +124,19 @@ function betweenRounds(check) {
124
124
  function outOfRounds(check) {
125
125
  return !!check.loop && check.status === "failed" && foundWanting(check) && loopRoundOf(check) >= check.loop.maxRounds;
126
126
  }
127
+ function loopEnded(check) {
128
+ return check.status === "completed" || check.status === "skipped" || outOfRounds(check);
129
+ }
127
130
  function checkOf(tasks, taskId) {
128
131
  return tasks.find((t) => t.loop?.body.includes(taskId));
129
132
  }
133
+ function checksOfBodies(tasks) {
134
+ const checks = /* @__PURE__ */ new Map();
135
+ for (const task of tasks) {
136
+ for (const id of task.loop?.body ?? []) if (!checks.has(id)) checks.set(id, task);
137
+ }
138
+ return checks;
139
+ }
130
140
  function inLoop(tasks, task) {
131
141
  return !!task.loop || checkOf(tasks, task.id) !== void 0;
132
142
  }
@@ -158,6 +168,7 @@ function loopErrors(tasks) {
158
168
  const fail = (message) => errors.push({ taskId: check.id, field: "loop", message: `${where} ${message}` });
159
169
  if (parent) fail(`has a "loop", but only a top-level task can be a loop's check`);
160
170
  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");
171
+ 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");
161
172
  if (check.subtasks?.length) fail("is a loop's check, which has no subtasks: each round runs it again whole");
162
173
  if (!isRoundCount(loop.maxRounds)) fail(roundsMessage(loop.maxRounds));
163
174
  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');
@@ -190,6 +201,7 @@ function loopErrors(tasks) {
190
201
  bodyOwner.set(id, check);
191
202
  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`);
192
203
  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`);
204
+ 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`);
193
205
  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`);
194
206
  }
195
207
  }
@@ -219,10 +231,25 @@ function loopRemovalRefusal(task) {
219
231
  return task.loop ? `"${task.title}" is a loop's check: remove its body tasks first, or drop its loop ("loop": null), so they do not run on their own` : null;
220
232
  }
221
233
  function loopReshapeRefusal(tasks, target, action) {
234
+ if (target.type === "delegated") return `"${target.title}" is a delegated task and cannot be ${action}: remove it and add what should replace it`;
222
235
  if (target.loop) return `"${target.title}" is a loop's check and cannot be ${action}: drop its loop first ("loop": null)`;
223
236
  return checkOf(tasks, target.id) ? `"${target.title}" is in a loop's body and cannot be ${action}: take it out of the loop first` : null;
224
237
  }
225
238
 
239
+ // src/models/lineBreaks.ts
240
+ var BREAKS = "\\n\\v\\f\\r\\u0085\\u2028\\u2029";
241
+ var LINE_BREAK = new RegExp(`\\r\\n|[${BREAKS}]`);
242
+ var ONE_LINE = new RegExp(`^[^${BREAKS}]*$`);
243
+ function splitLines(text2) {
244
+ return text2.split(LINE_BREAK);
245
+ }
246
+ function quotedLine(text2) {
247
+ return JSON.stringify(text2).replace(/[\u0085\u2028\u2029]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
248
+ }
249
+ function onOneLine(text2) {
250
+ return ONE_LINE.test(text2) ? text2 : quotedLine(text2);
251
+ }
252
+
226
253
  // src/services/JsonExtractor.ts
227
254
  var PLAN_ENVELOPE_KEY = "tasks";
228
255
  var TASK_OPS_ENVELOPE_KEY = "taskOps";
@@ -532,6 +559,173 @@ function clampThinkingEffort(effort, variants) {
532
559
  return void 0;
533
560
  }
534
561
 
562
+ // src/services/Mandate.ts
563
+ var GRANTS = ["observe", "steer", "control", "replan", "approve", "delegate", "effect", "research", "merge"];
564
+ var FLOOR_GRANTS = ["observe", "steer", "control", "replan", "approve", "merge"];
565
+ var SHAPE_LIMITS = {
566
+ depth: { default: 2, hard: 4 },
567
+ delegatedTasks: { default: 8, hard: 30 },
568
+ tasksPerPlan: { default: 30, hard: 100 }
569
+ };
570
+ var DEFAULT_CHILD_WALL_CLOCK_MS = 24 * 60 * 60 * 1e3;
571
+ function mandateRefusalText({ level, field, asked, there }) {
572
+ return `Level ${level} refused ${field}: asked ${asked}, and there is ${there}.`;
573
+ }
574
+ var LEVEL_GRANTS = {
575
+ off: ["research"],
576
+ watch: ["observe", "research"],
577
+ act: ["observe", "steer", "control", "replan", "research", "merge"]
578
+ };
579
+ var DELEGATED_GRANTS = [...FLOOR_GRANTS, "research", "delegate"];
580
+ function rootMandate(settings) {
581
+ const effect = (settings.effects?.rules.length ?? 0) > 0;
582
+ const delegation = settings.delegationTokens > 0;
583
+ const effectRules = settings.effects ? [settings.effects] : [];
584
+ return {
585
+ grants: /* @__PURE__ */ new Set([
586
+ ...LEVEL_GRANTS[settings.level],
587
+ ...settings.autorun ? ["approve"] : [],
588
+ ...effect ? ["effect"] : [],
589
+ ...delegation ? ["delegate"] : []
590
+ ]),
591
+ grantable: new Set(delegation ? [...DELEGATED_GRANTS, ...effect ? ["effect"] : []] : []),
592
+ autonomy: settings.autonomous ? "full" : "guarded",
593
+ limits: {
594
+ tokens: settings.delegationTokens,
595
+ wallClockMs: null,
596
+ depth: SHAPE_LIMITS.depth.default,
597
+ delegatedTasks: SHAPE_LIMITS.delegatedTasks.default,
598
+ tasksPerPlan: SHAPE_LIMITS.tasksPerPlan.default,
599
+ runners: settings.runners,
600
+ models: settings.models
601
+ },
602
+ exclusions: { protectedPaths: [] },
603
+ effectRules,
604
+ ownEffectRules: effectRules
605
+ };
606
+ }
607
+ function heldMandate(saved, ancestors) {
608
+ const within = (grants) => new Set([...grants].filter((grant) => ancestors.every((ancestor) => ancestor.grantable.has(grant))));
609
+ const effectRules = [...ancestors.flatMap((ancestor) => ancestor.effectRules), ...saved.ownEffectRules];
610
+ return { ...saved, grants: within(saved.grants), grantable: within(saved.grantable), effectRules };
611
+ }
612
+ var listed = (grants) => [...grants].sort().join(", ") || "nothing";
613
+ var lesser = (limit) => (parent, request) => {
614
+ const { hard } = SHAPE_LIMITS[limit];
615
+ const asked = request[limit];
616
+ if (asked !== void 0 && asked > hard) return { refused: { field: `limits.${limit}`, asked: String(asked), there: `a hard limit of ${hard}` } };
617
+ return { value: Math.min(parent[limit], asked ?? parent[limit]) };
618
+ };
619
+ function intersectModels(parent, asked = {}) {
620
+ const models = { ...asked, ...parent };
621
+ for (const runner of Object.keys(parent)) {
622
+ if (asked[runner]) models[runner] = parent[runner].filter((model) => asked[runner].includes(model));
623
+ }
624
+ return models;
625
+ }
626
+ var LIMIT_RULES = {
627
+ tokens: ({ tokens }, { tokens: asked }) => {
628
+ if (asked === void 0 || asked <= 0 || asked > tokens) return { refused: { field: "limits.tokens", asked: asked === void 0 ? "none" : String(asked), there: String(tokens) } };
629
+ return { value: asked };
630
+ },
631
+ wallClockMs: ({ wallClockMs: left }, { wallClockMs: asked }) => {
632
+ if (asked === void 0) return { value: left === null ? DEFAULT_CHILD_WALL_CLOCK_MS : Math.min(left, DEFAULT_CHILD_WALL_CLOCK_MS) };
633
+ if (asked <= 0 || left !== null && asked > left) return { refused: { field: "limits.wallClockMs", asked: String(asked), there: left === null ? "no limit" : String(left) } };
634
+ return { value: asked };
635
+ },
636
+ depth: ({ depth }, { depth: asked }) => {
637
+ if (depth <= 0) return { refused: { field: "limits.depth", asked: "a child", there: "no depth left" } };
638
+ const { hard } = SHAPE_LIMITS.depth;
639
+ if (asked !== void 0 && asked > hard) return { refused: { field: "limits.depth", asked: String(asked), there: `a hard limit of ${hard}` } };
640
+ return { value: Math.min(depth - 1, asked ?? depth - 1) };
641
+ },
642
+ delegatedTasks: lesser("delegatedTasks"),
643
+ tasksPerPlan: lesser("tasksPerPlan"),
644
+ runners: ({ runners }, { runners: asked }) => ({ value: asked ? runners.filter((runner) => asked.includes(runner)) : runners }),
645
+ models: ({ models }, { models: asked }) => ({ value: intersectModels(models, asked) })
646
+ };
647
+ function handedOn(parent, request) {
648
+ const asked = /* @__PURE__ */ new Set([...FLOOR_GRANTS, ...request.grants ?? []]);
649
+ const lost = FLOOR_GRANTS.some((grant) => !parent.grantable.has(grant));
650
+ if (lost) return { refused: { field: "grants", asked: listed(asked), there: listed(parent.grantable) } };
651
+ return { value: new Set([...asked].filter((grant) => parent.grantable.has(grant))) };
652
+ }
653
+ var MANDATE_RULES = {
654
+ grants: handedOn,
655
+ grantable: handedOn,
656
+ autonomy: ({ autonomy }, { autonomy: asked }) => ({ value: autonomy === "guarded" || asked === "guarded" ? "guarded" : "full" }),
657
+ limits: ({ limits }, request) => applyRules(LIMIT_RULES, limits, request),
658
+ exclusions: ({ exclusions }, { protectedPaths }) => ({ value: { protectedPaths: [.../* @__PURE__ */ new Set([...exclusions.protectedPaths, ...protectedPaths ?? []])] } }),
659
+ effectRules: ({ effectRules }, { effectRules: asked }) => ({ value: asked ? [...effectRules, asked] : effectRules }),
660
+ ownEffectRules: (_parent, { effectRules: asked }) => ({ value: asked ? [asked] : [] })
661
+ };
662
+ function applyRules(rules, parent, request) {
663
+ const value = {};
664
+ for (const key of Object.keys(rules)) {
665
+ const ruled = rules[key](parent, request);
666
+ if ("refused" in ruled) return ruled;
667
+ value[key] = ruled.value;
668
+ }
669
+ return { value };
670
+ }
671
+ function narrow(parent, request) {
672
+ const ruled = applyRules(MANDATE_RULES, parent, request);
673
+ return "refused" in ruled ? { ok: false, refusal: { level: request.level, ...ruled.refused } } : { ok: true, mandate: ruled.value };
674
+ }
675
+ function delegatedRequest(task, level) {
676
+ return {
677
+ level,
678
+ ...task.grants ? { grants: task.grants } : {},
679
+ ...task.budget?.tokens !== void 0 ? { tokens: task.budget.tokens } : {},
680
+ ...task.budget?.wallClockMs !== void 0 ? { wallClockMs: task.budget.wallClockMs } : {},
681
+ ...task.protectedPaths ? { protectedPaths: task.protectedPaths } : {}
682
+ };
683
+ }
684
+ var COMMAND_GRANTS = {
685
+ run_plan: "control",
686
+ run_task: "control",
687
+ stop_task: "control",
688
+ stop_run: "control",
689
+ retry_task: "control",
690
+ message_task: "steer",
691
+ answer_approval: "steer",
692
+ merge_all: "merge",
693
+ skip_task: "control",
694
+ force_start: null,
695
+ mark_complete: null,
696
+ mark_not_done: null,
697
+ revert_task: null,
698
+ interrupt_task: null,
699
+ force_send_queued: null,
700
+ remove_queued: null,
701
+ cancel_resume: null,
702
+ answer_checkpoint: "steer",
703
+ approve_review: null,
704
+ run_script: "control"
705
+ };
706
+ function allowsCommand(mandate, command) {
707
+ const grant = COMMAND_GRANTS[command];
708
+ return grant !== null && mandate.grants.has(grant);
709
+ }
710
+ function missingGrant(depth, mandate, needed) {
711
+ return `Level ${depth} refused this: it needs the ${needed} grant, and this node holds ${listed(mandate.grants)}.`;
712
+ }
713
+ var SCRIPT_PAST_THE_RULES = "its body is not covered at every level of the effect rules, and it would run unsealed";
714
+ var TOOL_GRANTS = [
715
+ ...Object.entries(COMMAND_GRANTS).flatMap(([tool, grant]) => grant ? [[tool, grant]] : []),
716
+ ["submit_plan", "replan"],
717
+ ["edit_plan", "replan"]
718
+ ];
719
+ function withheldPlannerTools(mandate) {
720
+ return TOOL_GRANTS.filter(([, grant]) => !mandate.grants.has(grant)).map(([tool]) => tool);
721
+ }
722
+ function autonomousAt(child, global) {
723
+ return child ? child.mandate("off").autonomy === "full" : global;
724
+ }
725
+ function rootAuthority(settings) {
726
+ return { depth: 0, mandate: (level) => rootMandate({ ...settings(), level }) };
727
+ }
728
+
535
729
  // src/services/TaskAssignment.ts
536
730
  function liveModelRefusal(task) {
537
731
  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`;
@@ -587,8 +781,8 @@ function resolveAssignment(write) {
587
781
  const live = writer !== "runner" && !!before && !!catalog.liveRunner?.(before.id);
588
782
  if (live && moved) return { ok: false, field: "assignedRunner", error: liveRunnerRefusal({ title }) };
589
783
  if (live && !sameAssignment(before?.assignedModel, written.assignedModel)) return { ok: false, field: "assignedModel", error: liveModelRefusal({ title }) };
590
- if (written.type !== "ai") return { ok: true, task: written, adjustments: [] };
591
- const mode = modeFor(write, written, moved);
784
+ if (written.type !== "ai" && written.type !== "delegated") return { ok: true, task: written, adjustments: [] };
785
+ const mode = written.type === "delegated" ? { ok: true, mode: void 0 } : modeFor(write, written, moved);
592
786
  if (!mode.ok) return { ok: false, field: "taskMode", error: mode.error };
593
787
  const model = live ? { ok: true, assignment: written.assignedModel } : modelFor(write, moved);
594
788
  if (!model.ok) return { ok: false, field: "assignedModel", error: model.error };
@@ -673,6 +867,179 @@ function resolveAssignments(tasks, planned, catalog) {
673
867
  return { tasks: walk(tasks), adjustments };
674
868
  }
675
869
 
870
+ // src/models/Delegation.ts
871
+ function countChildTasks(statuses) {
872
+ const counts = { completed: 0, running: 0, waiting: 0, pending: 0, failed: 0, skipped: 0 };
873
+ for (const status of statuses) {
874
+ if (status === "completed") counts.completed += 1;
875
+ else if (status === "in_progress") counts.running += 1;
876
+ else if (status === "awaiting_user") counts.waiting += 1;
877
+ else if (status === "failed") counts.failed += 1;
878
+ else if (status === "skipped") counts.skipped += 1;
879
+ else counts.pending += 1;
880
+ }
881
+ return counts;
882
+ }
883
+ var SCOPE_LIMITS = { item: 200, items: 50 };
884
+ var NOT_A_DELEGATED_TASKS_FIELDS = ["prompt", "taskMode", "skills", "userSteps", "ops", "commands", "script", "timeoutMs", "research"];
885
+ var DELEGATED_FIELDS = ["goal", "budget", "grants", "protectedPaths", "scope"];
886
+ var DELEGATION_UNAVAILABLE = "delegation is not available in this session";
887
+ function notOnADelegatedMessage(fields) {
888
+ return `is a delegated task, which holds a goal for a child plan: leave out ${fields.map((f) => `"${f}"`).join(", ")}`;
889
+ }
890
+ function isBudget(value) {
891
+ if (typeof value !== "object" || value === null) return false;
892
+ const { tokens } = value;
893
+ return typeof tokens === "number" && Number.isInteger(tokens) && tokens > 0;
894
+ }
895
+ function wallClockOk(budget) {
896
+ const { wallClockMs } = budget;
897
+ return wallClockMs === void 0 || Number.isInteger(wallClockMs) && wallClockMs > 0;
898
+ }
899
+ function readsOnly(task, runnerModes) {
900
+ if (task.type !== "ai") return false;
901
+ const mode = runnerModes ? taggedReadOnlyMode(runnerModes[task.assignedRunner]) : "plan";
902
+ return mode !== void 0 && task.taskMode === mode;
903
+ }
904
+ function runnerUnread(task, runnerModes) {
905
+ return task.type === "ai" && runnerModes !== void 0 && runnerModes[task.assignedRunner] === void 0;
906
+ }
907
+ function atAGate(task) {
908
+ return task.type === "script" && !task.readOnly || task.ops === true;
909
+ }
910
+ function reaches(tasks, from, target) {
911
+ const byId = new Map(tasks.map((t) => [t.id, t]));
912
+ const seen = /* @__PURE__ */ new Set();
913
+ const walk = (task) => task.dependencies.some((id) => {
914
+ if (seen.has(id)) return false;
915
+ seen.add(id);
916
+ const dep = byId.get(id);
917
+ return dep !== void 0 && (target(dep) || walk(dep));
918
+ });
919
+ return walk(from);
920
+ }
921
+ function delegationErrors(tasks, rules = {}) {
922
+ const errors = [];
923
+ const delegated = tasks.filter((t) => t.type === "delegated");
924
+ const fail = (task, field, message) => errors.push({ taskId: task.id, field, message });
925
+ for (const task of delegated) {
926
+ const where = `Task "${task.title}"`;
927
+ 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`);
928
+ 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`);
929
+ else if (!wallClockOk(task.budget)) fail(task, "budget", `${where} has a "wallClockMs" that is not a whole number of milliseconds above 0`);
930
+ const paths = task.protectedPaths;
931
+ if (!task.readOnly && (!Array.isArray(paths) || paths.length === 0 || !paths.every((p) => typeof p === "string" && p.trim()))) {
932
+ 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`);
933
+ }
934
+ 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`);
935
+ const unknown = (task.grants ?? []).filter((grant) => !GRANTS.includes(grant));
936
+ 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(", ")}`);
937
+ const dependents = tasks.filter((t) => t.dependencies.includes(task.id));
938
+ if (task.readOnly) {
939
+ for (const script of dependents.filter((t) => t.type === "script")) {
940
+ 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`);
941
+ }
942
+ }
943
+ if (!dependents.some((t) => judges(t, task, rules.runnerModes))) {
944
+ 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`);
945
+ }
946
+ }
947
+ for (const task of tasks.filter((t) => t.type === "script" && t.readOnly)) {
948
+ 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`);
949
+ }
950
+ for (const check of tasks.filter((t) => t.loop)) {
951
+ if (!reaches(tasks, check, (t) => t.type === "delegated")) continue;
952
+ for (const id of check.loop?.body ?? []) {
953
+ const body = tasks.find((t) => t.id === id);
954
+ if (body && atAGate(body)) {
955
+ 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`);
956
+ }
957
+ }
958
+ }
959
+ return errors;
960
+ }
961
+ function scopeOk(scope) {
962
+ if (typeof scope !== "object" || scope === null) return false;
963
+ const { paths, provides, consumes } = scope;
964
+ const itemOk = (item) => typeof item === "string" && item.trim().length > 0 && item.length <= SCOPE_LIMITS.item && ONE_LINE.test(item);
965
+ return [paths, provides, consumes].every((list) => list === void 0 || Array.isArray(list) && list.length <= SCOPE_LIMITS.items && list.every(itemOk));
966
+ }
967
+ function judges(task, delegated, runnerModes) {
968
+ return readsOnly(task, runnerModes) || !delegated.readOnly && task.type === "script" && task.readOnly === true;
969
+ }
970
+ function checksOf(tasks, delegated, rules = {}) {
971
+ return tasks.filter((t) => t.dependencies.includes(delegated.id) && (judges(t, delegated, rules.runnerModes) || runnerUnread(t, rules.runnerModes)));
972
+ }
973
+ function checksHoldingMerge(tasks, awaitsMerge, rules = {}) {
974
+ return tasks.filter((t) => t.type === "delegated" && awaitsMerge(t.id)).flatMap((t) => checksOf(tasks, t, rules)).filter((check) => check.status !== "completed" && check.status !== "skipped");
975
+ }
976
+ function commandsOf({ script, commands }) {
977
+ const written = splitLines(script ?? "").flatMap((line) => line.split(/[;|&]+/)).map((command) => command.trim()).filter((command) => !command.startsWith("#"));
978
+ return [...written, ...(commands ?? []).map((argv) => argv.join(" "))].filter(Boolean);
979
+ }
980
+ function checkWords(tasks) {
981
+ const delegated = new Set(tasks.filter((t) => t.type === "delegated").map((t) => t.id));
982
+ return tasks.filter((t) => t.dependencies.some((id) => delegated.has(id))).flatMap((t) => [
983
+ ...[t.title, t.prompt ?? ""].map((text2) => ({ text: text2, command: false })),
984
+ ...commandsOf(t).map((text2) => ({ text: text2, command: true }))
985
+ ]);
986
+ }
987
+ function checksDelegatedTask(tasks, task, rules = {}) {
988
+ if (!readsOnly(task, rules.runnerModes) && !runnerUnread(task, rules.runnerModes) && !(task.type === "script" && task.readOnly)) return false;
989
+ return tasks.some((t) => t.type === "delegated" && task.dependencies.includes(t.id));
990
+ }
991
+ function planSize(tasks) {
992
+ return flattenTasks(tasks).length;
993
+ }
994
+ function underProtectedPath(file, protectedPath) {
995
+ const rule = protectedPath.trim().replace(/^\.\//, "").replace(/\\/g, "/");
996
+ if (!rule) return false;
997
+ if (rule.includes("*")) return globRegExp(rule).test(file);
998
+ const bare = rule.replace(/\/+$/, "");
999
+ return file === bare || file.startsWith(`${bare}/`);
1000
+ }
1001
+ function fixedStart(rule) {
1002
+ const bare = rule.trim().replace(/^\.\//, "").replace(/\\/g, "/");
1003
+ const star = bare.indexOf("*");
1004
+ return star === -1 ? bare : bare.slice(0, star);
1005
+ }
1006
+ function pathsMeet(a, b) {
1007
+ if (underProtectedPath(a.replace(/\/+$/, ""), b) || underProtectedPath(b.replace(/\/+$/, ""), a)) return true;
1008
+ if (!a.includes("*") || !b.includes("*")) return false;
1009
+ const [one, other] = [fixedStart(a), fixedStart(b)];
1010
+ return one.startsWith(other) || other.startsWith(one);
1011
+ }
1012
+ function narrower(a, b) {
1013
+ return b.includes("*") || !a.includes("*") && a.length >= b.length ? a : b;
1014
+ }
1015
+ function ordered(tasks, a, b) {
1016
+ return reaches(tasks, a, (t) => t.id === b.id) || reaches(tasks, b, (t) => t.id === a.id);
1017
+ }
1018
+ function scopeOverlaps(tasks) {
1019
+ const scoped = tasks.filter((t) => t.type === "delegated" && t.scope);
1020
+ const said = [];
1021
+ scoped.forEach((a, i) => {
1022
+ for (const b of scoped.slice(i + 1)) {
1023
+ if (ordered(tasks, a, b)) continue;
1024
+ const declared = (a.scope?.paths ?? []).flatMap((one) => (b.scope?.paths ?? []).filter((other) => pathsMeet(one, other)).map((other) => narrower(one, other)));
1025
+ 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.`);
1026
+ for (const name of a.scope?.provides ?? []) {
1027
+ if (b.scope?.provides?.includes(name)) said.push(`Tasks "${a.title}" and "${b.title}" both provide "${name}", and nothing orders them: name one owner.`);
1028
+ }
1029
+ for (const [producer, consumer] of [[a, b], [b, a]]) {
1030
+ for (const name of producer.scope?.provides ?? []) {
1031
+ 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.`);
1032
+ }
1033
+ }
1034
+ }
1035
+ });
1036
+ return said;
1037
+ }
1038
+ function globRegExp(rule) {
1039
+ const pattern = rule.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\/?|\*/g, (star) => star === "*" ? "[^/]*" : "(?:.*/)?");
1040
+ return new RegExp(`^${pattern}(?:/.*)?$`);
1041
+ }
1042
+
676
1043
  // src/services/PlannedTask.ts
677
1044
  import { v4 as uuidv4 } from "uuid";
678
1045
  var MODE_NOT_THE_PLANNERS = `"taskMode" is the user's to choose: set "readOnly": true on a task that must not change files, and leave it out otherwise`;
@@ -691,6 +1058,28 @@ function notOnAScript(raw) {
691
1058
  return value !== void 0 && value !== null && value !== false && !(Array.isArray(value) && value.length === 0);
692
1059
  });
693
1060
  }
1061
+ function notOnADelegated(raw) {
1062
+ return NOT_A_DELEGATED_TASKS_FIELDS.filter((f) => {
1063
+ const value = raw[f];
1064
+ return value !== void 0 && value !== null && value !== false && !(Array.isArray(value) && value.length === 0);
1065
+ });
1066
+ }
1067
+ function delegatedFields(raw, base) {
1068
+ const list = (value) => Array.isArray(value) ? value.map(String) : void 0;
1069
+ const goal = text(raw.goal) ?? base.goal;
1070
+ const budget = (isRecord(raw.budget) ? raw.budget : void 0) ?? base.budget;
1071
+ const grants = list(raw.grants) ?? base.grants;
1072
+ const protectedPaths = list(raw.protectedPaths) ?? base.protectedPaths;
1073
+ const scope = (isRecord(raw.scope) ? raw.scope : void 0) ?? base.scope;
1074
+ return {
1075
+ ...goal !== void 0 ? { goal } : {},
1076
+ ...budget ? { budget } : {},
1077
+ ...grants ? { grants } : {},
1078
+ ...protectedPaths ? { protectedPaths } : {},
1079
+ ...scope ? { scope } : {},
1080
+ ...raw.readOnly === true || raw.readOnly === void 0 && base.readOnly ? { readOnly: true } : {}
1081
+ };
1082
+ }
694
1083
  function notOnAScriptMessage(fields) {
695
1084
  return `is a script task, which runs its "commands" or "script" with no model: leave out ${fields.map((f) => `"${f}"`).join(", ")}`;
696
1085
  }
@@ -751,27 +1140,35 @@ function plannedTask(spec, catalog, at = {}) {
751
1140
  const plannerId = text(raw.id);
752
1141
  const id = freshIds || !plannerId ? newId() : plannerId;
753
1142
  if (freshIds && plannerId) renamed.set(plannerId, id);
754
- const type = raw.type === "user" || raw.type === "ai" || raw.type === "script" ? raw.type : base.type ?? "ai";
1143
+ const delegating = catalog.delegationRefusal === null;
1144
+ const type = raw.type === "user" || raw.type === "ai" || raw.type === "script" || raw.type === "delegated" ? raw.type : base.type ?? "ai";
755
1145
  const ai = type === "ai";
756
1146
  const script = type === "script";
1147
+ const delegated = type === "delegated";
1148
+ 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);
757
1149
  const assignedRunner = text(raw.assignedRunner) ?? base.assignedRunner ?? catalog.runners[0] ?? DEFAULT_RUNNERS[0];
758
1150
  const description = text(raw.description) ?? base.description ?? title;
759
- const notScripts = script ? notOnAScript(raw) : [];
1151
+ const notScripts = script ? notOnAScript(raw).filter((f) => !(sealedScript && f === "readOnly")) : [];
760
1152
  if (notScripts.length > 0) errors.push({ taskId: id, field: notScripts[0], message: `${where} ${notOnAScriptMessage(notScripts)}` });
1153
+ if (delegated) {
1154
+ if (!delegating) errors.push({ taskId: id, field: "type", message: catalog.delegationRefusal ?? `${where} is a delegated task, and ${DELEGATION_UNAVAILABLE}.` });
1155
+ const extra = notOnADelegated(raw);
1156
+ if (extra.length > 0) errors.push({ taskId: id, field: extra[0], message: `${where} ${notOnADelegatedMessage(extra)}` });
1157
+ }
761
1158
  const body = bodyFields(raw, base);
762
1159
  const shapeErrors = scriptBodyErrors({ type, ...body.raw }).filter((e) => body.malformed.has(e.field));
763
1160
  for (const e of shapeErrors) errors.push({ taskId: id, field: e.field, message: `${where} ${e.message}` });
764
1161
  const loop = loopSpec(raw.loop);
765
1162
  if ("error" in loop) errors.push({ taskId: id, field: "loop", message: `${where} ${loop.error}` });
766
1163
  if (raw.ops === true && !ai && !script) errors.push({ taskId: id, field: "ops", message: `${where} ${opsPlacementRefusal(type, !!parent)}` });
767
- if (raw.taskMode !== void 0 && !script) ignored.push({ taskId: id, field: "taskMode", message: `${where}: ${MODE_NOT_THE_PLANNERS}` });
1164
+ if (raw.taskMode !== void 0 && !script && !delegated) ignored.push({ taskId: id, field: "taskMode", message: `${where}: ${MODE_NOT_THE_PLANNERS}` });
768
1165
  if (type === "user" && raw.readOnly === true) ignored.push({ taskId: id, field: "readOnly", message: `${where}: ${MANUAL_NOT_READ_ONLY}` });
769
1166
  const ops = raw.ops !== void 0 ? raw.ops === true : base.ops;
770
1167
  if (ai && ops && raw.readOnly === true) ignored.push({ taskId: id, field: "readOnly", message: `${where}: ${OPS_NOT_READ_ONLY}` });
771
1168
  const asked = typeof raw.readOnly === "boolean" ? raw.readOnly : void 0;
772
1169
  const readOnly = ai && !ops && (asked ?? from?.readOnly ?? parent?.readOnly ?? false);
773
1170
  if (raw.research !== void 0 && typeof raw.research !== "boolean") errors.push({ taskId: id, field: "research", message: `${where}: "research" must be a boolean` });
774
- const assignedModel = ai ? modelOf(raw, base.assignedModel) : void 0;
1171
+ const assignedModel = ai || delegated ? modelOf(raw, base.assignedModel) : void 0;
775
1172
  const slice = (value) => value === "AFK" || value === "HITL" ? value : void 0;
776
1173
  const autonomy = ai ? slice(raw.autonomy) ?? base.autonomy ?? parent?.autonomy : void 0;
777
1174
  const sliceType = slice(raw.sliceType) ?? base.sliceType ?? (ai ? parent?.sliceType : void 0);
@@ -794,7 +1191,7 @@ function plannedTask(spec, catalog, at = {}) {
794
1191
  type,
795
1192
  dependencies: Array.isArray(raw.dependencies) ? raw.dependencies.map(String) : base.dependencies ?? [],
796
1193
  // What a merge or split carries over for a runner is nothing a script runs.
797
- prompt: script ? void 0 : text(raw.prompt) || base.prompt || (ai ? description || title || void 0 : void 0),
1194
+ prompt: script || delegated ? void 0 : text(raw.prompt) || base.prompt || (ai ? description || title || void 0 : void 0),
798
1195
  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,
799
1196
  subtasks,
800
1197
  assignedModel,
@@ -802,15 +1199,17 @@ function plannedTask(spec, catalog, at = {}) {
802
1199
  autonomy,
803
1200
  sliceType,
804
1201
  userStoriesCovered: Array.isArray(raw.userStoriesCovered) ? raw.userStoriesCovered.map(String) : base.userStoriesCovered,
805
- ops: script ? void 0 : ops,
1202
+ ops: script || delegated ? void 0 : ops,
806
1203
  research: typeof raw.research === "boolean" ? raw.research : base.research,
807
- skills: script ? void 0 : from ? inheritedSkills(raw.skills, base.skills) : skillNames(raw.skills),
1204
+ skills: script || delegated ? void 0 : from ? inheritedSkills(raw.skills, base.skills) : skillNames(raw.skills),
808
1205
  ...body.fields,
809
- ..."loop" in loop && loop.loop ? { loop: loop.loop } : {}
1206
+ ..."loop" in loop && loop.loop ? { loop: loop.loop } : {},
1207
+ ...delegated ? delegatedFields(raw, base) : {},
1208
+ ...sealedScript ? { readOnly: true } : {}
810
1209
  };
811
- let taskMode = script ? void 0 : base.taskMode;
1210
+ let taskMode = script || delegated ? void 0 : base.taskMode;
812
1211
  let landedModel = assignedModel;
813
- if (ai && catalog.runners.includes(assignedRunner)) {
1212
+ if ((ai || delegated) && catalog.runners.includes(assignedRunner)) {
814
1213
  const resolved = resolveAssignment({
815
1214
  writer: catalog.models ? "edit" : "plan",
816
1215
  task: { ...createTask(fields), taskMode },
@@ -821,7 +1220,7 @@ function plannedTask(spec, catalog, at = {}) {
821
1220
  if (!resolved.ok) {
822
1221
  errors.push({ taskId: id, field: resolved.field === "taskMode" ? "readOnly" : resolved.field, message: `${where}: ${resolved.error}` });
823
1222
  } else {
824
- taskMode = resolved.task.taskMode;
1223
+ taskMode = delegated ? void 0 : resolved.task.taskMode;
825
1224
  landedModel = resolved.task.assignedModel;
826
1225
  adjustments.push(...resolved.adjustments);
827
1226
  }
@@ -833,13 +1232,13 @@ function plannedTask(spec, catalog, at = {}) {
833
1232
  }
834
1233
  function checkPlanRunners(tasks, runners) {
835
1234
  if (!runners) return [];
836
- return flattenTasksWithParents(tasks).filter(({ task: t }) => t.type === "ai" && !runners.includes(t.assignedRunner)).map(({ task: t, parent }) => ({
1235
+ return flattenTasksWithParents(tasks).filter(({ task: t }) => (t.type === "ai" || t.type === "delegated") && !runners.includes(t.assignedRunner)).map(({ task: t, parent }) => ({
837
1236
  taskId: t.id,
838
1237
  field: "assignedRunner",
839
1238
  message: `${taskWhere(t.title, parent?.title)} has invalid assignedRunner "${t.assignedRunner}". Expected one of: ${runners.join(", ")}`
840
1239
  }));
841
1240
  }
842
- function checkPlanInvariants(tasks, runners, runnerModes) {
1241
+ function checkPlanInvariants(tasks, runners, runnerModes, delegation = {}) {
843
1242
  const rows = flattenTasksWithParents(tasks);
844
1243
  const errors = [];
845
1244
  const seen = /* @__PURE__ */ new Set();
@@ -868,6 +1267,10 @@ function checkPlanInvariants(tasks, runners, runnerModes) {
868
1267
  const extra = notAScriptsFields(t);
869
1268
  if (extra.length > 0) fail(extra[0], `${where} ${notOnAScriptMessage(extra)}`);
870
1269
  }
1270
+ if (t.type === "delegated") {
1271
+ if (parent !== null) fail("type", `${where} is a delegated task: only a top-level task can be delegated`);
1272
+ 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`);
1273
+ }
871
1274
  for (const e of scriptBodyErrors(t)) fail(e.field, `${where} ${e.message}`);
872
1275
  if (t.ops && t.taskMode !== void 0 && t.taskMode === taggedReadOnlyMode(runnerModes?.[t.assignedRunner])) {
873
1276
  fail("taskMode", `${where} is an ops task, which acts in the workspace, so it cannot use the read-only profile`);
@@ -875,6 +1278,7 @@ function checkPlanInvariants(tasks, runners, runnerModes) {
875
1278
  }
876
1279
  errors.push(...checkPlanRunners(tasks, runners));
877
1280
  errors.push(...loopErrors(tasks));
1281
+ errors.push(...delegationErrors(tasks, { ...runnerModes ? { runnerModes } : {}, ...delegation }));
878
1282
  for (const { task: t, parent } of rows) {
879
1283
  for (const error of dependencyTargetErrors(tasks, t.id, t.dependencies.filter((id) => id !== t.id))) {
880
1284
  errors.push({ taskId: t.id, field: "dependencies", message: `${taskWhere(t.title, parent?.title)}: ${error}` });
@@ -908,11 +1312,17 @@ export {
908
1312
  loopRoundOf,
909
1313
  betweenRounds,
910
1314
  outOfRounds,
1315
+ loopEnded,
911
1316
  checkOf,
1317
+ checksOfBodies,
912
1318
  inLoop,
913
1319
  loopSpec,
914
1320
  loopRemovalRefusal,
915
1321
  loopReshapeRefusal,
1322
+ ONE_LINE,
1323
+ splitLines,
1324
+ quotedLine,
1325
+ onOneLine,
916
1326
  PLAN_ENVELOPE_KEY,
917
1327
  TASK_OPS_ENVELOPE_KEY,
918
1328
  TASK_QUERY_ENVELOPE_KEY,
@@ -924,6 +1334,7 @@ export {
924
1334
  stripTrailingCommas,
925
1335
  escapeControlCharsInStrings,
926
1336
  runnerModesFrom,
1337
+ resolveDefaultMode,
927
1338
  autonomyLevelLabel,
928
1339
  parseAutonomyLevel,
929
1340
  parseAutonomyFlag,
@@ -939,6 +1350,31 @@ export {
939
1350
  assignableRunners,
940
1351
  resolveAssignment,
941
1352
  resolveAssignments,
1353
+ GRANTS,
1354
+ FLOOR_GRANTS,
1355
+ SHAPE_LIMITS,
1356
+ mandateRefusalText,
1357
+ rootMandate,
1358
+ heldMandate,
1359
+ narrow,
1360
+ delegatedRequest,
1361
+ COMMAND_GRANTS,
1362
+ allowsCommand,
1363
+ missingGrant,
1364
+ SCRIPT_PAST_THE_RULES,
1365
+ withheldPlannerTools,
1366
+ autonomousAt,
1367
+ rootAuthority,
1368
+ countChildTasks,
1369
+ DELEGATED_FIELDS,
1370
+ notOnADelegatedMessage,
1371
+ checksOf,
1372
+ checksHoldingMerge,
1373
+ checkWords,
1374
+ checksDelegatedTask,
1375
+ planSize,
1376
+ underProtectedPath,
1377
+ scopeOverlaps,
942
1378
  MODE_NOT_THE_PLANNERS,
943
1379
  MANUAL_NOT_READ_ONLY,
944
1380
  OPS_NOT_READ_ONLY,
@@ -950,4 +1386,4 @@ export {
950
1386
  checkPlanInvariants,
951
1387
  detectCycle
952
1388
  };
953
- //# sourceMappingURL=chunk-YAFNYNFX.mjs.map
1389
+ //# sourceMappingURL=chunk-2IFKPYPO.mjs.map