@ordewell/core 0.12.0 → 0.12.2

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/dist/{ModeResolver-CHsaoV7M.d.ts → ModeResolver-B64aUZXJ.d.ts} +1 -1
  2. package/dist/{ModeResolver-b_7bPlBS.d.mts → ModeResolver-CA2fUU9h.d.mts} +1 -1
  3. package/dist/{Task-BTEH-ztX.d.mts → Task-CxGgN6I0.d.mts} +503 -16
  4. package/dist/{Task-BTEH-ztX.d.ts → Task-CxGgN6I0.d.ts} +503 -16
  5. package/dist/{chunk-BKTDYGIP.mjs → chunk-2IFKPYPO.mjs} +441 -17
  6. package/dist/chunk-2IFKPYPO.mjs.map +1 -0
  7. package/dist/{chunk-BFLVOYNR.mjs → chunk-FX33UKAD.mjs} +8 -6
  8. package/dist/chunk-FX33UKAD.mjs.map +1 -0
  9. package/dist/{chunk-F6WS3JLQ.mjs → chunk-PKJTN2PD.mjs} +20 -11
  10. package/dist/chunk-PKJTN2PD.mjs.map +1 -0
  11. package/dist/{chunk-HP2LNKST.mjs → chunk-RZ23BMVR.mjs} +345 -134
  12. package/dist/chunk-RZ23BMVR.mjs.map +1 -0
  13. package/dist/chunk-UDUATSCJ.mjs +48 -0
  14. package/dist/chunk-UDUATSCJ.mjs.map +1 -0
  15. package/dist/{IRunner-BwzyUVIL.d.mts → eventStore-CTCSKS9E.d.ts} +916 -129
  16. package/dist/{IRunner-DjJkYwko.d.ts → eventStore-CcB8yELZ.d.mts} +916 -129
  17. package/dist/index.d.mts +677 -97
  18. package/dist/index.d.ts +677 -97
  19. package/dist/index.js +8233 -2450
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +6951 -1772
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/parsing.d.mts +2 -2
  24. package/dist/parsing.d.ts +2 -2
  25. package/dist/parsing.js +277 -21
  26. package/dist/parsing.js.map +1 -1
  27. package/dist/parsing.mjs +3 -3
  28. package/dist/plan-utils.d.mts +30 -5
  29. package/dist/plan-utils.d.ts +30 -5
  30. package/dist/plan-utils.js +163 -16
  31. package/dist/plan-utils.js.map +1 -1
  32. package/dist/plan-utils.mjs +49 -3
  33. package/dist/plan-utils.mjs.map +1 -1
  34. package/dist/{runSettingsText-x-YML2ay.d.mts → runSettingsText-DFVpe6aU.d.mts} +1142 -934
  35. package/dist/{runSettingsText-CvZ40IPv.d.ts → runSettingsText-lKLDsyli.d.ts} +1142 -934
  36. package/dist/testing.d.mts +83 -4
  37. package/dist/testing.d.ts +83 -4
  38. package/dist/testing.js +142 -1
  39. package/dist/testing.js.map +1 -1
  40. package/dist/testing.mjs +126 -2
  41. package/dist/testing.mjs.map +1 -1
  42. package/docker/task.Dockerfile +22 -0
  43. package/package.json +2 -1
  44. package/skills/diagnose/SKILL.md +2 -2
  45. package/skills/explore/SKILL.md +1 -1
  46. package/dist/chunk-BFLVOYNR.mjs.map +0 -1
  47. package/dist/chunk-BKTDYGIP.mjs.map +0 -1
  48. package/dist/chunk-F6WS3JLQ.mjs.map +0 -1
  49. package/dist/chunk-HP2LNKST.mjs.map +0 -1
@@ -7,7 +7,7 @@ import {
7
7
  flattenTasksWithParents,
8
8
  inheritedSkills,
9
9
  skillNames
10
- } from "./chunk-F6WS3JLQ.mjs";
10
+ } from "./chunk-PKJTN2PD.mjs";
11
11
 
12
12
  // src/models/ScriptTask.ts
