@ordewell/core 0.10.5 → 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-Crkt3tHD.d.ts → IFileSystem-C4_mpfze.d.ts} +97 -14
- package/dist/{IRunner-lMQIa5Wc.d.mts → IFileSystem-DUgeWWQN.d.mts} +97 -14
- package/dist/{ModeResolver-B4hhABNf.d.mts → ModeResolver-4qNjxcDs.d.mts} +1 -1
- package/dist/{ModeResolver-CUR5jCSU.d.ts → ModeResolver-DGK2rWHE.d.ts} +1 -1
- package/dist/{Task-D4QHjdGv.d.mts → Task-C0o4i3Nn.d.mts} +23 -4
- package/dist/{Task-D4QHjdGv.d.ts → Task-C0o4i3Nn.d.ts} +23 -4
- package/dist/{chunk-6LEA6WAM.mjs → chunk-DMG4ISUG.mjs} +923 -45
- package/dist/chunk-DMG4ISUG.mjs.map +1 -0
- package/dist/{chunk-RT4NQ2BZ.mjs → chunk-EEE2GBLG.mjs} +114 -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-F3KIX7WR.mjs → chunk-LTHKFBKM.mjs} +4 -4
- package/dist/chunk-LTHKFBKM.mjs.map +1 -0
- package/dist/index.d.mts +278 -1701
- package/dist/index.d.ts +278 -1701
- package/dist/index.js +2729 -1903
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2171 -1523
- 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 +255 -97
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +33 -19
- package/dist/{plan-utils-wI8MuDBC.d.mts → runSettingsText-BgOmLxdw.d.mts} +1941 -108
- package/dist/{plan-utils-BVJCzNvm.d.ts → runSettingsText-BgXvajj2.d.ts} +1941 -108
- package/dist/testing.d.mts +2 -3
- package/dist/testing.d.ts +2 -3
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +1 -1
- package/dist/IFileSystem-ClBdp59f.d.ts +0 -78
- package/dist/IFileSystem-Db34maxC.d.mts +0 -78
- package/dist/chunk-4QLFM73A.mjs +0 -1407
- package/dist/chunk-4QLFM73A.mjs.map +0 -1
- package/dist/chunk-6LEA6WAM.mjs.map +0 -1
- package/dist/chunk-F3KIX7WR.mjs.map +0 -1
- package/dist/chunk-RT4NQ2BZ.mjs.map +0 -1
package/dist/chunk-4QLFM73A.mjs
DELETED
|
@@ -1,1407 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_RUNNERS,
|
|
3
|
-
addTaskToPlan,
|
|
4
|
-
createTask,
|
|
5
|
-
flattenTasks,
|
|
6
|
-
flattenTasksWithParents,
|
|
7
|
-
inheritedOps,
|
|
8
|
-
inheritedSkills,
|
|
9
|
-
opsFlag,
|
|
10
|
-
removeTaskFromPlan,
|
|
11
|
-
renumberTasks,
|
|
12
|
-
resolveTaskRef,
|
|
13
|
-
skillNames,
|
|
14
|
-
updateTaskInPlan
|
|
15
|
-
} from "./chunk-RT4NQ2BZ.mjs";
|
|
16
|
-
|
|
17
|
-
// src/services/JsonExtractor.ts
|
|
18
|
-
var PLAN_ENVELOPE_KEY = "tasks";
|
|
19
|
-
var TASK_OPS_ENVELOPE_KEY = "taskOps";
|
|
20
|
-
var TASK_QUERY_ENVELOPE_KEY = "taskQuery";
|
|
21
|
-
var PlanParseError = class extends Error {
|
|
22
|
-
raw;
|
|
23
|
-
/**
|
|
24
|
-
* True when the reply was cut off mid-object (output-token limit) rather
|
|
25
|
-
* than malformed — the two need different repairs: a truncated emission will
|
|
26
|
-
* be cut at the same point again unless the retry frees context or asks for
|
|
27
|
-
* terser output.
|
|
28
|
-
*/
|
|
29
|
-
truncated;
|
|
30
|
-
/**
|
|
31
|
-
* True when the JSON parsed fine and a *shape* rule rejected it. The generic
|
|
32
|
-
* "could not be parsed as JSON" corrective is a lie for these, so the model
|
|
33
|
-
* re-sends the same object; a semantic rejection must be repaired by naming
|
|
34
|
-
* the rule it broke.
|
|
35
|
-
*/
|
|
36
|
-
semantic;
|
|
37
|
-
constructor(message, raw, opts) {
|
|
38
|
-
super(message);
|
|
39
|
-
this.name = "PlanParseError";
|
|
40
|
-
this.raw = raw;
|
|
41
|
-
this.truncated = opts?.truncated ?? false;
|
|
42
|
-
this.semantic = opts?.semantic ?? false;
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
function stripModelNoise(raw) {
|
|
46
|
-
return raw.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/```json\s*/gi, "").replace(/```\s*/g, "").trim();
|
|
47
|
-
}
|
|
48
|
-
var THINK_OPEN = "<think>";
|
|
49
|
-
var THINK_CLOSE = "</think>";
|
|
50
|
-
var FENCE = "```";
|
|
51
|
-
var FENCE_JSON_TAG = "json";
|
|
52
|
-
function opensWithJsonObject(prefix) {
|
|
53
|
-
let rest = prefix;
|
|
54
|
-
for (; ; ) {
|
|
55
|
-
rest = rest.trimStart();
|
|
56
|
-
if (!rest) return void 0;
|
|
57
|
-
const head = rest.slice(0, THINK_OPEN.length).toLowerCase();
|
|
58
|
-
if (head === THINK_OPEN) {
|
|
59
|
-
const close = rest.toLowerCase().indexOf(THINK_CLOSE);
|
|
60
|
-
if (close < 0) return void 0;
|
|
61
|
-
rest = rest.slice(close + THINK_CLOSE.length);
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
if (rest.startsWith(FENCE)) {
|
|
65
|
-
rest = rest.slice(FENCE.length);
|
|
66
|
-
const tag = rest.slice(0, FENCE_JSON_TAG.length).toLowerCase();
|
|
67
|
-
if (tag === FENCE_JSON_TAG) rest = rest.slice(FENCE_JSON_TAG.length);
|
|
68
|
-
else if (tag === rest.toLowerCase() && FENCE_JSON_TAG.startsWith(tag)) return void 0;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (THINK_OPEN.startsWith(head) || FENCE.startsWith(rest)) return void 0;
|
|
72
|
-
return rest.startsWith("{");
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function balancedObjectAt(text2, start) {
|
|
76
|
-
let depth = 0;
|
|
77
|
-
let inString = false;
|
|
78
|
-
let escaped = false;
|
|
79
|
-
for (let i = start; i < text2.length; i++) {
|
|
80
|
-
const ch = text2[i];
|
|
81
|
-
if (inString) {
|
|
82
|
-
if (escaped) escaped = false;
|
|
83
|
-
else if (ch === "\\") escaped = true;
|
|
84
|
-
else if (ch === '"') inString = false;
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if (ch === '"') inString = true;
|
|
88
|
-
else if (ch === "{") depth++;
|
|
89
|
-
else if (ch === "}") {
|
|
90
|
-
depth--;
|
|
91
|
-
if (depth === 0) return { json: text2.slice(start, i + 1), balanced: true };
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return { json: text2.slice(start), balanced: false };
|
|
95
|
-
}
|
|
96
|
-
function extractObjectsWithKey(raw, key) {
|
|
97
|
-
const stripped = stripModelNoise(raw);
|
|
98
|
-
const keyRe = new RegExp(`"${key}"\\s*:`);
|
|
99
|
-
const matches = [];
|
|
100
|
-
let firstObject = null;
|
|
101
|
-
let sawBrace = false;
|
|
102
|
-
let sawUnbalanced = false;
|
|
103
|
-
let inString = false;
|
|
104
|
-
let escaped = false;
|
|
105
|
-
for (let i = 0; i < stripped.length; i++) {
|
|
106
|
-
const ch = stripped[i];
|
|
107
|
-
if (inString) {
|
|
108
|
-
if (escaped) escaped = false;
|
|
109
|
-
else if (ch === "\\") escaped = true;
|
|
110
|
-
else if (ch === '"') inString = false;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (ch === '"') {
|
|
114
|
-
inString = true;
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
if (ch !== "{") continue;
|
|
118
|
-
sawBrace = true;
|
|
119
|
-
const obj = balancedObjectAt(stripped, i);
|
|
120
|
-
if (obj.balanced && keyRe.test(obj.json)) {
|
|
121
|
-
matches.push(obj.json);
|
|
122
|
-
i += obj.json.length - 1;
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
if (!obj.balanced) sawUnbalanced = true;
|
|
126
|
-
if (!firstObject && obj.balanced) firstObject = obj.json;
|
|
127
|
-
}
|
|
128
|
-
return {
|
|
129
|
-
matches,
|
|
130
|
-
fallback: firstObject !== null ? { json: firstObject, balanced: true } : { json: stripped, balanced: false },
|
|
131
|
-
sawBrace,
|
|
132
|
-
sawUnbalanced
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
function extractObjectWithBalance(raw) {
|
|
136
|
-
const { matches, fallback, sawBrace } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);
|
|
137
|
-
if (matches.length > 0) return { json: matches[0], balanced: true, sawBrace };
|
|
138
|
-
return { ...fallback, sawBrace };
|
|
139
|
-
}
|
|
140
|
-
function extractJsonObject(raw) {
|
|
141
|
-
return extractObjectWithBalance(raw).json;
|
|
142
|
-
}
|
|
143
|
-
function stripTrailingCommas(json) {
|
|
144
|
-
return json.replace(/,(\s*[}\]])/g, "$1");
|
|
145
|
-
}
|
|
146
|
-
function escapeControlCharsInStrings(json) {
|
|
147
|
-
let out = "";
|
|
148
|
-
let inString = false;
|
|
149
|
-
let escaped = false;
|
|
150
|
-
for (const ch of json) {
|
|
151
|
-
if (inString) {
|
|
152
|
-
if (escaped) {
|
|
153
|
-
out += ch;
|
|
154
|
-
escaped = false;
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
if (ch === "\\") {
|
|
158
|
-
out += ch;
|
|
159
|
-
escaped = true;
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
if (ch === '"') {
|
|
163
|
-
out += ch;
|
|
164
|
-
inString = false;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (ch === "\n") {
|
|
168
|
-
out += "\\n";
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
if (ch === "\r") {
|
|
172
|
-
out += "\\r";
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (ch === " ") {
|
|
176
|
-
out += "\\t";
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
out += ch;
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
if (ch === '"') inString = true;
|
|
183
|
-
out += ch;
|
|
184
|
-
}
|
|
185
|
-
return out;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// src/services/ModeResolver.ts
|
|
189
|
-
function runnerModesFrom(registry, runners) {
|
|
190
|
-
return Object.fromEntries(runners.map((r) => [r, registry.getManifest(r)?.modes ?? []]));
|
|
191
|
-
}
|
|
192
|
-
function resolveDefaultMode(modes, autonomousDefault = true) {
|
|
193
|
-
if (!modes || modes.length === 0) return void 0;
|
|
194
|
-
const tag = autonomousDefault ? "autonomous" : "safe";
|
|
195
|
-
const tagged = modes.find((m) => m[tag]);
|
|
196
|
-
if (tagged) return tagged.id;
|
|
197
|
-
for (let i = modes.length - 1; i >= 0; i--) {
|
|
198
|
-
if (!modes[i].readOnly && modes[i].id !== "plan") return modes[i].id;
|
|
199
|
-
}
|
|
200
|
-
return void 0;
|
|
201
|
-
}
|
|
202
|
-
function autonomyLevelLabel(autonomous) {
|
|
203
|
-
return autonomous ? "Full" : "Guarded";
|
|
204
|
-
}
|
|
205
|
-
function parseAutonomyLevel(arg) {
|
|
206
|
-
switch (arg?.trim().toLowerCase()) {
|
|
207
|
-
case "full":
|
|
208
|
-
case "on":
|
|
209
|
-
return true;
|
|
210
|
-
case "guarded":
|
|
211
|
-
case "auto":
|
|
212
|
-
case "off":
|
|
213
|
-
return false;
|
|
214
|
-
default:
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
function taggedReadOnlyMode(modes) {
|
|
219
|
-
return modes?.find((m) => m.readOnly)?.id;
|
|
220
|
-
}
|
|
221
|
-
function readOnlyMode(modes) {
|
|
222
|
-
const mode = modes?.find((m) => m.readOnly);
|
|
223
|
-
return mode && !mode.unavailable ? mode.id : void 0;
|
|
224
|
-
}
|
|
225
|
-
function unavailableModeReason(modes, id) {
|
|
226
|
-
return id === void 0 ? void 0 : modes?.find((m) => m.id === id)?.unavailable;
|
|
227
|
-
}
|
|
228
|
-
function unavailableModeError(runner, id, reason) {
|
|
229
|
-
return `Mode "${id}" of runner "${runner}" is unavailable on this host: ${reason}`;
|
|
230
|
-
}
|
|
231
|
-
function unavailableTaskModeMessage(manifest, mode) {
|
|
232
|
-
const reason = unavailableModeReason(manifest.modes, mode);
|
|
233
|
-
return reason ? `Mode "${mode}" of ${manifest.displayName} is unavailable on this host: ${reason}` : void 0;
|
|
234
|
-
}
|
|
235
|
-
function plannedMode(runner, runnerModes, { readOnly, current }, autonomousDefault = true) {
|
|
236
|
-
if (!runnerModes) return { ok: true, mode: void 0 };
|
|
237
|
-
const modes = runnerModes[runner] ?? [];
|
|
238
|
-
const readOnlyId = readOnlyMode(modes);
|
|
239
|
-
if (readOnly === true) {
|
|
240
|
-
if (readOnlyId) return { ok: true, mode: readOnlyId };
|
|
241
|
-
const able = readOnlyRunners(runnerModes);
|
|
242
|
-
const reason2 = unavailableModeReason(modes, taggedReadOnlyMode(modes));
|
|
243
|
-
return {
|
|
244
|
-
ok: false,
|
|
245
|
-
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}` : ""}`
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
const keep = current !== void 0 && modes.some((m) => m.id === current) && !(readOnly === false && current === taggedReadOnlyMode(modes));
|
|
249
|
-
const mode = keep ? current : resolveDefaultMode(modes, autonomousDefault);
|
|
250
|
-
const reason = unavailableModeReason(modes, mode);
|
|
251
|
-
if (mode !== void 0 && reason) return { ok: false, error: unavailableModeError(runner, mode, reason) };
|
|
252
|
-
return { ok: true, mode };
|
|
253
|
-
}
|
|
254
|
-
function carryPlannedModes(before, rewrite, catalog) {
|
|
255
|
-
const prior = new Map(flattenTasks(before).map((t) => [t.id, t]));
|
|
256
|
-
const carry = (t) => {
|
|
257
|
-
const subtasks = (t.subtasks ?? []).map(carry);
|
|
258
|
-
const was = prior.get(t.id);
|
|
259
|
-
if (t.type === "user" || !was || was.type === "user" || t.taskMode === taggedReadOnlyMode(catalog.runnerModes[t.assignedRunner])) return { ...t, subtasks };
|
|
260
|
-
const mode = plannedMode(t.assignedRunner, catalog.runnerModes, { readOnly: false, current: was.taskMode }, catalog.autonomousDefault ?? true);
|
|
261
|
-
return { ...t, subtasks, ...mode.ok && mode.mode !== void 0 ? { taskMode: mode.mode } : {} };
|
|
262
|
-
};
|
|
263
|
-
return rewrite.map(carry);
|
|
264
|
-
}
|
|
265
|
-
function readOnlyRunners(runnerModes) {
|
|
266
|
-
return Object.entries(runnerModes).filter(([, ms]) => readOnlyMode(ms)).map(([id]) => id);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// src/taskRow/status.ts
|
|
270
|
-
var isTaskRunning = (task) => task.status === "in_progress" || task.status === "running";
|
|
271
|
-
var NOT_STARTED = /* @__PURE__ */ new Set(["pending", "approved", "blocked"]);
|
|
272
|
-
function taskStatusKind(task) {
|
|
273
|
-
switch (task.status) {
|
|
274
|
-
case "completed":
|
|
275
|
-
return "done";
|
|
276
|
-
case "failed":
|
|
277
|
-
return "failed";
|
|
278
|
-
case "skipped":
|
|
279
|
-
return "skipped";
|
|
280
|
-
case "blocked":
|
|
281
|
-
return "blocked";
|
|
282
|
-
case "awaiting_user":
|
|
283
|
-
return "awaiting";
|
|
284
|
-
}
|
|
285
|
-
if (!isTaskRunning(task)) return "todo";
|
|
286
|
-
return task.status === "in_progress" && task.idleSince ? "quiet" : "running";
|
|
287
|
-
}
|
|
288
|
-
var AWAITING_LABELS = {
|
|
289
|
-
input: "Waiting for your input",
|
|
290
|
-
checkpoint: "Checkpoint",
|
|
291
|
-
conflict: "Merge conflict",
|
|
292
|
-
"files-changed": "Changed tracked files",
|
|
293
|
-
"usage-limit": "Paused on a usage limit"
|
|
294
|
-
};
|
|
295
|
-
function awaitingLabel(task) {
|
|
296
|
-
return task.status === "awaiting_user" && task.awaitingReason ? AWAITING_LABELS[task.awaitingReason] : null;
|
|
297
|
-
}
|
|
298
|
-
function blockedByLabel(refs) {
|
|
299
|
-
return `Blocked by ${refs.join(", ")}`;
|
|
300
|
-
}
|
|
301
|
-
function skippedLabel(reason) {
|
|
302
|
-
return reason ? `Skipped: ${reason}` : "Skipped";
|
|
303
|
-
}
|
|
304
|
-
var WAITS_FOR_APPROVAL_LABEL = "Waits for your approval";
|
|
305
|
-
function approvalLabel(count) {
|
|
306
|
-
if (!count) return null;
|
|
307
|
-
return count > 1 ? `Waiting for approval (${count})` : "Waiting for approval";
|
|
308
|
-
}
|
|
309
|
-
function resumeTime(at, now = /* @__PURE__ */ new Date()) {
|
|
310
|
-
const clock = at.toLocaleTimeString("en-US", { hour: "numeric", minute: at.getMinutes() === 0 ? void 0 : "2-digit", hour12: true }).replace(/[ \u202f]([AP]M)$/i, (_m, meridiem) => meridiem.toLowerCase());
|
|
311
|
-
if (at.toDateString() === now.toDateString()) return clock;
|
|
312
|
-
return `${at.toLocaleDateString("en-US", { month: "short", day: "numeric" })}, ${clock}`;
|
|
313
|
-
}
|
|
314
|
-
function resumeLabel(task, now = /* @__PURE__ */ new Date()) {
|
|
315
|
-
if (task.status !== "awaiting_user" || task.awaitingReason !== "usage-limit" || !task.resumeAt) return null;
|
|
316
|
-
const at = new Date(task.resumeAt);
|
|
317
|
-
return Number.isNaN(at.getTime()) ? null : `Resumes at ${resumeTime(at, now)}`;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// src/services/TaskOps.ts
|
|
321
|
-
import { v4 as uuidv42 } from "uuid";
|
|
322
|
-
|
|
323
|
-
// src/services/ModelAllowlistResolver.ts
|
|
324
|
-
function effectiveAllowlist(allowlist, runner, modelsByRunner) {
|
|
325
|
-
if (!allowlist || allowlist.length === 0) return void 0;
|
|
326
|
-
if (!modelsByRunner) return allowlist;
|
|
327
|
-
const own = new Set((modelsByRunner[runner] ?? []).map((m) => m.modelId));
|
|
328
|
-
const elsewhere = /* @__PURE__ */ new Set();
|
|
329
|
-
for (const [other, models] of Object.entries(modelsByRunner)) {
|
|
330
|
-
if (other === runner) continue;
|
|
331
|
-
for (const m of models ?? []) elsewhere.add(m.modelId);
|
|
332
|
-
}
|
|
333
|
-
const kept = allowlist.filter((id) => own.has(id) || !elsewhere.has(id));
|
|
334
|
-
return kept.length > 0 ? kept : void 0;
|
|
335
|
-
}
|
|
336
|
-
function filterModelsForPrompt(modelsByRunner, perRunnerAllowlist) {
|
|
337
|
-
const result = {};
|
|
338
|
-
for (const [runner, models] of Object.entries(modelsByRunner)) {
|
|
339
|
-
if (!models) continue;
|
|
340
|
-
const allowed = effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
341
|
-
if (!allowed) {
|
|
342
|
-
result[runner] = models;
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
const allowedSet = new Set(allowed);
|
|
346
|
-
const intersection = models.filter((m) => allowedSet.has(m.modelId));
|
|
347
|
-
const covered = new Set(intersection.map((m) => m.modelId));
|
|
348
|
-
const missing = allowed.filter((id) => !covered.has(id)).map((id) => ({ modelId: id, modelLabel: id.split("/").pop() || id, variants: [] }));
|
|
349
|
-
result[runner] = [...intersection, ...missing];
|
|
350
|
-
}
|
|
351
|
-
return result;
|
|
352
|
-
}
|
|
353
|
-
var EFFORT_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
354
|
-
var EFFORT_ALIASES = { disabled: "none", adaptive: "medium" };
|
|
355
|
-
function clampThinkingEffort(effort, variants) {
|
|
356
|
-
if (!effort) return void 0;
|
|
357
|
-
if (variants.length === 0) return void 0;
|
|
358
|
-
const ids = new Set(variants.map((v) => v.id));
|
|
359
|
-
if (ids.has(effort)) return effort;
|
|
360
|
-
const canon = EFFORT_ALIASES[effort] ?? effort;
|
|
361
|
-
if (ids.has(canon)) return canon;
|
|
362
|
-
const idx = EFFORT_LADDER.indexOf(canon);
|
|
363
|
-
if (idx < 0) return void 0;
|
|
364
|
-
for (let d = 1; d < EFFORT_LADDER.length; d++) {
|
|
365
|
-
const lower = EFFORT_LADDER[idx - d];
|
|
366
|
-
if (lower && ids.has(lower)) return lower;
|
|
367
|
-
const higher = EFFORT_LADDER[idx + d];
|
|
368
|
-
if (higher && ids.has(higher)) return higher;
|
|
369
|
-
}
|
|
370
|
-
return void 0;
|
|
371
|
-
}
|
|
372
|
-
function coerceAssignments(tasks, perRunnerAllowlist, modelsByRunner, planned = []) {
|
|
373
|
-
const prior = new Map(flattenTasks(planned).map((t) => [t.id, t]));
|
|
374
|
-
const unwritten = (task) => {
|
|
375
|
-
const was = prior.get(task.id);
|
|
376
|
-
if (!was?.assignedModel || !task.assignedModel || was.assignedRunner !== task.assignedRunner) return void 0;
|
|
377
|
-
if (was.assignedModel.modelId !== task.assignedModel.modelId || was.assignedModel.thinkingEffort !== task.assignedModel.thinkingEffort) return void 0;
|
|
378
|
-
return was;
|
|
379
|
-
};
|
|
380
|
-
const clampVariant = (task) => {
|
|
381
|
-
if (!task.assignedModel || !modelsByRunner) return task;
|
|
382
|
-
const model = modelsByRunner[task.assignedRunner]?.find(
|
|
383
|
-
(m) => m.modelId === task.assignedModel.modelId
|
|
384
|
-
);
|
|
385
|
-
if (!model) return task;
|
|
386
|
-
const clamped = clampThinkingEffort(task.assignedModel.thinkingEffort, model.variants);
|
|
387
|
-
return {
|
|
388
|
-
...task,
|
|
389
|
-
assignedModel: {
|
|
390
|
-
...task.assignedModel,
|
|
391
|
-
thinkingEffort: clamped,
|
|
392
|
-
availableVariants: model.variants.map((v) => v.id)
|
|
393
|
-
}
|
|
394
|
-
};
|
|
395
|
-
};
|
|
396
|
-
const allowedFor = (runner) => effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
397
|
-
const assignmentFor = (runner, modelId) => {
|
|
398
|
-
const known = modelsByRunner?.[runner]?.find((m) => m.modelId === modelId);
|
|
399
|
-
return {
|
|
400
|
-
modelId,
|
|
401
|
-
modelLabel: known?.modelLabel ?? modelId,
|
|
402
|
-
thinkingEffort: void 0,
|
|
403
|
-
...known ? { availableVariants: known.variants.map((v) => v.id) } : {}
|
|
404
|
-
};
|
|
405
|
-
};
|
|
406
|
-
const coerceOne = (task) => {
|
|
407
|
-
if (task.type === "user") return task;
|
|
408
|
-
const kept = unwritten(task);
|
|
409
|
-
if (kept) {
|
|
410
|
-
const left = { ...task, assignedModel: kept.assignedModel };
|
|
411
|
-
return kept.status === "in_progress" || kept.status === "awaiting_user" ? left : clampVariant(left);
|
|
412
|
-
}
|
|
413
|
-
const allowlist = allowedFor(task.assignedRunner);
|
|
414
|
-
if (!task.assignedModel) {
|
|
415
|
-
if (allowlist && allowlist.length > 0) {
|
|
416
|
-
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) });
|
|
417
|
-
}
|
|
418
|
-
const discovered = modelsByRunner?.[task.assignedRunner]?.[0];
|
|
419
|
-
if (discovered) {
|
|
420
|
-
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, discovered.modelId) });
|
|
421
|
-
}
|
|
422
|
-
return task;
|
|
423
|
-
}
|
|
424
|
-
if (!allowlist || allowlist.length === 0) return clampVariant(task);
|
|
425
|
-
if (allowlist.includes(task.assignedModel.modelId)) return clampVariant(task);
|
|
426
|
-
return { ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) };
|
|
427
|
-
};
|
|
428
|
-
const coerced = [];
|
|
429
|
-
const walk = (list) => list.map((task) => {
|
|
430
|
-
const after = coerceOne(task);
|
|
431
|
-
const [fromModel, toModel] = [task.assignedModel?.modelId ?? null, after.assignedModel?.modelId ?? null];
|
|
432
|
-
if (fromModel !== toModel) coerced.push({ taskId: task.id, title: task.title, field: "assignedModel", from: fromModel, to: toModel });
|
|
433
|
-
const [fromEffort, toEffort] = [task.assignedModel?.thinkingEffort ?? null, after.assignedModel?.thinkingEffort ?? null];
|
|
434
|
-
if (fromEffort !== toEffort) coerced.push({ taskId: task.id, title: task.title, field: "thinkingEffort", from: fromEffort, to: toEffort });
|
|
435
|
-
return { ...after, subtasks: walk(task.subtasks ?? []) };
|
|
436
|
-
});
|
|
437
|
-
return { tasks: walk(tasks), coerced };
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// src/services/PlannedTask.ts
|
|
441
|
-
import { v4 as uuidv4 } from "uuid";
|
|
442
|
-
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`;
|
|
443
|
-
var MANUAL_NOT_READ_ONLY = '"readOnly" is for AI tasks: a manual task runs in no mode';
|
|
444
|
-
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`;
|
|
445
|
-
function isRecord(value) {
|
|
446
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
447
|
-
}
|
|
448
|
-
function text(value) {
|
|
449
|
-
return typeof value === "string" || typeof value === "number" ? String(value) : void 0;
|
|
450
|
-
}
|
|
451
|
-
function opsPlacementRefusal(type, isSubtask) {
|
|
452
|
-
if (type !== "ai") return "is a manual task: only an AI task can be an ops task";
|
|
453
|
-
if (isSubtask) return "is a subtask, which runs with its parent: only a top-level task can be an ops task";
|
|
454
|
-
return null;
|
|
455
|
-
}
|
|
456
|
-
function taskWhere(title, parentTitle) {
|
|
457
|
-
return parentTitle !== void 0 ? `subtask "${title}" of "${parentTitle}"` : `Task "${title}"`;
|
|
458
|
-
}
|
|
459
|
-
var skillsAsked = /* @__PURE__ */ new WeakMap();
|
|
460
|
-
function requestedSkills(task) {
|
|
461
|
-
return skillsAsked.get(task);
|
|
462
|
-
}
|
|
463
|
-
function modelOf(raw, from) {
|
|
464
|
-
const effort = text(raw.thinkingEffort);
|
|
465
|
-
if (isRecord(raw.assignedModel)) {
|
|
466
|
-
const m = raw.assignedModel;
|
|
467
|
-
return {
|
|
468
|
-
modelId: text(m.modelId) ?? "",
|
|
469
|
-
modelLabel: text(m.modelLabel) ?? "",
|
|
470
|
-
thinkingEffort: text(m.thinkingEffort) ?? effort
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
return from && effort !== void 0 ? { ...from, thinkingEffort: effort } : from;
|
|
474
|
-
}
|
|
475
|
-
function plannedTask(spec, catalog, at = {}) {
|
|
476
|
-
const raw = isRecord(spec) ? spec : {};
|
|
477
|
-
const { parent, from, freshIds, newId = uuidv4 } = at;
|
|
478
|
-
const base = from?.fields ?? {};
|
|
479
|
-
const errors = [];
|
|
480
|
-
const ignored = [];
|
|
481
|
-
const renamed = /* @__PURE__ */ new Map();
|
|
482
|
-
const title = text(raw.title) ?? "";
|
|
483
|
-
const where = taskWhere(title, parent?.title);
|
|
484
|
-
const plannerId = text(raw.id);
|
|
485
|
-
const id = freshIds || !plannerId ? newId() : plannerId;
|
|
486
|
-
if (freshIds && plannerId) renamed.set(plannerId, id);
|
|
487
|
-
const type = raw.type === "user" || raw.type === "ai" ? raw.type : base.type ?? "ai";
|
|
488
|
-
const ai = type === "ai";
|
|
489
|
-
const assignedRunner = text(raw.assignedRunner) ?? base.assignedRunner ?? catalog.runners[0] ?? DEFAULT_RUNNERS[0];
|
|
490
|
-
const description = text(raw.description) ?? base.description ?? title;
|
|
491
|
-
if (raw.ops === true && !ai) errors.push({ taskId: id, field: "ops", message: `${where} ${opsPlacementRefusal(type, !!parent)}` });
|
|
492
|
-
if (raw.taskMode !== void 0) ignored.push({ taskId: id, field: "taskMode", message: `${where}: ${MODE_NOT_THE_PLANNERS}` });
|
|
493
|
-
if (!ai && raw.readOnly === true) ignored.push({ taskId: id, field: "readOnly", message: `${where}: ${MANUAL_NOT_READ_ONLY}` });
|
|
494
|
-
const ops = raw.ops !== void 0 ? raw.ops === true : base.ops;
|
|
495
|
-
if (ai && ops && raw.readOnly === true) ignored.push({ taskId: id, field: "readOnly", message: `${where}: ${OPS_NOT_READ_ONLY}` });
|
|
496
|
-
const asked = typeof raw.readOnly === "boolean" ? raw.readOnly : void 0;
|
|
497
|
-
const readOnly = ai && !ops && (asked ?? from?.readOnly ?? parent?.readOnly ?? false);
|
|
498
|
-
let taskMode = base.taskMode;
|
|
499
|
-
if (ai && catalog.runners.includes(assignedRunner)) {
|
|
500
|
-
const mode = plannedMode(assignedRunner, catalog.runnerModes, { readOnly, current: base.taskMode }, catalog.autonomousDefault ?? true);
|
|
501
|
-
if (!mode.ok) errors.push({ taskId: id, field: "readOnly", message: `${where}: ${mode.error}` });
|
|
502
|
-
else if (mode.mode !== void 0) taskMode = mode.mode;
|
|
503
|
-
}
|
|
504
|
-
if (taskMode === void 0 && readOnly && !catalog.runnerModes) taskMode = "plan";
|
|
505
|
-
const assignedModel = ai ? modelOf(raw, base.assignedModel) : void 0;
|
|
506
|
-
if (assignedModel && isRecord(raw.assignedModel) && catalog.models && catalog.runners.includes(assignedRunner)) {
|
|
507
|
-
const validity = checkModelAndModeValidity(assignedRunner, assignedModel, void 0, catalog.models);
|
|
508
|
-
if (!validity.ok) errors.push({ taskId: id, field: "assignedModel", message: `${where}: ${validity.error}` });
|
|
509
|
-
}
|
|
510
|
-
const slice = (value) => value === "AFK" || value === "HITL" ? value : void 0;
|
|
511
|
-
const autonomy = ai ? slice(raw.autonomy) ?? base.autonomy ?? parent?.autonomy : void 0;
|
|
512
|
-
const sliceType = slice(raw.sliceType) ?? base.sliceType ?? (ai ? parent?.sliceType : void 0);
|
|
513
|
-
const subtasks = [];
|
|
514
|
-
if (Array.isArray(raw.subtasks)) {
|
|
515
|
-
for (const s of raw.subtasks) {
|
|
516
|
-
const child = plannedTask(s, catalog, { parent: { title, sliceType, autonomy, readOnly }, freshIds, newId });
|
|
517
|
-
subtasks.push(child.task);
|
|
518
|
-
errors.push(...child.errors);
|
|
519
|
-
ignored.push(...child.ignored);
|
|
520
|
-
for (const [was, now] of child.renamed) renamed.set(was, now);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
const task = createTask({
|
|
524
|
-
id,
|
|
525
|
-
order: Number(raw.order ?? 0),
|
|
526
|
-
title,
|
|
527
|
-
description,
|
|
528
|
-
type,
|
|
529
|
-
dependencies: Array.isArray(raw.dependencies) ? raw.dependencies.map(String) : base.dependencies ?? [],
|
|
530
|
-
prompt: text(raw.prompt) || base.prompt || (ai ? description || title || void 0 : void 0),
|
|
531
|
-
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,
|
|
532
|
-
subtasks,
|
|
533
|
-
assignedModel,
|
|
534
|
-
assignedRunner,
|
|
535
|
-
taskMode,
|
|
536
|
-
autonomy,
|
|
537
|
-
sliceType,
|
|
538
|
-
userStoriesCovered: Array.isArray(raw.userStoriesCovered) ? raw.userStoriesCovered.map(String) : base.userStoriesCovered,
|
|
539
|
-
ops,
|
|
540
|
-
skills: from ? inheritedSkills(raw.skills, base.skills) : skillNames(raw.skills)
|
|
541
|
-
});
|
|
542
|
-
if (Array.isArray(raw.skills)) skillsAsked.set(task, raw.skills);
|
|
543
|
-
return { task, errors, ignored, renamed };
|
|
544
|
-
}
|
|
545
|
-
function checkPlanInvariants(tasks, runners) {
|
|
546
|
-
const rows = flattenTasksWithParents(tasks);
|
|
547
|
-
const errors = [];
|
|
548
|
-
const seen = /* @__PURE__ */ new Set();
|
|
549
|
-
for (const { task: t, parent } of rows) {
|
|
550
|
-
const where = taskWhere(t.title, parent?.title);
|
|
551
|
-
const fail = (field, message) => errors.push({ taskId: t.id, field, message });
|
|
552
|
-
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`);
|
|
553
|
-
seen.add(t.id);
|
|
554
|
-
if (t.autonomy === "AFK" && t.userSteps && t.userSteps.length > 0) {
|
|
555
|
-
fail("userSteps", `${where} has autonomy "AFK" but contains userSteps. AFK tasks must not have user touchpoints.`);
|
|
556
|
-
}
|
|
557
|
-
if (t.type === "user" && t.sliceType && t.sliceType !== "HITL") {
|
|
558
|
-
fail("sliceType", `User ${where} has sliceType "${t.sliceType}". User tasks must have sliceType "HITL".`);
|
|
559
|
-
}
|
|
560
|
-
const opsRefused = t.ops ? opsPlacementRefusal(t.type, parent !== null) : null;
|
|
561
|
-
if (opsRefused) fail("ops", `${where} ${opsRefused}`);
|
|
562
|
-
if (t.type !== "user" && !runners.includes(t.assignedRunner)) {
|
|
563
|
-
fail("assignedRunner", `${where} has invalid assignedRunner "${t.assignedRunner}". Expected one of: ${runners.join(", ")}`);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
const ids = new Set(rows.map((r) => r.task.id));
|
|
567
|
-
for (const { task: t, parent } of rows) {
|
|
568
|
-
for (const dep of t.dependencies) {
|
|
569
|
-
if (!ids.has(dep)) errors.push({ taskId: t.id, field: "dependencies", message: `${taskWhere(t.title, parent?.title)} depends on "${dep}", which is not a task in the plan` });
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
const cycle = findCycle(rows.map((r) => r.task));
|
|
573
|
-
if (cycle) errors.push({ taskId: cycle[0].id, field: "dependencies", message: `Dependency cycle detected: ${[...cycle, cycle[0]].map((t) => `"${t.title}"`).join(" \u2192 ")}` });
|
|
574
|
-
return errors;
|
|
575
|
-
}
|
|
576
|
-
function findCycle(tasks) {
|
|
577
|
-
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
578
|
-
const path = [];
|
|
579
|
-
const done = /* @__PURE__ */ new Set();
|
|
580
|
-
const visit = (id) => {
|
|
581
|
-
if (done.has(id)) return null;
|
|
582
|
-
const at = path.indexOf(id);
|
|
583
|
-
if (at >= 0) return path.slice(at);
|
|
584
|
-
path.push(id);
|
|
585
|
-
for (const dep of byId.get(id)?.dependencies ?? []) {
|
|
586
|
-
const hit = visit(dep);
|
|
587
|
-
if (hit) return hit;
|
|
588
|
-
}
|
|
589
|
-
path.pop();
|
|
590
|
-
done.add(id);
|
|
591
|
-
return null;
|
|
592
|
-
};
|
|
593
|
-
for (const t of tasks) {
|
|
594
|
-
const hit = visit(t.id);
|
|
595
|
-
if (hit) return hit.map((id) => byId.get(id));
|
|
596
|
-
}
|
|
597
|
-
return null;
|
|
598
|
-
}
|
|
599
|
-
function detectCycle(tasks) {
|
|
600
|
-
return findCycle(tasks)?.[0].title ?? null;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// src/services/TaskEditValidator.ts
|
|
604
|
-
function lockedTaskRefusal(task, action = "changed") {
|
|
605
|
-
if (task.status === "in_progress") return `"${task.title}" is running and cannot be ${action} now: the user can stop it first, or change it after it finishes`;
|
|
606
|
-
if (task.status === "completed") return `"${task.title}" is completed and cannot be ${action}: rearm it to run it again, or add a new task for further work`;
|
|
607
|
-
return null;
|
|
608
|
-
}
|
|
609
|
-
function liveModelRefusal(task) {
|
|
610
|
-
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`;
|
|
611
|
-
}
|
|
612
|
-
function liveRunnerRefusal(task) {
|
|
613
|
-
return `"${task.title}" has a live runner, so its runner cannot change now: stop the task first`;
|
|
614
|
-
}
|
|
615
|
-
function sameAssignment(a, b) {
|
|
616
|
-
return a?.modelId === b?.modelId && a?.thinkingEffort === b?.thinkingEffort;
|
|
617
|
-
}
|
|
618
|
-
var AI_ONLY_FIELDS = ["assignedModel", "taskMode", "autonomy", "ops"];
|
|
619
|
-
function checkModelAndModeValidity(runner, assignedModel, taskMode, catalog) {
|
|
620
|
-
if (!catalog) return { ok: true };
|
|
621
|
-
if (assignedModel) {
|
|
622
|
-
const models = catalog.modelsByRunner[runner];
|
|
623
|
-
if (models) {
|
|
624
|
-
const modelId = assignedModel.modelId;
|
|
625
|
-
const allowed = effectiveAllowlist(catalog.perRunnerAllowlist?.[runner], runner, catalog.modelsByRunner);
|
|
626
|
-
if (!models.some((m) => m.modelId === modelId) && !allowed?.includes(modelId)) {
|
|
627
|
-
return { ok: false, error: `Runner "${runner}" does not offer model "${modelId}"` };
|
|
628
|
-
}
|
|
629
|
-
if (allowed && !allowed.includes(modelId)) {
|
|
630
|
-
return { ok: false, error: `Model "${modelId}" is excluded by the allowlist for runner "${runner}"` };
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
if (taskMode !== void 0) {
|
|
635
|
-
const modes = catalog.runnerModes[runner];
|
|
636
|
-
if (modes && modes.length > 0 && !modes.some((m) => m.id === taskMode)) {
|
|
637
|
-
return { ok: false, error: `Runner "${runner}" has no task mode "${taskMode}": valid modes: ${modes.map((m) => m.id).join(", ")}` };
|
|
638
|
-
}
|
|
639
|
-
const unavailable = unavailableModeReason(modes, taskMode);
|
|
640
|
-
if (unavailable) return { ok: false, error: `Mode "${taskMode}" of runner "${runner}" is unavailable on this host: ${unavailable}` };
|
|
641
|
-
}
|
|
642
|
-
return { ok: true };
|
|
643
|
-
}
|
|
644
|
-
function typeCoherenceCheck(target, changes) {
|
|
645
|
-
if (!changes.type || changes.type === target.type) return null;
|
|
646
|
-
if (changes.type === "user") {
|
|
647
|
-
if (!Array.isArray(changes.userSteps) || changes.userSteps.length === 0) {
|
|
648
|
-
return { ok: false, error: `"${target.title}" cannot become a manual task without user steps: include "userSteps" in this edit` };
|
|
649
|
-
}
|
|
650
|
-
return {
|
|
651
|
-
ok: true,
|
|
652
|
-
clear: AI_ONLY_FIELDS.filter((f) => target[f] !== void 0),
|
|
653
|
-
// User tasks are held to HITL, so a flip that kept the AI task's slice would be refused by the plan's own rules.
|
|
654
|
-
...target.sliceType !== "HITL" ? { set: { sliceType: "HITL" } } : {}
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
if (!changes.prompt || !changes.prompt.trim()) {
|
|
658
|
-
return { ok: false, error: `"${target.title}" cannot become an AI task without a prompt: include "prompt" in this edit` };
|
|
659
|
-
}
|
|
660
|
-
return { ok: true, clear: target.userSteps && target.userSteps.length > 0 ? ["userSteps"] : [] };
|
|
661
|
-
}
|
|
662
|
-
function opsCheck(target, isSubtask, changes) {
|
|
663
|
-
if (!("ops" in changes) || !!changes.ops === !!target.ops) return null;
|
|
664
|
-
if (!NOT_STARTED.has(target.status)) {
|
|
665
|
-
return { ok: false, error: `"${target.title}" has started, so whether it is an ops task is fixed` };
|
|
666
|
-
}
|
|
667
|
-
if (!changes.ops) return null;
|
|
668
|
-
const refused = opsPlacementRefusal(changes.type ?? target.type, isSubtask);
|
|
669
|
-
return refused ? { ok: false, error: `"${target.title}" ${refused}` } : null;
|
|
670
|
-
}
|
|
671
|
-
function validateTaskEdit(actor, tasks, taskId, changes, catalog) {
|
|
672
|
-
const found = flattenTasksWithParents(tasks).find(({ task }) => task.id === taskId);
|
|
673
|
-
if (!found) return { ok: false, error: "Task not found" };
|
|
674
|
-
const { task: target, parent } = found;
|
|
675
|
-
const locked = actor === "planner" ? lockedTaskRefusal(target, "modified") : null;
|
|
676
|
-
if (locked) return { ok: false, error: locked };
|
|
677
|
-
const coherence = typeCoherenceCheck(target, changes);
|
|
678
|
-
const landing = coherence?.clear?.includes("assignedModel") ? { ...changes, assignedModel: void 0 } : changes;
|
|
679
|
-
if ("assignedModel" in landing && !sameAssignment(landing.assignedModel, target.assignedModel) && catalog?.liveRunner?.(taskId)) {
|
|
680
|
-
return { ok: false, error: liveModelRefusal(target) };
|
|
681
|
-
}
|
|
682
|
-
if ("assignedRunner" in changes && changes.assignedRunner !== target.assignedRunner && catalog?.liveRunner?.(taskId)) {
|
|
683
|
-
return { ok: false, error: liveRunnerRefusal(target) };
|
|
684
|
-
}
|
|
685
|
-
if (changes.dependencies) {
|
|
686
|
-
const depCheck = canSetDependencies(parent?.subtasks ?? tasks, taskId, changes.dependencies);
|
|
687
|
-
if (!depCheck.ok) return depCheck;
|
|
688
|
-
}
|
|
689
|
-
if (coherence && !coherence.ok) return coherence;
|
|
690
|
-
const ops = opsCheck(target, parent !== null, changes);
|
|
691
|
-
if (ops) return ops;
|
|
692
|
-
const validity = checkModelAndModeValidity(changes.assignedRunner ?? target.assignedRunner, changes.assignedModel, changes.taskMode, catalog);
|
|
693
|
-
if (!validity.ok) return validity;
|
|
694
|
-
return coherence ?? { ok: true };
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// src/services/TaskOps.ts
|
|
698
|
-
var UPDATABLE_FIELDS = [
|
|
699
|
-
"title",
|
|
700
|
-
"description",
|
|
701
|
-
"prompt",
|
|
702
|
-
"dependencies",
|
|
703
|
-
"assignedRunner",
|
|
704
|
-
"assignedModel",
|
|
705
|
-
"taskMode",
|
|
706
|
-
"type",
|
|
707
|
-
"userSteps",
|
|
708
|
-
"autonomy",
|
|
709
|
-
"sliceType",
|
|
710
|
-
"ops",
|
|
711
|
-
"skills"
|
|
712
|
-
];
|
|
713
|
-
function taskOpsProtocol(executionNote = "", tools = false) {
|
|
714
|
-
const updateChanges = [...UPDATABLE_FIELDS.filter((f) => f !== "taskMode"), "thinkingEffort", "readOnly"].map((f) => f === "dependencies" ? '"dependencies"?:["<id or #order>"]' : `"${f}"?`).join(",");
|
|
715
|
-
return [
|
|
716
|
-
tools ? "- To modify specific tasks, call edit_plan, never a JSON reply. Its argument is:" : "- To modify specific tasks, reply with ONLY this JSON object:",
|
|
717
|
-
tools ? ' {"ops": [' : ' {"taskOps": [',
|
|
718
|
-
` {"op":"update","taskId":"<id or #order>","changes":{${updateChanges}}},`,
|
|
719
|
-
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?,"readOnly"?,"ops"?},"handle"?:"<name>"},',
|
|
720
|
-
' {"op":"remove","taskId":"<id or #order>"},',
|
|
721
|
-
' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]}, // only to re-prioritise INDEPENDENT tasks',
|
|
722
|
-
' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...},"handle"?:"<name>"},',
|
|
723
|
-
' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...],"handle"?:"<name>"},',
|
|
724
|
-
` {"op":"rearm","taskId":"<id or #order>","changes"?:{${updateChanges}}}`,
|
|
725
|
-
" ]}",
|
|
726
|
-
tools ? "- For sweeping changes, you may instead call submit_plan with the full plan." : '- For sweeping changes, you may instead emit a full {"tasks":[...]} plan JSON.',
|
|
727
|
-
'Every "<id or #order>" ref in a batch resolves against the plan shown above, before any op in the batch runs \u2014 an earlier remove/merge/split never shifts what a later "#N" means. "#N" is top-level task N; a subtask is named by its dotted label ("#2.1"), as the plan lists it.',
|
|
728
|
-
'Give "add", "merge", or "split" a "handle" (any name you choose, unused elsewhere in this batch) to let a LATER op in the same batch reference the task it produces \u2014 for "split", the handle names its last part. A handle used before its defining op is rejected.',
|
|
729
|
-
`When creating or changing tasks, set "assignedRunner" and "assignedModel" ({"modelId","modelLabel","thinkingEffort"?}) using only the runners and models ${tools ? "list_runners and list_models return" : "listed in <available_models>"}.`,
|
|
730
|
-
'Set "readOnly": true on a task that must not change any file, never on one that must run commands (tests, scripts, builds) or an ops task, and "readOnly": false to lift it; you never set "taskMode". A task you move, split or edit keeps being read-only or not unless you say otherwise, and so do the subtasks of a read-only task you add.',
|
|
731
|
-
'Keep dependencies consistent: no cycles, no references to removed tasks, and never touch running or completed tasks \u2014 "rearm" is the one exception, below.',
|
|
732
|
-
...tools ? ["edit_plan checks the batch when you call it and answers with what it will change, or with the error to fix \u2014 call it again in the same reply. The edit is applied when your reply ends; edits you make in one reply join one batch."] : [],
|
|
733
|
-
'Just declare the dependencies you want \u2014 display order is repaired for you afterwards, so a rewire or a newly added prerequisite never needs a "reorder" op. Only a task that is running or completed cannot be shifted, so an edit that would need one to move is rejected.' + executionNote,
|
|
734
|
-
'Flipping "type" between "ai" and "user" is a content change, not just a label: an update to "user" needs "userSteps" in the SAME op, and an update to "ai" needs "prompt" in the SAME op \u2014 the model/mode/effort/autonomy fields (flipping to "user", which also makes the task HITL) or the userSteps (flipping to "ai") are cleared automatically.',
|
|
735
|
-
`"skills" in "changes" REPLACES the task's whole skill list. To add one, send the names the plan block shows in its skills:[...] plus the new one; to drop one, send the rest; [] removes them all.`,
|
|
736
|
-
'"rearm" puts a failed OR completed task back to pending \u2014 verdict and output summary are cleared, any dependents it had blocked are released, and it may carry field changes (e.g. a corrected "prompt") applied in the same op. A running task cannot be re-armed. Never rearm a task just to relabel it \u2014 only when it should actually run again.'
|
|
737
|
-
];
|
|
738
|
-
}
|
|
739
|
-
function textHasTaskOps(text2) {
|
|
740
|
-
return text2.includes(`"${TASK_OPS_ENVELOPE_KEY}"`);
|
|
741
|
-
}
|
|
742
|
-
function parseTaskOpsJson(text2) {
|
|
743
|
-
const { matches, fallback } = extractObjectsWithKey(text2, TASK_OPS_ENVELOPE_KEY);
|
|
744
|
-
const candidates = matches.length > 0 ? [...matches].reverse() : [fallback.json];
|
|
745
|
-
let lastError = null;
|
|
746
|
-
for (const json of candidates) {
|
|
747
|
-
try {
|
|
748
|
-
return parseTaskOpsObject(json, text2);
|
|
749
|
-
} catch (err) {
|
|
750
|
-
if (!(err instanceof PlanParseError)) throw err;
|
|
751
|
-
if (!lastError) lastError = err;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
throw lastError ?? new PlanParseError("No JSON object found in task-ops reply", text2);
|
|
755
|
-
}
|
|
756
|
-
function parseTaskOpsObject(json, text2) {
|
|
757
|
-
let parsed;
|
|
758
|
-
try {
|
|
759
|
-
parsed = JSON.parse(escapeControlCharsInStrings(stripTrailingCommas(json)));
|
|
760
|
-
} catch (err) {
|
|
761
|
-
throw new PlanParseError(`Task-ops JSON is invalid: ${err instanceof Error ? err.message : String(err)}`, text2);
|
|
762
|
-
}
|
|
763
|
-
const ops = parsed.taskOps;
|
|
764
|
-
if (!Array.isArray(ops) || ops.length === 0) {
|
|
765
|
-
throw new PlanParseError('"taskOps" must be a non-empty array', text2);
|
|
766
|
-
}
|
|
767
|
-
for (const op of ops) {
|
|
768
|
-
if (typeof op !== "object" || op === null || typeof op.op !== "string") {
|
|
769
|
-
throw new PlanParseError('Every task op needs an "op" field (update | add | remove | reorder | merge | split)', text2);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
return ops;
|
|
773
|
-
}
|
|
774
|
-
function resolveRef(ref, originalTree, liveTree, handleOwner, handleId, opIndex) {
|
|
775
|
-
if (typeof ref !== "string" && typeof ref !== "number") return { error: `invalid reference "${String(ref)}"` };
|
|
776
|
-
const s = String(ref).trim();
|
|
777
|
-
const ownerIdx = handleOwner.get(s);
|
|
778
|
-
if (ownerIdx !== void 0) {
|
|
779
|
-
if (ownerIdx >= opIndex) {
|
|
780
|
-
return { error: `handle "${s}" is defined by a later op in this batch and cannot be referenced yet` };
|
|
781
|
-
}
|
|
782
|
-
const id = handleId.get(s);
|
|
783
|
-
if (!id) return { error: `handle "${s}" was never resolved` };
|
|
784
|
-
return { id };
|
|
785
|
-
}
|
|
786
|
-
if (findInTasks(liveTree, s)) return { id: s };
|
|
787
|
-
const task = resolveTaskRef(originalTree, s);
|
|
788
|
-
return task ? { id: task.id } : { error: `task "${s}" not found` };
|
|
789
|
-
}
|
|
790
|
-
function updatableChanges(raw) {
|
|
791
|
-
const changes = {};
|
|
792
|
-
for (const key of UPDATABLE_FIELDS) {
|
|
793
|
-
if (raw && key in raw) changes[key] = raw[key];
|
|
794
|
-
}
|
|
795
|
-
if ("ops" in changes) changes.ops = opsFlag(changes.ops);
|
|
796
|
-
if ("skills" in changes) changes.skills = skillNames(changes.skills);
|
|
797
|
-
return changes;
|
|
798
|
-
}
|
|
799
|
-
function findInTasks(tasks, id) {
|
|
800
|
-
return flattenTasks(tasks).find((t) => t.id === id);
|
|
801
|
-
}
|
|
802
|
-
function findTopLevel(tasks, id) {
|
|
803
|
-
return tasks.find((t) => t.id === id);
|
|
804
|
-
}
|
|
805
|
-
function isPinned(t) {
|
|
806
|
-
return t.status === "in_progress" || t.status === "completed";
|
|
807
|
-
}
|
|
808
|
-
function repairOrder(tasks) {
|
|
809
|
-
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
810
|
-
const placed = /* @__PURE__ */ new Set();
|
|
811
|
-
const movable = tasks.filter((t) => !isPinned(t));
|
|
812
|
-
const result = [];
|
|
813
|
-
const blockerOf = (t) => {
|
|
814
|
-
const id = t.dependencies.find((d) => byId.has(d) && !placed.has(d));
|
|
815
|
-
return id ? byId.get(id) : void 0;
|
|
816
|
-
};
|
|
817
|
-
const place = (t) => {
|
|
818
|
-
result.push(t);
|
|
819
|
-
placed.add(t.id);
|
|
820
|
-
};
|
|
821
|
-
for (const occupant of tasks) {
|
|
822
|
-
if (isPinned(occupant)) {
|
|
823
|
-
const blocker = blockerOf(occupant);
|
|
824
|
-
if (blocker) return { error: pinnedRefusal(blocker, occupant, "before") };
|
|
825
|
-
place(occupant);
|
|
826
|
-
continue;
|
|
827
|
-
}
|
|
828
|
-
const next = movable.findIndex((t) => !blockerOf(t));
|
|
829
|
-
if (next < 0) {
|
|
830
|
-
const [stuck, pin] = trailToPin(movable[0], blockerOf);
|
|
831
|
-
return { error: pinnedRefusal(stuck, pin, "after") };
|
|
832
|
-
}
|
|
833
|
-
place(movable.splice(next, 1)[0]);
|
|
834
|
-
}
|
|
835
|
-
return { tasks: result };
|
|
836
|
-
}
|
|
837
|
-
function trailToPin(task, blockerOf) {
|
|
838
|
-
let at = task;
|
|
839
|
-
for (; ; ) {
|
|
840
|
-
const blocker = blockerOf(at);
|
|
841
|
-
if (!blocker || isPinned(blocker)) return [at, blocker ?? at];
|
|
842
|
-
at = blocker;
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
function describeMoves(before, after) {
|
|
846
|
-
const wasAt = new Map(before.map((t, i) => [t.id, i + 1]));
|
|
847
|
-
const moved = after.map((t, i) => ({ t, from: wasAt.get(t.id), to: i + 1 })).filter((m) => m.from !== m.to);
|
|
848
|
-
return moved.length ? moved.map((m) => `"${m.t.title}" #${m.from}\u2192#${m.to}`).join(", ") : null;
|
|
849
|
-
}
|
|
850
|
-
function pinnedRefusal(other, pin, side) {
|
|
851
|
-
const word = pin.status === "in_progress" ? "running" : "completed";
|
|
852
|
-
return `"${other.title}" would have to run ${side} "${pin.title}", which is ${word} and cannot be moved`;
|
|
853
|
-
}
|
|
854
|
-
function canMergeTasks(tasks, selectedIds) {
|
|
855
|
-
if (selectedIds.length < 2) return { ok: false, error: "Select at least two tasks to merge" };
|
|
856
|
-
const idSet = new Set(selectedIds);
|
|
857
|
-
const toMerge = tasks.filter((t) => idSet.has(t.id));
|
|
858
|
-
if (toMerge.length < 2) return { ok: false, error: "At least two valid tasks are required to merge" };
|
|
859
|
-
for (const t of toMerge) {
|
|
860
|
-
if (t.status === "in_progress") return { ok: false, error: `"${t.title}" is running and cannot be merged` };
|
|
861
|
-
if (t.status === "completed") return { ok: false, error: `"${t.title}" is completed and cannot be merged` };
|
|
862
|
-
}
|
|
863
|
-
const ordered = [...toMerge].sort((a, b) => a.order - b.order);
|
|
864
|
-
const survivorId = "__merge_sim__";
|
|
865
|
-
const sim = tasks.filter((t) => !idSet.has(t.id)).map((t) => ({ ...t, dependencies: t.dependencies.map((d) => idSet.has(d) ? survivorId : d).filter((d) => d !== t.id) }));
|
|
866
|
-
const unionDeps = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));
|
|
867
|
-
sim.push({ ...ordered[0], id: survivorId, title: "__merge_sim__", order: ordered[0].order, dependencies: unionDeps });
|
|
868
|
-
const cycle = detectCycle(sim);
|
|
869
|
-
if (cycle) return { ok: false, error: `Merging would create a dependency cycle involving "${cycle}"` };
|
|
870
|
-
return { ok: true };
|
|
871
|
-
}
|
|
872
|
-
function dependentsOf(tasks, taskId) {
|
|
873
|
-
return tasks.filter((t) => t.id !== taskId && t.dependencies.includes(taskId));
|
|
874
|
-
}
|
|
875
|
-
function dependencyCandidates(tasks, taskId) {
|
|
876
|
-
if (!taskId) return [...tasks];
|
|
877
|
-
const target = tasks.find((t) => t.id === taskId);
|
|
878
|
-
return target ? tasks.filter((t) => t.order < target.order) : [];
|
|
879
|
-
}
|
|
880
|
-
function canSetDependencies(tasks, taskId, dependencies) {
|
|
881
|
-
const target = tasks.find((t) => t.id === taskId);
|
|
882
|
-
if (!target) return { ok: false, error: "Task not found" };
|
|
883
|
-
const allowed = new Set(dependencyCandidates(tasks, taskId).map((t) => t.id));
|
|
884
|
-
for (const dep of dependencies) {
|
|
885
|
-
if (dep === taskId) return { ok: false, error: `"${target.title}" cannot depend on itself` };
|
|
886
|
-
const found = tasks.find((t) => t.id === dep);
|
|
887
|
-
if (!found) return { ok: false, error: `Unknown dependency "${dep}"` };
|
|
888
|
-
if (!allowed.has(dep)) {
|
|
889
|
-
return { ok: false, error: `"${target.title}" cannot depend on "${found.title}", which comes after it in the plan` };
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
return { ok: true };
|
|
893
|
-
}
|
|
894
|
-
function canSplitTask(tasks, taskId) {
|
|
895
|
-
const target = tasks.find((t) => t.id === taskId);
|
|
896
|
-
if (!target) return { ok: false, error: "Task not found" };
|
|
897
|
-
if (target.status === "in_progress") return { ok: false, error: `"${target.title}" is running and cannot be split` };
|
|
898
|
-
if (target.status === "completed") return { ok: false, error: `"${target.title}" is completed and cannot be split` };
|
|
899
|
-
return { ok: true };
|
|
900
|
-
}
|
|
901
|
-
function askedReadOnly(spec) {
|
|
902
|
-
return typeof spec?.readOnly === "boolean" ? spec.readOnly : void 0;
|
|
903
|
-
}
|
|
904
|
-
function isReadOnly(task, catalog) {
|
|
905
|
-
return task.taskMode !== void 0 && task.taskMode === taggedReadOnlyMode(catalog?.runnerModes[task.assignedRunner]);
|
|
906
|
-
}
|
|
907
|
-
function landingMode(runner, readOnly, current, catalog) {
|
|
908
|
-
return plannedMode(runner, catalog?.runnerModes, { readOnly, current }, catalog?.autonomousDefault ?? true);
|
|
909
|
-
}
|
|
910
|
-
function changedFields(target, changes, catalog) {
|
|
911
|
-
const fields = Object.keys(changes).filter((f) => f !== "taskMode");
|
|
912
|
-
if ("taskMode" in changes && isReadOnly({ ...target, ...changes }, catalog) !== isReadOnly(target, catalog)) fields.push("readOnly");
|
|
913
|
-
return fields;
|
|
914
|
-
}
|
|
915
|
-
var keptIds = /* @__PURE__ */ new WeakMap();
|
|
916
|
-
function idsOf(op) {
|
|
917
|
-
if (typeof op !== "object" || op === null) return uuidv42;
|
|
918
|
-
const ids = keptIds.get(op) ?? [];
|
|
919
|
-
keptIds.set(op, ids);
|
|
920
|
-
let next = 0;
|
|
921
|
-
return () => {
|
|
922
|
-
if (next === ids.length) ids.push(uuidv42());
|
|
923
|
-
return ids[next++];
|
|
924
|
-
};
|
|
925
|
-
}
|
|
926
|
-
function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
927
|
-
const ops = [...held, ...newOps];
|
|
928
|
-
const opLabel = (i) => i < held.length ? `earlier op ${i + 1} (${ops[i].op})` : `op ${i - held.length + 1} (${ops[i].op})`;
|
|
929
|
-
let tasks = JSON.parse(JSON.stringify(currentTasks));
|
|
930
|
-
const errors = [];
|
|
931
|
-
const summary = [];
|
|
932
|
-
const handleOwner = /* @__PURE__ */ new Map();
|
|
933
|
-
const handleId = /* @__PURE__ */ new Map();
|
|
934
|
-
for (const [i, op] of ops.entries()) {
|
|
935
|
-
const handle = op.handle;
|
|
936
|
-
if (handle === void 0) continue;
|
|
937
|
-
if (typeof handle !== "string" || !handle.trim()) {
|
|
938
|
-
errors.push(`${opLabel(i)}: handle must be a non-empty string`);
|
|
939
|
-
continue;
|
|
940
|
-
}
|
|
941
|
-
if (handleOwner.has(handle)) {
|
|
942
|
-
errors.push(`${opLabel(i)}: handle "${handle}" is already used earlier in this batch`);
|
|
943
|
-
continue;
|
|
944
|
-
}
|
|
945
|
-
if (resolveTaskRef(currentTasks, handle)) {
|
|
946
|
-
errors.push(`${opLabel(i)}: handle "${handle}" collides with an existing task reference`);
|
|
947
|
-
continue;
|
|
948
|
-
}
|
|
949
|
-
handleOwner.set(handle, i);
|
|
950
|
-
}
|
|
951
|
-
if (errors.length) return { ok: false, tasks: currentTasks, errors, summary: [] };
|
|
952
|
-
const resolveDeps = (deps, opIndex) => {
|
|
953
|
-
const ids = [];
|
|
954
|
-
const bad = [];
|
|
955
|
-
const liveFlat = flattenTasks(tasks);
|
|
956
|
-
for (const d of Array.isArray(deps) ? deps : []) {
|
|
957
|
-
const r = resolveRef(d, currentTasks, tasks, handleOwner, handleId, opIndex);
|
|
958
|
-
if ("error" in r) {
|
|
959
|
-
bad.push(`${String(d)} (${r.error})`);
|
|
960
|
-
continue;
|
|
961
|
-
}
|
|
962
|
-
if (!liveFlat.some((t) => t.id === r.id)) {
|
|
963
|
-
bad.push(`${String(d)} (no longer exists in this batch)`);
|
|
964
|
-
continue;
|
|
965
|
-
}
|
|
966
|
-
ids.push(r.id);
|
|
967
|
-
}
|
|
968
|
-
return { ids, bad };
|
|
969
|
-
};
|
|
970
|
-
const intake = { runners, runnerModes: catalog?.runnerModes, autonomousDefault: catalog?.autonomousDefault, models: catalog };
|
|
971
|
-
const resolveTree = (planned, opIndex) => {
|
|
972
|
-
const walk = (t, parentTitle) => {
|
|
973
|
-
const ids = [];
|
|
974
|
-
const bad = [];
|
|
975
|
-
for (const d of t.dependencies) {
|
|
976
|
-
const own = planned.renamed.get(d);
|
|
977
|
-
if (own) {
|
|
978
|
-
ids.push(own);
|
|
979
|
-
continue;
|
|
980
|
-
}
|
|
981
|
-
const r = resolveDeps([d], opIndex);
|
|
982
|
-
ids.push(...r.ids);
|
|
983
|
-
bad.push(...r.bad);
|
|
984
|
-
}
|
|
985
|
-
if (bad.length) return `unknown dependencies${parentTitle !== void 0 ? ` of ${taskWhere(t.title, parentTitle)}` : ""}: ${bad.join(", ")}`;
|
|
986
|
-
const subtasks = [];
|
|
987
|
-
for (const sub of t.subtasks) {
|
|
988
|
-
const done2 = walk(sub, t.title);
|
|
989
|
-
if (typeof done2 === "string") return done2;
|
|
990
|
-
subtasks.push(done2);
|
|
991
|
-
}
|
|
992
|
-
return { ...t, dependencies: ids.filter((id) => id !== t.id), subtasks };
|
|
993
|
-
};
|
|
994
|
-
const done = walk(planned.task);
|
|
995
|
-
return typeof done === "string" ? { error: done } : { task: done };
|
|
996
|
-
};
|
|
997
|
-
const refusalOf = (planned) => [...planned.ignored, ...planned.errors][0]?.message;
|
|
998
|
-
const prepareEdit = (op, opIndex, label, audience, refuseRunning = false) => {
|
|
999
|
-
const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, opIndex);
|
|
1000
|
-
if ("error" in ref) return { error: `${label}: ${ref.error}` };
|
|
1001
|
-
const target = findInTasks(tasks, ref.id);
|
|
1002
|
-
if (!target) return { error: `${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)` };
|
|
1003
|
-
if (refuseRunning && target.status === "in_progress") return { error: `${label}: ${lockedTaskRefusal(target, "re-armed")}` };
|
|
1004
|
-
if (op.changes && "taskMode" in op.changes) return { error: `${label}: ${MODE_NOT_THE_PLANNERS}` };
|
|
1005
|
-
const changes = updatableChanges(op.changes);
|
|
1006
|
-
if (op.changes && "thinkingEffort" in op.changes) {
|
|
1007
|
-
const model = changes.assignedModel ?? target.assignedModel;
|
|
1008
|
-
if (!model) return { error: `${label}: "thinkingEffort" is the assigned model's \u2014 set "assignedModel" with it` };
|
|
1009
|
-
changes.assignedModel = { ...model, thinkingEffort: op.changes.thinkingEffort ?? void 0 };
|
|
1010
|
-
}
|
|
1011
|
-
const changesForCheck = { ...changes };
|
|
1012
|
-
delete changesForCheck.dependencies;
|
|
1013
|
-
const check = validateTaskEdit(audience, tasks, target.id, changesForCheck, catalog);
|
|
1014
|
-
if (!check.ok) return { error: `${label}: ${check.error}` };
|
|
1015
|
-
for (const field of check.clear ?? []) changes[field] = void 0;
|
|
1016
|
-
Object.assign(changes, check.set);
|
|
1017
|
-
const readOnly = askedReadOnly(op.changes);
|
|
1018
|
-
if (readOnly === true && (changes.type ?? target.type) === "user") return { error: `${label}: ${MANUAL_NOT_READ_ONLY}` };
|
|
1019
|
-
if ((changes.ops ?? target.ops) && (readOnly ?? isReadOnly(target, catalog))) return { error: `${label}: ${OPS_NOT_READ_ONLY}` };
|
|
1020
|
-
const moved = changes.assignedRunner !== void 0 && changes.assignedRunner !== target.assignedRunner;
|
|
1021
|
-
const becomesAi = changes.type === "ai" && target.type === "user";
|
|
1022
|
-
const known = runners.includes(changes.assignedRunner ?? target.assignedRunner);
|
|
1023
|
-
if (known && (readOnly !== void 0 || moved || becomesAi) && (changes.type ?? target.type) !== "user") {
|
|
1024
|
-
const mode = landingMode(changes.assignedRunner ?? target.assignedRunner, readOnly ?? isReadOnly(target, catalog), target.taskMode, catalog);
|
|
1025
|
-
if (!mode.ok) return { error: `${label}: ${mode.error}` };
|
|
1026
|
-
if (mode.mode !== void 0 && mode.mode !== target.taskMode) changes.taskMode = mode.mode;
|
|
1027
|
-
}
|
|
1028
|
-
if ("dependencies" in changes) {
|
|
1029
|
-
const { ids, bad } = resolveDeps(changes.dependencies, opIndex);
|
|
1030
|
-
if (bad.length) return { error: `${label}: unknown dependencies: ${bad.join(", ")}` };
|
|
1031
|
-
changes.dependencies = ids.filter((id) => id !== target.id);
|
|
1032
|
-
}
|
|
1033
|
-
return { target, changes, check };
|
|
1034
|
-
};
|
|
1035
|
-
const breachKey = (e) => `${e.taskId ?? ""}\0${e.field}`;
|
|
1036
|
-
const standing = new Set(checkPlanInvariants(currentTasks, runners).map(breachKey));
|
|
1037
|
-
const brokenBy = /* @__PURE__ */ new Map();
|
|
1038
|
-
let lastLabel = opLabel(0);
|
|
1039
|
-
for (const [i, op] of ops.entries()) {
|
|
1040
|
-
const label = opLabel(i);
|
|
1041
|
-
const newId = idsOf(op);
|
|
1042
|
-
switch (op.op) {
|
|
1043
|
-
case "update": {
|
|
1044
|
-
const prep = prepareEdit(op, i, label, "planner");
|
|
1045
|
-
if ("error" in prep) {
|
|
1046
|
-
errors.push(prep.error);
|
|
1047
|
-
break;
|
|
1048
|
-
}
|
|
1049
|
-
const { target, changes, check } = prep;
|
|
1050
|
-
if (Object.keys(changes).length === 0) {
|
|
1051
|
-
const readOnly = askedReadOnly(op.changes);
|
|
1052
|
-
if (readOnly !== void 0 && isReadOnly(target, catalog) === readOnly) {
|
|
1053
|
-
summary.push(`"${target.title}" is already ${readOnly ? "" : "not "}read-only`);
|
|
1054
|
-
break;
|
|
1055
|
-
}
|
|
1056
|
-
errors.push(`${label}: no valid changes provided`);
|
|
1057
|
-
break;
|
|
1058
|
-
}
|
|
1059
|
-
tasks = updateTaskInPlan(tasks, target.id, changes);
|
|
1060
|
-
const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
|
|
1061
|
-
const fields = changedFields(target, changes, catalog);
|
|
1062
|
-
summary.push(`Updated "${target.title}"${fields.length ? ` (${fields.join(", ")})` : ""}${clearedNote}`);
|
|
1063
|
-
break;
|
|
1064
|
-
}
|
|
1065
|
-
case "add": {
|
|
1066
|
-
const spec = op.task ?? {};
|
|
1067
|
-
if (!spec.title) {
|
|
1068
|
-
errors.push(`${label}: new task needs a title`);
|
|
1069
|
-
break;
|
|
1070
|
-
}
|
|
1071
|
-
const planned = plannedTask(spec, intake, { freshIds: true, newId });
|
|
1072
|
-
const refused = refusalOf(planned);
|
|
1073
|
-
if (refused) {
|
|
1074
|
-
errors.push(`${label}: ${refused}`);
|
|
1075
|
-
break;
|
|
1076
|
-
}
|
|
1077
|
-
const resolved = resolveTree(planned, i);
|
|
1078
|
-
if ("error" in resolved) {
|
|
1079
|
-
errors.push(`${label}: ${resolved.error}`);
|
|
1080
|
-
break;
|
|
1081
|
-
}
|
|
1082
|
-
tasks = addTaskToPlan(tasks, resolved.task);
|
|
1083
|
-
if (op.handle) handleId.set(op.handle, resolved.task.id);
|
|
1084
|
-
summary.push(`Added "${spec.title}"`);
|
|
1085
|
-
break;
|
|
1086
|
-
}
|
|
1087
|
-
case "remove": {
|
|
1088
|
-
const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, i);
|
|
1089
|
-
if ("error" in ref) {
|
|
1090
|
-
errors.push(`${label}: ${ref.error}`);
|
|
1091
|
-
break;
|
|
1092
|
-
}
|
|
1093
|
-
const target = findInTasks(tasks, ref.id);
|
|
1094
|
-
if (!target) {
|
|
1095
|
-
errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
|
|
1096
|
-
break;
|
|
1097
|
-
}
|
|
1098
|
-
const locked = lockedTaskRefusal(target, "removed");
|
|
1099
|
-
if (locked) {
|
|
1100
|
-
errors.push(`${label}: ${locked}`);
|
|
1101
|
-
break;
|
|
1102
|
-
}
|
|
1103
|
-
tasks = removeTaskFromPlan(tasks, target.id);
|
|
1104
|
-
summary.push(`Removed "${target.title}"`);
|
|
1105
|
-
break;
|
|
1106
|
-
}
|
|
1107
|
-
case "reorder": {
|
|
1108
|
-
const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
|
|
1109
|
-
const badRef = refs.find((r) => "error" in r);
|
|
1110
|
-
if (badRef) {
|
|
1111
|
-
errors.push(`${label}: ${badRef.error}`);
|
|
1112
|
-
break;
|
|
1113
|
-
}
|
|
1114
|
-
const resolved = refs.map((r) => findTopLevel(tasks, r.id));
|
|
1115
|
-
if (resolved.some((t) => !t)) {
|
|
1116
|
-
errors.push(`${label}: unknown task in reorder list`);
|
|
1117
|
-
break;
|
|
1118
|
-
}
|
|
1119
|
-
const orderIds = resolved.map((t) => t.id);
|
|
1120
|
-
if (new Set(orderIds).size !== tasks.length) {
|
|
1121
|
-
errors.push(`${label}: reorder must list every top-level task exactly once (${tasks.length} tasks)`);
|
|
1122
|
-
break;
|
|
1123
|
-
}
|
|
1124
|
-
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
1125
|
-
tasks = renumberTasks(orderIds.map((id) => byId.get(id)));
|
|
1126
|
-
summary.push("Reordered tasks");
|
|
1127
|
-
break;
|
|
1128
|
-
}
|
|
1129
|
-
case "merge": {
|
|
1130
|
-
const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
|
|
1131
|
-
const badRef = refs.find((r) => "error" in r);
|
|
1132
|
-
if (badRef) {
|
|
1133
|
-
errors.push(`${label}: ${badRef.error}`);
|
|
1134
|
-
break;
|
|
1135
|
-
}
|
|
1136
|
-
const resolved = refs.map((r) => findTopLevel(tasks, r.id));
|
|
1137
|
-
if (resolved.some((t) => !t)) {
|
|
1138
|
-
errors.push(`${label}: unknown task in merge list (already removed, merged, or split by an earlier op)`);
|
|
1139
|
-
break;
|
|
1140
|
-
}
|
|
1141
|
-
const toMerge = [...new Map(resolved.map((t) => [t.id, t])).values()].sort((a, b) => a.order - b.order);
|
|
1142
|
-
if (toMerge.length < 2) {
|
|
1143
|
-
errors.push(`${label}: merge needs at least two tasks`);
|
|
1144
|
-
break;
|
|
1145
|
-
}
|
|
1146
|
-
const locked = toMerge.map((t) => lockedTaskRefusal(t, "merged")).find((refusal) => refusal !== null);
|
|
1147
|
-
if (locked) {
|
|
1148
|
-
errors.push(`${label}: ${locked}`);
|
|
1149
|
-
break;
|
|
1150
|
-
}
|
|
1151
|
-
const idSet = new Set(toMerge.map((t) => t.id));
|
|
1152
|
-
const unionDepsRaw = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));
|
|
1153
|
-
const { ids, bad } = resolveDeps(unionDepsRaw, i);
|
|
1154
|
-
if (bad.length) {
|
|
1155
|
-
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
1156
|
-
break;
|
|
1157
|
-
}
|
|
1158
|
-
const spec = op.merged ?? {};
|
|
1159
|
-
if (!spec.title) {
|
|
1160
|
-
errors.push(`${label}: merged task needs a title`);
|
|
1161
|
-
break;
|
|
1162
|
-
}
|
|
1163
|
-
const writes = toMerge.find((t) => !isReadOnly(t, catalog));
|
|
1164
|
-
const stories = [...new Set(toMerge.flatMap((t) => t.userStoriesCovered ?? []))];
|
|
1165
|
-
const planned = plannedTask(spec, intake, {
|
|
1166
|
-
freshIds: true,
|
|
1167
|
-
newId,
|
|
1168
|
-
from: {
|
|
1169
|
-
readOnly: !writes,
|
|
1170
|
-
fields: {
|
|
1171
|
-
description: toMerge.map((t) => t.description).filter(Boolean).join("\n\n"),
|
|
1172
|
-
prompt: toMerge.map((t) => t.prompt).filter(Boolean).join("\n\n---\n\n") || void 0,
|
|
1173
|
-
type: toMerge.some((t) => t.type === "user") ? "user" : "ai",
|
|
1174
|
-
assignedRunner: toMerge[0].assignedRunner,
|
|
1175
|
-
assignedModel: toMerge.find((t) => t.assignedModel)?.assignedModel,
|
|
1176
|
-
taskMode: (writes ?? toMerge[0]).taskMode,
|
|
1177
|
-
autonomy: toMerge.find((t) => t.autonomy)?.autonomy,
|
|
1178
|
-
sliceType: toMerge.find((t) => t.sliceType)?.sliceType,
|
|
1179
|
-
ops: inheritedOps(toMerge),
|
|
1180
|
-
skills: toMerge.flatMap((t) => t.skills ?? []),
|
|
1181
|
-
userStoriesCovered: stories.length ? stories : void 0
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
});
|
|
1185
|
-
const refused = refusalOf(planned);
|
|
1186
|
-
if (refused) {
|
|
1187
|
-
errors.push(`${label}: ${refused}`);
|
|
1188
|
-
break;
|
|
1189
|
-
}
|
|
1190
|
-
const built = resolveTree({ ...planned, task: { ...planned.task, dependencies: [] } }, i);
|
|
1191
|
-
if ("error" in built) {
|
|
1192
|
-
errors.push(`${label}: ${built.error}`);
|
|
1193
|
-
break;
|
|
1194
|
-
}
|
|
1195
|
-
const mergedTask = { ...built.task, dependencies: ids };
|
|
1196
|
-
const mergedId = mergedTask.id;
|
|
1197
|
-
const firstId = toMerge[0].id;
|
|
1198
|
-
const result = [];
|
|
1199
|
-
let inserted = false;
|
|
1200
|
-
for (const t of tasks) {
|
|
1201
|
-
if (idSet.has(t.id)) {
|
|
1202
|
-
if (!inserted && t.id === firstId) {
|
|
1203
|
-
result.push(mergedTask);
|
|
1204
|
-
inserted = true;
|
|
1205
|
-
}
|
|
1206
|
-
continue;
|
|
1207
|
-
}
|
|
1208
|
-
result.push({ ...t, dependencies: t.dependencies.map((d) => idSet.has(d) ? mergedId : d) });
|
|
1209
|
-
}
|
|
1210
|
-
if (!inserted) result.push(mergedTask);
|
|
1211
|
-
tasks = renumberTasks(result);
|
|
1212
|
-
if (op.handle) handleId.set(op.handle, mergedId);
|
|
1213
|
-
summary.push(`Merged ${toMerge.length} tasks into "${spec.title}"`);
|
|
1214
|
-
break;
|
|
1215
|
-
}
|
|
1216
|
-
case "split": {
|
|
1217
|
-
const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, i);
|
|
1218
|
-
if ("error" in ref) {
|
|
1219
|
-
errors.push(`${label}: ${ref.error}`);
|
|
1220
|
-
break;
|
|
1221
|
-
}
|
|
1222
|
-
const target = findTopLevel(tasks, ref.id);
|
|
1223
|
-
if (!target) {
|
|
1224
|
-
errors.push(`${label}: task "${op.taskId}" not found`);
|
|
1225
|
-
break;
|
|
1226
|
-
}
|
|
1227
|
-
const locked = lockedTaskRefusal(target, "split");
|
|
1228
|
-
if (locked) {
|
|
1229
|
-
errors.push(`${label}: ${locked}`);
|
|
1230
|
-
break;
|
|
1231
|
-
}
|
|
1232
|
-
const parts = op.parts ?? [];
|
|
1233
|
-
if (parts.length < 2) {
|
|
1234
|
-
errors.push(`${label}: split needs at least two parts`);
|
|
1235
|
-
break;
|
|
1236
|
-
}
|
|
1237
|
-
const newTasks = [];
|
|
1238
|
-
let prevId;
|
|
1239
|
-
let buildError = false;
|
|
1240
|
-
for (let p = 0; p < parts.length; p++) {
|
|
1241
|
-
const spec = parts[p];
|
|
1242
|
-
if (!spec || !spec.title) {
|
|
1243
|
-
errors.push(`${label}: split part ${p + 1} needs a title`);
|
|
1244
|
-
buildError = true;
|
|
1245
|
-
break;
|
|
1246
|
-
}
|
|
1247
|
-
let deps;
|
|
1248
|
-
if (p === 0) {
|
|
1249
|
-
const { ids, bad } = resolveDeps(target.dependencies, i);
|
|
1250
|
-
if (bad.length) {
|
|
1251
|
-
errors.push(`${label}: split part 1 unknown dependencies: ${bad.join(", ")}`);
|
|
1252
|
-
buildError = true;
|
|
1253
|
-
break;
|
|
1254
|
-
}
|
|
1255
|
-
deps = ids;
|
|
1256
|
-
} else {
|
|
1257
|
-
deps = prevId ? [prevId] : [];
|
|
1258
|
-
}
|
|
1259
|
-
const planned = plannedTask(spec, intake, {
|
|
1260
|
-
freshIds: true,
|
|
1261
|
-
newId,
|
|
1262
|
-
from: {
|
|
1263
|
-
readOnly: isReadOnly(target, catalog),
|
|
1264
|
-
fields: {
|
|
1265
|
-
type: target.type,
|
|
1266
|
-
assignedRunner: target.assignedRunner,
|
|
1267
|
-
assignedModel: target.assignedModel,
|
|
1268
|
-
taskMode: target.taskMode,
|
|
1269
|
-
autonomy: target.autonomy,
|
|
1270
|
-
sliceType: target.sliceType,
|
|
1271
|
-
ops: inheritedOps([target]),
|
|
1272
|
-
skills: target.skills,
|
|
1273
|
-
userStoriesCovered: target.userStoriesCovered
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
});
|
|
1277
|
-
const refused = refusalOf(planned);
|
|
1278
|
-
if (refused) {
|
|
1279
|
-
errors.push(`${label}: split part ${p + 1}: ${refused}`);
|
|
1280
|
-
buildError = true;
|
|
1281
|
-
break;
|
|
1282
|
-
}
|
|
1283
|
-
const resolved = resolveTree({ ...planned, task: { ...planned.task, dependencies: [] } }, i);
|
|
1284
|
-
if ("error" in resolved) {
|
|
1285
|
-
errors.push(`${label}: split part ${p + 1}: ${resolved.error}`);
|
|
1286
|
-
buildError = true;
|
|
1287
|
-
break;
|
|
1288
|
-
}
|
|
1289
|
-
const nt = { ...resolved.task, dependencies: deps };
|
|
1290
|
-
newTasks.push(nt);
|
|
1291
|
-
prevId = nt.id;
|
|
1292
|
-
}
|
|
1293
|
-
if (buildError) break;
|
|
1294
|
-
const tailId = newTasks[newTasks.length - 1].id;
|
|
1295
|
-
const result = [];
|
|
1296
|
-
for (const t of tasks) {
|
|
1297
|
-
if (t.id === target.id) {
|
|
1298
|
-
result.push(...newTasks);
|
|
1299
|
-
continue;
|
|
1300
|
-
}
|
|
1301
|
-
result.push({ ...t, dependencies: t.dependencies.map((d) => d === target.id ? tailId : d) });
|
|
1302
|
-
}
|
|
1303
|
-
tasks = renumberTasks(result);
|
|
1304
|
-
if (op.handle) handleId.set(op.handle, tailId);
|
|
1305
|
-
summary.push(`Split "${target.title}" into ${newTasks.length} tasks`);
|
|
1306
|
-
break;
|
|
1307
|
-
}
|
|
1308
|
-
case "rearm": {
|
|
1309
|
-
const prep = prepareEdit(op, i, label, "direct", true);
|
|
1310
|
-
if ("error" in prep) {
|
|
1311
|
-
errors.push(prep.error);
|
|
1312
|
-
break;
|
|
1313
|
-
}
|
|
1314
|
-
const { target, changes, check } = prep;
|
|
1315
|
-
tasks = updateTaskInPlan(tasks, target.id, { ...changes, status: "pending", verdict: void 0, outputSummary: void 0, awaitingReason: void 0, resumeAt: void 0, skipReason: void 0 });
|
|
1316
|
-
for (const dep of flattenTasks(tasks)) {
|
|
1317
|
-
if (dep.status === "blocked" && dep.dependencies.includes(target.id)) {
|
|
1318
|
-
tasks = updateTaskInPlan(tasks, dep.id, { status: "pending" });
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
|
|
1322
|
-
const fields = changedFields(target, changes, catalog);
|
|
1323
|
-
const changedNote = fields.length ? ` (${fields.join(", ")})` : "";
|
|
1324
|
-
summary.push(`Re-armed "${target.title}"${changedNote}${clearedNote}`);
|
|
1325
|
-
break;
|
|
1326
|
-
}
|
|
1327
|
-
default:
|
|
1328
|
-
errors.push(`${label}: unknown op "${op.op}"`);
|
|
1329
|
-
}
|
|
1330
|
-
if (errors.length) break;
|
|
1331
|
-
lastLabel = label;
|
|
1332
|
-
const now = new Set(checkPlanInvariants(tasks, runners).map(breachKey));
|
|
1333
|
-
for (const key of brokenBy.keys()) if (!now.has(key)) brokenBy.delete(key);
|
|
1334
|
-
for (const key of now) if (!standing.has(key) && !brokenBy.has(key)) brokenBy.set(key, label);
|
|
1335
|
-
}
|
|
1336
|
-
if (!errors.length) {
|
|
1337
|
-
for (const e of checkPlanInvariants(tasks, runners)) {
|
|
1338
|
-
const key = breachKey(e);
|
|
1339
|
-
if (!standing.has(key)) errors.push(`${brokenBy.get(key) ?? lastLabel}: ${e.message}`);
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
if (!errors.length) {
|
|
1343
|
-
const repaired = repairOrder(tasks);
|
|
1344
|
-
if ("error" in repaired) {
|
|
1345
|
-
errors.push(repaired.error);
|
|
1346
|
-
} else {
|
|
1347
|
-
const moves = describeMoves(tasks, repaired.tasks);
|
|
1348
|
-
if (moves) summary.push(`Reordered to keep dependencies first: ${moves}`);
|
|
1349
|
-
tasks = renumberTasks(repaired.tasks);
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
return errors.length ? { ok: false, tasks: currentTasks, errors, summary: [] } : { ok: true, tasks, errors: [], summary, handles: handleId };
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
export {
|
|
1356
|
-
PLAN_ENVELOPE_KEY,
|
|
1357
|
-
TASK_OPS_ENVELOPE_KEY,
|
|
1358
|
-
TASK_QUERY_ENVELOPE_KEY,
|
|
1359
|
-
PlanParseError,
|
|
1360
|
-
stripModelNoise,
|
|
1361
|
-
opensWithJsonObject,
|
|
1362
|
-
extractObjectsWithKey,
|
|
1363
|
-
extractJsonObject,
|
|
1364
|
-
stripTrailingCommas,
|
|
1365
|
-
escapeControlCharsInStrings,
|
|
1366
|
-
runnerModesFrom,
|
|
1367
|
-
resolveDefaultMode,
|
|
1368
|
-
autonomyLevelLabel,
|
|
1369
|
-
parseAutonomyLevel,
|
|
1370
|
-
taggedReadOnlyMode,
|
|
1371
|
-
readOnlyMode,
|
|
1372
|
-
unavailableModeReason,
|
|
1373
|
-
unavailableTaskModeMessage,
|
|
1374
|
-
carryPlannedModes,
|
|
1375
|
-
readOnlyRunners,
|
|
1376
|
-
effectiveAllowlist,
|
|
1377
|
-
filterModelsForPrompt,
|
|
1378
|
-
clampThinkingEffort,
|
|
1379
|
-
coerceAssignments,
|
|
1380
|
-
isTaskRunning,
|
|
1381
|
-
NOT_STARTED,
|
|
1382
|
-
taskStatusKind,
|
|
1383
|
-
awaitingLabel,
|
|
1384
|
-
blockedByLabel,
|
|
1385
|
-
skippedLabel,
|
|
1386
|
-
WAITS_FOR_APPROVAL_LABEL,
|
|
1387
|
-
approvalLabel,
|
|
1388
|
-
resumeTime,
|
|
1389
|
-
resumeLabel,
|
|
1390
|
-
taskWhere,
|
|
1391
|
-
requestedSkills,
|
|
1392
|
-
plannedTask,
|
|
1393
|
-
checkPlanInvariants,
|
|
1394
|
-
liveRunnerRefusal,
|
|
1395
|
-
validateTaskEdit,
|
|
1396
|
-
UPDATABLE_FIELDS,
|
|
1397
|
-
taskOpsProtocol,
|
|
1398
|
-
textHasTaskOps,
|
|
1399
|
-
parseTaskOpsJson,
|
|
1400
|
-
canMergeTasks,
|
|
1401
|
-
dependentsOf,
|
|
1402
|
-
dependencyCandidates,
|
|
1403
|
-
canSetDependencies,
|
|
1404
|
-
canSplitTask,
|
|
1405
|
-
applyTaskOps
|
|
1406
|
-
};
|
|
1407
|
-
//# sourceMappingURL=chunk-4QLFM73A.mjs.map
|