@ordewell/core 0.10.12 → 0.11.0

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 (50) hide show
  1. package/AUTHORS.md +37 -0
  2. package/NOTICE +71 -0
  3. package/README.md +1 -1
  4. package/dist/{IFileSystem-1PqRCjhu.d.mts → IRunner-B5l9PCTv.d.ts} +926 -102
  5. package/dist/{IFileSystem-lTRL_G9Z.d.ts → IRunner-DBgDx_Mr.d.mts} +926 -102
  6. package/dist/{ModeResolver-CoJAJD-f.d.ts → ModeResolver-BDAgouuE.d.mts} +9 -2
  7. package/dist/{ModeResolver-6TQXVsde.d.mts → ModeResolver-DyXeZ9ib.d.ts} +9 -2
  8. package/dist/{Task-CfBM0wkV.d.mts → Task-BL4Idm9r.d.mts} +656 -26
  9. package/dist/{Task-CfBM0wkV.d.ts → Task-BL4Idm9r.d.ts} +656 -26
  10. package/dist/{chunk-ESKK5I4F.mjs → chunk-4QXOWEGC.mjs} +1888 -334
  11. package/dist/chunk-4QXOWEGC.mjs.map +1 -0
  12. package/dist/{chunk-KWS3VEER.mjs → chunk-MYXMIVWA.mjs} +6 -6
  13. package/dist/chunk-MYXMIVWA.mjs.map +1 -0
  14. package/dist/{chunk-PFH7WFIB.mjs → chunk-QTZ5XTBY.mjs} +228 -15
  15. package/dist/chunk-QTZ5XTBY.mjs.map +1 -0
  16. package/dist/{chunk-24EL4Z2W.mjs → chunk-YAFNYNFX.mjs} +342 -36
  17. package/dist/chunk-YAFNYNFX.mjs.map +1 -0
  18. package/dist/index.d.mts +1488 -769
  19. package/dist/index.d.ts +1488 -769
  20. package/dist/index.js +32003 -20350
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +29380 -19553
  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 +232 -25
  27. package/dist/parsing.js.map +1 -1
  28. package/dist/parsing.mjs +3 -3
  29. package/dist/plan-utils.d.mts +4 -4
  30. package/dist/plan-utils.d.ts +4 -4
  31. package/dist/plan-utils.js +1156 -96
  32. package/dist/plan-utils.js.map +1 -1
  33. package/dist/plan-utils.mjs +144 -8
  34. package/dist/{runSettingsText-Jb_efnI4.d.mts → runSettingsText-BKwWs052.d.mts} +2882 -621
  35. package/dist/{runSettingsText-C5uv2axI.d.ts → runSettingsText-CAXiTpsj.d.ts} +2882 -621
  36. package/dist/testing.d.mts +10 -4
  37. package/dist/testing.d.ts +10 -4
  38. package/dist/testing.js +30 -7
  39. package/dist/testing.js.map +1 -1
  40. package/dist/testing.mjs +26 -7
  41. package/dist/testing.mjs.map +1 -1
  42. package/package.json +7 -3
  43. package/skills/diagnose/SKILL.md +52 -0
  44. package/skills/explore/SKILL.md +65 -0
  45. package/dist/chunk-24EL4Z2W.mjs.map +0 -1
  46. package/dist/chunk-ESKK5I4F.mjs.map +0 -1
  47. package/dist/chunk-KWS3VEER.mjs.map +0 -1
  48. package/dist/chunk-NJGID6BG.mjs +0 -115
  49. package/dist/chunk-NJGID6BG.mjs.map +0 -1
  50. package/dist/chunk-PFH7WFIB.mjs.map +0 -1