13
13
  var DEFAULT_SCRIPT_TIMEOUT_MS = 10 * 6e4;
@@ -168,6 +168,7 @@ function loopErrors(tasks) {
168
168
  const fail = (message) => errors.push({ taskId: check.id, field: "loop", message: `${where} ${message}` });
169
169
  if (parent) fail(`has a "loop", but only a top-level task can be a loop's check`);
170
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");
171
172
  if (check.subtasks?.length) fail("is a loop's check, which has no subtasks: each round runs it again whole");
172
173
  if (!isRoundCount(loop.maxRounds)) fail(roundsMessage(loop.maxRounds));
173
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');
@@ -200,6 +201,7 @@ function loopErrors(tasks) {
200
201
  bodyOwner.set(id, check);
201
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`);
202
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`);
203
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`);
204
206
  }
205
207
  }
@@ -229,10 +231,25 @@ function loopRemovalRefusal(task) {
229
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;
230
232
  }
231
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`;
232
235
  if (target.loop) return `"${target.title}" is a loop's check and cannot be ${action}: drop its loop first ("loop": null)`;
233
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;
234
237
  }
235
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
+
236
253
  // src/services/JsonExtractor.ts
237
254
  var PLAN_ENVELOPE_KEY = "tasks";
238
255
  var TASK_OPS_ENVELOPE_KEY = "taskOps";
@@ -542,6 +559,173 @@ function clampThinkingEffort(effort, variants) {
542
559
  return void 0;
543
560
  }
544
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
+
545
729
  // src/services/TaskAssignment.ts
546
730
  function liveModelRefusal(task) {
547
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`;
@@ -597,8 +781,8 @@ function resolveAssignment(write) {
597
781
  const live = writer !== "runner" && !!before && !!catalog.liveRunner?.(before.id);
598
782
  if (live && moved) return { ok: false, field: "assignedRunner", error: liveRunnerRefusal({ title }) };
599
783
  if (live && !sameAssignment(before?.assignedModel, written.assignedModel)) return { ok: false, field: "assignedModel", error: liveModelRefusal({ title }) };
600
- if (written.type !== "ai") return { ok: true, task: written, adjustments: [] };
601
- 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);
602
786
  if (!mode.ok) return { ok: false, field: "taskMode", error: mode.error };
603
787
  const model = live ? { ok: true, assignment: written.assignedModel } : modelFor(write, moved);
604
788
  if (!model.ok) return { ok: false, field: "assignedModel", error: model.error };
@@ -683,6 +867,179 @@ function resolveAssignments(tasks, planned, catalog) {
683
867
  return { tasks: walk(tasks), adjustments };
684
868
  }
685
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
+
686
1043
  // src/services/PlannedTask.ts
687
1044
  import { v4 as uuidv4 } from "uuid";
688
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`;
@@ -701,6 +1058,28 @@ function notOnAScript(raw) {
701
1058
  return value !== void 0 && value !== null && value !== false && !(Array.isArray(value) && value.length === 0);
702
1059
  });
703
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
+ }
704
1083
  function notOnAScriptMessage(fields) {
705
1084
  return `is a script task, which runs its "commands" or "script" with no model: leave out ${fields.map((f) => `"${f}"`).join(", ")}`;
706
1085
  }
@@ -761,27 +1140,35 @@ function plannedTask(spec, catalog, at = {}) {
761
1140
  const plannerId = text(raw.id);
762
1141
  const id = freshIds || !plannerId ? newId() : plannerId;
763
1142
  if (freshIds && plannerId) renamed.set(plannerId, id);
764
- 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";
765
1145
  const ai = type === "ai";
766
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);
767
1149
  const assignedRunner = text(raw.assignedRunner) ?? base.assignedRunner ?? catalog.runners[0] ?? DEFAULT_RUNNERS[0];
768
1150
  const description = text(raw.description) ?? base.description ?? title;
769
- const notScripts = script ? notOnAScript(raw) : [];
1151
+ const notScripts = script ? notOnAScript(raw).filter((f) => !(sealedScript && f === "readOnly")) : [];
770
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
+ }
771
1158
  const body = bodyFields(raw, base);
