@ordewell/core 0.10.11 → 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.
- package/AUTHORS.md +37 -0
- package/NOTICE +71 -0
- package/README.md +1 -1
- package/dist/{IFileSystem-1PqRCjhu.d.mts → IRunner-B5l9PCTv.d.ts} +926 -102
- package/dist/{IFileSystem-lTRL_G9Z.d.ts → IRunner-DBgDx_Mr.d.mts} +926 -102
- package/dist/{ModeResolver-CoJAJD-f.d.ts → ModeResolver-BDAgouuE.d.mts} +9 -2
- package/dist/{ModeResolver-6TQXVsde.d.mts → ModeResolver-DyXeZ9ib.d.ts} +9 -2
- package/dist/{Task-CfBM0wkV.d.mts → Task-BL4Idm9r.d.mts} +656 -26
- package/dist/{Task-CfBM0wkV.d.ts → Task-BL4Idm9r.d.ts} +656 -26
- package/dist/{chunk-ESKK5I4F.mjs → chunk-4QXOWEGC.mjs} +1888 -334
- package/dist/chunk-4QXOWEGC.mjs.map +1 -0
- package/dist/{chunk-KWS3VEER.mjs → chunk-MYXMIVWA.mjs} +6 -6
- package/dist/chunk-MYXMIVWA.mjs.map +1 -0
- package/dist/{chunk-PFH7WFIB.mjs → chunk-QTZ5XTBY.mjs} +228 -15
- package/dist/chunk-QTZ5XTBY.mjs.map +1 -0
- package/dist/{chunk-24EL4Z2W.mjs → chunk-YAFNYNFX.mjs} +342 -36
- package/dist/chunk-YAFNYNFX.mjs.map +1 -0
- package/dist/index.d.mts +1490 -769
- package/dist/index.d.ts +1490 -769
- package/dist/index.js +32009 -20354
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +27785 -17956
- package/dist/index.mjs.map +1 -1
- package/dist/parsing.d.mts +2 -2
- package/dist/parsing.d.ts +2 -2
- package/dist/parsing.js +232 -25
- package/dist/parsing.js.map +1 -1
- package/dist/parsing.mjs +3 -3
- package/dist/plan-utils.d.mts +4 -4
- package/dist/plan-utils.d.ts +4 -4
- package/dist/plan-utils.js +1156 -96
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +144 -8
- package/dist/{runSettingsText-Jb_efnI4.d.mts → runSettingsText-BKwWs052.d.mts} +2882 -621
- package/dist/{runSettingsText-C5uv2axI.d.ts → runSettingsText-CAXiTpsj.d.ts} +2882 -621
- package/dist/testing.d.mts +10 -4
- package/dist/testing.d.ts +10 -4
- package/dist/testing.js +30 -7
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +26 -7
- package/dist/testing.mjs.map +1 -1
- package/package.json +7 -3
- package/skills/diagnose/SKILL.md +52 -0
- package/skills/explore/SKILL.md +65 -0
- package/dist/chunk-24EL4Z2W.mjs.map +0 -1
- package/dist/chunk-ESKK5I4F.mjs.map +0 -1
- package/dist/chunk-KWS3VEER.mjs.map +0 -1
- package/dist/chunk-NJGID6BG.mjs +0 -115
- package/dist/chunk-NJGID6BG.mjs.map +0 -1
- package/dist/chunk-PFH7WFIB.mjs.map +0 -1
package/dist/parsing.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { R as RunnerModeInfo } from './ModeResolver-
|
|
1
|
+
import { az as RunnerId, aS as Task } from './Task-BL4Idm9r.mjs';
|
|
2
|
+
import { R as RunnerModeInfo } from './ModeResolver-BDAgouuE.mjs';
|
|
3
3
|
|
|
4
4
|
declare function parsePlanJson(raw: string, runners: RunnerId[], runnerModes?: Record<RunnerId, RunnerModeInfo[]>, autonomousDefault?: boolean): Task[];
|
|
5
5
|
|
package/dist/parsing.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { R as RunnerModeInfo } from './ModeResolver-
|
|
1
|
+
import { az as RunnerId, aS as Task } from './Task-BL4Idm9r.js';
|
|
2
|
+
import { R as RunnerModeInfo } from './ModeResolver-DyXeZ9ib.js';
|
|
3
3
|
|
|
4
4
|
declare function parsePlanJson(raw: string, runners: RunnerId[], runnerModes?: Record<RunnerId, RunnerModeInfo[]>, autonomousDefault?: boolean): Task[];
|
|
5
5
|
|
package/dist/parsing.js
CHANGED
|
@@ -101,12 +101,13 @@ function findDependencyCycle(tasks) {
|
|
|
101
101
|
// src/models/Task.ts
|
|
102
102
|
function createTask(overrides = {}) {
|
|
103
103
|
const skills = skillNames(overrides.skills);
|
|
104
|
+
const type = overrides.type ?? "ai";
|
|
104
105
|
return {
|
|
105
106
|
id: overrides.id ?? (0, import_uuid.v4)(),
|
|
106
107
|
order: overrides.order ?? 0,
|
|
107
108
|
title: overrides.title ?? "",
|
|
108
109
|
description: overrides.description ?? "",
|
|
109
|
-
type
|
|
110
|
+
type,
|
|
110
111
|
status: overrides.status ?? "pending",
|
|
111
112
|
dependencies: overrides.dependencies ?? [],
|
|
112
113
|
prompt: overrides.prompt,
|
|
@@ -114,16 +115,25 @@ function createTask(overrides = {}) {
|
|
|
114
115
|
subtasks: overrides.subtasks ?? [],
|
|
115
116
|
verdict: overrides.verdict,
|
|
116
117
|
outputSummary: overrides.outputSummary,
|
|
118
|
+
...overrides.handoff ? { handoff: structuredClone(overrides.handoff) } : {},
|
|
119
|
+
...overrides.attempted ? { attempted: true } : {},
|
|
117
120
|
...overrides.attemptUsage ? { attemptUsage: structuredClone(overrides.attemptUsage) } : {},
|
|
118
121
|
assignedModel: overrides.assignedModel,
|
|
119
122
|
assignedRunner: overrides.assignedRunner ?? DEFAULT_RUNNERS[0],
|
|
120
|
-
|
|
123
|
+
// A script task runs in no mode (ADR-0035): one given it would read as a mode it runs in.
|
|
124
|
+
taskMode: overrides.taskMode ?? (type === "script" ? void 0 : "build"),
|
|
121
125
|
autonomy: overrides.autonomy,
|
|
122
126
|
sliceType: overrides.sliceType,
|
|
123
127
|
userStoriesCovered: overrides.userStoriesCovered,
|
|
124
128
|
...opsFlag(overrides.ops, overrides.type) ? { ops: true } : {},
|
|
125
129
|
...skills ? { skills } : {},
|
|
126
|
-
...overrides.addedBy === "supervisor" ? { addedBy: "supervisor" } : {}
|
|
130
|
+
...overrides.addedBy === "supervisor" ? { addedBy: "supervisor" } : {},
|
|
131
|
+
...overrides.commands !== void 0 ? { commands: overrides.commands.map((argv) => [...argv]) } : {},
|
|
132
|
+
...overrides.script !== void 0 ? { script: overrides.script } : {},
|
|
133
|
+
...overrides.timeoutMs !== void 0 ? { timeoutMs: overrides.timeoutMs } : {},
|
|
134
|
+
...overrides.loop ? { loop: { body: [...overrides.loop.body], maxRounds: overrides.loop.maxRounds } } : {},
|
|
135
|
+
...overrides.loopRound !== void 0 ? { loopRound: overrides.loopRound } : {},
|
|
136
|
+
...overrides.research !== void 0 ? { research: overrides.research } : {}
|
|
127
137
|
};
|
|
128
138
|
}
|
|
129
139
|
var SKILL_NAME = new RegExp(`^${SKILL_NAME_PATTERN}$`);
|
|
@@ -293,7 +303,7 @@ function escapeControlCharsInStrings(json) {
|
|
|
293
303
|
var import_uuid2 = require("uuid");
|
|
294
304
|
|
|
295
305
|
// src/services/ModeResolver.ts
|
|
296
|
-
function resolveDefaultMode(modes, autonomousDefault =
|
|
306
|
+
function resolveDefaultMode(modes, autonomousDefault = false) {
|
|
297
307
|
if (!modes || modes.length === 0) return void 0;
|
|
298
308
|
const tag = autonomousDefault ? "autonomous" : "safe";
|
|
299
309
|
const tagged = modes.find((m) => m[tag]);
|
|
@@ -316,7 +326,7 @@ function unavailableModeReason(modes, id) {
|
|
|
316
326
|
function unavailableModeError(runner, id, reason) {
|
|
317
327
|
return `Mode "${id}" of runner "${runner}" is unavailable on this host: ${reason}`;
|
|
318
328
|
}
|
|
319
|
-
function plannedMode(runner, runnerModes, { readOnly, current }, autonomousDefault =
|
|
329
|
+
function plannedMode(runner, runnerModes, { readOnly, current }, autonomousDefault = false) {
|
|
320
330
|
if (!runnerModes) return { ok: true, mode: void 0 };
|
|
321
331
|
const modes = runnerModes[runner] ?? [];
|
|
322
332
|
const readOnlyId = readOnlyMode(modes);
|
|
@@ -399,7 +409,7 @@ function modeFor(write, task, moved) {
|
|
|
399
409
|
if (unavailable) return { ok: false, error: `Mode "${task.taskMode}" of runner "${runner}" is unavailable on this host: ${unavailable}` };
|
|
400
410
|
return { ok: true, mode: task.taskMode };
|
|
401
411
|
}
|
|
402
|
-
const becameAi = before
|
|
412
|
+
const becameAi = before !== void 0 && before.type !== "ai";
|
|
403
413
|
if (writer === "runner" || !catalog.runnerModes || before && !moved && !becameAi && write.readOnly === void 0) return { ok: true, mode: task.taskMode };
|
|
404
414
|
const carried = prior !== void 0 && task.taskMode === prior.taskMode && readOnlyOn(prior.taskMode, prior.assignedRunner, catalog);
|
|
405
415
|
const readOnly = write.readOnly ?? (carried || readOnlyOn(task.taskMode, runner, catalog));
|
|
@@ -411,7 +421,7 @@ function modeFor(write, task, moved) {
|
|
|
411
421
|
error: `"${task.title}" is read-only, and runner "${runner}" has no read-only mode${reason ? " available on this host" : ""}: give it another mode first${able.length > 0 ? `, or move it to one of: ${able.join(", ")}` : ""}${reason ? `. ${reason}` : ""}`
|
|
412
422
|
};
|
|
413
423
|
}
|
|
414
|
-
const mode = plannedMode(runner, catalog.runnerModes, { readOnly, current: task.taskMode }, catalog.autonomousDefault ??
|
|
424
|
+
const mode = plannedMode(runner, catalog.runnerModes, { readOnly, current: task.taskMode }, catalog.autonomousDefault ?? false);
|
|
415
425
|
return mode.ok ? { ok: true, mode: mode.mode ?? task.taskMode } : mode;
|
|
416
426
|
}
|
|
417
427
|
function resolveAssignment(write) {
|
|
@@ -423,7 +433,7 @@ function resolveAssignment(write) {
|
|
|
423
433
|
const live = writer !== "runner" && !!before && !!catalog.liveRunner?.(before.id);
|
|
424
434
|
if (live && moved) return { ok: false, field: "assignedRunner", error: liveRunnerRefusal({ title }) };
|
|
425
435
|
if (live && !sameAssignment(before?.assignedModel, written.assignedModel)) return { ok: false, field: "assignedModel", error: liveModelRefusal({ title }) };
|
|
426
|
-
if (written.type
|
|
436
|
+
if (written.type !== "ai") return { ok: true, task: written, adjustments: [] };
|
|
427
437
|
const mode = modeFor(write, written, moved);
|
|
428
438
|
if (!mode.ok) return { ok: false, field: "taskMode", error: mode.error };
|
|
429
439
|
const model = live ? { ok: true, assignment: written.assignedModel } : modelFor(write, moved);
|
|
@@ -492,17 +502,162 @@ function modelFor(write, moved) {
|
|
|
492
502
|
return { ok: true, assignment: disowned || !allows(id) ? refill(written.thinkingEffort) : settled(written) };
|
|
493
503
|
}
|
|
494
504
|
|
|
505
|
+
// src/models/ScriptTask.ts
|
|
506
|
+
var DEFAULT_SCRIPT_TIMEOUT_MS = 10 * 6e4;
|
|
507
|
+
var MAX_SCRIPT_TIMEOUT_MS = 24 * 60 * 6e4;
|
|
508
|
+
var SCRIPT_BODY_FIELDS = ["commands", "script", "timeoutMs"];
|
|
509
|
+
var NOT_A_SCRIPTS_FIELDS = ["assignedModel", "taskMode", "prompt", "skills", "userSteps", "ops"];
|
|
510
|
+
function scriptBodyErrors(task) {
|
|
511
|
+
if (task.type !== "script") {
|
|
512
|
+
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` }));
|
|
513
|
+
}
|
|
514
|
+
const errors = [];
|
|
515
|
+
const { commands, script, timeoutMs } = task;
|
|
516
|
+
if (commands !== void 0 && script !== void 0) {
|
|
517
|
+
errors.push({ field: "commands", message: 'has both "commands" and "script": a script task runs exactly one of them' });
|
|
518
|
+
} else if (commands === void 0 && script === void 0) {
|
|
519
|
+
errors.push({ field: "commands", message: 'is a script task with neither "commands" nor "script": give it the argument vectors to run, or the script' });
|
|
520
|
+
}
|
|
521
|
+
if (commands !== void 0) errors.push(...commandsErrors(commands));
|
|
522
|
+
if (script !== void 0 && (typeof script !== "string" || !script.trim())) {
|
|
523
|
+
errors.push({ field: "script", message: 'has an empty "script": write the script it runs' });
|
|
524
|
+
}
|
|
525
|
+
if (timeoutMs !== void 0 && !(typeof timeoutMs === "number" && Number.isInteger(timeoutMs) && timeoutMs > 0 && timeoutMs <= MAX_SCRIPT_TIMEOUT_MS)) {
|
|
526
|
+
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` });
|
|
527
|
+
}
|
|
528
|
+
return errors;
|
|
529
|
+
}
|
|
530
|
+
function commandsErrors(commands) {
|
|
531
|
+
const shape = 'a list of argument vectors, each a list of strings, like [["git", "push", "origin", "main"]]';
|
|
532
|
+
if (!Array.isArray(commands) || commands.length === 0) return [{ field: "commands", message: `has "commands" that is not ${shape}` }];
|
|
533
|
+
for (const [i, argv] of commands.entries()) {
|
|
534
|
+
const n = i + 1;
|
|
535
|
+
if (!Array.isArray(argv) || !argv.every((word) => typeof word === "string")) {
|
|
536
|
+
return [{ field: "commands", message: `has command ${n} that is not an argument vector: "commands" is ${shape}` }];
|
|
537
|
+
}
|
|
538
|
+
if (argv.length === 0) return [{ field: "commands", message: `has command ${n} as an empty argument vector: name the program it runs` }];
|
|
539
|
+
if (!argv[0].trim()) return [{ field: "commands", message: `has command ${n} naming no program: its first word is the program it runs` }];
|
|
540
|
+
}
|
|
541
|
+
return [];
|
|
542
|
+
}
|
|
543
|
+
function notAScriptsFields(task) {
|
|
544
|
+
return NOT_A_SCRIPTS_FIELDS.filter((f) => {
|
|
545
|
+
const value = task[f];
|
|
546
|
+
return value !== void 0 && value !== false && !(Array.isArray(value) && value.length === 0);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/models/Loop.ts
|
|
551
|
+
var DEFAULT_LOOP_ROUNDS = 3;
|
|
552
|
+
var MAX_LOOP_ROUNDS = 10;
|
|
553
|
+
var SHAPE = 'is not {"body": ["<task id>", ...], "maxRounds"?: <rounds>}';
|
|
554
|
+
function roundsMessage(value) {
|
|
555
|
+
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`;
|
|
556
|
+
}
|
|
557
|
+
function isRoundCount(value) {
|
|
558
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= MAX_LOOP_ROUNDS;
|
|
559
|
+
}
|
|
560
|
+
function loopSpec(value) {
|
|
561
|
+
if (value === void 0 || value === null) return { loop: void 0 };
|
|
562
|
+
if (typeof value !== "object" || Array.isArray(value)) return { error: `has a "loop" that ${SHAPE}` };
|
|
563
|
+
const { body, maxRounds } = value;
|
|
564
|
+
if (!Array.isArray(body) || !body.every((id) => typeof id === "string" || typeof id === "number")) return { error: `has a "loop" whose "body" ${SHAPE}` };
|
|
565
|
+
if (body.length === 0) return { error: 'has a "loop" with an empty "body": name the tasks that fix what it finds' };
|
|
566
|
+
if (maxRounds !== void 0 && !isRoundCount(maxRounds)) return { error: roundsMessage(maxRounds) };
|
|
567
|
+
return { loop: { body: body.map(String), maxRounds: maxRounds ?? DEFAULT_LOOP_ROUNDS } };
|
|
568
|
+
}
|
|
569
|
+
function loopErrors(tasks) {
|
|
570
|
+
const rows = flattenTasksWithParents(tasks);
|
|
571
|
+
const byId = new Map(rows.map((row) => [row.task.id, row]));
|
|
572
|
+
const errors = [];
|
|
573
|
+
const bodyOwner = /* @__PURE__ */ new Map();
|
|
574
|
+
for (const { task: check, parent } of rows) {
|
|
575
|
+
const loop = check.loop;
|
|
576
|
+
if (!loop) continue;
|
|
577
|
+
const where = parent ? `subtask "${check.title}" of "${parent.title}"` : `Task "${check.title}"`;
|
|
578
|
+
const fail = (message) => errors.push({ taskId: check.id, field: "loop", message: `${where} ${message}` });
|
|
579
|
+
if (parent) fail(`has a "loop", but only a top-level task can be a loop's check`);
|
|
580
|
+
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");
|
|
581
|
+
if (check.subtasks?.length) fail("is a loop's check, which has no subtasks: each round runs it again whole");
|
|
582
|
+
if (!isRoundCount(loop.maxRounds)) fail(roundsMessage(loop.maxRounds));
|
|
583
|
+
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');
|
|
584
|
+
const named = /* @__PURE__ */ new Set();
|
|
585
|
+
for (const id of loop.body ?? []) {
|
|
586
|
+
const row = byId.get(id);
|
|
587
|
+
if (named.has(id)) {
|
|
588
|
+
fail(`names "${row?.task.title ?? id}" twice in its loop's "body"`);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
named.add(id);
|
|
592
|
+
if (!row) {
|
|
593
|
+
fail(`names "${id}" in its loop's "body", which is no task in the plan`);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
const body = row.task;
|
|
597
|
+
if (body.id === check.id) {
|
|
598
|
+
fail(`names itself in its loop's "body": the body is the tasks that fix what it finds`);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (row.parent) {
|
|
602
|
+
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`);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
const other = bodyOwner.get(id);
|
|
606
|
+
if (other) {
|
|
607
|
+
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`);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
bodyOwner.set(id, check);
|
|
611
|
+
if (body.loop) fail(`names "${body.title}" in its loop's "body", which has a loop of its own: a loop has no loop inside its body`);
|
|
612
|
+
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`);
|
|
613
|
+
if (body.subtasks?.length) fail(`names "${body.title}" in its loop's "body", which has subtasks: each round runs a body task again whole, so make each step a task of the body`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
for (const [bodyId, check] of bodyOwner) {
|
|
617
|
+
const body = byId.get(bodyId).task;
|
|
618
|
+
const where = `Task "${body.title}" is in the body of "${check.title}"'s loop`;
|
|
619
|
+
if (!body.dependencies.includes(check.id)) {
|
|
620
|
+
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` });
|
|
621
|
+
}
|
|
622
|
+
for (const dep of body.dependencies) {
|
|
623
|
+
if (dep === check.id || check.dependencies.includes(dep) || bodyOwner.get(dep) === check) continue;
|
|
624
|
+
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` });
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
for (const { task } of rows) {
|
|
628
|
+
for (const dep of task.dependencies) {
|
|
629
|
+
const check = bodyOwner.get(dep);
|
|
630
|
+
if (!check || bodyOwner.get(task.id) === check) continue;
|
|
631
|
+
const body = byId.get(dep).task;
|
|
632
|
+
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`;
|
|
633
|
+
errors.push({ taskId: task.id, field: "dependencies", message });
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return errors;
|
|
637
|
+
}
|
|
638
|
+
|
|
495
639
|
// src/services/PlannedTask.ts
|
|
496
640
|
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`;
|
|
497
641
|
var MANUAL_NOT_READ_ONLY = '"readOnly" is for AI tasks: a manual task runs in no mode';
|
|
498
|
-
var OPS_NOT_READ_ONLY =
|
|
642
|
+
var OPS_NOT_READ_ONLY = '"readOnly" is not for an ops task: it acts in the workspace, while a read-only task lands nothing';
|
|
499
643
|
function isRecord(value) {
|
|
500
644
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
501
645
|
}
|
|
502
646
|
function text(value) {
|
|
503
647
|
return typeof value === "string" || typeof value === "number" ? String(value) : void 0;
|
|
504
648
|
}
|
|
649
|
+
var NOT_ON_A_SCRIPT = ["assignedRunner", "assignedModel", "thinkingEffort", "taskMode", "readOnly", "prompt", "skills", "userSteps", "ops", "subtasks"];
|
|
650
|
+
function notOnAScript(raw) {
|
|
651
|
+
return NOT_ON_A_SCRIPT.filter((f) => {
|
|
652
|
+
const value = raw[f];
|
|
653
|
+
return value !== void 0 && value !== null && value !== false && !(Array.isArray(value) && value.length === 0);
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
function notOnAScriptMessage(fields) {
|
|
657
|
+
return `is a script task, which runs its "commands" or "script" with no model: leave out ${fields.map((f) => `"${f}"`).join(", ")}`;
|
|
658
|
+
}
|
|
505
659
|
function opsPlacementRefusal(type, isSubtask) {
|
|
660
|
+
if (type === "script") return 'is a script task, which is an ops task by nature: leave "ops" out';
|
|
506
661
|
if (type !== "ai") return "is a manual task: only an AI task can be an ops task";
|
|
507
662
|
if (isSubtask) return "is a subtask, which runs with its parent: only a top-level task can be an ops task";
|
|
508
663
|
return null;
|
|
@@ -523,6 +678,25 @@ function modelOf(raw, from) {
|
|
|
523
678
|
}
|
|
524
679
|
return from && effort !== void 0 ? { ...from, thinkingEffort: effort } : from;
|
|
525
680
|
}
|
|
681
|
+
function bodyFields(raw, base) {
|
|
682
|
+
const pick = (field) => raw[field] !== void 0 && raw[field] !== null ? raw[field] : base[field];
|
|
683
|
+
const judged = { commands: pick("commands"), script: pick("script"), timeoutMs: pick("timeoutMs") };
|
|
684
|
+
const malformed = /* @__PURE__ */ new Set();
|
|
685
|
+
const commands = isCommands(judged.commands) ? judged.commands : void 0;
|
|
686
|
+
if (judged.commands !== void 0 && !commands) malformed.add("commands");
|
|
687
|
+
const script = typeof judged.script === "string" ? judged.script : void 0;
|
|
688
|
+
if (judged.script !== void 0 && script === void 0) malformed.add("script");
|
|
689
|
+
const timeoutMs = typeof judged.timeoutMs === "number" ? judged.timeoutMs : void 0;
|
|
690
|
+
if (judged.timeoutMs !== void 0 && timeoutMs === void 0) malformed.add("timeoutMs");
|
|
691
|
+
return {
|
|
692
|
+
fields: { ...commands ? { commands } : {}, ...script !== void 0 ? { script } : {}, ...timeoutMs !== void 0 ? { timeoutMs } : {} },
|
|
693
|
+
raw: judged,
|
|
694
|
+
malformed
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function isCommands(value) {
|
|
698
|
+
return Array.isArray(value) && value.every((argv) => Array.isArray(argv) && argv.every((word) => typeof word === "string"));
|
|
699
|
+
}
|
|
526
700
|
function plannedTask(spec, catalog, at = {}) {
|
|
527
701
|
const raw = isRecord(spec) ? spec : {};
|
|
528
702
|
const { parent, from, freshIds, newId = import_uuid2.v4 } = at;
|
|
@@ -536,17 +710,26 @@ function plannedTask(spec, catalog, at = {}) {
|
|
|
536
710
|
const plannerId = text(raw.id);
|
|
537
711
|
const id = freshIds || !plannerId ? newId() : plannerId;
|
|
538
712
|
if (freshIds && plannerId) renamed.set(plannerId, id);
|
|
539
|
-
const type = raw.type === "user" || raw.type === "ai" ? raw.type : base.type ?? "ai";
|
|
713
|
+
const type = raw.type === "user" || raw.type === "ai" || raw.type === "script" ? raw.type : base.type ?? "ai";
|
|
540
714
|
const ai = type === "ai";
|
|
715
|
+
const script = type === "script";
|
|
541
716
|
const assignedRunner = text(raw.assignedRunner) ?? base.assignedRunner ?? catalog.runners[0] ?? DEFAULT_RUNNERS[0];
|
|
542
717
|
const description = text(raw.description) ?? base.description ?? title;
|
|
543
|
-
|
|
544
|
-
if (
|
|
545
|
-
|
|
718
|
+
const notScripts = script ? notOnAScript(raw) : [];
|
|
719
|
+
if (notScripts.length > 0) errors.push({ taskId: id, field: notScripts[0], message: `${where} ${notOnAScriptMessage(notScripts)}` });
|
|
720
|
+
const body = bodyFields(raw, base);
|
|
721
|
+
const shapeErrors = scriptBodyErrors({ type, ...body.raw }).filter((e) => body.malformed.has(e.field));
|
|
722
|
+
for (const e of shapeErrors) errors.push({ taskId: id, field: e.field, message: `${where} ${e.message}` });
|
|
723
|
+
const loop = loopSpec(raw.loop);
|
|
724
|
+
if ("error" in loop) errors.push({ taskId: id, field: "loop", message: `${where} ${loop.error}` });
|
|
725
|
+
if (raw.ops === true && !ai && !script) errors.push({ taskId: id, field: "ops", message: `${where} ${opsPlacementRefusal(type, !!parent)}` });
|
|
726
|
+
if (raw.taskMode !== void 0 && !script) ignored.push({ taskId: id, field: "taskMode", message: `${where}: ${MODE_NOT_THE_PLANNERS}` });
|
|
727
|
+
if (type === "user" && raw.readOnly === true) ignored.push({ taskId: id, field: "readOnly", message: `${where}: ${MANUAL_NOT_READ_ONLY}` });
|
|
546
728
|
const ops = raw.ops !== void 0 ? raw.ops === true : base.ops;
|
|
547
729
|
if (ai && ops && raw.readOnly === true) ignored.push({ taskId: id, field: "readOnly", message: `${where}: ${OPS_NOT_READ_ONLY}` });
|
|
548
730
|
const asked = typeof raw.readOnly === "boolean" ? raw.readOnly : void 0;
|
|
549
731
|
const readOnly = ai && !ops && (asked ?? from?.readOnly ?? parent?.readOnly ?? false);
|
|
732
|
+
if (raw.research !== void 0 && typeof raw.research !== "boolean") errors.push({ taskId: id, field: "research", message: `${where}: "research" must be a boolean` });
|
|
550
733
|
const assignedModel = ai ? modelOf(raw, base.assignedModel) : void 0;
|
|
551
734
|
const slice = (value) => value === "AFK" || value === "HITL" ? value : void 0;
|
|
552
735
|
const autonomy = ai ? slice(raw.autonomy) ?? base.autonomy ?? parent?.autonomy : void 0;
|
|
@@ -569,7 +752,8 @@ function plannedTask(spec, catalog, at = {}) {
|
|
|
569
752
|
description,
|
|
570
753
|
type,
|
|
571
754
|
dependencies: Array.isArray(raw.dependencies) ? raw.dependencies.map(String) : base.dependencies ?? [],
|
|
572
|
-
|
|
755
|
+
// What a merge or split carries over for a runner is nothing a script runs.
|
|
756
|
+
prompt: script ? void 0 : text(raw.prompt) || base.prompt || (ai ? description || title || void 0 : void 0),
|
|
573
757
|
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,
|
|
574
758
|
subtasks,
|
|
575
759
|
assignedModel,
|
|
@@ -577,10 +761,13 @@ function plannedTask(spec, catalog, at = {}) {
|
|
|
577
761
|
autonomy,
|
|
578
762
|
sliceType,
|
|
579
763
|
userStoriesCovered: Array.isArray(raw.userStoriesCovered) ? raw.userStoriesCovered.map(String) : base.userStoriesCovered,
|
|
580
|
-
ops,
|
|
581
|
-
|
|
764
|
+
ops: script ? void 0 : ops,
|
|
765
|
+
research: typeof raw.research === "boolean" ? raw.research : base.research,
|
|
766
|
+
skills: script ? void 0 : from ? inheritedSkills(raw.skills, base.skills) : skillNames(raw.skills),
|
|
767
|
+
...body.fields,
|
|
768
|
+
..."loop" in loop && loop.loop ? { loop: loop.loop } : {}
|
|
582
769
|
};
|
|
583
|
-
let taskMode = base.taskMode;
|
|
770
|
+
let taskMode = script ? void 0 : base.taskMode;
|
|
584
771
|
let landedModel = assignedModel;
|
|
585
772
|
if (ai && catalog.runners.includes(assignedRunner)) {
|
|
586
773
|
const resolved = resolveAssignment({
|
|
@@ -603,30 +790,50 @@ function plannedTask(spec, catalog, at = {}) {
|
|
|
603
790
|
if (Array.isArray(raw.skills)) skillsAsked.set(task, raw.skills);
|
|
604
791
|
return { task, errors, ignored, renamed, adjustments };
|
|
605
792
|
}
|
|
793
|
+
function checkPlanRunners(tasks, runners) {
|
|
794
|
+
if (!runners) return [];
|
|
795
|
+
return flattenTasksWithParents(tasks).filter(({ task: t }) => t.type === "ai" && !runners.includes(t.assignedRunner)).map(({ task: t, parent }) => ({
|
|
796
|
+
taskId: t.id,
|
|
797
|
+
field: "assignedRunner",
|
|
798
|
+
message: `${taskWhere(t.title, parent?.title)} has invalid assignedRunner "${t.assignedRunner}". Expected one of: ${runners.join(", ")}`
|
|
799
|
+
}));
|
|
800
|
+
}
|
|
606
801
|
function checkPlanInvariants(tasks, runners, runnerModes) {
|
|
607
802
|
const rows = flattenTasksWithParents(tasks);
|
|
608
803
|
const errors = [];
|
|
609
804
|
const seen = /* @__PURE__ */ new Set();
|
|
805
|
+
const body = new Set(tasks.flatMap((t) => t.loop?.body ?? []));
|
|
610
806
|
for (const { task: t, parent } of rows) {
|
|
611
807
|
const where = taskWhere(t.title, parent?.title);
|
|
612
808
|
const fail = (field, message) => errors.push({ taskId: t.id, field, message });
|
|
613
809
|
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`);
|
|
614
810
|
seen.add(t.id);
|
|
811
|
+
if (t.research !== void 0 && typeof t.research !== "boolean") fail("research", `${where}: "research" must be a boolean`);
|
|
812
|
+
const readOnly = runnerModes ? taggedReadOnlyMode(runnerModes[t.assignedRunner]) : "plan";
|
|
813
|
+
if (t.research && (t.type !== "ai" || parent !== null || t.ops || body.has(t.id) || readOnly === void 0 || t.taskMode !== readOnly)) {
|
|
814
|
+
fail("research", `${where}: research is only for top-level read-only AI tasks, never a loop's body`);
|
|
815
|
+
}
|
|
615
816
|
if (t.autonomy === "AFK" && t.userSteps && t.userSteps.length > 0) {
|
|
616
817
|
fail("userSteps", `${where} has autonomy "AFK" but contains userSteps. AFK tasks must not have user touchpoints.`);
|
|
617
818
|
}
|
|
618
819
|
if (t.type === "user" && t.sliceType && t.sliceType !== "HITL") {
|
|
619
820
|
fail("sliceType", `User ${where} has sliceType "${t.sliceType}". User tasks must have sliceType "HITL".`);
|
|
620
821
|
}
|
|
621
|
-
const opsRefused = t.ops ? opsPlacementRefusal(t.type, parent !== null) : null;
|
|
822
|
+
const opsRefused = t.ops && t.type !== "script" ? opsPlacementRefusal(t.type, parent !== null) : null;
|
|
622
823
|
if (opsRefused) fail("ops", `${where} ${opsRefused}`);
|
|
623
|
-
if (t.
|
|
624
|
-
fail("
|
|
824
|
+
if (t.type === "script") {
|
|
825
|
+
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`);
|
|
826
|
+
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`);
|
|
827
|
+
const extra = notAScriptsFields(t);
|
|
828
|
+
if (extra.length > 0) fail(extra[0], `${where} ${notOnAScriptMessage(extra)}`);
|
|
625
829
|
}
|
|
626
|
-
|
|
627
|
-
|
|
830
|
+
for (const e of scriptBodyErrors(t)) fail(e.field, `${where} ${e.message}`);
|
|
831
|
+
if (t.ops && t.taskMode !== void 0 && t.taskMode === taggedReadOnlyMode(runnerModes?.[t.assignedRunner])) {
|
|
832
|
+
fail("taskMode", `${where} is an ops task, which acts in the workspace, so it cannot use the read-only profile`);
|
|
628
833
|
}
|
|
629
834
|
}
|
|
835
|
+
errors.push(...checkPlanRunners(tasks, runners));
|
|
836
|
+
errors.push(...loopErrors(tasks));
|
|
630
837
|
for (const { task: t, parent } of rows) {
|
|
631
838
|
for (const error of dependencyTargetErrors(tasks, t.id, t.dependencies.filter((id) => id !== t.id))) {
|
|
632
839
|
errors.push({ taskId: t.id, field: "dependencies", message: `${taskWhere(t.title, parent?.title)}: ${error}` });
|
|
@@ -641,7 +848,7 @@ function findCycle(tasks) {
|
|
|
641
848
|
}
|
|
642
849
|
|
|
643
850
|
// src/services/PlanValidator.ts
|
|
644
|
-
function parsePlanJson(raw, runners, runnerModes, autonomousDefault =
|
|
851
|
+
function parsePlanJson(raw, runners, runnerModes, autonomousDefault = false) {
|
|
645
852
|
const { matches, fallback, sawUnbalanced } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);
|
|
646
853
|
const candidates = matches.length > 0 ? [...matches].reverse() : [fallback.json];
|
|
647
854
|
let lastError = null;
|
|
@@ -662,7 +869,7 @@ function parsePlanJson(raw, runners, runnerModes, autonomousDefault = true) {
|
|
|
662
869
|
}
|
|
663
870
|
throw lastError ?? new PlanParseError("Invalid plan: missing tasks array", raw);
|
|
664
871
|
}
|
|
665
|
-
function parsePlanObject(json, raw, runners, runnerModes, autonomousDefault =
|
|
872
|
+
function parsePlanObject(json, raw, runners, runnerModes, autonomousDefault = false) {
|
|
666
873
|
const cleaned = stripTrailingCommas(escapeControlCharsInStrings(json));
|
|
667
874
|
let parsed;
|
|
668
875
|
try {
|
|
@@ -676,7 +883,7 @@ function parsePlanObject(json, raw, runners, runnerModes, autonomousDefault = tr
|
|
|
676
883
|
const [first] = result.errors;
|
|
677
884
|
throw new PlanParseError(first.message, raw, { semantic: first.taskId !== void 0 });
|
|
678
885
|
}
|
|
679
|
-
function validatePlanTasks(obj, runners, runnerModes, autonomousDefault =
|
|
886
|
+
function validatePlanTasks(obj, runners, runnerModes, autonomousDefault = false) {
|
|
680
887
|
const rawTasks = isRecord2(obj) ? obj.tasks : void 0;
|
|
681
888
|
if (!Array.isArray(rawTasks)) {
|
|
682
889
|
return { ok: false, errors: [{ field: "tasks", message: "Invalid plan: missing tasks array" }] };
|