@@ -7,7 +7,221 @@ import {
7
7
  flattenTasksWithParents,
8
8
  inheritedSkills,
9
9
  skillNames
10
- } from "./chunk-PFH7WFIB.mjs";
10
+ } from "./chunk-QTZ5XTBY.mjs";
11
+
12
+ // src/models/ScriptTask.ts
13
+ var DEFAULT_SCRIPT_TIMEOUT_MS = 10 * 6e4;
14
+ var MAX_SCRIPT_TIMEOUT_MS = 24 * 60 * 6e4;
15
+ var SCRIPT_BODY_FIELDS = ["commands", "script", "timeoutMs"];
16
+ var NOT_A_SCRIPTS_FIELDS = ["assignedModel", "taskMode", "prompt", "skills", "userSteps", "ops"];
17
+ function scriptBodyOf(task) {
18
+ if (task.type !== "script" || scriptBodyErrors(task).length > 0) return null;
19
+ const timeoutMs = task.timeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS;
20
+ return task.commands ? { commands: task.commands.map((argv) => [...argv]), timeoutMs } : { script: task.script ?? "", timeoutMs };
21
+ }
22
+ function sameScriptBody(a, b) {
23
+ return JSON.stringify(bodyKey(a)) === JSON.stringify(bodyKey(b));
24
+ }
25
+ function rewrittenScripts(before, after) {
26
+ const prior = new Map(flattenTasks(before).map((t) => [t.id, t]));
27
+ return flattenTasks(after).filter((t) => {
28
+ const was = prior.get(t.id);
29
+ return was !== void 0 && (t.type === "script" || was.type === "script") && !sameScriptBody(was, t);
30
+ }).map((t) => t.id);
31
+ }
32
+ function bodyKey(task) {
33
+ return task.type === "script" ? [task.commands ?? null, task.script ?? null, task.timeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS] : null;
34
+ }
35
+ function scriptBodyErrors(task) {
36
+ if (task.type !== "script") {
37
+ return SCRIPT_BODY_FIELDS.filter((f) => task[f] !== void 0).map((field) => ({ field, message: `has "${field}", which only a script task has: set "type": "script", or leave it out` }));
38
+ }
39
+ const errors = [];
40
+ const { commands, script, timeoutMs } = task;
41
+ if (commands !== void 0 && script !== void 0) {
42
+ errors.push({ field: "commands", message: 'has both "commands" and "script": a script task runs exactly one of them' });
43
+ } else if (commands === void 0 && script === void 0) {
44
+ errors.push({ field: "commands", message: 'is a script task with neither "commands" nor "script": give it the argument vectors to run, or the script' });
45
+ }
46
+ if (commands !== void 0) errors.push(...commandsErrors(commands));
47
+ if (script !== void 0 && (typeof script !== "string" || !script.trim())) {
48
+ errors.push({ field: "script", message: 'has an empty "script": write the script it runs' });
49
+ }
50
+ if (timeoutMs !== void 0 && !(typeof timeoutMs === "number" && Number.isInteger(timeoutMs) && timeoutMs > 0 && timeoutMs <= MAX_SCRIPT_TIMEOUT_MS)) {
51
+ errors.push({ field: "timeoutMs", message: `has "timeoutMs" ${JSON.stringify(timeoutMs)}: the time limit is a whole number of milliseconds from 1 to ${MAX_SCRIPT_TIMEOUT_MS}, or left out for ten minutes` });
52
+ }
53
+ return errors;
54
+ }
55
+ function commandsErrors(commands) {
56
+ const shape = 'a list of argument vectors, each a list of strings, like [["git", "push", "origin", "main"]]';
57
+ if (!Array.isArray(commands) || commands.length === 0) return [{ field: "commands", message: `has "commands" that is not ${shape}` }];
58
+ for (const [i, argv] of commands.entries()) {
59
+ const n = i + 1;
60
+ if (!Array.isArray(argv) || !argv.every((word) => typeof word === "string")) {
61
+ return [{ field: "commands", message: `has command ${n} that is not an argument vector: "commands" is ${shape}` }];
62
+ }
63
+ if (argv.length === 0) return [{ field: "commands", message: `has command ${n} as an empty argument vector: name the program it runs` }];
64
+ if (!argv[0].trim()) return [{ field: "commands", message: `has command ${n} naming no program: its first word is the program it runs` }];
65
+ }
66
+ return [];
67
+ }
68
+ function notAScriptsFields(task) {
69
+ return NOT_A_SCRIPTS_FIELDS.filter((f) => {
70
+ const value = task[f];
71
+ return value !== void 0 && value !== false && !(Array.isArray(value) && value.length === 0);
72
+ });
73
+ }
74
+ function clearedBodyFields(changes) {
75
+ for (const field of SCRIPT_BODY_FIELDS) if (changes[field] === null) changes[field] = void 0;
76
+ return changes;
77
+ }
78
+ var PLAIN_WORD = /^[A-Za-z0-9_@%+=:,./-]+$/;
79
+ var UNSEEN = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
80
+ function commandLine(argv) {
81
+ return argv.map(quotedWord).join(" ");
82
+ }
83
+ function quotedWord(word) {
84
+ if (PLAIN_WORD.test(word)) return word;
85
+ if (!UNSEEN.test(word)) return `'${word.replace(/'/g, `'\\''`)}'`;
86
+ return `$'${[...word].map(escaped).join("")}'`;
87
+ }
88
+ var ESCAPES = { "\\": "\\\\", "'": "\\'", "\n": "\\n", "\r": "\\r", " ": "\\t" };
89
+ function escaped(char) {
90
+ return ESCAPES[char] ?? (UNSEEN.test(char) ? codeOf(char) : char);
91
+ }
92
+ function codeOf(char) {
93
+ const code = char.codePointAt(0) ?? 0;
94
+ const hex = code.toString(16).toUpperCase();
95
+ return code <= 255 ? `\\x${hex.padStart(2, "0")}` : code <= 65535 ? `\\u${hex.padStart(4, "0")}` : `\\U${hex.padStart(8, "0")}`;
96
+ }
97
+ function scriptBodyText(body) {
98
+ const shown = "commands" in body ? body.commands.map(commandLine) : [body.script.replace(/\r\n|[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, (char) => char === "\r\n" || char === "\n" || char === " " ? char : `<U+${(char.codePointAt(0) ?? 0).toString(16).toUpperCase().padStart(4, "0")}>`)];
99
+ return [...shown, "", `Time limit: ${timeLimitText(body.timeoutMs)}.`].join("\n");
100
+ }
101
+ function scriptTaskText(task) {
102
+ const body = scriptBodyOf(task);
103
+ return body ? scriptBodyText(body) : "This script task has no valid body, so it cannot run.";
104
+ }
105
+ function timeLimitText(ms) {
106
+ const units = [[36e5, "hour"], [6e4, "minute"], [1e3, "second"], [1, "millisecond"]];
107
+ const [size, name] = units.find(([unit]) => ms % unit === 0) ?? units[units.length - 1];
108
+ const n = ms / size;
109
+ return `${n} ${name}${n === 1 ? "" : "s"}`;
110
+ }
111
+
112
+ // src/models/Loop.ts
113
+ var DEFAULT_LOOP_ROUNDS = 3;
114
+ var MAX_LOOP_ROUNDS = 10;
115
+ function loopRoundOf(check) {
116
+ return check.loopRound ?? 1;
117
+ }
118
+ function foundWanting(check) {
119
+ return check.verdict?.outcome === "fail" && check.verdict.reported === "failed";
120
+ }
121
+ function betweenRounds(check) {
122
+ return !!check.loop && check.status === "failed" && foundWanting(check) && loopRoundOf(check) < check.loop.maxRounds;
123
+ }
124
+ function outOfRounds(check) {
125
+ return !!check.loop && check.status === "failed" && foundWanting(check) && loopRoundOf(check) >= check.loop.maxRounds;
126
+ }
127
+ function checkOf(tasks, taskId) {
128
+ return tasks.find((t) => t.loop?.body.includes(taskId));
129
+ }
130
+ function inLoop(tasks, task) {
131
+ return !!task.loop || checkOf(tasks, task.id) !== void 0;
132
+ }
133
+ var SHAPE = 'is not {"body": ["<task id>", ...], "maxRounds"?: <rounds>}';
134
+ function roundsMessage(value) {
135
+ return `has "maxRounds" ${JSON.stringify(value)}: a loop runs its check from 1 to ${MAX_LOOP_ROUNDS} times, or ${DEFAULT_LOOP_ROUNDS} when it is left out`;
136
+ }
137
+ function isRoundCount(value) {
138
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= MAX_LOOP_ROUNDS;
139
+ }
140
+ function loopSpec(value) {
141
+ if (value === void 0 || value === null) return { loop: void 0 };
142
+ if (typeof value !== "object" || Array.isArray(value)) return { error: `has a "loop" that ${SHAPE}` };
143
+ const { body, maxRounds } = value;
144
+ if (!Array.isArray(body) || !body.every((id) => typeof id === "string" || typeof id === "number")) return { error: `has a "loop" whose "body" ${SHAPE}` };
145
+ if (body.length === 0) return { error: 'has a "loop" with an empty "body": name the tasks that fix what it finds' };
146
+ if (maxRounds !== void 0 && !isRoundCount(maxRounds)) return { error: roundsMessage(maxRounds) };
147
+ return { loop: { body: body.map(String), maxRounds: maxRounds ?? DEFAULT_LOOP_ROUNDS } };
148
+ }
149
+ function loopErrors(tasks) {
150
+ const rows = flattenTasksWithParents(tasks);
151
+ const byId = new Map(rows.map((row) => [row.task.id, row]));
152
+ const errors = [];
153
+ const bodyOwner = /* @__PURE__ */ new Map();
154
+ for (const { task: check, parent } of rows) {
155
+ const loop = check.loop;
156
+ if (!loop) continue;
157
+ const where = parent ? `subtask "${check.title}" of "${parent.title}"` : `Task "${check.title}"`;
158
+ const fail = (message) => errors.push({ taskId: check.id, field: "loop", message: `${where} ${message}` });
159
+ if (parent) fail(`has a "loop", but only a top-level task can be a loop's check`);
160
+ 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");
161
+ if (check.subtasks?.length) fail("is a loop's check, which has no subtasks: each round runs it again whole");
162
+ if (!isRoundCount(loop.maxRounds)) fail(roundsMessage(loop.maxRounds));
163
+ 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');
164
+ const named = /* @__PURE__ */ new Set();
165
+ for (const id of loop.body ?? []) {
166
+ const row = byId.get(id);
167
+ if (named.has(id)) {
168
+ fail(`names "${row?.task.title ?? id}" twice in its loop's "body"`);
169
+ continue;
170
+ }
171
+ named.add(id);
172
+ if (!row) {
173
+ fail(`names "${id}" in its loop's "body", which is no task in the plan`);
174
+ continue;
175
+ }
176
+ const body = row.task;
177
+ if (body.id === check.id) {
178
+ fail(`names itself in its loop's "body": the body is the tasks that fix what it finds`);
179
+ continue;
180
+ }
181
+ if (row.parent) {
182
+ fail(`names subtask "${body.title}" of "${row.parent.title}" in its loop's "body": only a top-level task can be in a loop's body`);
183
+ continue;
184
+ }
185
+ const other = bodyOwner.get(id);
186
+ if (other) {
187
+ fail(`names "${body.title}" in its loop's "body", which is in the body of "${other.title}"'s loop too: a task belongs to one loop only`);
188
+ continue;
189
+ }
190
+ bodyOwner.set(id, check);
191
+ 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
+ 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`);
193
+ 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
+ }
195
+ }
196
+ for (const [bodyId, check] of bodyOwner) {
197
+ const body = byId.get(bodyId).task;
198
+ const where = `Task "${body.title}" is in the body of "${check.title}"'s loop`;
199
+ if (!body.dependencies.includes(check.id)) {
200
+ errors.push({ taskId: body.id, field: "dependencies", message: `${where}, so it depends on "${check.title}": the check's report is what it fixes. Add "${check.title}" to its dependencies` });
201
+ }
202
+ for (const dep of body.dependencies) {
203
+ if (dep === check.id || check.dependencies.includes(dep) || bodyOwner.get(dep) === check) continue;
204
+ errors.push({ taskId: body.id, field: "dependencies", message: `${where}, so it depends only on "${check.title}", on what "${check.title}" depends on, or on other tasks of that body; "${byId.get(dep)?.task.title ?? dep}" is none of these` });
205
+ }
206
+ }
207
+ for (const { task } of rows) {
208
+ for (const dep of task.dependencies) {
209
+ const check = bodyOwner.get(dep);
210
+ if (!check || bodyOwner.get(task.id) === check) continue;
211
+ const body = byId.get(dep).task;
212
+ const message = task.id === check.id ? `Task "${task.title}" depends on "${body.title}" from its own loop's body: the body runs after the check, so the check cannot wait for it` : `Task "${task.title}" depends on "${body.title}", which is in the body of "${check.title}"'s loop and runs only if "${check.title}" fails: depend on "${check.title}" instead`;
213
+ errors.push({ taskId: task.id, field: "dependencies", message });
214
+ }
215
+ }
216
+ return errors;
217
+ }
218
+ function loopRemovalRefusal(task) {
219
+ 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
+ }
221
+ function loopReshapeRefusal(tasks, target, action) {
222
+ if (target.loop) return `"${target.title}" is a loop's check and cannot be ${action}: drop its loop first ("loop": null)`;
223
+ 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
+ }
11
225
 
