@ordewell/core 0.10.4 → 0.10.6
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/dist/{IRunner-D_UkkMbJ.d.ts → IFileSystem-C4_mpfze.d.ts} +159 -15
- package/dist/{IRunner-NtTHQgLR.d.mts → IFileSystem-DUgeWWQN.d.mts} +159 -15
- package/dist/{ModeResolver-wnTFsDnF.d.mts → ModeResolver-4qNjxcDs.d.mts} +1 -1
- package/dist/{ModeResolver-Dy3Ef6p-.d.ts → ModeResolver-DGK2rWHE.d.ts} +1 -1
- package/dist/{Task-DZs9eKgg.d.mts → Task-C0o4i3Nn.d.mts} +114 -13
- package/dist/{Task-DZs9eKgg.d.ts → Task-C0o4i3Nn.d.ts} +114 -13
- package/dist/{chunk-FH6M5QZG.mjs → chunk-DMG4ISUG.mjs} +1008 -49
- package/dist/chunk-DMG4ISUG.mjs.map +1 -0
- package/dist/{chunk-SM6BV6XS.mjs → chunk-EEE2GBLG.mjs} +116 -27
- package/dist/chunk-EEE2GBLG.mjs.map +1 -0
- package/dist/chunk-H7QX32BH.mjs +647 -0
- package/dist/chunk-H7QX32BH.mjs.map +1 -0
- package/dist/{chunk-645FALZX.mjs → chunk-LTHKFBKM.mjs} +4 -4
- package/dist/chunk-LTHKFBKM.mjs.map +1 -0
- package/dist/{chunk-OYUSADFM.mjs → chunk-NJGID6BG.mjs} +3 -1
- package/dist/chunk-NJGID6BG.mjs.map +1 -0
- package/dist/index.d.mts +498 -1689
- package/dist/index.d.ts +498 -1689
- package/dist/index.js +4108 -2082
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3380 -1639
- 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 +231 -75
- package/dist/parsing.js.map +1 -1
- package/dist/parsing.mjs +3 -3
- package/dist/plan-utils.d.mts +20 -4
- package/dist/plan-utils.d.ts +20 -4
- package/dist/plan-utils.js +335 -88
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +43 -19
- package/dist/{plan-utils-CtKceSsQ.d.mts → runSettingsText-BgOmLxdw.d.mts} +2242 -224
- package/dist/{plan-utils-POIz5d8z.d.ts → runSettingsText-BgXvajj2.d.ts} +2242 -224
- package/dist/testing.d.mts +5 -3
- package/dist/testing.d.ts +5 -3
- package/dist/testing.js +7 -0
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +7 -2
- package/dist/testing.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/IFileSystem-_effm8-b.d.ts +0 -78
- package/dist/IFileSystem-vyqMdL-x.d.mts +0 -78
- package/dist/chunk-645FALZX.mjs.map +0 -1
- package/dist/chunk-A57ITQE7.mjs +0 -1378
- package/dist/chunk-A57ITQE7.mjs.map +0 -1
- package/dist/chunk-FH6M5QZG.mjs.map +0 -1
- package/dist/chunk-OYUSADFM.mjs.map +0 -1
- package/dist/chunk-SM6BV6XS.mjs.map +0 -1
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_RUNNERS,
|
|
3
|
+
createTask,
|
|
4
|
+
dependencyTargetErrors,
|
|
5
|
+
findDependencyCycle,
|
|
6
|
+
flattenTasks,
|
|
7
|
+
flattenTasksWithParents,
|
|
8
|
+
inheritedSkills,
|
|
9
|
+
skillNames
|
|
10
|
+
} from "./chunk-EEE2GBLG.mjs";
|
|
11
|
+
|
|
12
|
+
// src/services/JsonExtractor.ts
|
|
13
|
+
var PLAN_ENVELOPE_KEY = "tasks";
|
|
14
|
+
var TASK_OPS_ENVELOPE_KEY = "taskOps";
|
|
15
|
+
var TASK_QUERY_ENVELOPE_KEY = "taskQuery";
|
|
16
|
+
var PlanParseError = class extends Error {
|
|
17
|
+
raw;
|
|
18
|
+
/**
|
|
19
|
+
* True when the reply was cut off mid-object (output-token limit) rather
|
|
20
|
+
* than malformed — the two need different repairs: a truncated emission will
|
|
21
|
+
* be cut at the same point again unless the retry frees context or asks for
|
|
22
|
+
* terser output.
|
|
23
|
+
*/
|
|
24
|
+
truncated;
|
|
25
|
+
/**
|
|
26
|
+
* True when the JSON parsed fine and a *shape* rule rejected it. The generic
|
|
27
|
+
* "could not be parsed as JSON" corrective is a lie for these, so the model
|
|
28
|
+
* re-sends the same object; a semantic rejection must be repaired by naming
|
|
29
|
+
* the rule it broke.
|
|
30
|
+
*/
|
|
31
|
+
semantic;
|
|
32
|
+
constructor(message, raw, opts) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "PlanParseError";
|
|
35
|
+
this.raw = raw;
|
|
36
|
+
this.truncated = opts?.truncated ?? false;
|
|
37
|
+
this.semantic = opts?.semantic ?? false;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function stripModelNoise(raw) {
|
|
41
|
+
return raw.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/```json\s*/gi, "").replace(/```\s*/g, "").trim();
|
|
42
|
+
}
|
|
43
|
+
var THINK_OPEN = "<think>";
|
|
44
|
+
var THINK_CLOSE = "</think>";
|
|
45
|
+
var FENCE = "```";
|
|
46
|
+
var FENCE_JSON_TAG = "json";
|
|
47
|
+
function opensWithJsonObject(prefix) {
|
|
48
|
+
let rest = prefix;
|
|
49
|
+
for (; ; ) {
|
|
50
|
+
rest = rest.trimStart();
|
|
51
|
+
if (!rest) return void 0;
|
|
52
|
+
const head = rest.slice(0, THINK_OPEN.length).toLowerCase();
|
|
53
|
+
if (head === THINK_OPEN) {
|
|
54
|
+
const close = rest.toLowerCase().indexOf(THINK_CLOSE);
|
|
55
|
+
if (close < 0) return void 0;
|
|
56
|
+
rest = rest.slice(close + THINK_CLOSE.length);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (rest.startsWith(FENCE)) {
|
|
60
|
+
rest = rest.slice(FENCE.length);
|
|
61
|
+
const tag = rest.slice(0, FENCE_JSON_TAG.length).toLowerCase();
|
|
62
|
+
if (tag === FENCE_JSON_TAG) rest = rest.slice(FENCE_JSON_TAG.length);
|
|
63
|
+
else if (tag === rest.toLowerCase() && FENCE_JSON_TAG.startsWith(tag)) return void 0;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (THINK_OPEN.startsWith(head) || FENCE.startsWith(rest)) return void 0;
|
|
67
|
+
return rest.startsWith("{");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function balancedObjectAt(text2, start) {
|
|
71
|
+
let depth = 0;
|
|
72
|
+
let inString = false;
|
|
73
|
+
let escaped = false;
|
|
74
|
+
for (let i = start; i < text2.length; i++) {
|
|
75
|
+
const ch = text2[i];
|
|
76
|
+
if (inString) {
|
|
77
|
+
if (escaped) escaped = false;
|
|
78
|
+
else if (ch === "\\") escaped = true;
|
|
79
|
+
else if (ch === '"') inString = false;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (ch === '"') inString = true;
|
|
83
|
+
else if (ch === "{") depth++;
|
|
84
|
+
else if (ch === "}") {
|
|
85
|
+
depth--;
|
|
86
|
+
if (depth === 0) return { json: text2.slice(start, i + 1), balanced: true };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { json: text2.slice(start), balanced: false };
|
|
90
|
+
}
|
|
91
|
+
function extractObjectsWithKey(raw, key) {
|
|
92
|
+
const stripped = stripModelNoise(raw);
|
|
93
|
+
const keyRe = new RegExp(`"${key}"\\s*:`);
|
|
94
|
+
const matches = [];
|
|
95
|
+
let firstObject = null;
|
|
96
|
+
let sawBrace = false;
|
|
97
|
+
let sawUnbalanced = false;
|
|
98
|
+
let inString = false;
|
|
99
|
+
let escaped = false;
|
|
100
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
101
|
+
const ch = stripped[i];
|
|
102
|
+
if (inString) {
|
|
103
|
+
if (escaped) escaped = false;
|
|
104
|
+
else if (ch === "\\") escaped = true;
|
|
105
|
+
else if (ch === '"') inString = false;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (ch === '"') {
|
|
109
|
+
inString = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (ch !== "{") continue;
|
|
113
|
+
sawBrace = true;
|
|
114
|
+
const obj = balancedObjectAt(stripped, i);
|
|
115
|
+
if (obj.balanced && keyRe.test(obj.json)) {
|
|
116
|
+
matches.push(obj.json);
|
|
117
|
+
i += obj.json.length - 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!obj.balanced) sawUnbalanced = true;
|
|
121
|
+
if (!firstObject && obj.balanced) firstObject = obj.json;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
matches,
|
|
125
|
+
fallback: firstObject !== null ? { json: firstObject, balanced: true } : { json: stripped, balanced: false },
|
|
126
|
+
sawBrace,
|
|
127
|
+
sawUnbalanced
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function extractObjectWithBalance(raw) {
|
|
131
|
+
const { matches, fallback, sawBrace } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);
|
|
132
|
+
if (matches.length > 0) return { json: matches[0], balanced: true, sawBrace };
|
|
133
|
+
return { ...fallback, sawBrace };
|
|
134
|
+
}
|
|
135
|
+
function extractJsonObject(raw) {
|
|
136
|
+
return extractObjectWithBalance(raw).json;
|
|
137
|
+
}
|
|
138
|
+
function stripTrailingCommas(json) {
|
|
139
|
+
return json.replace(/,(\s*[}\]])/g, "$1");
|
|
140
|
+
}
|
|
141
|
+
function escapeControlCharsInStrings(json) {
|
|
142
|
+
let out = "";
|
|
143
|
+
let inString = false;
|
|
144
|
+
let escaped = false;
|
|
145
|
+
for (const ch of json) {
|
|
146
|
+
if (inString) {
|
|
147
|
+
if (escaped) {
|
|
148
|
+
out += ch;
|
|
149
|
+
escaped = false;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (ch === "\\") {
|
|
153
|
+
out += ch;
|
|
154
|
+
escaped = true;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (ch === '"') {
|
|
158
|
+
out += ch;
|
|
159
|
+
inString = false;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (ch === "\n") {
|
|
163
|
+
out += "\\n";
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (ch === "\r") {
|
|
167
|
+
out += "\\r";
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (ch === " ") {
|
|
171
|
+
out += "\\t";
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
out += ch;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (ch === '"') inString = true;
|
|
178
|
+
out += ch;
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/services/ModeResolver.ts
|
|
184
|
+
function runnerModesFrom(registry, runners) {
|
|
185
|
+
return Object.fromEntries(runners.map((r) => [r, registry.getManifest(r)?.modes ?? []]));
|
|
186
|
+
}
|
|
187
|
+
function resolveDefaultMode(modes, autonomousDefault = true) {
|
|
188
|
+
if (!modes || modes.length === 0) return void 0;
|
|
189
|
+
const tag = autonomousDefault ? "autonomous" : "safe";
|
|
190
|
+
const tagged = modes.find((m) => m[tag]);
|
|
191
|
+
if (tagged) return tagged.id;
|
|
192
|
+
for (let i = modes.length - 1; i >= 0; i--) {
|
|
193
|
+
if (!modes[i].readOnly && modes[i].id !== "plan") return modes[i].id;
|
|
194
|
+
}
|
|
195
|
+
return void 0;
|
|
196
|
+
}
|
|
197
|
+
function autonomyLevelLabel(autonomous) {
|
|
198
|
+
return autonomous ? "Full" : "Guarded";
|
|
199
|
+
}
|
|
200
|
+
function parseAutonomyLevel(arg) {
|
|
201
|
+
switch (arg?.trim().toLowerCase()) {
|
|
202
|
+
case "full":
|
|
203
|
+
case "on":
|
|
204
|
+
return true;
|
|
205
|
+
case "guarded":
|
|
206
|
+
case "auto":
|
|
207
|
+
case "off":
|
|
208
|
+
return false;
|
|
209
|
+
default:
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function taggedReadOnlyMode(modes) {
|
|
214
|
+
return modes?.find((m) => m.readOnly)?.id;
|
|
215
|
+
}
|
|
216
|
+
function readOnlyMode(modes) {
|
|
217
|
+
const mode = modes?.find((m) => m.readOnly);
|
|
218
|
+
return mode && !mode.unavailable ? mode.id : void 0;
|
|
219
|
+
}
|
|
220
|
+
function unavailableModeReason(modes, id) {
|
|
221
|
+
return id === void 0 ? void 0 : modes?.find((m) => m.id === id)?.unavailable;
|
|
222
|
+
}
|
|
223
|
+
function unavailableModeError(runner, id, reason) {
|
|
224
|
+
return `Mode "${id}" of runner "${runner}" is unavailable on this host: ${reason}`;
|
|
225
|
+
}
|
|
226
|
+
function unavailableTaskModeMessage(manifest, mode) {
|
|
227
|
+
const reason = unavailableModeReason(manifest.modes, mode);
|
|
228
|
+
return reason ? `Mode "${mode}" of ${manifest.displayName} is unavailable on this host: ${reason}` : void 0;
|
|
229
|
+
}
|
|
230
|
+
function plannedMode(runner, runnerModes, { readOnly, current }, autonomousDefault = true) {
|
|
231
|
+
if (!runnerModes) return { ok: true, mode: void 0 };
|
|
232
|
+
const modes = runnerModes[runner] ?? [];
|
|
233
|
+
const readOnlyId = readOnlyMode(modes);
|
|
234
|
+
if (readOnly === true) {
|
|
235
|
+
if (readOnlyId) return { ok: true, mode: readOnlyId };
|
|
236
|
+
const able = readOnlyRunners(runnerModes);
|
|
237
|
+
const reason2 = unavailableModeReason(modes, taggedReadOnlyMode(modes));
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
error: `Runner "${runner}" has no read-only mode${reason2 ? " available on this host" : ""}: assign a read-only task to ${able.length > 0 ? `one of: ${able.join(", ")}` : "no runner: none of the enabled runners has one"}, or drop "readOnly"${reason2 ? `. ${reason2}` : ""}`
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const keep = current !== void 0 && modes.some((m) => m.id === current) && !(readOnly === false && current === taggedReadOnlyMode(modes));
|
|
244
|
+
const mode = keep ? current : resolveDefaultMode(modes, autonomousDefault);
|
|
245
|
+
const reason = unavailableModeReason(modes, mode);
|
|
246
|
+
if (mode !== void 0 && reason) return { ok: false, error: unavailableModeError(runner, mode, reason) };
|
|
247
|
+
return { ok: true, mode };
|
|
248
|
+
}
|
|
249
|
+
function carryPlannedModes(before, rewrite, catalog) {
|
|
250
|
+
const prior = new Map(flattenTasks(before).map((t) => [t.id, t]));
|
|
251
|
+
const carry = (t) => {
|
|
252
|
+
const subtasks = (t.subtasks ?? []).map(carry);
|
|
253
|
+
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);
|
|
256
|
+
return { ...t, subtasks, ...mode.ok && mode.mode !== void 0 ? { taskMode: mode.mode } : {} };
|
|
257
|
+
};
|
|
258
|
+
return rewrite.map(carry);
|
|
259
|
+
}
|
|
260
|
+
function readOnlyRunners(runnerModes) {
|
|
261
|
+
return Object.entries(runnerModes).filter(([, ms]) => readOnlyMode(ms)).map(([id]) => id);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/services/ModelAllowlistResolver.ts
|
|
265
|
+
function effectiveAllowlist(allowlist, runner, modelsByRunner) {
|
|
266
|
+
if (!allowlist || allowlist.length === 0) return void 0;
|
|
267
|
+
if (!modelsByRunner) return allowlist;
|
|
268
|
+
const own = new Set((modelsByRunner[runner] ?? []).map((m) => m.modelId));
|
|
269
|
+
const elsewhere = /* @__PURE__ */ new Set();
|
|
270
|
+
for (const [other, models] of Object.entries(modelsByRunner)) {
|
|
271
|
+
if (other === runner) continue;
|
|
272
|
+
for (const m of models ?? []) elsewhere.add(m.modelId);
|
|
273
|
+
}
|
|
274
|
+
const kept = allowlist.filter((id) => own.has(id) || !elsewhere.has(id));
|
|
275
|
+
return kept.length > 0 ? kept : void 0;
|
|
276
|
+
}
|
|
277
|
+
function filterModelsForPrompt(modelsByRunner, perRunnerAllowlist) {
|
|
278
|
+
const result = {};
|
|
279
|
+
for (const [runner, models] of Object.entries(modelsByRunner)) {
|
|
280
|
+
if (!models) continue;
|
|
281
|
+
const allowed = effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
282
|
+
if (!allowed) {
|
|
283
|
+
result[runner] = models;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const allowedSet = new Set(allowed);
|
|
287
|
+
const intersection = models.filter((m) => allowedSet.has(m.modelId));
|
|
288
|
+
const covered = new Set(intersection.map((m) => m.modelId));
|
|
289
|
+
const missing = allowed.filter((id) => !covered.has(id)).map((id) => ({ modelId: id, modelLabel: id.split("/").pop() || id, variants: [] }));
|
|
290
|
+
result[runner] = [...intersection, ...missing];
|
|
291
|
+
}
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
var EFFORT_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
295
|
+
var EFFORT_ALIASES = { disabled: "none", adaptive: "medium" };
|
|
296
|
+
function clampThinkingEffort(effort, variants) {
|
|
297
|
+
if (!effort) return void 0;
|
|
298
|
+
if (variants.length === 0) return void 0;
|
|
299
|
+
const ids = new Set(variants.map((v) => v.id));
|
|
300
|
+
if (ids.has(effort)) return effort;
|
|
301
|
+
const canon = EFFORT_ALIASES[effort] ?? effort;
|
|
302
|
+
if (ids.has(canon)) return canon;
|
|
303
|
+
const idx = EFFORT_LADDER.indexOf(canon);
|
|
304
|
+
if (idx < 0) return void 0;
|
|
305
|
+
for (let d = 1; d < EFFORT_LADDER.length; d++) {
|
|
306
|
+
const lower = EFFORT_LADDER[idx - d];
|
|
307
|
+
if (lower && ids.has(lower)) return lower;
|
|
308
|
+
const higher = EFFORT_LADDER[idx + d];
|
|
309
|
+
if (higher && ids.has(higher)) return higher;
|
|
310
|
+
}
|
|
311
|
+
return void 0;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/services/TaskAssignment.ts
|
|
315
|
+
function liveModelRefusal(task) {
|
|
316
|
+
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`;
|
|
317
|
+
}
|
|
318
|
+
function liveRunnerRefusal(task) {
|
|
319
|
+
return `"${task.title}" has a live runner, so its runner cannot change now: stop the task first`;
|
|
320
|
+
}
|
|
321
|
+
function assignableRunners(writer, enabled, planRunners) {
|
|
322
|
+
if (writer === "plan") return [...enabled];
|
|
323
|
+
if (writer === "edit") return [.../* @__PURE__ */ new Set([...planRunners, ...enabled])];
|
|
324
|
+
return void 0;
|
|
325
|
+
}
|
|
326
|
+
function sameAssignment(a, b) {
|
|
327
|
+
return a?.modelId === b?.modelId && a?.thinkingEffort === b?.thinkingEffort;
|
|
328
|
+
}
|
|
329
|
+
function readOnlyOn(mode, runner, catalog) {
|
|
330
|
+
return mode !== void 0 && mode === taggedReadOnlyMode(catalog.runnerModes?.[runner]);
|
|
331
|
+
}
|
|
332
|
+
function modeFor(write, task, moved) {
|
|
333
|
+
const { writer, before, catalog } = write;
|
|
334
|
+
const prior = before ?? write.from;
|
|
335
|
+
const runner = task.assignedRunner;
|
|
336
|
+
const modes = catalog.runnerModes?.[runner];
|
|
337
|
+
const picked = (writer === "user" || writer === "userAdd") && task.taskMode !== void 0 && task.taskMode !== prior?.taskMode;
|
|
338
|
+
if (picked) {
|
|
339
|
+
if (modes && modes.length > 0 && !modes.some((m) => m.id === task.taskMode)) {
|
|
340
|
+
return { ok: false, error: `Runner "${runner}" has no task mode "${task.taskMode}": valid modes: ${modes.map((m) => m.id).join(", ")}` };
|
|
341
|
+
}
|
|
342
|
+
const unavailable = unavailableModeReason(modes, task.taskMode);
|
|
343
|
+
if (unavailable) return { ok: false, error: `Mode "${task.taskMode}" of runner "${runner}" is unavailable on this host: ${unavailable}` };
|
|
344
|
+
return { ok: true, mode: task.taskMode };
|
|
345
|
+
}
|
|
346
|
+
const becameAi = before?.type === "user";
|
|
347
|
+
if (writer === "runner" || !catalog.runnerModes || before && !moved && !becameAi && write.readOnly === void 0) return { ok: true, mode: task.taskMode };
|
|
348
|
+
const carried = prior !== void 0 && task.taskMode === prior.taskMode && readOnlyOn(prior.taskMode, prior.assignedRunner, catalog);
|
|
349
|
+
const readOnly = write.readOnly ?? (carried || readOnlyOn(task.taskMode, runner, catalog));
|
|
350
|
+
if (readOnly && !readOnlyMode(modes) && writer !== "plan" && writer !== "edit") {
|
|
351
|
+
const reason = unavailableModeReason(modes, taggedReadOnlyMode(modes));
|
|
352
|
+
const able = readOnlyRunners(catalog.runnerModes).filter((r) => r !== runner);
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
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
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const mode = plannedMode(runner, catalog.runnerModes, { readOnly, current: task.taskMode }, catalog.autonomousDefault ?? true);
|
|
359
|
+
return mode.ok ? { ok: true, mode: mode.mode ?? task.taskMode } : mode;
|
|
360
|
+
}
|
|
361
|
+
function resolveAssignment(write) {
|
|
362
|
+
const { writer, before, catalog } = write;
|
|
363
|
+
const written = write.task;
|
|
364
|
+
const prior = before ?? write.from;
|
|
365
|
+
const moved = prior !== void 0 && prior.assignedRunner !== written.assignedRunner;
|
|
366
|
+
const title = (before ?? written).title;
|
|
367
|
+
const live = writer !== "runner" && !!before && !!catalog.liveRunner?.(before.id);
|
|
368
|
+
if (live && moved) return { ok: false, field: "assignedRunner", error: liveRunnerRefusal({ title }) };
|
|
369
|
+
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: [] };
|
|
371
|
+
const mode = modeFor(write, written, moved);
|
|
372
|
+
if (!mode.ok) return { ok: false, field: "taskMode", error: mode.error };
|
|
373
|
+
const model = live ? { ok: true, assignment: written.assignedModel } : modelFor(write, moved);
|
|
374
|
+
if (!model.ok) return { ok: false, field: "assignedModel", error: model.error };
|
|
375
|
+
const task = model.assignment === written.assignedModel && mode.mode === written.taskMode ? written : { ...written, assignedModel: model.assignment, taskMode: mode.mode };
|
|
376
|
+
const adjustments = [];
|
|
377
|
+
const note = (field, from, to) => {
|
|
378
|
+
if ((from ?? null) !== (to ?? null)) adjustments.push({ taskId: written.id, title: written.title, field, from: from ?? null, to: to ?? null });
|
|
379
|
+
};
|
|
380
|
+
note("assignedModel", written.assignedModel?.modelId, task.assignedModel?.modelId);
|
|
381
|
+
note("thinkingEffort", written.assignedModel?.thinkingEffort, task.assignedModel?.thinkingEffort);
|
|
382
|
+
return { ok: true, task, adjustments };
|
|
383
|
+
}
|
|
384
|
+
function modelFor(write, moved) {
|
|
385
|
+
const { writer, before, catalog } = write;
|
|
386
|
+
const prior = before ?? write.from;
|
|
387
|
+
const written = write.task.assignedModel;
|
|
388
|
+
const runner = write.task.assignedRunner;
|
|
389
|
+
const models = catalog.modelsByRunner;
|
|
390
|
+
const own = models[runner] ?? [];
|
|
391
|
+
const listing = (id2) => own.find((m) => m.modelId === id2);
|
|
392
|
+
const carriedFrom = moved && written && prior?.assignedModel?.modelId === written.modelId ? prior.assignedRunner : void 0;
|
|
393
|
+
const seen = carriedFrom && written && !models[carriedFrom]?.some((m) => m.modelId === written.modelId) ? { ...models, [carriedFrom]: [...models[carriedFrom] ?? [], { modelId: written.modelId, modelLabel: written.modelLabel, variants: [] }] } : models;
|
|
394
|
+
const allowed = effectiveAllowlist(catalog.perRunnerAllowlist?.[runner], runner, seen);
|
|
395
|
+
const allows = (id2) => !allowed || allowed.includes(id2);
|
|
396
|
+
const settled = (assignment) => {
|
|
397
|
+
const known = listing(assignment.modelId);
|
|
398
|
+
if (!known) return assignment;
|
|
399
|
+
const thinkingEffort = clampThinkingEffort(assignment.thinkingEffort, known.variants);
|
|
400
|
+
const availableVariants = known.variants.map((v) => v.id);
|
|
401
|
+
if (thinkingEffort === assignment.thinkingEffort && assignment.availableVariants?.join() === availableVariants.join()) return assignment;
|
|
402
|
+
const next = { ...assignment, availableVariants };
|
|
403
|
+
if (thinkingEffort === void 0) delete next.thinkingEffort;
|
|
404
|
+
else next.thinkingEffort = thinkingEffort;
|
|
405
|
+
return next;
|
|
406
|
+
};
|
|
407
|
+
const refill = (effort) => {
|
|
408
|
+
const id2 = allowed?.find((a) => listing(a)) ?? allowed?.[0] ?? own[0]?.modelId;
|
|
409
|
+
if (id2 === void 0) return void 0;
|
|
410
|
+
const known = listing(id2);
|
|
411
|
+
const thinkingEffort = clampThinkingEffort(effort, known?.variants ?? []);
|
|
412
|
+
return {
|
|
413
|
+
modelId: id2,
|
|
414
|
+
modelLabel: known?.modelLabel ?? id2,
|
|
415
|
+
...thinkingEffort !== void 0 ? { thinkingEffort } : {},
|
|
416
|
+
...known ? { availableVariants: known.variants.map((v) => v.id) } : {}
|
|
417
|
+
};
|
|
418
|
+
};
|
|
419
|
+
if (!written) return { ok: true, assignment: writer === "runner" ? void 0 : refill(void 0) };
|
|
420
|
+
if (writer === "runner") return { ok: true, assignment: settled(written) };
|
|
421
|
+
const id = written.modelId;
|
|
422
|
+
const same = prior?.assignedModel?.modelId === id;
|
|
423
|
+
if (before && same && !moved) return { ok: true, assignment: settled(written) };
|
|
424
|
+
const listedElsewhere = Object.entries(models).some(([other, list]) => other !== runner && list?.some((m) => m.modelId === id));
|
|
425
|
+
if (same) {
|
|
426
|
+
const fits = !moved || !!listing(id) || own.length === 0 && !listedElsewhere;
|
|
427
|
+
return { ok: true, assignment: fits && allows(id) ? settled(written) : refill(written.thinkingEffort) };
|
|
428
|
+
}
|
|
429
|
+
if (writer === "edit" || writer === "user") {
|
|
430
|
+
const offered = own.length === 0 || !!listing(id) || !!allowed?.includes(id);
|
|
431
|
+
if (!offered) return { ok: false, error: `Runner "${runner}" does not offer model "${id}"` };
|
|
432
|
+
if (!allows(id)) return { ok: false, error: `Model "${id}" is excluded by the allowlist for runner "${runner}"` };
|
|
433
|
+
return { ok: true, assignment: settled(written) };
|
|
434
|
+
}
|
|
435
|
+
const disowned = !listing(id) && listedElsewhere;
|
|
436
|
+
return { ok: true, assignment: disowned || !allows(id) ? refill(written.thinkingEffort) : settled(written) };
|
|
437
|
+
}
|
|
438
|
+
function resolveAssignments(tasks, planned, catalog) {
|
|
439
|
+
const prior = /* @__PURE__ */ new Map();
|
|
440
|
+
const index = (list) => list.forEach((t) => {
|
|
441
|
+
prior.set(t.id, t);
|
|
442
|
+
index(t.subtasks ?? []);
|
|
443
|
+
});
|
|
444
|
+
index(planned);
|
|
445
|
+
const models = { ...catalog, runnerModes: void 0 };
|
|
446
|
+
const adjustments = [];
|
|
447
|
+
const walk = (list) => list.map((task) => {
|
|
448
|
+
const outcome = resolveAssignment({ writer: "plan", task, before: prior.get(task.id), catalog: models });
|
|
449
|
+
const resolved = outcome.ok ? outcome.task : task;
|
|
450
|
+
if (outcome.ok) adjustments.push(...outcome.adjustments);
|
|
451
|
+
return { ...resolved, subtasks: walk(task.subtasks ?? []) };
|
|
452
|
+
});
|
|
453
|
+
return { tasks: walk(tasks), adjustments };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/services/PlannedTask.ts
|
|
457
|
+
import { v4 as uuidv4 } from "uuid";
|
|
458
|
+
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
|
+
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`;
|
|
461
|
+
function isRecord(value) {
|
|
462
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
463
|
+
}
|
|
464
|
+
function text(value) {
|
|
465
|
+
return typeof value === "string" || typeof value === "number" ? String(value) : void 0;
|
|
466
|
+
}
|
|
467
|
+
function opsPlacementRefusal(type, isSubtask) {
|
|
468
|
+
if (type !== "ai") return "is a manual task: only an AI task can be an ops task";
|
|
469
|
+
if (isSubtask) return "is a subtask, which runs with its parent: only a top-level task can be an ops task";
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
function taskWhere(title, parentTitle) {
|
|
473
|
+
return parentTitle !== void 0 ? `subtask "${title}" of "${parentTitle}"` : `Task "${title}"`;
|
|
474
|
+
}
|
|
475
|
+
var skillsAsked = /* @__PURE__ */ new WeakMap();
|
|
476
|
+
function requestedSkills(task) {
|
|
477
|
+
return skillsAsked.get(task);
|
|
478
|
+
}
|
|
479
|
+
function modelOf(raw, from) {
|
|
480
|
+
const effort = text(raw.thinkingEffort);
|
|
481
|
+
if (isRecord(raw.assignedModel)) {
|
|
482
|
+
const m = raw.assignedModel;
|
|
483
|
+
return {
|
|
484
|
+
modelId: text(m.modelId) ?? "",
|
|
485
|
+
modelLabel: text(m.modelLabel) ?? "",
|
|
486
|
+
thinkingEffort: text(m.thinkingEffort) ?? effort
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
return from && effort !== void 0 ? { ...from, thinkingEffort: effort } : from;
|
|
490
|
+
}
|
|
491
|
+
function plannedTask(spec, catalog, at = {}) {
|
|
492
|
+
const raw = isRecord(spec) ? spec : {};
|
|
493
|
+
const { parent, from, freshIds, newId = uuidv4 } = at;
|
|
494
|
+
const base = from?.fields ?? {};
|
|
495
|
+
const errors = [];
|
|
496
|
+
const ignored = [];
|
|
497
|
+
const renamed = /* @__PURE__ */ new Map();
|
|
498
|
+
const adjustments = [];
|
|
499
|
+
const title = text(raw.title) ?? "";
|
|
500
|
+
const where = taskWhere(title, parent?.title);
|
|
501
|
+
const plannerId = text(raw.id);
|
|
502
|
+
const id = freshIds || !plannerId ? newId() : plannerId;
|
|
503
|
+
if (freshIds && plannerId) renamed.set(plannerId, id);
|
|
504
|
+
const type = raw.type === "user" || raw.type === "ai" ? raw.type : base.type ?? "ai";
|
|
505
|
+
const ai = type === "ai";
|
|
506
|
+
const assignedRunner = text(raw.assignedRunner) ?? base.assignedRunner ?? catalog.runners[0] ?? DEFAULT_RUNNERS[0];
|
|
507
|
+
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}` });
|
|
511
|
+
const ops = raw.ops !== void 0 ? raw.ops === true : base.ops;
|
|
512
|
+
if (ai && ops && raw.readOnly === true) ignored.push({ taskId: id, field: "readOnly", message: `${where}: ${OPS_NOT_READ_ONLY}` });
|
|
513
|
+
const asked = typeof raw.readOnly === "boolean" ? raw.readOnly : void 0;
|
|
514
|
+
const readOnly = ai && !ops && (asked ?? from?.readOnly ?? parent?.readOnly ?? false);
|
|
515
|
+
const assignedModel = ai ? modelOf(raw, base.assignedModel) : void 0;
|
|
516
|
+
const slice = (value) => value === "AFK" || value === "HITL" ? value : void 0;
|
|
517
|
+
const autonomy = ai ? slice(raw.autonomy) ?? base.autonomy ?? parent?.autonomy : void 0;
|
|
518
|
+
const sliceType = slice(raw.sliceType) ?? base.sliceType ?? (ai ? parent?.sliceType : void 0);
|
|
519
|
+
const subtasks = [];
|
|
520
|
+
if (Array.isArray(raw.subtasks)) {
|
|
521
|
+
for (const s of raw.subtasks) {
|
|
522
|
+
const child = plannedTask(s, catalog, { parent: { title, sliceType, autonomy, readOnly }, freshIds, newId });
|
|
523
|
+
subtasks.push(child.task);
|
|
524
|
+
errors.push(...child.errors);
|
|
525
|
+
ignored.push(...child.ignored);
|
|
526
|
+
adjustments.push(...child.adjustments);
|
|
527
|
+
for (const [was, now] of child.renamed) renamed.set(was, now);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const fields = {
|
|
531
|
+
id,
|
|
532
|
+
order: Number(raw.order ?? 0),
|
|
533
|
+
title,
|
|
534
|
+
description,
|
|
535
|
+
type,
|
|
536
|
+
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),
|
|
538
|
+
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
|
+
subtasks,
|
|
540
|
+
assignedModel,
|
|
541
|
+
assignedRunner,
|
|
542
|
+
autonomy,
|
|
543
|
+
sliceType,
|
|
544
|
+
userStoriesCovered: Array.isArray(raw.userStoriesCovered) ? raw.userStoriesCovered.map(String) : base.userStoriesCovered,
|
|
545
|
+
ops,
|
|
546
|
+
skills: from ? inheritedSkills(raw.skills, base.skills) : skillNames(raw.skills)
|
|
547
|
+
};
|
|
548
|
+
let taskMode = base.taskMode;
|
|
549
|
+
let landedModel = assignedModel;
|
|
550
|
+
if (ai && catalog.runners.includes(assignedRunner)) {
|
|
551
|
+
const resolved = resolveAssignment({
|
|
552
|
+
writer: catalog.models ? "edit" : "plan",
|
|
553
|
+
task: { ...createTask(fields), taskMode },
|
|
554
|
+
from: from ? { assignedRunner: base.assignedRunner ?? assignedRunner, assignedModel: base.assignedModel, taskMode: base.taskMode } : void 0,
|
|
555
|
+
readOnly,
|
|
556
|
+
catalog: { modelsByRunner: {}, ...catalog.models, runnerModes: catalog.runnerModes, autonomousDefault: catalog.autonomousDefault }
|
|
557
|
+
});
|
|
558
|
+
if (!resolved.ok) {
|
|
559
|
+
errors.push({ taskId: id, field: resolved.field === "taskMode" ? "readOnly" : resolved.field, message: `${where}: ${resolved.error}` });
|
|
560
|
+
} else {
|
|
561
|
+
taskMode = resolved.task.taskMode;
|
|
562
|
+
landedModel = resolved.task.assignedModel;
|
|
563
|
+
adjustments.push(...resolved.adjustments);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (taskMode === void 0 && readOnly && !catalog.runnerModes) taskMode = "plan";
|
|
567
|
+
const task = createTask({ ...fields, assignedModel: landedModel, taskMode });
|
|
568
|
+
if (Array.isArray(raw.skills)) skillsAsked.set(task, raw.skills);
|
|
569
|
+
return { task, errors, ignored, renamed, adjustments };
|
|
570
|
+
}
|
|
571
|
+
function checkPlanInvariants(tasks, runners, runnerModes) {
|
|
572
|
+
const rows = flattenTasksWithParents(tasks);
|
|
573
|
+
const errors = [];
|
|
574
|
+
const seen = /* @__PURE__ */ new Set();
|
|
575
|
+
for (const { task: t, parent } of rows) {
|
|
576
|
+
const where = taskWhere(t.title, parent?.title);
|
|
577
|
+
const fail = (field, message) => errors.push({ taskId: t.id, field, message });
|
|
578
|
+
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
|
+
seen.add(t.id);
|
|
580
|
+
if (t.autonomy === "AFK" && t.userSteps && t.userSteps.length > 0) {
|
|
581
|
+
fail("userSteps", `${where} has autonomy "AFK" but contains userSteps. AFK tasks must not have user touchpoints.`);
|
|
582
|
+
}
|
|
583
|
+
if (t.type === "user" && t.sliceType && t.sliceType !== "HITL") {
|
|
584
|
+
fail("sliceType", `User ${where} has sliceType "${t.sliceType}". User tasks must have sliceType "HITL".`);
|
|
585
|
+
}
|
|
586
|
+
const opsRefused = t.ops ? opsPlacementRefusal(t.type, parent !== null) : null;
|
|
587
|
+
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`);
|
|
590
|
+
}
|
|
591
|
+
if (runners && t.type !== "user" && !runners.includes(t.assignedRunner)) {
|
|
592
|
+
fail("assignedRunner", `${where} has invalid assignedRunner "${t.assignedRunner}". Expected one of: ${runners.join(", ")}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
for (const { task: t, parent } of rows) {
|
|
596
|
+
for (const error of dependencyTargetErrors(tasks, t.id, t.dependencies.filter((id) => id !== t.id))) {
|
|
597
|
+
errors.push({ taskId: t.id, field: "dependencies", message: `${taskWhere(t.title, parent?.title)}: ${error}` });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const cycle = findCycle(rows.map((r) => r.task));
|
|
601
|
+
if (cycle) errors.push({ taskId: cycle[0].id, field: "dependencies", message: `Dependency cycle detected: ${[...cycle, cycle[0]].map((t) => `"${t.title}"`).join(" \u2192 ")}` });
|
|
602
|
+
return errors;
|
|
603
|
+
}
|
|
604
|
+
function findCycle(tasks) {
|
|
605
|
+
return findDependencyCycle(tasks);
|
|
606
|
+
}
|
|
607
|
+
function detectCycle(tasks) {
|
|
608
|
+
return findCycle(tasks)?.[0].title ?? null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export {
|
|
612
|
+
PLAN_ENVELOPE_KEY,
|
|
613
|
+
TASK_OPS_ENVELOPE_KEY,
|
|
614
|
+
TASK_QUERY_ENVELOPE_KEY,
|
|
615
|
+
PlanParseError,
|
|
616
|
+
stripModelNoise,
|
|
617
|
+
opensWithJsonObject,
|
|
618
|
+
extractObjectsWithKey,
|
|
619
|
+
extractJsonObject,
|
|
620
|
+
stripTrailingCommas,
|
|
621
|
+
escapeControlCharsInStrings,
|
|
622
|
+
runnerModesFrom,
|
|
623
|
+
autonomyLevelLabel,
|
|
624
|
+
parseAutonomyLevel,
|
|
625
|
+
taggedReadOnlyMode,
|
|
626
|
+
readOnlyMode,
|
|
627
|
+
unavailableTaskModeMessage,
|
|
628
|
+
carryPlannedModes,
|
|
629
|
+
readOnlyRunners,
|
|
630
|
+
effectiveAllowlist,
|
|
631
|
+
filterModelsForPrompt,
|
|
632
|
+
clampThinkingEffort,
|
|
633
|
+
liveRunnerRefusal,
|
|
634
|
+
assignableRunners,
|
|
635
|
+
resolveAssignment,
|
|
636
|
+
resolveAssignments,
|
|
637
|
+
MODE_NOT_THE_PLANNERS,
|
|
638
|
+
MANUAL_NOT_READ_ONLY,
|
|
639
|
+
OPS_NOT_READ_ONLY,
|
|
640
|
+
opsPlacementRefusal,
|
|
641
|
+
taskWhere,
|
|
642
|
+
requestedSkills,
|
|
643
|
+
plannedTask,
|
|
644
|
+
checkPlanInvariants,
|
|
645
|
+
detectCycle
|
|
646
|
+
};
|
|
647
|
+
//# sourceMappingURL=chunk-H7QX32BH.mjs.map
|