772
1159
  const shapeErrors = scriptBodyErrors({ type, ...body.raw }).filter((e) => body.malformed.has(e.field));
773
1160
  for (const e of shapeErrors) errors.push({ taskId: id, field: e.field, message: `${where} ${e.message}` });
774
1161
  const loop = loopSpec(raw.loop);
775
1162
  if ("error" in loop) errors.push({ taskId: id, field: "loop", message: `${where} ${loop.error}` });
776
1163
  if (raw.ops === true && !ai && !script) errors.push({ taskId: id, field: "ops", message: `${where} ${opsPlacementRefusal(type, !!parent)}` });
777
- 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}` });
778
1165
  if (type === "user" && raw.readOnly === true) ignored.push({ taskId: id, field: "readOnly", message: `${where}: ${MANUAL_NOT_READ_ONLY}` });
779
1166
  const ops = raw.ops !== void 0 ? raw.ops === true : base.ops;
780
1167
  if (ai && ops && raw.readOnly === true) ignored.push({ taskId: id, field: "readOnly", message: `${where}: ${OPS_NOT_READ_ONLY}` });
781
1168
  const asked = typeof raw.readOnly === "boolean" ? raw.readOnly : void 0;
782
1169
  const readOnly = ai && !ops && (asked ?? from?.readOnly ?? parent?.readOnly ?? false);
783
1170
  if (raw.research !== void 0 && typeof raw.research !== "boolean") errors.push({ taskId: id, field: "research", message: `${where}: "research" must be a boolean` });
784
- const assignedModel = ai ? modelOf(raw, base.assignedModel) : void 0;
1171
+ const assignedModel = ai || delegated ? modelOf(raw, base.assignedModel) : void 0;
785
1172
  const slice = (value) => value === "AFK" || value === "HITL" ? value : void 0;
786
1173
  const autonomy = ai ? slice(raw.autonomy) ?? base.autonomy ?? parent?.autonomy : void 0;
787
1174
  const sliceType = slice(raw.sliceType) ?? base.sliceType ?? (ai ? parent?.sliceType : void 0);
@@ -804,7 +1191,7 @@ function plannedTask(spec, catalog, at = {}) {
804
1191
  type,
805
1192
  dependencies: Array.isArray(raw.dependencies) ? raw.dependencies.map(String) : base.dependencies ?? [],
806
1193
  // What a merge or split carries over for a runner is nothing a script runs.
807
- 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),
808
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,
809
1196
  subtasks,
810
1197
  assignedModel,
@@ -812,15 +1199,17 @@ function plannedTask(spec, catalog, at = {}) {
812
1199
  autonomy,
813
1200
  sliceType,
814
1201
  userStoriesCovered: Array.isArray(raw.userStoriesCovered) ? raw.userStoriesCovered.map(String) : base.userStoriesCovered,
815
- ops: script ? void 0 : ops,
1202
+ ops: script || delegated ? void 0 : ops,
816
1203
  research: typeof raw.research === "boolean" ? raw.research : base.research,
817
- 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),
818
1205
  ...body.fields,
819
- ..."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 } : {}
820
1209
  };
821
- let taskMode = script ? void 0 : base.taskMode;
1210
+ let taskMode = script || delegated ? void 0 : base.taskMode;
822
1211
  let landedModel = assignedModel;
823
- if (ai && catalog.runners.includes(assignedRunner)) {
1212
+ if ((ai || delegated) && catalog.runners.includes(assignedRunner)) {
824
1213
  const resolved = resolveAssignment({
825
1214
  writer: catalog.models ? "edit" : "plan",
826
1215
  task: { ...createTask(fields), taskMode },
@@ -831,7 +1220,7 @@ function plannedTask(spec, catalog, at = {}) {
831
1220
  if (!resolved.ok) {
832
1221
  errors.push({ taskId: id, field: resolved.field === "taskMode" ? "readOnly" : resolved.field, message: `${where}: ${resolved.error}` });
833
1222
  } else {
834
- taskMode = resolved.task.taskMode;
1223
+ taskMode = delegated ? void 0 : resolved.task.taskMode;
835
1224
  landedModel = resolved.task.assignedModel;
836
1225
  adjustments.push(...resolved.adjustments);
837
1226
  }
@@ -843,13 +1232,13 @@ function plannedTask(spec, catalog, at = {}) {
843
1232
  }
844
1233
  function checkPlanRunners(tasks, runners) {
845
1234
  if (!runners) return [];
846
- 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 }) => ({
847
1236
  taskId: t.id,
848
1237
  field: "assignedRunner",
849
1238
  message: `${taskWhere(t.title, parent?.title)} has invalid assignedRunner "${t.assignedRunner}". Expected one of: ${runners.join(", ")}`
850
1239
  }));
851
1240
  }
852
- function checkPlanInvariants(tasks, runners, runnerModes) {
1241
+ function checkPlanInvariants(tasks, runners, runnerModes, delegation = {}) {
853
1242
  const rows = flattenTasksWithParents(tasks);
854
1243
  const errors = [];
855
1244
  const seen = /* @__PURE__ */ new Set();
@@ -878,6 +1267,10 @@ function checkPlanInvariants(tasks, runners, runnerModes) {
878
1267
  const extra = notAScriptsFields(t);
879
1268
  if (extra.length > 0) fail(extra[0], `${where} ${notOnAScriptMessage(extra)}`);
880
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
+ }
881
1274
  for (const e of scriptBodyErrors(t)) fail(e.field, `${where} ${e.message}`);
882
1275
  if (t.ops && t.taskMode !== void 0 && t.taskMode === taggedReadOnlyMode(runnerModes?.[t.assignedRunner])) {
883
1276
  fail("taskMode", `${where} is an ops task, which acts in the workspace, so it cannot use the read-only profile`);
@@ -885,6 +1278,7 @@ function checkPlanInvariants(tasks, runners, runnerModes) {
885
1278
  }
886
1279
  errors.push(...checkPlanRunners(tasks, runners));
887
1280
  errors.push(...loopErrors(tasks));
1281
+ errors.push(...delegationErrors(tasks, { ...runnerModes ? { runnerModes } : {}, ...delegation }));
888
1282
  for (const { task: t, parent } of rows) {
889
1283
  for (const error of dependencyTargetErrors(tasks, t.id, t.dependencies.filter((id) => id !== t.id))) {
890
1284
  errors.push({ taskId: t.id, field: "dependencies", message: `${taskWhere(t.title, parent?.title)}: ${error}` });
@@ -925,6 +1319,10 @@ export {
925
1319
  loopSpec,
926
1320
  loopRemovalRefusal,
927
1321
  loopReshapeRefusal,
1322
+ ONE_LINE,
1323
+ splitLines,
1324
+ quotedLine,
1325
+ onOneLine,
928
1326
  PLAN_ENVELOPE_KEY,
929
1327
  TASK_OPS_ENVELOPE_KEY,
930
1328
  TASK_QUERY_ENVELOPE_KEY,
@@ -936,6 +1334,7 @@ export {
936
1334
  stripTrailingCommas,
937
1335
  escapeControlCharsInStrings,
938
1336
  runnerModesFrom,
1337
+ resolveDefaultMode,
939
1338
  autonomyLevelLabel,
940
1339
  parseAutonomyLevel,
941
1340
  parseAutonomyFlag,
@@ -951,6 +1350,31 @@ export {
951
1350
  assignableRunners,
952
1351
  resolveAssignment,
953
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,
954
1378
  MODE_NOT_THE_PLANNERS,
955
1379
  MANUAL_NOT_READ_ONLY,
956
1380
  OPS_NOT_READ_ONLY,
@@ -962,4 +1386,4 @@ export {
962
1386
  checkPlanInvariants,
963
1387
  detectCycle
964
1388
  };
965
- //# sourceMappingURL=chunk-BKTDYGIP.mjs.map
1389
+ //# sourceMappingURL=chunk-2IFKPYPO.mjs.map