12
226
  // src/services/JsonExtractor.ts
13
227
  var PLAN_ENVELOPE_KEY = "tasks";
@@ -70,12 +284,12 @@ function opensWithJsonObject(prefix) {
70
284
  function balancedObjectAt(text2, start) {
71
285
  let depth = 0;
72
286
  let inString = false;
73
- let escaped = false;
287
+ let escaped2 = false;
74
288
  for (let i = start; i < text2.length; i++) {
75
289
  const ch = text2[i];
76
290
  if (inString) {
77
- if (escaped) escaped = false;
78
- else if (ch === "\\") escaped = true;
291
+ if (escaped2) escaped2 = false;
292
+ else if (ch === "\\") escaped2 = true;
79
293
  else if (ch === '"') inString = false;
80
294
  continue;
81
295
  }
@@ -96,12 +310,12 @@ function extractObjectsWithKey(raw, key) {
96
310
  let sawBrace = false;
97
311
  let sawUnbalanced = false;
98
312
  let inString = false;
99
- let escaped = false;
313
+ let escaped2 = false;
100
314
  for (let i = 0; i < stripped.length; i++) {
101
315
  const ch = stripped[i];
102
316
  if (inString) {
103
- if (escaped) escaped = false;
104
- else if (ch === "\\") escaped = true;
317
+ if (escaped2) escaped2 = false;
318
+ else if (ch === "\\") escaped2 = true;
105
319
  else if (ch === '"') inString = false;
106
320
  continue;
107
321
  }
@@ -141,17 +355,17 @@ function stripTrailingCommas(json) {
141
355
  function escapeControlCharsInStrings(json) {
142
356
  let out = "";
143
357
  let inString = false;
144
- let escaped = false;
358
+ let escaped2 = false;
145
359
  for (const ch of json) {
146
360
  if (inString) {
147
- if (escaped) {
361
+ if (escaped2) {
148
362
  out += ch;
149
- escaped = false;
363
+ escaped2 = false;
150
364
  continue;
151
365
  }
152
366
  if (ch === "\\") {
153
367
  out += ch;
154
- escaped = true;
368
+ escaped2 = true;
155
369
  continue;
156
370
  }
157
371
  if (ch === '"') {
@@ -184,7 +398,7 @@ function escapeControlCharsInStrings(json) {
184
398
  function runnerModesFrom(registry, runners) {
185
399
  return Object.fromEntries(runners.map((r) => [r, registry.getManifest(r)?.modes ?? []]));
186
400
  }
187
- function resolveDefaultMode(modes, autonomousDefault = true) {
401
+ function resolveDefaultMode(modes, autonomousDefault = false) {
188
402
  if (!modes || modes.length === 0) return void 0;
189
403
  const tag = autonomousDefault ? "autonomous" : "safe";
190
404
  const tagged = modes.find((m) => m[tag]);
@@ -210,6 +424,13 @@ function parseAutonomyLevel(arg) {
210
424
  return null;
211
425
  }
212
426
  }
427
+ function parseAutonomyFlag(value) {
428
+ const normalized = value?.trim().toLowerCase();
429
+ if (!normalized) return void 0;
430
+ if (normalized === "true" || normalized === "1") return true;
431
+ if (normalized === "false" || normalized === "0") return false;
432
+ return parseAutonomyLevel(normalized) ?? void 0;
433
+ }
213
434
  function taggedReadOnlyMode(modes) {
214
435
  return modes?.find((m) => m.readOnly)?.id;
215
436
  }
@@ -227,7 +448,7 @@ function unavailableTaskModeMessage(manifest, mode) {
227
448
  const reason = unavailableModeReason(manifest.modes, mode);
228
449
  return reason ? `Mode "${mode}" of ${manifest.displayName} is unavailable on this host: ${reason}` : void 0;
229
450
  }
230
- function plannedMode(runner, runnerModes, { readOnly, current }, autonomousDefault = true) {
451
+ function plannedMode(runner, runnerModes, { readOnly, current }, autonomousDefault = false) {
231
452
  if (!runnerModes) return { ok: true, mode: void 0 };
232
453
  const modes = runnerModes[runner] ?? [];
233
454
  const readOnlyId = readOnlyMode(modes);
@@ -251,8 +472,8 @@ function carryPlannedModes(before, rewrite, catalog) {
251
472
  const carry = (t) => {
252
473
  const subtasks = (t.subtasks ?? []).map(carry);
253
474
  const was = prior.get(t.id);
254
- if (t.type === "user" || !was || was.type === "user" || t.taskMode === taggedReadOnlyMode(catalog.runnerModes[t.assignedRunner])) return { ...t, subtasks };
255
- const mode = plannedMode(t.assignedRunner, catalog.runnerModes, { readOnly: false, current: was.taskMode }, catalog.autonomousDefault ?? true);
475
+ if (t.type !== "ai" || !was || was.type !== "ai" || t.taskMode === taggedReadOnlyMode(catalog.runnerModes[t.assignedRunner])) return { ...t, subtasks };
476
+ const mode = plannedMode(t.assignedRunner, catalog.runnerModes, { readOnly: false, current: was.taskMode }, catalog.autonomousDefault ?? false);
256
477
  return { ...t, subtasks, ...mode.ok && mode.mode !== void 0 ? { taskMode: mode.mode } : {} };
257
478
  };
258
479
  return rewrite.map(carry);
@@ -318,9 +539,8 @@ function liveModelRefusal(task) {
318
539
  function liveRunnerRefusal(task) {
319
540
  return `"${task.title}" has a live runner, so its runner cannot change now: stop the task first`;
320
541
  }
321
- function assignableRunners(writer, enabled, planRunners) {
322
- if (writer === "plan") return [...enabled];
323
- if (writer === "edit") return [.../* @__PURE__ */ new Set([...planRunners, ...enabled])];
542
+ function assignableRunners(writer, enabled) {
543
+ if (writer === "plan" || writer === "edit") return [...enabled];
324
544
  return void 0;
325
545
  }
326
546
  function sameAssignment(a, b) {
@@ -343,7 +563,7 @@ function modeFor(write, task, moved) {
343
563
  if (unavailable) return { ok: false, error: `Mode "${task.taskMode}" of runner "${runner}" is unavailable on this host: ${unavailable}` };
344
564
  return { ok: true, mode: task.taskMode };
345
565
  }
346
- const becameAi = before?.type === "user";
566
+ const becameAi = before !== void 0 && before.type !== "ai";
347
567
  if (writer === "runner" || !catalog.runnerModes || before && !moved && !becameAi && write.readOnly === void 0) return { ok: true, mode: task.taskMode };
348
568
  const carried = prior !== void 0 && task.taskMode === prior.taskMode && readOnlyOn(prior.taskMode, prior.assignedRunner, catalog);
349
569
  const readOnly = write.readOnly ?? (carried || readOnlyOn(task.taskMode, runner, catalog));
@@ -355,7 +575,7 @@ function modeFor(write, task, moved) {
355
575
  error: `"${task.title}" is read-only, and runner "${runner}" has no read-only mode${reason ? " available on this host" : ""}: give it another mode first${able.length > 0 ? `, or move it to one of: ${able.join(", ")}` : ""}${reason ? `. ${reason}` : ""}`
356
576
  };
357
577
  }
358
- const mode = plannedMode(runner, catalog.runnerModes, { readOnly, current: task.taskMode }, catalog.autonomousDefault ?? true);
578
+ const mode = plannedMode(runner, catalog.runnerModes, { readOnly, current: task.taskMode }, catalog.autonomousDefault ?? false);
359
579
  return mode.ok ? { ok: true, mode: mode.mode ?? task.taskMode } : mode;
360
580
  }
361
581
  function resolveAssignment(write) {
@@ -367,7 +587,7 @@ function resolveAssignment(write) {
367
587
  const live = writer !== "runner" && !!before && !!catalog.liveRunner?.(before.id);
368
588
  if (live && moved) return { ok: false, field: "assignedRunner", error: liveRunnerRefusal({ title }) };
369
589
  if (live && !sameAssignment(before?.assignedModel, written.assignedModel)) return { ok: false, field: "assignedModel", error: liveModelRefusal({ title }) };
370
- if (written.type === "user") return { ok: true, task: written, adjustments: [] };
590
+ if (written.type !== "ai") return { ok: true, task: written, adjustments: [] };
371
591
  const mode = modeFor(write, written, moved);
372
592
  if (!mode.ok) return { ok: false, field: "taskMode", error: mode.error };
373
593
  const model = live ? { ok: true, assignment: written.assignedModel } : modelFor(write, moved);
@@ -457,14 +677,25 @@ function resolveAssignments(tasks, planned, catalog) {
457
677
  import { v4 as uuidv4 } from "uuid";
458
678
  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`;
459
679
  var MANUAL_NOT_READ_ONLY = '"readOnly" is for AI tasks: a manual task runs in no mode';
460
- var OPS_NOT_READ_ONLY = `"readOnly" is not for an ops task: it runs commands, in the session's mode, and a read-only mode refuses them`;
680
+ var OPS_NOT_READ_ONLY = '"readOnly" is not for an ops task: it acts in the workspace, while a read-only task lands nothing';
461
681
  function isRecord(value) {
462
682
  return typeof value === "object" && value !== null && !Array.isArray(value);
463
683
  }
464
684
  function text(value) {
465
685
  return typeof value === "string" || typeof value === "number" ? String(value) : void 0;
466
686
  }
687
+ var NOT_ON_A_SCRIPT = ["assignedRunner", "assignedModel", "thinkingEffort", "taskMode", "readOnly", "prompt", "skills", "userSteps", "ops", "subtasks"];
688
+ function notOnAScript(raw) {
689
+ return NOT_ON_A_SCRIPT.filter((f) => {
690
+ const value = raw[f];
691
+ return value !== void 0 && value !== null && value !== false && !(Array.isArray(value) && value.length === 0);
692
+ });
693
+ }
694
+ function notOnAScriptMessage(fields) {
695
+ return `is a script task, which runs its "commands" or "script" with no model: leave out ${fields.map((f) => `"${f}"`).join(", ")}`;
696
+ }
467
697
  function opsPlacementRefusal(type, isSubtask) {
698
+ if (type === "script") return 'is a script task, which is an ops task by nature: leave "ops" out';
468
699
  if (type !== "ai") return "is a manual task: only an AI task can be an ops task";
469
700
  if (isSubtask) return "is a subtask, which runs with its parent: only a top-level task can be an ops task";
470
701
  return null;
@@ -488,6 +719,25 @@ function modelOf(raw, from) {
488
719
  }
489
720
  return from && effort !== void 0 ? { ...from, thinkingEffort: effort } : from;
490
721
  }
722
+ function bodyFields(raw, base) {
723
+ const pick = (field) => raw[field] !== void 0 && raw[field] !== null ? raw[field] : base[field];
724
+ const judged = { commands: pick("commands"), script: pick("script"), timeoutMs: pick("timeoutMs") };
725
+ const malformed = /* @__PURE__ */ new Set();
726
+ const commands = isCommands(judged.commands) ? judged.commands : void 0;
727
+ if (judged.commands !== void 0 && !commands) malformed.add("commands");
728
+ const script = typeof judged.script === "string" ? judged.script : void 0;
729
+ if (judged.script !== void 0 && script === void 0) malformed.add("script");
730
+ const timeoutMs = typeof judged.timeoutMs === "number" ? judged.timeoutMs : void 0;
731
+ if (judged.timeoutMs !== void 0 && timeoutMs === void 0) malformed.add("timeoutMs");
732
+ return {
733
+ fields: { ...commands ? { commands } : {}, ...script !== void 0 ? { script } : {}, ...timeoutMs !== void 0 ? { timeoutMs } : {} },
734
+ raw: judged,
735
+ malformed
736
+ };
737
+ }
738
+ function isCommands(value) {
739
+ return Array.isArray(value) && value.every((argv) => Array.isArray(argv) && argv.every((word) => typeof word === "string"));
740
+ }
491
741
  function plannedTask(spec, catalog, at = {}) {
492
742
  const raw = isRecord(spec) ? spec : {};
493
743
  const { parent, from, freshIds, newId = uuidv4 } = at;
@@ -501,17 +751,26 @@ function plannedTask(spec, catalog, at = {}) {
501
751
  const plannerId = text(raw.id);
502
752
  const id = freshIds || !plannerId ? newId() : plannerId;
503
753
  if (freshIds && plannerId) renamed.set(plannerId, id);
504
- const type = raw.type === "user" || raw.type === "ai" ? raw.type : base.type ?? "ai";
754
+ const type = raw.type === "user" || raw.type === "ai" || raw.type === "script" ? raw.type : base.type ?? "ai";
505
755
  const ai = type === "ai";
756
+ const script = type === "script";
506
757
  const assignedRunner = text(raw.assignedRunner) ?? base.assignedRunner ?? catalog.runners[0] ?? DEFAULT_RUNNERS[0];
507
758
  const description = text(raw.description) ?? base.description ?? title;
508
- if (raw.ops === true && !ai) errors.push({ taskId: id, field: "ops", message: `${where} ${opsPlacementRefusal(type, !!parent)}` });
509
- if (raw.taskMode !== void 0) ignored.push({ taskId: id, field: "taskMode", message: `${where}: ${MODE_NOT_THE_PLANNERS}` });
510
- if (!ai && raw.readOnly === true) ignored.push({ taskId: id, field: "readOnly", message: `${where}: ${MANUAL_NOT_READ_ONLY}` });
759
+ const notScripts = script ? notOnAScript(raw) : [];
760
+ if (notScripts.length > 0) errors.push({ taskId: id, field: notScripts[0], message: `${where} ${notOnAScriptMessage(notScripts)}` });
761
+ const body = bodyFields(raw, base);
762
+ const shapeErrors = scriptBodyErrors({ type, ...body.raw }).filter((e) => body.malformed.has(e.field));
763
+ for (const e of shapeErrors) errors.push({ taskId: id, field: e.field, message: `${where} ${e.message}` });
764
+ const loop = loopSpec(raw.loop);
765
+ if ("error" in loop) errors.push({ taskId: id, field: "loop", message: `${where} ${loop.error}` });
766
+ 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}` });
768
+ if (type === "user" && raw.readOnly === true) ignored.push({ taskId: id, field: "readOnly", message: `${where}: ${MANUAL_NOT_READ_ONLY}` });
511
769
  const ops = raw.ops !== void 0 ? raw.ops === true : base.ops;
512
770
  if (ai && ops && raw.readOnly === true) ignored.push({ taskId: id, field: "readOnly", message: `${where}: ${OPS_NOT_READ_ONLY}` });
513
771
  const asked = typeof raw.readOnly === "boolean" ? raw.readOnly : void 0;
514
772
  const readOnly = ai && !ops && (asked ?? from?.readOnly ?? parent?.readOnly ?? false);
773
+ if (raw.research !== void 0 && typeof raw.research !== "boolean") errors.push({ taskId: id, field: "research", message: `${where}: "research" must be a boolean` });
515
774
  const assignedModel = ai ? modelOf(raw, base.assignedModel) : void 0;
516
775
  const slice = (value) => value === "AFK" || value === "HITL" ? value : void 0;
517
776
  const autonomy = ai ? slice(raw.autonomy) ?? base.autonomy ?? parent?.autonomy : void 0;
@@ -534,7 +793,8 @@ function plannedTask(spec, catalog, at = {}) {
534
793
  description,
535
794
  type,
536
795
  dependencies: Array.isArray(raw.dependencies) ? raw.dependencies.map(String) : base.dependencies ?? [],
537
- prompt: text(raw.prompt) || base.prompt || (ai ? description || title || void 0 : void 0),
796
+ // 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),
538
798
  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,
539
799
  subtasks,
540
800
  assignedModel,
@@ -542,10 +802,13 @@ function plannedTask(spec, catalog, at = {}) {
542
802
  autonomy,
543
803
  sliceType,
544
804
  userStoriesCovered: Array.isArray(raw.userStoriesCovered) ? raw.userStoriesCovered.map(String) : base.userStoriesCovered,
545
- ops,
546
- skills: from ? inheritedSkills(raw.skills, base.skills) : skillNames(raw.skills)
805
+ ops: script ? void 0 : ops,
806
+ research: typeof raw.research === "boolean" ? raw.research : base.research,
807
+ skills: script ? void 0 : from ? inheritedSkills(raw.skills, base.skills) : skillNames(raw.skills),
808
+ ...body.fields,
809
+ ..."loop" in loop && loop.loop ? { loop: loop.loop } : {}
547
810
  };
548
- let taskMode = base.taskMode;
811
+ let taskMode = script ? void 0 : base.taskMode;
549
812
  let landedModel = assignedModel;
550
813
  if (ai && catalog.runners.includes(assignedRunner)) {
551
814
  const resolved = resolveAssignment({
@@ -568,30 +831,50 @@ function plannedTask(spec, catalog, at = {}) {
568
831
  if (Array.isArray(raw.skills)) skillsAsked.set(task, raw.skills);
569
832
  return { task, errors, ignored, renamed, adjustments };
570
833
  }
834
+ function checkPlanRunners(tasks, runners) {
835
+ if (!runners) return [];
836
+ return flattenTasksWithParents(tasks).filter(({ task: t }) => t.type === "ai" && !runners.includes(t.assignedRunner)).map(({ task: t, parent }) => ({
837
+ taskId: t.id,
838
+ field: "assignedRunner",
839
+ message: `${taskWhere(t.title, parent?.title)} has invalid assignedRunner "${t.assignedRunner}". Expected one of: ${runners.join(", ")}`
840
+ }));
841
+ }
571
842
  function checkPlanInvariants(tasks, runners, runnerModes) {
572
843
  const rows = flattenTasksWithParents(tasks);
573
844
  const errors = [];
574
845
  const seen = /* @__PURE__ */ new Set();
846
+ const body = new Set(tasks.flatMap((t) => t.loop?.body ?? []));
575
847
  for (const { task: t, parent } of rows) {
576
848
  const where = taskWhere(t.title, parent?.title);
577
849
  const fail = (field, message) => errors.push({ taskId: t.id, field, message });
578
850
  if (seen.has(t.id)) fail("id", `${where} has id "${t.id}", which another task in the plan has too: every task, subtasks included, needs an id of its own`);
579
851
  seen.add(t.id);
852
+ if (t.research !== void 0 && typeof t.research !== "boolean") fail("research", `${where}: "research" must be a boolean`);
853
+ const readOnly = runnerModes ? taggedReadOnlyMode(runnerModes[t.assignedRunner]) : "plan";
854
+ if (t.research && (t.type !== "ai" || parent !== null || t.ops || body.has(t.id) || readOnly === void 0 || t.taskMode !== readOnly)) {
855
+ fail("research", `${where}: research is only for top-level read-only AI tasks, never a loop's body`);
856
+ }
580
857
  if (t.autonomy === "AFK" && t.userSteps && t.userSteps.length > 0) {
581
858
  fail("userSteps", `${where} has autonomy "AFK" but contains userSteps. AFK tasks must not have user touchpoints.`);
582
859
  }
583
860
  if (t.type === "user" && t.sliceType && t.sliceType !== "HITL") {
584
861
  fail("sliceType", `User ${where} has sliceType "${t.sliceType}". User tasks must have sliceType "HITL".`);
585
862
  }
586
- const opsRefused = t.ops ? opsPlacementRefusal(t.type, parent !== null) : null;
863
+ const opsRefused = t.ops && t.type !== "script" ? opsPlacementRefusal(t.type, parent !== null) : null;
587
864
  if (opsRefused) fail("ops", `${where} ${opsRefused}`);
588
- if (t.ops && t.taskMode !== void 0 && t.taskMode === taggedReadOnlyMode(runnerModes?.[t.assignedRunner])) {
589
- fail("taskMode", `${where} is an ops task, which runs commands, so it cannot run in its runner's read-only mode`);
865
+ if (t.type === "script") {
866
+ if (parent !== null) fail("type", `${where} is a script task: only a top-level task can be one, as only a top-level task can be an ops task`);
867
+ if (t.subtasks?.length) fail("subtasks", `${where} is a script task, which has no subtasks: make each step a command of its own, or a task of its own`);
868
+ const extra = notAScriptsFields(t);
869
+ if (extra.length > 0) fail(extra[0], `${where} ${notOnAScriptMessage(extra)}`);
590
870
  }
591
- if (runners && t.type !== "user" && !runners.includes(t.assignedRunner)) {
592
- fail("assignedRunner", `${where} has invalid assignedRunner "${t.assignedRunner}". Expected one of: ${runners.join(", ")}`);
871
+ for (const e of scriptBodyErrors(t)) fail(e.field, `${where} ${e.message}`);
872
+ if (t.ops && t.taskMode !== void 0 && t.taskMode === taggedReadOnlyMode(runnerModes?.[t.assignedRunner])) {
873
+ fail("taskMode", `${where} is an ops task, which acts in the workspace, so it cannot use the read-only profile`);
593
874
  }
594
875
  }
876
+ errors.push(...checkPlanRunners(tasks, runners));
877
+ errors.push(...loopErrors(tasks));
595
878
  for (const { task: t, parent } of rows) {
596
879
  for (const error of dependencyTargetErrors(tasks, t.id, t.dependencies.filter((id) => id !== t.id))) {
597
880
  errors.push({ taskId: t.id, field: "dependencies", message: `${taskWhere(t.title, parent?.title)}: ${error}` });
@@ -609,6 +892,27 @@ function detectCycle(tasks) {
609
892
  }
610
893
 
611
894
  export {
895
+ DEFAULT_SCRIPT_TIMEOUT_MS,
896
+ SCRIPT_BODY_FIELDS,
897
+ scriptBodyOf,
898
+ sameScriptBody,
899
+ rewrittenScripts,
900
+ notAScriptsFields,
901
+ clearedBodyFields,
902
+ commandLine,
903
+ scriptBodyText,
904
+ scriptTaskText,
905
+ timeLimitText,
906
+ DEFAULT_LOOP_ROUNDS,
907
+ MAX_LOOP_ROUNDS,
908
+ loopRoundOf,
909
+ betweenRounds,
910
+ outOfRounds,
911
+ checkOf,
912
+ inLoop,
913
+ loopSpec,
914
+ loopRemovalRefusal,
915
+ loopReshapeRefusal,
612
916
  PLAN_ENVELOPE_KEY,
613
917
  TASK_OPS_ENVELOPE_KEY,
614
918
  TASK_QUERY_ENVELOPE_KEY,
@@ -622,6 +926,7 @@ export {
622
926
  runnerModesFrom,
623
927
  autonomyLevelLabel,
624
928
  parseAutonomyLevel,
929
+ parseAutonomyFlag,
625
930
  taggedReadOnlyMode,
626
931
  readOnlyMode,
627
932
  unavailableTaskModeMessage,
@@ -637,6 +942,7 @@ export {
637
942
  MODE_NOT_THE_PLANNERS,
638
943
  MANUAL_NOT_READ_ONLY,
639
944
  OPS_NOT_READ_ONLY,
945
+ notOnAScriptMessage,
640
946
  opsPlacementRefusal,
641
947
  taskWhere,
642
948
  requestedSkills,
@@ -644,4 +950,4 @@ export {
644
950
  checkPlanInvariants,
645
951
  detectCycle
646
952
  };
647
- //# sourceMappingURL=chunk-24EL4Z2W.mjs.map
953
+ //# sourceMappingURL=chunk-YAFNYNFX.mjs.map