@ordewell/core 0.10.11 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORS.md +37 -0
- package/NOTICE +71 -0
- package/README.md +1 -1
- package/dist/{IFileSystem-1PqRCjhu.d.mts → IRunner-B5l9PCTv.d.ts} +926 -102
- package/dist/{IFileSystem-lTRL_G9Z.d.ts → IRunner-DBgDx_Mr.d.mts} +926 -102
- package/dist/{ModeResolver-CoJAJD-f.d.ts → ModeResolver-BDAgouuE.d.mts} +9 -2
- package/dist/{ModeResolver-6TQXVsde.d.mts → ModeResolver-DyXeZ9ib.d.ts} +9 -2
- package/dist/{Task-CfBM0wkV.d.mts → Task-BL4Idm9r.d.mts} +656 -26
- package/dist/{Task-CfBM0wkV.d.ts → Task-BL4Idm9r.d.ts} +656 -26
- package/dist/{chunk-ESKK5I4F.mjs → chunk-4QXOWEGC.mjs} +1888 -334
- package/dist/chunk-4QXOWEGC.mjs.map +1 -0
- package/dist/{chunk-KWS3VEER.mjs → chunk-MYXMIVWA.mjs} +6 -6
- package/dist/chunk-MYXMIVWA.mjs.map +1 -0
- package/dist/{chunk-PFH7WFIB.mjs → chunk-QTZ5XTBY.mjs} +228 -15
- package/dist/chunk-QTZ5XTBY.mjs.map +1 -0
- package/dist/{chunk-24EL4Z2W.mjs → chunk-YAFNYNFX.mjs} +342 -36
- package/dist/chunk-YAFNYNFX.mjs.map +1 -0
- package/dist/index.d.mts +1490 -769
- package/dist/index.d.ts +1490 -769
- package/dist/index.js +32009 -20354
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +27785 -17956
- package/dist/index.mjs.map +1 -1
- package/dist/parsing.d.mts +2 -2
- package/dist/parsing.d.ts +2 -2
- package/dist/parsing.js +232 -25
- package/dist/parsing.js.map +1 -1
- package/dist/parsing.mjs +3 -3
- package/dist/plan-utils.d.mts +4 -4
- package/dist/plan-utils.d.ts +4 -4
- package/dist/plan-utils.js +1156 -96
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +144 -8
- package/dist/{runSettingsText-Jb_efnI4.d.mts → runSettingsText-BKwWs052.d.mts} +2882 -621
- package/dist/{runSettingsText-C5uv2axI.d.ts → runSettingsText-CAXiTpsj.d.ts} +2882 -621
- package/dist/testing.d.mts +10 -4
- package/dist/testing.d.ts +10 -4
- package/dist/testing.js +30 -7
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +26 -7
- package/dist/testing.mjs.map +1 -1
- package/package.json +7 -3
- package/skills/diagnose/SKILL.md +52 -0
- package/skills/explore/SKILL.md +65 -0
- package/dist/chunk-24EL4Z2W.mjs.map +0 -1
- package/dist/chunk-ESKK5I4F.mjs.map +0 -1
- package/dist/chunk-KWS3VEER.mjs.map +0 -1
- package/dist/chunk-NJGID6BG.mjs +0 -115
- package/dist/chunk-NJGID6BG.mjs.map +0 -1
- package/dist/chunk-PFH7WFIB.mjs.map +0 -1
|
@@ -3,25 +3,39 @@ import {
|
|
|
3
3
|
MODE_NOT_THE_PLANNERS,
|
|
4
4
|
OPS_NOT_READ_ONLY,
|
|
5
5
|
PlanParseError,
|
|
6
|
+
SCRIPT_BODY_FIELDS,
|
|
6
7
|
TASK_OPS_ENVELOPE_KEY,
|
|
8
|
+
betweenRounds,
|
|
9
|
+
checkOf,
|
|
7
10
|
checkPlanInvariants,
|
|
11
|
+
clearedBodyFields,
|
|
12
|
+
commandLine,
|
|
8
13
|
detectCycle,
|
|
9
14
|
effectiveAllowlist,
|
|
10
15
|
escapeControlCharsInStrings,
|
|
11
16
|
extractObjectsWithKey,
|
|
17
|
+
loopRemovalRefusal,
|
|
18
|
+
loopReshapeRefusal,
|
|
19
|
+
loopRoundOf,
|
|
20
|
+
loopSpec,
|
|
21
|
+
notAScriptsFields,
|
|
22
|
+
notOnAScriptMessage,
|
|
12
23
|
opsPlacementRefusal,
|
|
24
|
+
outOfRounds,
|
|
13
25
|
plannedTask,
|
|
14
26
|
resolveAssignment,
|
|
15
27
|
stripTrailingCommas,
|
|
16
28
|
taggedReadOnlyMode,
|
|
17
29
|
taskWhere
|
|
18
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-YAFNYNFX.mjs";
|
|
19
31
|
import {
|
|
32
|
+
SELF_REPO,
|
|
20
33
|
addTaskToPlan,
|
|
21
34
|
canSetDependencies,
|
|
22
35
|
flattenTasks,
|
|
23
36
|
flattenTasksWithParents,
|
|
24
37
|
inheritedOps,
|
|
38
|
+
isUserMessage,
|
|
25
39
|
opsFlag,
|
|
26
40
|
pausedOnLimit,
|
|
27
41
|
rejoinPlanTree,
|
|
@@ -35,8 +49,9 @@ import {
|
|
|
35
49
|
titledTaskRef,
|
|
36
50
|
titledTaskRefOf,
|
|
37
51
|
updateTaskInPlan,
|
|
52
|
+
waitsMidAttempt,
|
|
38
53
|
walkPlan
|
|
39
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-QTZ5XTBY.mjs";
|
|
40
55
|
|
|
41
56
|
// src/models/Usage.ts
|
|
42
57
|
function partedPromptUsage(parts) {
|
|
@@ -215,13 +230,17 @@ function formatCost(currency, amount) {
|
|
|
215
230
|
function contextMeterText(fill) {
|
|
216
231
|
if (!fill) return null;
|
|
217
232
|
const used = `ctx ${formatTokenCount(fill.usedTokens)}`;
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
return `${
|
|
233
|
+
const share = fill.windowTokens ? fill.usedTokens / fill.windowTokens * 100 : void 0;
|
|
234
|
+
const meter = share === void 0 ? used : `${used} (${share < 1 ? "<1" : Math.round(share)}%)`;
|
|
235
|
+
return fill.autoCompaction ? `${meter} \xB7 no auto-compact` : meter;
|
|
221
236
|
}
|
|
222
237
|
function contextMeterTitle(fill) {
|
|
223
238
|
const used = fill.usedTokens.toLocaleString("en-US");
|
|
224
|
-
|
|
239
|
+
const counts = fill.windowTokens ? `${used} of ${fill.windowTokens.toLocaleString("en-US")} tokens` : `${used} tokens`;
|
|
240
|
+
const off = fill.autoCompaction;
|
|
241
|
+
if (!off) return counts;
|
|
242
|
+
const why = off.off === "settings" ? "Automatic compaction is off in the settings file" : off.model ? `Automatic compaction is off: no catalog Ordewell holds states ${off.model}'s window` : "Automatic compaction is off: no planner model is selected";
|
|
243
|
+
return `${counts}. ${why}; /compact still condenses the conversation.`;
|
|
225
244
|
}
|
|
226
245
|
function costParts(totals) {
|
|
227
246
|
const rank = (currency) => currency.toLowerCase() === "usd" ? "" : currency.toUpperCase();
|
|
@@ -268,6 +287,611 @@ function tasksTotalText(tasks) {
|
|
|
268
287
|
return parts.length > 0 ? `Tasks total: ${parts.join(" \xB7 ")}` : null;
|
|
269
288
|
}
|
|
270
289
|
|
|
290
|
+
// src/models/AccountLimits.ts
|
|
291
|
+
function hasAccountLimits(snapshot) {
|
|
292
|
+
return (snapshot.status === "fresh" || snapshot.status === "stale") && snapshot.buckets.some((bucket) => bucket.windows.some((window) => window.usedPercent !== null));
|
|
293
|
+
}
|
|
294
|
+
function accountLimitsOf(snapshots, runner, model) {
|
|
295
|
+
if (!runner) return [];
|
|
296
|
+
return snapshots.filter((snapshot) => snapshot.harness === runner && (snapshot.provider === void 0 || (model?.startsWith(`${snapshot.provider}/`) ?? false)));
|
|
297
|
+
}
|
|
298
|
+
var WEEK = 7 * 24 * 60;
|
|
299
|
+
var DAY = 24 * 60;
|
|
300
|
+
var SLOT_LABELS = { primary: "Primary", secondary: "Secondary", tertiary: "Tertiary" };
|
|
301
|
+
function accountLimitWindowLabel(window) {
|
|
302
|
+
if (window.period === "month") return "Monthly";
|
|
303
|
+
const minutes = window.durationMinutes;
|
|
304
|
+
if (minutes === null) return SLOT_LABELS[window.slot];
|
|
305
|
+
if (minutes === WEEK) return "Weekly";
|
|
306
|
+
if (minutes === DAY) return "Daily";
|
|
307
|
+
if (minutes % WEEK === 0) return `${minutes / WEEK}w`;
|
|
308
|
+
if (minutes % DAY === 0) return `${minutes / DAY}d`;
|
|
309
|
+
if (minutes % 60 === 0) return `${minutes / 60}h`;
|
|
310
|
+
return `${minutes}m`;
|
|
311
|
+
}
|
|
312
|
+
function accountLimitPercentText(percent) {
|
|
313
|
+
const shown = percent > 0 && percent < 1 ? "<1" : String(Math.round(percent));
|
|
314
|
+
return `${shown}% used`;
|
|
315
|
+
}
|
|
316
|
+
function accountLimitsText(snapshot) {
|
|
317
|
+
if (!hasAccountLimits(snapshot)) return null;
|
|
318
|
+
const parts = snapshot.buckets.flatMap((bucket, index) => {
|
|
319
|
+
const windows = bucket.windows.flatMap((window) => window.usedPercent === null ? [] : [`${accountLimitWindowLabel(window)} ${accountLimitPercentText(window.usedPercent)}`]);
|
|
320
|
+
if (windows.length === 0) return [];
|
|
321
|
+
return index === 0 ? windows : [`${bucket.name ?? bucket.id} ${windows.join(" \xB7 ")}`];
|
|
322
|
+
});
|
|
323
|
+
return `${snapshot.label} ${parts.join(" \xB7 ")}${snapshot.status === "stale" ? " (stale)" : ""}`;
|
|
324
|
+
}
|
|
325
|
+
function accountLimitsTitle(snapshot, time) {
|
|
326
|
+
const lines2 = snapshot.buckets.flatMap((bucket, index) => bucket.windows.map((window) => {
|
|
327
|
+
const owner = index === 0 ? snapshot.label : `${snapshot.label} ${bucket.name ?? bucket.id}`;
|
|
328
|
+
const share = window.usedPercent === null ? "not reported" : `${window.usedPercent}% used`;
|
|
329
|
+
const reset = window.resetsAt ? `, resets ${time(new Date(window.resetsAt))}` : "";
|
|
330
|
+
return `${owner} ${accountLimitWindowLabel(window)}: ${share}${reset}`;
|
|
331
|
+
}));
|
|
332
|
+
if (snapshot.plan) lines2.push(`Plan: ${snapshot.plan}`);
|
|
333
|
+
if (snapshot.readAt) {
|
|
334
|
+
const read = time(new Date(snapshot.readAt));
|
|
335
|
+
lines2.push(snapshot.status === "stale" ? `Last read ${read}; not current, a newer read is pending or failed` : `Read ${read}`);
|
|
336
|
+
}
|
|
337
|
+
return lines2.join("\n");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/models/TaskLog.ts
|
|
341
|
+
var HEAD_LINES = 60;
|
|
342
|
+
var TAIL_LINES = 40;
|
|
343
|
+
var MAX_LINE_CHARS = 2e3;
|
|
344
|
+
var DIGEST_ARGS_CHARS = 120;
|
|
345
|
+
var DIGEST_MESSAGE_CHARS = 1e3;
|
|
346
|
+
function capLine(line) {
|
|
347
|
+
return line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS)}\u2026` : line;
|
|
348
|
+
}
|
|
349
|
+
function trimToolOutput(output) {
|
|
350
|
+
const lines2 = output.split("\n");
|
|
351
|
+
const long = lines2.some((l) => l.length > MAX_LINE_CHARS);
|
|
352
|
+
if (lines2.length <= HEAD_LINES + TAIL_LINES) return long ? { output: lines2.map(capLine).join("\n") } : { output };
|
|
353
|
+
const omittedLines = lines2.length - HEAD_LINES - TAIL_LINES;
|
|
354
|
+
const kept = [
|
|
355
|
+
...lines2.slice(0, HEAD_LINES),
|
|
356
|
+
`\u2026 ${omittedLines} line${omittedLines === 1 ? "" : "s"} omitted \u2026`,
|
|
357
|
+
...lines2.slice(-TAIL_LINES)
|
|
358
|
+
];
|
|
359
|
+
return { output: kept.map(capLine).join("\n"), omittedLines };
|
|
360
|
+
}
|
|
361
|
+
function sender({ from }) {
|
|
362
|
+
return from ? { from } : {};
|
|
363
|
+
}
|
|
364
|
+
function withSubagent(event, subagentId) {
|
|
365
|
+
return subagentId ? { ...event, subagentId } : event;
|
|
366
|
+
}
|
|
367
|
+
function toTaskLogEvent(event) {
|
|
368
|
+
switch (event.type) {
|
|
369
|
+
case "turn_start":
|
|
370
|
+
if (event.command) {
|
|
371
|
+
return { type: "command", name: event.command.name, ...event.command.args ? { args: event.command.args } : {}, ...event.messageId ? { messageId: event.messageId } : {} };
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
type: "turn_start",
|
|
375
|
+
message: event.text,
|
|
376
|
+
...event.messageId ? { messageId: event.messageId } : {},
|
|
377
|
+
...event.forced ? { forced: true } : {},
|
|
378
|
+
...sender(event)
|
|
379
|
+
};
|
|
380
|
+
case "turn_end":
|
|
381
|
+
return { type: "turn_end", reason: event.reason };
|
|
382
|
+
case "assistant_text_delta":
|
|
383
|
+
return { type: "text_delta", runId: event.runId, text: event.text };
|
|
384
|
+
case "assistant_text":
|
|
385
|
+
return { type: "text", runId: event.runId, text: event.text };
|
|
386
|
+
case "thinking_delta":
|
|
387
|
+
case "thinking":
|
|
388
|
+
return withSubagent({ type: event.type, text: event.text }, event.subagentId);
|
|
389
|
+
case "tool_call":
|
|
390
|
+
return withSubagent({ type: "tool_call", id: event.id, name: event.name, args: JSON.stringify(event.args) }, event.subagentId);
|
|
391
|
+
case "tool_result": {
|
|
392
|
+
const { output, omittedLines } = trimToolOutput(event.output);
|
|
393
|
+
return withSubagent({ type: "tool_result", id: event.id, output, success: event.success, ...omittedLines ? { omittedLines } : {} }, event.subagentId);
|
|
394
|
+
}
|
|
395
|
+
case "subagent_started":
|
|
396
|
+
return { type: "subagent_started", subagentId: event.subagentId, brief: event.brief, ...event.model ? { model: event.model } : {} };
|
|
397
|
+
case "subagent_finished":
|
|
398
|
+
return { type: "subagent_finished", subagentId: event.subagentId, outcome: event.outcome, digest: event.digest };
|
|
399
|
+
case "usage":
|
|
400
|
+
return { type: "usage", record: event.record };
|
|
401
|
+
case "message_queued":
|
|
402
|
+
return { type: "message_queued", messageId: event.messageId, text: event.text, ...event.forced ? { forced: true } : {}, ...sender(event) };
|
|
403
|
+
case "message_undelivered":
|
|
404
|
+
return { type: "message_undelivered", messageId: event.messageId, text: event.text, ...sender(event) };
|
|
405
|
+
case "message_removed":
|
|
406
|
+
return { type: "message_removed", messageId: event.messageId };
|
|
407
|
+
case "message_handed_over":
|
|
408
|
+
return { type: "message_handed_over", messageId: event.messageId };
|
|
409
|
+
case "message_delivered":
|
|
410
|
+
return { type: "message_delivered", messageId: event.messageId, text: event.text, ...sender(event) };
|
|
411
|
+
case "error":
|
|
412
|
+
return { type: "error", message: event.message };
|
|
413
|
+
case "permission_request":
|
|
414
|
+
return {
|
|
415
|
+
type: "approval_requested",
|
|
416
|
+
approvalId: event.id,
|
|
417
|
+
tool: event.name,
|
|
418
|
+
args: event.detail,
|
|
419
|
+
allowForTask: (event.suggestions?.length ?? 0) > 0,
|
|
420
|
+
...event.decided ? { decided: event.decided.decision } : {},
|
|
421
|
+
...event.toolUseId ? { toolCallId: event.toolUseId } : {}
|
|
422
|
+
};
|
|
423
|
+
case "permission_decided": {
|
|
424
|
+
const note = event.decision.decision === "deny" ? event.decision.note : void 0;
|
|
425
|
+
return { type: "approval_decided", approvalId: event.id, decision: event.decision.decision, ...note ? { note } : {} };
|
|
426
|
+
}
|
|
427
|
+
case "permission_withdrawn":
|
|
428
|
+
return { type: "approval_withdrawn", approvalId: event.id };
|
|
429
|
+
case "runner_loaded":
|
|
430
|
+
return { type: "runner_loaded", commands: event.commands, mcpServers: event.mcpServers };
|
|
431
|
+
case "compaction":
|
|
432
|
+
return {
|
|
433
|
+
type: "compaction",
|
|
434
|
+
trigger: event.trigger,
|
|
435
|
+
...event.preTokens !== void 0 ? { preTokens: event.preTokens } : {},
|
|
436
|
+
...event.postTokens !== void 0 ? { postTokens: event.postTokens } : {}
|
|
437
|
+
};
|
|
438
|
+
case "command_output":
|
|
439
|
+
return { type: "command_output", text: event.text };
|
|
440
|
+
case "script_run":
|
|
441
|
+
return "argv" in event ? { type: "script_run", argv: [...event.argv] } : { type: "script_run", script: event.script };
|
|
442
|
+
case "script_output":
|
|
443
|
+
return { type: "script_output", text: event.text, ...event.omittedChars ? { omittedChars: event.omittedChars } : {} };
|
|
444
|
+
case "script_exit":
|
|
445
|
+
return {
|
|
446
|
+
type: "script_exit",
|
|
447
|
+
code: event.code,
|
|
448
|
+
signal: event.signal,
|
|
449
|
+
durationMs: event.durationMs,
|
|
450
|
+
...event.timedOut ? { timedOut: true } : {},
|
|
451
|
+
...event.error !== void 0 ? { error: event.error } : {}
|
|
452
|
+
};
|
|
453
|
+
// The call is in the log already; its receipt is the task's state, not an entry.
|
|
454
|
+
case "completion_received":
|
|
455
|
+
return null;
|
|
456
|
+
// Logged once the plan's write-back settles: see `TaskLogRecorder.note`.
|
|
457
|
+
case "model_switched":
|
|
458
|
+
case "model_drifted":
|
|
459
|
+
case "background_agent":
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function lastServedModel(events) {
|
|
464
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
465
|
+
const event = events[i];
|
|
466
|
+
if (event.type === "usage" && event.record.model && !event.record.subagentId && !event.record.aggregate) return event.record.model;
|
|
467
|
+
}
|
|
468
|
+
return void 0;
|
|
469
|
+
}
|
|
470
|
+
function taskLogForSurface(events) {
|
|
471
|
+
return events.map((event) => event.type === "task_skills" ? { type: "task_skills", skills: event.skills.map(({ name, source, path }) => ({ name, source, path })) } : event);
|
|
472
|
+
}
|
|
473
|
+
function coalesceTaskLog(events) {
|
|
474
|
+
const out = [];
|
|
475
|
+
for (const event of events) {
|
|
476
|
+
const last = out[out.length - 1];
|
|
477
|
+
if (last?.type === "text_delta" && event.type === "text_delta" && last.runId === event.runId) {
|
|
478
|
+
out[out.length - 1] = { ...last, text: last.text + event.text };
|
|
479
|
+
} else if (last?.type === "thinking_delta" && event.type === "thinking_delta" && last.subagentId === event.subagentId) {
|
|
480
|
+
out[out.length - 1] = { ...last, text: last.text + event.text };
|
|
481
|
+
} else if (last?.type === "script_output" && event.type === "script_output" && !last.omittedChars && !event.omittedChars) {
|
|
482
|
+
out[out.length - 1] = { ...last, text: last.text + event.text };
|
|
483
|
+
} else {
|
|
484
|
+
out.push(event);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return out;
|
|
488
|
+
}
|
|
489
|
+
function scriptRunText(run) {
|
|
490
|
+
if ("argv" in run) return commandLine(run.argv);
|
|
491
|
+
const lines2 = run.script.trim().split("\n");
|
|
492
|
+
return lines2.length > 1 ? `${lines2[0]} \u2026 (${lines2.length} lines)` : lines2[0];
|
|
493
|
+
}
|
|
494
|
+
function secondsText(ms) {
|
|
495
|
+
return `${Math.round(ms / 100) / 10} s`;
|
|
496
|
+
}
|
|
497
|
+
function scriptExitText(exit) {
|
|
498
|
+
const seconds = secondsText(exit.durationMs);
|
|
499
|
+
if (exit.error !== void 0) return `could not start: ${exit.error}`;
|
|
500
|
+
const how = exit.signal ? `ended by ${exit.signal}` : `exited ${exit.code ?? "unknown"}`;
|
|
501
|
+
return `${how} after ${seconds}${exit.timedOut ? ": the time limit ran out" : ""}`;
|
|
502
|
+
}
|
|
503
|
+
function digestTaskLog(events, maxCalls) {
|
|
504
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
505
|
+
let lastText;
|
|
506
|
+
let lastError;
|
|
507
|
+
for (const event of events) {
|
|
508
|
+
if (event.type === "tool_result") outcomes.set(event.id, event.success);
|
|
509
|
+
else if (event.type === "text") lastText = event.text;
|
|
510
|
+
else if (event.type === "error") lastError = event.message;
|
|
511
|
+
}
|
|
512
|
+
const lines2 = [];
|
|
513
|
+
let step;
|
|
514
|
+
for (const event of events) {
|
|
515
|
+
if (event.type === "script_run") {
|
|
516
|
+
if (step !== void 0) lines2.push(`- ${step} \u2192 no result`);
|
|
517
|
+
step = scriptRunText(event);
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (event.type === "script_exit" && step !== void 0) {
|
|
521
|
+
lines2.push(`- ${step} \u2192 ${scriptExitText(event)}`);
|
|
522
|
+
step = void 0;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (event.type !== "tool_call") continue;
|
|
526
|
+
const outcome = outcomes.get(event.id);
|
|
527
|
+
const args = event.args.length > DIGEST_ARGS_CHARS ? `${event.args.slice(0, DIGEST_ARGS_CHARS)}\u2026` : event.args;
|
|
528
|
+
lines2.push(`- ${event.name} ${args} \u2192 ${outcome === void 0 ? "no result" : outcome ? "ok" : "failed"}`);
|
|
529
|
+
}
|
|
530
|
+
if (step !== void 0) lines2.push(`- ${step} \u2192 no result`);
|
|
531
|
+
const omitted = lines2.length - maxCalls;
|
|
532
|
+
const out = omitted > 0 ? [`\u2026 ${omitted} earlier call${omitted === 1 ? "" : "s"} omitted \u2026`, ...lines2.slice(omitted)] : lines2;
|
|
533
|
+
const gap = () => out.length > 0 ? [""] : [];
|
|
534
|
+
const said = lastText?.trim();
|
|
535
|
+
if (said) {
|
|
536
|
+
const end = said.length > DIGEST_MESSAGE_CHARS ? `\u2026${said.slice(-DIGEST_MESSAGE_CHARS)}` : said;
|
|
537
|
+
out.push(...gap(), "Its last message:", ...end.split("\n").map((l) => ` ${l}`));
|
|
538
|
+
}
|
|
539
|
+
if (lastError) out.push(...gap(), `It stopped with an error: ${lastError}`);
|
|
540
|
+
return out.join("\n");
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/models/Handoff.ts
|
|
544
|
+
var SEVERITY_LABEL = {
|
|
545
|
+
blocker: "Blocker",
|
|
546
|
+
major: "Major",
|
|
547
|
+
minor: "Minor",
|
|
548
|
+
info: "Info"
|
|
549
|
+
};
|
|
550
|
+
function severityLabel(severity) {
|
|
551
|
+
return SEVERITY_LABEL[severity];
|
|
552
|
+
}
|
|
553
|
+
var SEVERITY_RANK = { blocker: 0, major: 1, minor: 2, info: 3 };
|
|
554
|
+
function worstSeverity(findings) {
|
|
555
|
+
let worst = null;
|
|
556
|
+
for (const { severity } of findings) {
|
|
557
|
+
if (worst === null || SEVERITY_RANK[severity] < SEVERITY_RANK[worst]) worst = severity;
|
|
558
|
+
}
|
|
559
|
+
return worst;
|
|
560
|
+
}
|
|
561
|
+
var REPORT_MAX_CHARS = 2e4;
|
|
562
|
+
var HANDOFF_LIMITS = {
|
|
563
|
+
summary: 1500,
|
|
564
|
+
commitSubject: 100,
|
|
565
|
+
commitBody: 500,
|
|
566
|
+
findings: 50,
|
|
567
|
+
title: 200,
|
|
568
|
+
detail: 2e3,
|
|
569
|
+
reference: 2e3,
|
|
570
|
+
evidence: 10,
|
|
571
|
+
artifacts: 20,
|
|
572
|
+
report: REPORT_MAX_CHARS
|
|
573
|
+
};
|
|
574
|
+
var DIGEST_FINDINGS = 25;
|
|
575
|
+
function storedHandoff(handoff) {
|
|
576
|
+
const summary = handoff.summary.trim();
|
|
577
|
+
const findings = (handoff.findings ?? []).map((f, i) => ({ id: `F${i + 1}`, ...f }));
|
|
578
|
+
const artifacts = handoff.artifacts ?? [];
|
|
579
|
+
const report = handoff.report?.trim();
|
|
580
|
+
const commit = handoff.status === "done" && handoff.commit ? withoutTrailers(handoff.commit) : void 0;
|
|
581
|
+
if (!commit && !summary && findings.length === 0 && artifacts.length === 0 && !report) return void 0;
|
|
582
|
+
return {
|
|
583
|
+
...commit ? { commit } : {},
|
|
584
|
+
summary,
|
|
585
|
+
findings,
|
|
586
|
+
findingCount: findings.length,
|
|
587
|
+
...artifacts.length > 0 ? { artifacts } : {},
|
|
588
|
+
...report ? { report } : {}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function withPatch(handoff, patch) {
|
|
592
|
+
if (!handoff) return { summary: "", findings: [], findingCount: 0, artifacts: [patch] };
|
|
593
|
+
return { ...handoff, artifacts: [...handoff.artifacts ?? [], patch] };
|
|
594
|
+
}
|
|
595
|
+
function savedPatchOf(handoff) {
|
|
596
|
+
return handoff?.artifacts?.find((a) => a.kind === "patch");
|
|
597
|
+
}
|
|
598
|
+
var ATTRIBUTION_TRAILER = /^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)+:\s*\S/;
|
|
599
|
+
var GENERATED_WITH = /^\W*Generated (?:with|by)\b/i;
|
|
600
|
+
var TRAILER = /^[A-Za-z0-9-]+:\s*\S/;
|
|
601
|
+
function withoutTrailers({ subject, body }) {
|
|
602
|
+
const kept = (body ?? "").split("\n").filter((line) => !ATTRIBUTION_TRAILER.test(line.trim()) && !GENERATED_WITH.test(line.trim()));
|
|
603
|
+
const paragraphs = kept.join("\n").trim().split(/\n\s*\n/);
|
|
604
|
+
const last = paragraphs.at(-1).split("\n");
|
|
605
|
+
if (paragraphs.length > 1 && last.every((line) => TRAILER.test(line.trim()))) paragraphs.pop();
|
|
606
|
+
const cleaned = paragraphs.join("\n\n").trim();
|
|
607
|
+
return { subject, ...cleaned ? { body: cleaned } : {} };
|
|
608
|
+
}
|
|
609
|
+
function handoffDigest(handoff) {
|
|
610
|
+
return {
|
|
611
|
+
...handoff.commit ? { commit: handoff.commit } : {},
|
|
612
|
+
summary: handoff.summary,
|
|
613
|
+
findings: handoff.findings.slice(0, DIGEST_FINDINGS),
|
|
614
|
+
findingCount: handoff.findingCount,
|
|
615
|
+
...handoff.artifacts ? { artifacts: handoff.artifacts } : {}
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function namedHandoff(plan, handoff) {
|
|
619
|
+
const named = (refs) => refs.map((r) => r.kind === "task" ? { ...r, task: taskRefOf(plan, r.task) ?? r.task } : r);
|
|
620
|
+
return {
|
|
621
|
+
...handoff,
|
|
622
|
+
findings: handoff.findings.map((f) => f.evidence ? { ...f, evidence: named(f.evidence) } : f),
|
|
623
|
+
...handoff.artifacts ? { artifacts: named(handoff.artifacts) } : {}
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function handoffReferenceText(ref, taskName = () => void 0) {
|
|
627
|
+
switch (ref.kind) {
|
|
628
|
+
case "file":
|
|
629
|
+
return `${ref.line !== void 0 ? `${ref.path}:${ref.line}` : ref.path}${ref.resolved === true ? "" : " (unresolved)"}`;
|
|
630
|
+
case "commit":
|
|
631
|
+
return `commit ${ref.sha}${ref.repo ? ` in ${ref.repo}` : ""}${ref.resolved === true ? "" : " (unresolved)"}`;
|
|
632
|
+
case "task":
|
|
633
|
+
return `task ${taskName(ref.task) ?? ref.task}${ref.finding ? ` ${ref.finding}` : ""}`;
|
|
634
|
+
case "url":
|
|
635
|
+
return ref.url;
|
|
636
|
+
case "patch":
|
|
637
|
+
return patchText(ref);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
var FLAG_WORDS = {
|
|
641
|
+
excluded: "not carried (links and submodules)",
|
|
642
|
+
dropped: "dropped over the size cap",
|
|
643
|
+
normalized: "normalized by git attributes"
|
|
644
|
+
};
|
|
645
|
+
function counted(count, noun) {
|
|
646
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
647
|
+
}
|
|
648
|
+
function byteSize(bytes) {
|
|
649
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
650
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
651
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
652
|
+
}
|
|
653
|
+
function patchFlagText(flag) {
|
|
654
|
+
const more = flag.count - flag.paths.length;
|
|
655
|
+
return `${FLAG_WORDS[flag.kind]}: ${counted(flag.count, "file")}${flag.paths.length ? ` (${flag.paths.join(", ")}${more > 0 ? `, +${more} more` : ""})` : ""}`;
|
|
656
|
+
}
|
|
657
|
+
function patchChangesText(patch) {
|
|
658
|
+
const binary = patch.binary > 0 ? `, ${patch.binary} binary` : "";
|
|
659
|
+
return `${counted(patch.files, "file")}${binary}, +${patch.added} -${patch.removed}, ${byteSize(patch.bytes)}`;
|
|
660
|
+
}
|
|
661
|
+
function patchLinesText(patch) {
|
|
662
|
+
const binary = patch.binary > 0 ? `, ${patch.binary} binary` : "";
|
|
663
|
+
return `${counted(patch.files, "file")}${binary}, +${patch.added} -${patch.removed}`;
|
|
664
|
+
}
|
|
665
|
+
function patchText(patch) {
|
|
666
|
+
const flags = patch.flags?.length ? `; ${patch.flags.map(patchFlagText).join("; ")}` : "";
|
|
667
|
+
return `patch ${patch.path} (attempt ${patch.attempt}): ${patchChangesText(patch)}, from commit ${patch.base.slice(0, 12)}${flags}`;
|
|
668
|
+
}
|
|
669
|
+
function patchLines(patch) {
|
|
670
|
+
const view = patchView(patch);
|
|
671
|
+
return [`Patch: ${view.changes}, ${view.base}`, view.saved, ...view.flags];
|
|
672
|
+
}
|
|
673
|
+
function patchView(patch) {
|
|
674
|
+
return {
|
|
675
|
+
path: patch.path,
|
|
676
|
+
attempt: `attempt ${patch.attempt}`,
|
|
677
|
+
changes: patchChangesText(patch),
|
|
678
|
+
base: `from commit ${patch.base.slice(0, 12)}`,
|
|
679
|
+
saved: `saved as ${patch.path} (attempt ${patch.attempt})`,
|
|
680
|
+
flags: (patch.flags ?? []).map(patchFlagText),
|
|
681
|
+
peek: `patch: ${patchLinesText(patch)}`
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function savedPatchViews(handoff) {
|
|
685
|
+
return (handoff?.artifacts ?? []).filter((a) => a.kind === "patch").map(patchView);
|
|
686
|
+
}
|
|
687
|
+
function savedPatchView(handoff) {
|
|
688
|
+
return savedPatchViews(handoff)[0] ?? null;
|
|
689
|
+
}
|
|
690
|
+
function findingLines(f, taskName = () => void 0) {
|
|
691
|
+
return [
|
|
692
|
+
`- ${f.id} [${f.severity}] ${f.title}`,
|
|
693
|
+
...f.detail ? f.detail.split("\n").map((l) => ` ${l}`) : [],
|
|
694
|
+
...f.evidence?.length ? [` Evidence: ${f.evidence.map((e) => handoffReferenceText(e, taskName)).join("; ")}`] : []
|
|
695
|
+
];
|
|
696
|
+
}
|
|
697
|
+
function artifactLines(handoff, taskName = () => void 0) {
|
|
698
|
+
return handoff.artifacts?.length ? [`Artifacts: ${handoff.artifacts.map((a) => handoffReferenceText(a, taskName)).join("; ")}`] : [];
|
|
699
|
+
}
|
|
700
|
+
function openingLines(handoff) {
|
|
701
|
+
const { commit, summary } = handoff;
|
|
702
|
+
if (!commit) return [summary];
|
|
703
|
+
return [`Commit: ${commit.subject}`, ...commit.body ? commit.body.split("\n") : [], ...summary ? ["", summary] : []];
|
|
704
|
+
}
|
|
705
|
+
var HANDOFF_PUSH_CHARS = 4e3;
|
|
706
|
+
function boundedHandoffLines(handoff, omission, taskName = () => void 0, footer = "") {
|
|
707
|
+
const lines2 = openingLines(handoff);
|
|
708
|
+
let shown = 0;
|
|
709
|
+
const closing = (count) => [
|
|
710
|
+
...handoff.findingCount > count ? [omission(handoff.findingCount - count)] : [],
|
|
711
|
+
...footer ? ["", footer] : []
|
|
712
|
+
];
|
|
713
|
+
for (const finding of handoff.findings) {
|
|
714
|
+
const next = [...shown === 0 ? ["", "Findings:"] : [], ...findingLines(finding, taskName)];
|
|
715
|
+
if ([...lines2, ...next, ...closing(shown + 1)].join("\n").length > HANDOFF_PUSH_CHARS) break;
|
|
716
|
+
lines2.push(...next);
|
|
717
|
+
shown++;
|
|
718
|
+
}
|
|
719
|
+
lines2.push(...closing(shown));
|
|
720
|
+
const artifacts = artifactLines(handoff, taskName);
|
|
721
|
+
if (artifacts.length) lines2.push("", ...artifacts);
|
|
722
|
+
return lines2;
|
|
723
|
+
}
|
|
724
|
+
function handoffPreviewLines(handoff, options = {}) {
|
|
725
|
+
const limit = Math.max(0, options.maxFindings ?? handoff.findings.length);
|
|
726
|
+
const shown = handoff.findings.slice(0, limit);
|
|
727
|
+
const more = handoff.findingCount - shown.length;
|
|
728
|
+
const summary = handoff.summary.trim();
|
|
729
|
+
const { commit } = handoff;
|
|
730
|
+
const patches = (handoff.artifacts ?? []).filter((a) => a.kind === "patch");
|
|
731
|
+
const others = (handoff.artifacts ?? []).filter((a) => a.kind !== "patch");
|
|
732
|
+
const artifacts = others.length ? artifactLines({ ...handoff, artifacts: others }, options.taskName) : [];
|
|
733
|
+
return [
|
|
734
|
+
...commit ? [`Commit: ${commit.subject}`, ...commit.body ? commit.body.split("\n") : []] : [],
|
|
735
|
+
...summary ? summary.split("\n") : [],
|
|
736
|
+
...shown.map((f) => `${f.id} [${f.severity}] ${f.title}`),
|
|
737
|
+
...more > 0 ? [`+${more} more finding${more === 1 ? "" : "s"}, in the attempt's log`] : [],
|
|
738
|
+
...artifacts,
|
|
739
|
+
...patches.flatMap(patchLines)
|
|
740
|
+
];
|
|
741
|
+
}
|
|
742
|
+
function handoffWakeLines(handoff, taskName = () => void 0) {
|
|
743
|
+
return boundedHandoffLines(
|
|
744
|
+
handoff,
|
|
745
|
+
(more) => `+${more} more finding${more === 1 ? "" : "s"}; read with task_output.`,
|
|
746
|
+
taskName,
|
|
747
|
+
"The report, if the task wrote one, is in the attempt's log: read it with task_output."
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
function handoffLines(handoff, taskName = () => void 0) {
|
|
751
|
+
const lines2 = openingLines(handoff);
|
|
752
|
+
if (handoff.findings.length > 0) lines2.push("", "Findings:", ...handoff.findings.flatMap((f) => findingLines(f, taskName)));
|
|
753
|
+
const more = handoff.findingCount - handoff.findings.length;
|
|
754
|
+
if (more > 0) lines2.push(`+${more} more finding${more === 1 ? "" : "s"}, in the attempt's log`);
|
|
755
|
+
const artifacts = artifactLines(handoff, taskName);
|
|
756
|
+
if (artifacts.length > 0) lines2.push("", ...artifacts);
|
|
757
|
+
if ("report" in handoff && handoff.report) lines2.push("", "Report:", handoff.report);
|
|
758
|
+
return lines2;
|
|
759
|
+
}
|
|
760
|
+
function readableTaskIds(plan, taskId) {
|
|
761
|
+
const tasks = flattenTasks(plan);
|
|
762
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
763
|
+
const readable = /* @__PURE__ */ new Set([taskId]);
|
|
764
|
+
const pending = [...byId.get(taskId)?.dependencies ?? []];
|
|
765
|
+
for (let id = pending.pop(); id !== void 0; id = pending.pop()) {
|
|
766
|
+
if (readable.has(id)) continue;
|
|
767
|
+
readable.add(id);
|
|
768
|
+
pending.push(...byId.get(id)?.dependencies ?? []);
|
|
769
|
+
}
|
|
770
|
+
for (const id of byId.get(taskId)?.loop?.body ?? []) readable.add(id);
|
|
771
|
+
const check = checkOf(tasks, taskId);
|
|
772
|
+
if (check) readable.add(check.id);
|
|
773
|
+
return readable;
|
|
774
|
+
}
|
|
775
|
+
var RESEND = "Fix or drop each reference named, then call task_complete again; nothing was recorded.";
|
|
776
|
+
function checkHandoff(handoff, producerId, plan) {
|
|
777
|
+
const readable = readableTaskIds(plan, producerId);
|
|
778
|
+
const refusals = [];
|
|
779
|
+
const check = (ref, field) => {
|
|
780
|
+
if (ref.kind === "url") {
|
|
781
|
+
if (!isWebUrl(ref.url)) refusals.push(`${field}.url: ${JSON.stringify(ref.url)} is refused: only an http or https URL is accepted.`);
|
|
782
|
+
return ref;
|
|
783
|
+
}
|
|
784
|
+
if (ref.kind !== "task") return ref;
|
|
785
|
+
const target = resolveTaskRef(plan, ref.task);
|
|
786
|
+
if (!target) {
|
|
787
|
+
refusals.push(`${field}.task: ${JSON.stringify(ref.task)} names no task in the plan: give its "#order", its id or its title.`);
|
|
788
|
+
return ref;
|
|
789
|
+
}
|
|
790
|
+
const named = titledTaskRefOf(plan, target.id) ?? target.id;
|
|
791
|
+
if (!readable.has(target.id)) {
|
|
792
|
+
refusals.push(`${field}.task: ${JSON.stringify(ref.task)} is ${named}, which this task may not read: only itself, a task it depends on, or its loop's other end.`);
|
|
793
|
+
return ref;
|
|
794
|
+
}
|
|
795
|
+
if (ref.finding !== void 0) {
|
|
796
|
+
const count = target.handoff?.findingCount ?? 0;
|
|
797
|
+
if (Number(ref.finding.slice(1)) > count) {
|
|
798
|
+
refusals.push(`${field}.finding: ${JSON.stringify(ref.finding)} is not a finding of ${named}: ${count > 0 ? `its latest handoff has F1 to F${count}` : "its latest handoff has none"}.`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return { ...ref, task: target.id };
|
|
802
|
+
};
|
|
803
|
+
const findings = handoff.findings?.map((f, i) => f.evidence ? { ...f, evidence: f.evidence.map((e, j) => check(e, `findings[${i}].evidence[${j}]`)) } : f);
|
|
804
|
+
const artifacts = handoff.artifacts?.map((a, i) => a.kind === "patch" ? a : check(a, `artifacts[${i}]`));
|
|
805
|
+
if (refusals.length > 0) return { ok: false, refusal: [...refusals, RESEND].join("\n") };
|
|
806
|
+
return { ok: true, handoff: { ...handoff, ...findings ? { findings } : {}, ...artifacts ? { artifacts } : {} } };
|
|
807
|
+
}
|
|
808
|
+
function isWebUrl(url) {
|
|
809
|
+
try {
|
|
810
|
+
const { protocol } = new URL(url);
|
|
811
|
+
return protocol === "http:" || protocol === "https:";
|
|
812
|
+
} catch {
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/services/researchStepSummary.ts
|
|
818
|
+
function ellipsize(text2, max) {
|
|
819
|
+
const oneLine2 = text2.replace(/\s+/g, " ").trim();
|
|
820
|
+
return oneLine2.length > max ? `${oneLine2.slice(0, max)}\u2026` : oneLine2;
|
|
821
|
+
}
|
|
822
|
+
function summarizeToolCall(tool, argsJson, toolLabel) {
|
|
823
|
+
const display = toolLabel?.trim() || tool;
|
|
824
|
+
if (tool === "agent_tool") {
|
|
825
|
+
try {
|
|
826
|
+
const args = JSON.parse(argsJson);
|
|
827
|
+
const hint = args.path ?? args.file_path ?? args.filePath ?? args.pattern ?? args.query ?? args.url ?? args.command;
|
|
828
|
+
if (hint) return `${display} ${ellipsize(String(hint), 50)}`;
|
|
829
|
+
} catch {
|
|
830
|
+
}
|
|
831
|
+
return display;
|
|
832
|
+
}
|
|
833
|
+
if (tool === "spawn_research_agent") {
|
|
834
|
+
try {
|
|
835
|
+
const args = JSON.parse(argsJson);
|
|
836
|
+
const prompt = typeof args.prompt === "string" ? args.prompt : "";
|
|
837
|
+
const oneLine2 = prompt.replace(/\s+/g, " ").trim();
|
|
838
|
+
return `spawn: "${oneLine2.slice(0, 60)}${oneLine2.length > 60 ? "\u2026" : ""}"`;
|
|
839
|
+
} catch {
|
|
840
|
+
return "spawn_research_agent";
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
try {
|
|
844
|
+
const args = JSON.parse(argsJson);
|
|
845
|
+
if (tool === "find_symbol" && args.symbol) {
|
|
846
|
+
return `find_symbol ${args.symbol}${args.language ? ` [${args.language}]` : ""}`;
|
|
847
|
+
}
|
|
848
|
+
if (tool === "web_search" && args.query) {
|
|
849
|
+
return `web_search "${ellipsize(String(args.query), 50)}"`;
|
|
850
|
+
}
|
|
851
|
+
if (tool === "fetch" && args.url) {
|
|
852
|
+
return `fetch ${ellipsize(String(args.url), 60)}`;
|
|
853
|
+
}
|
|
854
|
+
if (tool === "glob" && args.pattern) {
|
|
855
|
+
return `glob ${ellipsize(String(args.pattern), 50)}`;
|
|
856
|
+
}
|
|
857
|
+
if (tool === "grep" && args.pattern) {
|
|
858
|
+
const mode = args.output_mode && args.output_mode !== "content" ? ` (${args.output_mode})` : "";
|
|
859
|
+
const scope = args.include ? ` in ${args.include}` : "";
|
|
860
|
+
return `grep ${ellipsize(String(args.pattern), 40)}${scope}${mode}`;
|
|
861
|
+
}
|
|
862
|
+
const path = args.filePath || args.file_path || args.path;
|
|
863
|
+
if (path) {
|
|
864
|
+
const basename = String(path).split("/").pop() || String(path);
|
|
865
|
+
return `${display} ${basename}`;
|
|
866
|
+
}
|
|
867
|
+
if (args.pattern) {
|
|
868
|
+
return `${display} ${args.pattern}`;
|
|
869
|
+
}
|
|
870
|
+
if (args.command) {
|
|
871
|
+
return `bash: ${String(args.command).slice(0, 40)}`;
|
|
872
|
+
}
|
|
873
|
+
} catch {
|
|
874
|
+
}
|
|
875
|
+
return display;
|
|
876
|
+
}
|
|
877
|
+
var SEALED_RUN_MARK = "[Ran sealed";
|
|
878
|
+
function ranSealed(output) {
|
|
879
|
+
return output.startsWith(SEALED_RUN_MARK);
|
|
880
|
+
}
|
|
881
|
+
function withoutSealedMark(output) {
|
|
882
|
+
if (!ranSealed(output)) return output;
|
|
883
|
+
const end = output.indexOf("\n");
|
|
884
|
+
return end < 0 ? "" : output.slice(end + 1);
|
|
885
|
+
}
|
|
886
|
+
function classifyOutcome(success, output) {
|
|
887
|
+
if (!success) {
|
|
888
|
+
if (/^(Command|Fetch|Web search) refused:/.test(output) || /is not available\. You are a read-only research subagent/.test(output)) return "refused";
|
|
889
|
+
if (/not approved/i.test(output) || /^Access denied:/.test(output) || /needs user approval/i.test(output)) return "denied";
|
|
890
|
+
return "failure";
|
|
891
|
+
}
|
|
892
|
+
return "success";
|
|
893
|
+
}
|
|
894
|
+
|
|
271
895
|
// src/services/harness/agentTools.ts
|
|
272
896
|
var KNOWN_TOOLS = {
|
|
273
897
|
// Claude Code
|
|
@@ -275,6 +899,8 @@ var KNOWN_TOOLS = {
|
|
|
275
899
|
grep: "grep",
|
|
276
900
|
glob: "glob",
|
|
277
901
|
bash: "bash",
|
|
902
|
+
// Its second shell tool, under the same opt-in and launcher (ADR-0036 N6).
|
|
903
|
+
monitor: "bash",
|
|
278
904
|
ls: "list_dir",
|
|
279
905
|
webfetch: "fetch",
|
|
280
906
|
websearch: "web_search",
|
|
@@ -501,6 +1127,20 @@ function subagentBlock(id, { toolCallId, model, usage, turnId, ...fields }) {
|
|
|
501
1127
|
...turnId ? { turnId } : {}
|
|
502
1128
|
};
|
|
503
1129
|
}
|
|
1130
|
+
function compactionBlock(id, { trigger, compactionId, origin, status, summary, preTokens, postTokens, turnId }) {
|
|
1131
|
+
return {
|
|
1132
|
+
type: "compaction",
|
|
1133
|
+
id,
|
|
1134
|
+
trigger,
|
|
1135
|
+
...compactionId ? { compactionId } : {},
|
|
1136
|
+
...origin ? { origin } : {},
|
|
1137
|
+
...status ? { status } : {},
|
|
1138
|
+
...summary ? { summary } : {},
|
|
1139
|
+
...preTokens !== void 0 ? { preTokens } : {},
|
|
1140
|
+
...postTokens !== void 0 ? { postTokens } : {},
|
|
1141
|
+
...turnId ? { turnId } : {}
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
504
1144
|
function usageBlock(id, { totals, bySubagent, byModel, model, contextFill }) {
|
|
505
1145
|
return { type: "usage", id, totals, ...byModel ? { byModel } : {}, ...model ? { model } : {}, ...bySubagent ? { bySubagent } : {}, ...contextFill ? { contextFill } : {} };
|
|
506
1146
|
}
|
|
@@ -513,7 +1153,9 @@ var STATUS_OF_OUTCOME = {
|
|
|
513
1153
|
};
|
|
514
1154
|
function finishedTool(call, output, outcome) {
|
|
515
1155
|
const diff = outcome === "success" && isFileEditTool(call.toolLabel ?? call.tool) ? diffStat(output) : null;
|
|
516
|
-
|
|
1156
|
+
const sealed2 = call.tool === "bash" && ranSealed(output);
|
|
1157
|
+
const shown = sealed2 ? withoutSealedMark(output) : output;
|
|
1158
|
+
return { ...call, status: STATUS_OF_OUTCOME[outcome], outcome, output: shown, outputLineCount: outputLines(shown).length, ...diff ? { diff } : {}, ...sealed2 ? { sealed: sealed2 } : {} };
|
|
517
1159
|
}
|
|
518
1160
|
function settledTool(call, step) {
|
|
519
1161
|
return finishedTool(call, step.result, step.outcome);
|
|
@@ -654,7 +1296,11 @@ function closeSubagent(block, cut) {
|
|
|
654
1296
|
return status === block.status && children.every((c, i) => c === block.children[i]) ? block : { ...block, status, children };
|
|
655
1297
|
}
|
|
656
1298
|
function closeOpenBlocks(view, cut) {
|
|
657
|
-
const blocks = view.blocks.map((b) =>
|
|
1299
|
+
const blocks = view.blocks.map((b) => {
|
|
1300
|
+
if (b.type === "subagent") return closeSubagent(b, cut);
|
|
1301
|
+
if (b.type === "compaction") return b.status === "running" && (cut === "stopped" || cut === "failed") ? { ...b, status: cut } : b;
|
|
1302
|
+
return closeBlock(b, cut !== null);
|
|
1303
|
+
});
|
|
658
1304
|
return blocks.every((b, i) => b === view.blocks[i]) ? view : { ...view, blocks };
|
|
659
1305
|
}
|
|
660
1306
|
|
|
@@ -864,17 +1510,17 @@ function endTurn(view, { reason }) {
|
|
|
864
1510
|
}
|
|
865
1511
|
function queueMessage(view, { messageId, text: text2, forced, from }) {
|
|
866
1512
|
const listed = view.queued.find((m) => m.id === messageId);
|
|
867
|
-
const
|
|
1513
|
+
const sender2 = from ? { from } : {};
|
|
868
1514
|
if (forced) {
|
|
869
1515
|
const rest = view.queued.filter((m) => m !== listed);
|
|
870
1516
|
const at = rest.findIndex((m) => !m.forced);
|
|
871
1517
|
const placed = at < 0 ? rest.length : at;
|
|
872
1518
|
const handedOver = listed?.handedOver ? { handedOver: true } : {};
|
|
873
|
-
return { ...view, queued: [...rest.slice(0, placed), { id: messageId, text: text2, forced: true, ...handedOver, ...
|
|
1519
|
+
return { ...view, queued: [...rest.slice(0, placed), { id: messageId, text: text2, forced: true, ...handedOver, ...sender2 }, ...rest.slice(placed)] };
|
|
874
1520
|
}
|
|
875
|
-
if (!listed) return { ...view, queued: [...view.queued, { id: messageId, text: text2, ...
|
|
1521
|
+
if (!listed) return { ...view, queued: [...view.queued, { id: messageId, text: text2, ...sender2 }] };
|
|
876
1522
|
if (!listed.handedOver) return view;
|
|
877
|
-
return { ...view, queued: view.queued.map((m) => m === listed ? { id: m.id, text: m.text, ...m.forced ? { forced: true } : {}, ...
|
|
1523
|
+
return { ...view, queued: view.queued.map((m) => m === listed ? { id: m.id, text: m.text, ...m.forced ? { forced: true } : {}, ...sender2 } : m) };
|
|
878
1524
|
}
|
|
879
1525
|
function handOver(view, { messageId }) {
|
|
880
1526
|
const listed = view.queued.find((m) => m.id === messageId);
|
|
@@ -888,6 +1534,43 @@ function unqueueMessage(view, { messageId }) {
|
|
|
888
1534
|
const queued = view.queued.filter((m) => m.id !== messageId);
|
|
889
1535
|
return queued.length === view.queued.length ? view : { ...view, queued };
|
|
890
1536
|
}
|
|
1537
|
+
function runScriptStep(view, event) {
|
|
1538
|
+
const [command, toolLabel] = "argv" in event ? [commandLine(event.argv), "Run"] : [event.script, "Script"];
|
|
1539
|
+
return appendOutput({ ...view, working: true }, null, (id) => pendingTool(id, { tool: "bash", toolLabel, args: JSON.stringify({ command }) }));
|
|
1540
|
+
}
|
|
1541
|
+
function runningScriptStep(view) {
|
|
1542
|
+
return findLastIndex(view.blocks, (b) => b.type === "tool" && b.status === "pending" && b.tool === "bash");
|
|
1543
|
+
}
|
|
1544
|
+
function scriptStepOutput(view, { text: text2, omittedChars }) {
|
|
1545
|
+
const said = omittedChars ? `\u2026 ${omittedChars} characters omitted \u2026
|
|
1546
|
+
${text2}` : text2;
|
|
1547
|
+
const i = runningScriptStep(view);
|
|
1548
|
+
const block = view.blocks[i];
|
|
1549
|
+
if (block?.type !== "tool") return append(view, (id) => settledMessage(id, "system", said));
|
|
1550
|
+
const output = block.output + said;
|
|
1551
|
+
return { ...view, blocks: replaceAt(view.blocks, i, { ...block, output, outputLineCount: outputLines(output).length }) };
|
|
1552
|
+
}
|
|
1553
|
+
function endScriptStep(view, event) {
|
|
1554
|
+
const status = scriptExitText(event);
|
|
1555
|
+
const passed = event.code === 0 && !event.signal && event.error === void 0;
|
|
1556
|
+
const i = runningScriptStep(view);
|
|
1557
|
+
const block = view.blocks[i];
|
|
1558
|
+
const ended = { ...view, working: false };
|
|
1559
|
+
if (block?.type !== "tool") return append(ended, (id) => settledMessage(id, "system", status));
|
|
1560
|
+
const output = block.output.trim() ? `${block.output.replace(/\n+$/, "")}
|
|
1561
|
+
${status}` : status;
|
|
1562
|
+
return { ...ended, blocks: replaceAt(view.blocks, i, finishedTool(block, output, passed ? "success" : "failure")) };
|
|
1563
|
+
}
|
|
1564
|
+
function sameHandoff(a, b) {
|
|
1565
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1566
|
+
}
|
|
1567
|
+
function showHandoff(view, { handoff }) {
|
|
1568
|
+
const i = findLastIndex(view.blocks, (b) => b.type === "handoff");
|
|
1569
|
+
const block = view.blocks[i];
|
|
1570
|
+
if (block?.type !== "handoff") return appendStep(view, (id) => ({ type: "handoff", id, handoff }));
|
|
1571
|
+
if (sameHandoff(block.handoff, handoff)) return view;
|
|
1572
|
+
return { ...view, blocks: replaceAt(view.blocks, i, { ...block, handoff }) };
|
|
1573
|
+
}
|
|
891
1574
|
function endCall(block) {
|
|
892
1575
|
return block.type === "tool" && block.status === "pending" ? finishedTool(block, "", "not_executed") : block;
|
|
893
1576
|
}
|
|
@@ -960,6 +1643,14 @@ function reduceTaskLog(view, event) {
|
|
|
960
1643
|
return decideApproval(view, event);
|
|
961
1644
|
case "approval_withdrawn":
|
|
962
1645
|
return settleApproval(view, event.approvalId, (block) => ({ ...block, status: "withdrawn" }));
|
|
1646
|
+
case "script_run":
|
|
1647
|
+
return runScriptStep(view, event);
|
|
1648
|
+
case "script_output":
|
|
1649
|
+
return scriptStepOutput(view, event);
|
|
1650
|
+
case "script_exit":
|
|
1651
|
+
return endScriptStep(view, event);
|
|
1652
|
+
case "handoff":
|
|
1653
|
+
return showHandoff(view, event);
|
|
963
1654
|
case "runner_ended":
|
|
964
1655
|
return closeOpenBlocks({ ...endOpenCalls(view), working: false, streams: {}, latestText: void 0 }, "stopped");
|
|
965
1656
|
default:
|
|
@@ -1002,10 +1693,21 @@ function awaitingLabel(task) {
|
|
|
1002
1693
|
function blockedByLabel(refs) {
|
|
1003
1694
|
return `Blocked by ${refs.join(", ")}`;
|
|
1004
1695
|
}
|
|
1696
|
+
function holdLabel(reason) {
|
|
1697
|
+
return `On hold: ${reason}`;
|
|
1698
|
+
}
|
|
1005
1699
|
function skippedLabel(reason) {
|
|
1006
1700
|
return reason ? `Skipped: ${reason}` : "Skipped";
|
|
1007
1701
|
}
|
|
1008
1702
|
var WAITS_FOR_APPROVAL_LABEL = "Waits for your approval";
|
|
1703
|
+
var RECEIPT_LABELS = {
|
|
1704
|
+
done: "Completion received",
|
|
1705
|
+
failed: "Failure report received",
|
|
1706
|
+
blocked: "Blocked report received"
|
|
1707
|
+
};
|
|
1708
|
+
function receiptLabel(task) {
|
|
1709
|
+
return isTaskRunning(task) && task.completionReceived ? RECEIPT_LABELS[task.completionReceived] : null;
|
|
1710
|
+
}
|
|
1009
1711
|
function approvalLabel(count) {
|
|
1010
1712
|
if (!count) return null;
|
|
1011
1713
|
return count > 1 ? `Waiting for approval (${count})` : "Waiting for approval";
|
|
@@ -1026,28 +1728,62 @@ import { v4 as uuidv4 } from "uuid";
|
|
|
1026
1728
|
|
|
1027
1729
|
// src/services/TaskEditValidator.ts
|
|
1028
1730
|
function lockedTaskRefusal(task, action = "changed") {
|
|
1029
|
-
if (task.status === "in_progress") return
|
|
1731
|
+
if (task.status === "in_progress") return runningRefusal(task, action);
|
|
1030
1732
|
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`;
|
|
1031
1733
|
return null;
|
|
1032
1734
|
}
|
|
1735
|
+
function reshapeRefusal(task, action) {
|
|
1736
|
+
return waitsMidAttempt(task) ? runningRefusal(task, action) : lockedTaskRefusal(task, action);
|
|
1737
|
+
}
|
|
1738
|
+
function runningRefusal(task, action) {
|
|
1739
|
+
return `"${task.title}" is running and cannot be ${action} now: the user can stop it first, or change it after it finishes`;
|
|
1740
|
+
}
|
|
1741
|
+
function startedEditRefusal(task) {
|
|
1742
|
+
return `"${task.title}" started before this change was saved, so it was not changed: only a task that has not started can be edited while the plan runs`;
|
|
1743
|
+
}
|
|
1033
1744
|
var AI_ONLY_FIELDS = ["assignedModel", "taskMode", "autonomy", "ops"];
|
|
1745
|
+
var NOT_KEPT_BY_A_SCRIPT = [...AI_ONLY_FIELDS, "prompt", "skills", "userSteps"];
|
|
1034
1746
|
function typeCoherenceCheck(target, changes) {
|
|
1035
1747
|
if (!changes.type || changes.type === target.type) return null;
|
|
1748
|
+
if ((changes.type === "script" || target.type === "script") && !NOT_STARTED.has(target.status)) {
|
|
1749
|
+
return { ok: false, error: `"${target.title}" has started, so whether it is a script task is fixed: add a new task instead` };
|
|
1750
|
+
}
|
|
1751
|
+
const body = target.type === "script" ? SCRIPT_BODY_FIELDS.filter((f) => target[f] !== void 0) : [];
|
|
1036
1752
|
if (changes.type === "user") {
|
|
1037
1753
|
if (!Array.isArray(changes.userSteps) || changes.userSteps.length === 0) {
|
|
1038
1754
|
return { ok: false, error: `"${target.title}" cannot become a manual task without user steps: include "userSteps" in this edit` };
|
|
1039
1755
|
}
|
|
1040
1756
|
return {
|
|
1041
1757
|
ok: true,
|
|
1042
|
-
clear: AI_ONLY_FIELDS.filter((f) => target[f] !== void 0),
|
|
1758
|
+
clear: [...AI_ONLY_FIELDS.filter((f) => target[f] !== void 0), ...body],
|
|
1043
1759
|
// 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.
|
|
1044
1760
|
...target.sliceType !== "HITL" ? { set: { sliceType: "HITL" } } : {}
|
|
1045
1761
|
};
|
|
1046
1762
|
}
|
|
1763
|
+
if (changes.type === "script") {
|
|
1764
|
+
if (changes.commands === void 0 && changes.script === void 0) {
|
|
1765
|
+
return { ok: false, error: `"${target.title}" cannot become a script task without a body: include "commands" or "script" in this edit` };
|
|
1766
|
+
}
|
|
1767
|
+
return { ok: true, clear: NOT_KEPT_BY_A_SCRIPT.filter((f) => target[f] !== void 0) };
|
|
1768
|
+
}
|
|
1047
1769
|
if (!changes.prompt || !changes.prompt.trim()) {
|
|
1048
1770
|
return { ok: false, error: `"${target.title}" cannot become an AI task without a prompt: include "prompt" in this edit` };
|
|
1049
1771
|
}
|
|
1050
|
-
return { ok: true, clear: target.userSteps && target.userSteps.length > 0 ? ["userSteps"] : [] };
|
|
1772
|
+
return { ok: true, clear: [...target.userSteps && target.userSteps.length > 0 ? ["userSteps"] : [], ...body] };
|
|
1773
|
+
}
|
|
1774
|
+
function scriptCheck(target, changes) {
|
|
1775
|
+
if ((changes.type ?? target.type) !== "script") {
|
|
1776
|
+
const body = SCRIPT_BODY_FIELDS.filter((f) => changes[f] !== void 0);
|
|
1777
|
+
if (body.length === 0) return null;
|
|
1778
|
+
return { ok: false, error: `"${target.title}" is not a script task, so it has no ${body.map((f) => `"${f}"`).join(", ")}: set "type": "script" with it, or leave it out` };
|
|
1779
|
+
}
|
|
1780
|
+
const extra = [..."assignedRunner" in changes && changes.assignedRunner !== void 0 ? ["assignedRunner"] : [], ...notAScriptsFields(changes)];
|
|
1781
|
+
if (extra.length > 0) return { ok: false, error: `"${target.title}" ${notOnAScriptMessage(extra)}` };
|
|
1782
|
+
const replaced = [
|
|
1783
|
+
...changes.commands !== void 0 && !("script" in changes) && target.script !== void 0 ? ["script"] : [],
|
|
1784
|
+
...changes.script !== void 0 && !("commands" in changes) && target.commands !== void 0 ? ["commands"] : []
|
|
1785
|
+
];
|
|
1786
|
+
return replaced.length > 0 ? { ok: true, clear: replaced } : null;
|
|
1051
1787
|
}
|
|
1052
1788
|
function opsCheck(target, isSubtask, changes) {
|
|
1053
1789
|
if (!("ops" in changes) || !!changes.ops === !!target.ops) return null;
|
|
@@ -1055,8 +1791,8 @@ function opsCheck(target, isSubtask, changes) {
|
|
|
1055
1791
|
return { ok: false, error: `"${target.title}" has started, so whether it is an ops task is fixed` };
|
|
1056
1792
|
}
|
|
1057
1793
|
if (!changes.ops) return null;
|
|
1058
|
-
const
|
|
1059
|
-
return
|
|
1794
|
+
const refused2 = opsPlacementRefusal(changes.type ?? target.type, isSubtask);
|
|
1795
|
+
return refused2 ? { ok: false, error: `"${target.title}" ${refused2}` } : null;
|
|
1060
1796
|
}
|
|
1061
1797
|
function validateTaskEdit(actor, tasks, taskId, changes) {
|
|
1062
1798
|
const found = flattenTasksWithParents(tasks).find(({ task }) => task.id === taskId);
|
|
@@ -1070,9 +1806,12 @@ function validateTaskEdit(actor, tasks, taskId, changes) {
|
|
|
1070
1806
|
}
|
|
1071
1807
|
const coherence = typeCoherenceCheck(target, changes);
|
|
1072
1808
|
if (coherence && !coherence.ok) return coherence;
|
|
1809
|
+
const script = scriptCheck(target, changes);
|
|
1810
|
+
if (script && !script.ok) return script;
|
|
1073
1811
|
const ops = opsCheck(target, parent !== null, changes);
|
|
1074
1812
|
if (ops) return ops;
|
|
1075
|
-
return coherence ?? { ok: true };
|
|
1813
|
+
if (!script) return coherence ?? { ok: true };
|
|
1814
|
+
return { ...coherence, ok: true, clear: [...coherence?.clear ?? [], ...script.clear ?? []] };
|
|
1076
1815
|
}
|
|
1077
1816
|
|
|
1078
1817
|
// src/services/TaskOps.ts
|
|
@@ -1089,7 +1828,12 @@ var UPDATABLE_FIELDS = [
|
|
|
1089
1828
|
"autonomy",
|
|
1090
1829
|
"sliceType",
|
|
1091
1830
|
"ops",
|
|
1092
|
-
"skills"
|
|
1831
|
+
"skills",
|
|
1832
|
+
"commands",
|
|
1833
|
+
"script",
|
|
1834
|
+
"timeoutMs",
|
|
1835
|
+
"loop",
|
|
1836
|
+
"research"
|
|
1093
1837
|
];
|
|
1094
1838
|
function taskOpsProtocol(executionNote = "", tools = false) {
|
|
1095
1839
|
const updateChanges = [...UPDATABLE_FIELDS.filter((f) => f !== "taskMode"), "thinkingEffort", "readOnly"].map((f) => f === "dependencies" ? '"dependencies"?:["<id or #order>"]' : `"${f}"?`).join(",");
|
|
@@ -1097,7 +1841,7 @@ function taskOpsProtocol(executionNote = "", tools = false) {
|
|
|
1097
1841
|
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:",
|
|
1098
1842
|
tools ? ' {"ops": [' : ' {"taskOps": [',
|
|
1099
1843
|
` {"op":"update","taskId":"<id or #order>","changes":{${updateChanges}}},`,
|
|
1100
|
-
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?,"readOnly"?,"ops"?},"handle"?:"<name>"},',
|
|
1844
|
+
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?,"readOnly"?,"research"?,"ops"?},"handle"?:"<name>"},',
|
|
1101
1845
|
' {"op":"remove","taskId":"<id or #order>"},',
|
|
1102
1846
|
' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]}, // only to re-prioritise INDEPENDENT tasks',
|
|
1103
1847
|
' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...},"handle"?:"<name>"},',
|
|
@@ -1108,11 +1852,14 @@ function taskOpsProtocol(executionNote = "", tools = false) {
|
|
|
1108
1852
|
'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.',
|
|
1109
1853
|
'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.',
|
|
1110
1854
|
`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>"}.`,
|
|
1111
|
-
'Set "readOnly": true on a task that must
|
|
1855
|
+
'Set "readOnly": true on a task that must land nothing: it runs tests, scripts and builds sealed through Ordewell run_command (no network), and may try changes in its throwaway worktree, which is thrown away when it ends. Never on 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.',
|
|
1856
|
+
`"research": true on a read-only top-level AI task makes it a research task, whose task_complete report wakes you when it ends; "research": false lifts it. Lifting "readOnly" lifts it too. It is never a subtask, an ops task or a loop's body.`,
|
|
1857
|
+
`"loop" makes a task the check of a loop (see LOOPS in your instructions): {"body": ["<id, #order or handle>", ...], "maxRounds"?: 1 to 10}; null drops the loop. A handle cannot be referenced before the op that defines it, so adding a loop takes three ops in this order: "add" the check without "loop", with a "handle"; "add" each body task, depending on the check's handle, with a handle of its own; then "update" the check, by its handle, with "loop": {"body": [the body handles]}.`,
|
|
1112
1858
|
'Keep dependencies consistent: no cycles, no references to removed tasks, and never touch running or completed tasks \u2014 "rearm" is the one exception, below.',
|
|
1113
|
-
...tools ? ["edit_plan checks the batch when you call it and answers with what it
|
|
1859
|
+
...tools ? ["edit_plan checks the batch when you call it and applies it at once, whole or not at all: it answers with what it changed and the id and ref of each task it added, or with the error to fix, having changed nothing \u2014 call it again in the same reply. Each call is a batch of its own against the plan as it then stands, and a later call, run_task or retry_task in the same reply may name a task an earlier call added by the handle it gave."] : [],
|
|
1114
1860
|
'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,
|
|
1115
|
-
'Flipping "type" between "ai" and "
|
|
1861
|
+
'Flipping "type" between "ai", "user" and "script" is a content change, not just a label: an update to "user" needs "userSteps" in the SAME op, an update to "ai" needs "prompt", and an update to "script" needs "commands" or "script" \u2014 the fields the old type had and the new one does not are cleared automatically (flipping to "user" also makes the task HITL). Only a task that has not started can flip to or from "script".',
|
|
1862
|
+
'A script task ("type":"script", top-level only) is run by Ordewell with no model: "commands" is a list of argument vectors run in order with no shell, stopping at the first that fails (e.g. [["git","push","origin","main"]]), or "script" is one script for the shell; "timeoutMs" is its time limit (ten minutes when left out). Give it exactly one of "commands" or "script", and never "prompt", "assignedRunner", "assignedModel", "skills", "readOnly" or "ops". Writing one body replaces the other. Changing its body or time limit takes it out of what the user approved, so it waits for their approval again.',
|
|
1116
1863
|
`"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.`,
|
|
1117
1864
|
'"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.'
|
|
1118
1865
|
];
|
|
@@ -1152,7 +1899,7 @@ function parseTaskOpsObject(json, text2) {
|
|
|
1152
1899
|
}
|
|
1153
1900
|
return ops;
|
|
1154
1901
|
}
|
|
1155
|
-
function resolveRef(ref, originalTree, liveTree, handleOwner, handleId, opIndex) {
|
|
1902
|
+
function resolveRef(ref, originalTree, liveTree, handleOwner, handleId, opIndex, earlierHandles) {
|
|
1156
1903
|
if (typeof ref !== "string" && typeof ref !== "number") return { error: `invalid reference "${String(ref)}"` };
|
|
1157
1904
|
const s = String(ref).trim();
|
|
1158
1905
|
const ownerIdx = handleOwner.get(s);
|
|
@@ -1164,6 +1911,8 @@ function resolveRef(ref, originalTree, liveTree, handleOwner, handleId, opIndex)
|
|
|
1164
1911
|
if (!id) return { error: `handle "${s}" was never resolved` };
|
|
1165
1912
|
return { id };
|
|
1166
1913
|
}
|
|
1914
|
+
const earlier = earlierHandles.get(s);
|
|
1915
|
+
if (earlier !== void 0) return findInTasks(liveTree, earlier) ? { id: earlier } : { error: `handle "${s}" names a task that is no longer in the plan` };
|
|
1167
1916
|
if (findInTasks(liveTree, s)) return { id: s };
|
|
1168
1917
|
const task = resolveTaskRef(originalTree, s);
|
|
1169
1918
|
return task ? { id: task.id } : { error: `task "${s}" not found` };
|
|
@@ -1175,7 +1924,7 @@ function updatableChanges(raw) {
|
|
|
1175
1924
|
}
|
|
1176
1925
|
if ("ops" in changes) changes.ops = opsFlag(changes.ops);
|
|
1177
1926
|
if ("skills" in changes) changes.skills = skillNames(changes.skills);
|
|
1178
|
-
return changes;
|
|
1927
|
+
return clearedBodyFields(changes);
|
|
1179
1928
|
}
|
|
1180
1929
|
function findInTasks(tasks, id) {
|
|
1181
1930
|
return taskPlacement(tasks, id)?.task;
|
|
@@ -1184,11 +1933,11 @@ function findTopLevel(tasks, id) {
|
|
|
1184
1933
|
return tasks.find((t) => t.id === id);
|
|
1185
1934
|
}
|
|
1186
1935
|
function isPinned(t) {
|
|
1187
|
-
return t.status === "in_progress" || t.status === "completed";
|
|
1936
|
+
return t.status === "in_progress" || waitsMidAttempt(t) || t.status === "completed";
|
|
1188
1937
|
}
|
|
1189
1938
|
function lockedInSubtree(parent) {
|
|
1190
1939
|
for (const sub of parent.subtasks ?? []) {
|
|
1191
|
-
const locked =
|
|
1940
|
+
const locked = reshapeRefusal(sub, "removed") ?? lockedInSubtree(sub);
|
|
1192
1941
|
if (locked) return locked;
|
|
1193
1942
|
}
|
|
1194
1943
|
return null;
|
|
@@ -1236,9 +1985,18 @@ function describeMoves(before, after) {
|
|
|
1236
1985
|
return moved.length ? moved.map((m) => `"${m.t.title}" #${m.from}\u2192#${m.to}`).join(", ") : null;
|
|
1237
1986
|
}
|
|
1238
1987
|
function pinnedRefusal(other, pin, side) {
|
|
1239
|
-
const word = pin.status === "
|
|
1988
|
+
const word = pin.status === "completed" ? "completed" : "running";
|
|
1240
1989
|
return `"${other.title}" would have to run ${side} "${pin.title}", which is ${word} and cannot be moved`;
|
|
1241
1990
|
}
|
|
1991
|
+
function opLabel(ref) {
|
|
1992
|
+
return `op ${ref.number} (${ref.kind})`;
|
|
1993
|
+
}
|
|
1994
|
+
function opRefusalText({ op, message }) {
|
|
1995
|
+
return op ? `${opLabel(op)}: ${message}` : message;
|
|
1996
|
+
}
|
|
1997
|
+
function refused(tasks, refusals) {
|
|
1998
|
+
return { ok: false, tasks, errors: refusals.map(opRefusalText), refusals, summary: [] };
|
|
1999
|
+
}
|
|
1242
2000
|
function canMergeTasks(tasks, selectedIds) {
|
|
1243
2001
|
if (selectedIds.length < 2) return { ok: false, error: "Select at least two tasks to merge" };
|
|
1244
2002
|
const idSet = new Set(selectedIds);
|
|
@@ -1247,6 +2005,8 @@ function canMergeTasks(tasks, selectedIds) {
|
|
|
1247
2005
|
for (const t of toMerge) {
|
|
1248
2006
|
if (t.status === "in_progress") return { ok: false, error: `"${t.title}" is running and cannot be merged` };
|
|
1249
2007
|
if (t.status === "completed") return { ok: false, error: `"${t.title}" is completed and cannot be merged` };
|
|
2008
|
+
const looped = loopReshapeRefusal(tasks, t, "merged");
|
|
2009
|
+
if (looped) return { ok: false, error: looped };
|
|
1250
2010
|
}
|
|
1251
2011
|
const ordered = [...toMerge].sort((a, b) => a.order - b.order);
|
|
1252
2012
|
const survivorId = "__merge_sim__";
|
|
@@ -1262,7 +2022,8 @@ function canSplitTask(tasks, taskId) {
|
|
|
1262
2022
|
if (!target) return { ok: false, error: "Task not found" };
|
|
1263
2023
|
if (target.status === "in_progress") return { ok: false, error: `"${target.title}" is running and cannot be split` };
|
|
1264
2024
|
if (target.status === "completed") return { ok: false, error: `"${target.title}" is completed and cannot be split` };
|
|
1265
|
-
|
|
2025
|
+
const looped = loopReshapeRefusal(tasks, target, "split");
|
|
2026
|
+
return looped ? { ok: false, error: looped } : { ok: true };
|
|
1266
2027
|
}
|
|
1267
2028
|
function askedReadOnly(spec) {
|
|
1268
2029
|
return typeof spec?.readOnly === "boolean" ? spec.readOnly : void 0;
|
|
@@ -1276,20 +2037,8 @@ function changedFields(target, written, landed, catalog) {
|
|
|
1276
2037
|
return fields;
|
|
1277
2038
|
}
|
|
1278
2039
|
var NO_CATALOG = { modelsByRunner: {} };
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
if (typeof op !== "object" || op === null) return uuidv4;
|
|
1282
|
-
const ids = keptIds.get(op) ?? [];
|
|
1283
|
-
keptIds.set(op, ids);
|
|
1284
|
-
let next = 0;
|
|
1285
|
-
return () => {
|
|
1286
|
-
if (next === ids.length) ids.push(uuidv4());
|
|
1287
|
-
return ids[next++];
|
|
1288
|
-
};
|
|
1289
|
-
}
|
|
1290
|
-
function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
1291
|
-
const ops = [...held, ...newOps];
|
|
1292
|
-
const opLabel = (i) => i < held.length ? `earlier op ${i + 1} (${ops[i].op})` : `op ${i - held.length + 1} (${ops[i].op})`;
|
|
2040
|
+
function applyTaskOps(currentTasks, ops, runners, catalog, earlierHandles = /* @__PURE__ */ new Map()) {
|
|
2041
|
+
const opLabel2 = (i) => ({ number: i + 1, kind: ops[i].op });
|
|
1293
2042
|
let tasks = JSON.parse(JSON.stringify(currentTasks));
|
|
1294
2043
|
const errors = [];
|
|
1295
2044
|
const summary = [];
|
|
@@ -1300,26 +2049,30 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
|
1300
2049
|
const handle = op.handle;
|
|
1301
2050
|
if (handle === void 0) continue;
|
|
1302
2051
|
if (typeof handle !== "string" || !handle.trim()) {
|
|
1303
|
-
errors.push(
|
|
2052
|
+
errors.push({ op: opLabel2(i), message: `handle must be a non-empty string` });
|
|
1304
2053
|
continue;
|
|
1305
2054
|
}
|
|
1306
2055
|
if (handleOwner.has(handle)) {
|
|
1307
|
-
errors.push(
|
|
2056
|
+
errors.push({ op: opLabel2(i), message: `handle "${handle}" is already used earlier in this batch` });
|
|
2057
|
+
continue;
|
|
2058
|
+
}
|
|
2059
|
+
if (earlierHandles.has(handle)) {
|
|
2060
|
+
errors.push({ op: opLabel2(i), message: `handle "${handle}" already names a task an earlier edit in this reply added: choose another` });
|
|
1308
2061
|
continue;
|
|
1309
2062
|
}
|
|
1310
2063
|
if (resolveTaskRef(currentTasks, handle)) {
|
|
1311
|
-
errors.push(
|
|
2064
|
+
errors.push({ op: opLabel2(i), message: `handle "${handle}" collides with an existing task reference` });
|
|
1312
2065
|
continue;
|
|
1313
2066
|
}
|
|
1314
2067
|
handleOwner.set(handle, i);
|
|
1315
2068
|
}
|
|
1316
|
-
if (errors.length) return
|
|
2069
|
+
if (errors.length) return refused(currentTasks, errors);
|
|
1317
2070
|
const resolveDeps = (deps, opIndex) => {
|
|
1318
2071
|
const ids = [];
|
|
1319
2072
|
const bad = [];
|
|
1320
2073
|
const liveFlat = flattenTasks(tasks);
|
|
1321
2074
|
for (const d of Array.isArray(deps) ? deps : []) {
|
|
1322
|
-
const r = resolveRef(d, currentTasks, tasks, handleOwner, handleId, opIndex);
|
|
2075
|
+
const r = resolveRef(d, currentTasks, tasks, handleOwner, handleId, opIndex, earlierHandles);
|
|
1323
2076
|
if ("error" in r) {
|
|
1324
2077
|
bad.push(`${String(d)} (${r.error})`);
|
|
1325
2078
|
continue;
|
|
@@ -1354,62 +2107,87 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
|
1354
2107
|
if (typeof done2 === "string") return done2;
|
|
1355
2108
|
subtasks.push(done2);
|
|
1356
2109
|
}
|
|
1357
|
-
return { ...t, dependencies: ids, subtasks };
|
|
2110
|
+
if (!t.loop) return { ...t, dependencies: ids, subtasks };
|
|
2111
|
+
const body = resolveDeps(t.loop.body, opIndex);
|
|
2112
|
+
if (body.bad.length) return `unknown tasks in the loop's "body"${parentTitle !== void 0 ? ` of ${taskWhere(t.title, parentTitle)}` : ""}: ${body.bad.join(", ")}`;
|
|
2113
|
+
return { ...t, dependencies: ids, subtasks, loop: { ...t.loop, body: body.ids } };
|
|
1358
2114
|
};
|
|
1359
2115
|
const done = walk(planned.task);
|
|
1360
2116
|
return typeof done === "string" ? { error: done } : { task: done };
|
|
1361
2117
|
};
|
|
1362
2118
|
const refusalOf = (planned) => [...planned.ignored, ...planned.errors][0]?.message;
|
|
1363
|
-
const prepareEdit = (op, opIndex,
|
|
1364
|
-
const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, opIndex);
|
|
1365
|
-
if ("error" in ref) return { error:
|
|
2119
|
+
const prepareEdit = (op, opIndex, audience, refuseRunning = false) => {
|
|
2120
|
+
const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, opIndex, earlierHandles);
|
|
2121
|
+
if ("error" in ref) return { error: ref.error };
|
|
1366
2122
|
const target = findInTasks(tasks, ref.id);
|
|
1367
|
-
if (!target) return { error:
|
|
1368
|
-
if (refuseRunning && target.status === "in_progress") return { error:
|
|
1369
|
-
|
|
2123
|
+
if (!target) return { error: `task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)` };
|
|
2124
|
+
if (refuseRunning && target.status === "in_progress") return { error: lockedTaskRefusal(target, "re-armed") };
|
|
2125
|
+
const planners = op.changes ? ["readOnly", "thinkingEffort"].filter((f) => op.changes?.[f] !== void 0 && op.changes?.[f] !== null && op.changes?.[f] !== false) : [];
|
|
2126
|
+
if ((op.changes?.type ?? target.type) === "script" && planners.length > 0) return { error: `"${target.title}" ${notOnAScriptMessage(planners)}` };
|
|
2127
|
+
if (op.changes && "taskMode" in op.changes) return { error: MODE_NOT_THE_PLANNERS };
|
|
1370
2128
|
const changes = updatableChanges(op.changes);
|
|
1371
2129
|
if (op.changes && "thinkingEffort" in op.changes) {
|
|
1372
2130
|
const model = changes.assignedModel ?? target.assignedModel;
|
|
1373
|
-
if (!model) return { error:
|
|
2131
|
+
if (!model) return { error: `"thinkingEffort" is the assigned model's \u2014 set "assignedModel" with it` };
|
|
1374
2132
|
changes.assignedModel = { ...model, thinkingEffort: op.changes.thinkingEffort ?? void 0 };
|
|
1375
2133
|
}
|
|
1376
2134
|
const changesForCheck = { ...changes };
|
|
1377
2135
|
delete changesForCheck.dependencies;
|
|
1378
2136
|
const check = validateTaskEdit(audience, tasks, target.id, changesForCheck);
|
|
1379
|
-
if (!check.ok) return { error:
|
|
2137
|
+
if (!check.ok) return { error: check.error };
|
|
1380
2138
|
for (const field of check.clear ?? []) changes[field] = void 0;
|
|
1381
2139
|
Object.assign(changes, check.set);
|
|
1382
2140
|
const readOnly = askedReadOnly(op.changes);
|
|
1383
|
-
if (readOnly === true && (changes.type ?? target.type) === "user") return { error:
|
|
1384
|
-
if ((changes.ops ?? target.ops) && (readOnly ?? isReadOnly(target, catalog))) return { error:
|
|
2141
|
+
if (readOnly === true && (changes.type ?? target.type) === "user") return { error: MANUAL_NOT_READ_ONLY };
|
|
2142
|
+
if ((changes.ops ?? target.ops) && (readOnly ?? isReadOnly(target, catalog))) return { error: OPS_NOT_READ_ONLY };
|
|
1385
2143
|
const landed2 = { ...changes };
|
|
2144
|
+
if (readOnly === false && target.research && op.changes?.research !== true) landed2.research = false;
|
|
1386
2145
|
const written = { ...target, ...changes };
|
|
1387
|
-
|
|
2146
|
+
const kept = target.type === "ai" && written.assignedRunner === target.assignedRunner;
|
|
2147
|
+
if (written.type !== "ai" || kept || runners.includes(written.assignedRunner)) {
|
|
1388
2148
|
const resolved = resolveAssignment({ writer: "edit", task: written, before: target, readOnly, catalog: catalog ?? NO_CATALOG });
|
|
1389
|
-
if (!resolved.ok) return { error:
|
|
2149
|
+
if (!resolved.ok) return { error: resolved.error };
|
|
1390
2150
|
if (resolved.task.assignedModel !== written.assignedModel) landed2.assignedModel = resolved.task.assignedModel;
|
|
1391
2151
|
if (resolved.task.taskMode !== written.taskMode) landed2.taskMode = resolved.task.taskMode;
|
|
1392
2152
|
adjustments.push(...resolved.adjustments);
|
|
1393
2153
|
}
|
|
1394
2154
|
if ("dependencies" in changes) {
|
|
1395
2155
|
const { ids, bad } = resolveDeps(changes.dependencies, opIndex);
|
|
1396
|
-
if (bad.length) return { error:
|
|
2156
|
+
if (bad.length) return { error: `unknown dependencies: ${bad.join(", ")}` };
|
|
1397
2157
|
landed2.dependencies = ids;
|
|
1398
2158
|
}
|
|
2159
|
+
if ("loop" in changes) {
|
|
2160
|
+
const spec = loopSpec(changes.loop);
|
|
2161
|
+
if ("error" in spec) return { error: `"${target.title}" ${spec.error}` };
|
|
2162
|
+
if (spec.loop) {
|
|
2163
|
+
const { ids, bad } = resolveDeps(spec.loop.body, opIndex);
|
|
2164
|
+
if (bad.length) return { error: `unknown tasks in the loop's "body": ${bad.join(", ")}` };
|
|
2165
|
+
landed2.loop = { ...spec.loop, body: ids };
|
|
2166
|
+
} else {
|
|
2167
|
+
landed2.loop = void 0;
|
|
2168
|
+
landed2.loopRound = void 0;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
1399
2171
|
return { target, changes: landed2, fields: changedFields(target, changes, landed2, catalog), check };
|
|
1400
2172
|
};
|
|
1401
|
-
const
|
|
1402
|
-
|
|
2173
|
+
const breaches = (list) => {
|
|
2174
|
+
const runnerOf = new Map(flattenTasks(list).map((t) => [t.id, t.assignedRunner]));
|
|
2175
|
+
return checkPlanInvariants(list, runners, catalog?.runnerModes).map((error) => {
|
|
2176
|
+
const runner = error.field === "assignedRunner" ? `\0${runnerOf.get(error.taskId ?? "") ?? ""}` : "";
|
|
2177
|
+
return { error, key: `${error.taskId ?? ""}\0${error.field}${runner}` };
|
|
2178
|
+
});
|
|
2179
|
+
};
|
|
2180
|
+
const standing = new Set(breaches(currentTasks).map((b) => b.key));
|
|
1403
2181
|
const brokenBy = /* @__PURE__ */ new Map();
|
|
1404
|
-
let lastLabel =
|
|
2182
|
+
let lastLabel = opLabel2(0);
|
|
1405
2183
|
for (const [i, op] of ops.entries()) {
|
|
1406
|
-
const label =
|
|
1407
|
-
const newId =
|
|
2184
|
+
const label = opLabel2(i);
|
|
2185
|
+
const newId = uuidv4;
|
|
1408
2186
|
switch (op.op) {
|
|
1409
2187
|
case "update": {
|
|
1410
|
-
const prep = prepareEdit(op, i,
|
|
2188
|
+
const prep = prepareEdit(op, i, "planner");
|
|
1411
2189
|
if ("error" in prep) {
|
|
1412
|
-
errors.push(prep.error);
|
|
2190
|
+
errors.push({ op: label, message: prep.error });
|
|
1413
2191
|
break;
|
|
1414
2192
|
}
|
|
1415
2193
|
const { target, changes, fields, check } = prep;
|
|
@@ -1419,7 +2197,7 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
|
1419
2197
|
summary.push(`"${target.title}" is already ${readOnly ? "" : "not "}read-only`);
|
|
1420
2198
|
break;
|
|
1421
2199
|
}
|
|
1422
|
-
errors.push(
|
|
2200
|
+
errors.push({ op: label, message: `no valid changes provided` });
|
|
1423
2201
|
break;
|
|
1424
2202
|
}
|
|
1425
2203
|
tasks = updateTaskInPlan(tasks, target.id, changes);
|
|
@@ -1430,19 +2208,19 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
|
1430
2208
|
case "add": {
|
|
1431
2209
|
const spec = op.task ?? {};
|
|
1432
2210
|
if (!spec.title) {
|
|
1433
|
-
errors.push(
|
|
2211
|
+
errors.push({ op: label, message: `new task needs a title` });
|
|
1434
2212
|
break;
|
|
1435
2213
|
}
|
|
1436
2214
|
const planned = plannedTask(spec, intake, { freshIds: true, newId });
|
|
1437
|
-
const
|
|
1438
|
-
if (
|
|
1439
|
-
errors.push(
|
|
2215
|
+
const refused2 = refusalOf(planned);
|
|
2216
|
+
if (refused2) {
|
|
2217
|
+
errors.push({ op: label, message: `${refused2}` });
|
|
1440
2218
|
break;
|
|
1441
2219
|
}
|
|
1442
2220
|
adjustments.push(...planned.adjustments);
|
|
1443
2221
|
const resolved = resolveTree(planned, i);
|
|
1444
2222
|
if ("error" in resolved) {
|
|
1445
|
-
errors.push(
|
|
2223
|
+
errors.push({ op: label, message: `${resolved.error}` });
|
|
1446
2224
|
break;
|
|
1447
2225
|
}
|
|
1448
2226
|
tasks = addTaskToPlan(tasks, resolved.task);
|
|
@@ -1451,19 +2229,19 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
|
1451
2229
|
break;
|
|
1452
2230
|
}
|
|
1453
2231
|
case "remove": {
|
|
1454
|
-
const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, i);
|
|
2232
|
+
const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, i, earlierHandles);
|
|
1455
2233
|
if ("error" in ref) {
|
|
1456
|
-
errors.push(
|
|
2234
|
+
errors.push({ op: label, message: `${ref.error}` });
|
|
1457
2235
|
break;
|
|
1458
2236
|
}
|
|
1459
2237
|
const target = findInTasks(tasks, ref.id);
|
|
1460
2238
|
if (!target) {
|
|
1461
|
-
errors.push(
|
|
2239
|
+
errors.push({ op: label, message: `task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)` });
|
|
1462
2240
|
break;
|
|
1463
2241
|
}
|
|
1464
|
-
const locked =
|
|
2242
|
+
const locked = reshapeRefusal(target, "removed") ?? lockedInSubtree(target) ?? loopRemovalRefusal(target);
|
|
1465
2243
|
if (locked) {
|
|
1466
|
-
errors.push(
|
|
2244
|
+
errors.push({ op: label, message: `${locked}` });
|
|
1467
2245
|
break;
|
|
1468
2246
|
}
|
|
1469
2247
|
tasks = removeTaskFromPlan(tasks, target.id);
|
|
@@ -1471,20 +2249,20 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
|
1471
2249
|
break;
|
|
1472
2250
|
}
|
|
1473
2251
|
case "reorder": {
|
|
1474
|
-
const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
|
|
2252
|
+
const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i, earlierHandles));
|
|
1475
2253
|
const badRef = refs.find((r) => "error" in r);
|
|
1476
2254
|
if (badRef) {
|
|
1477
|
-
errors.push(
|
|
2255
|
+
errors.push({ op: label, message: `${badRef.error}` });
|
|
1478
2256
|
break;
|
|
1479
2257
|
}
|
|
1480
2258
|
const resolved = refs.map((r) => findTopLevel(tasks, r.id));
|
|
1481
2259
|
if (resolved.some((t) => !t)) {
|
|
1482
|
-
errors.push(
|
|
2260
|
+
errors.push({ op: label, message: `unknown task in reorder list` });
|
|
1483
2261
|
break;
|
|
1484
2262
|
}
|
|
1485
2263
|
const orderIds = resolved.map((t) => t.id);
|
|
1486
|
-
if (new Set(orderIds).size !== tasks.length) {
|
|
1487
|
-
errors.push(
|
|
2264
|
+
if (orderIds.length !== tasks.length || new Set(orderIds).size !== tasks.length) {
|
|
2265
|
+
errors.push({ op: label, message: `reorder must list every top-level task exactly once (${tasks.length} tasks)` });
|
|
1488
2266
|
break;
|
|
1489
2267
|
}
|
|
1490
2268
|
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
@@ -1493,37 +2271,37 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
|
1493
2271
|
break;
|
|
1494
2272
|
}
|
|
1495
2273
|
case "merge": {
|
|
1496
|
-
const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
|
|
2274
|
+
const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i, earlierHandles));
|
|
1497
2275
|
const badRef = refs.find((r) => "error" in r);
|
|
1498
2276
|
if (badRef) {
|
|
1499
|
-
errors.push(
|
|
2277
|
+
errors.push({ op: label, message: `${badRef.error}` });
|
|
1500
2278
|
break;
|
|
1501
2279
|
}
|
|
1502
2280
|
const resolved = refs.map((r) => findTopLevel(tasks, r.id));
|
|
1503
2281
|
if (resolved.some((t) => !t)) {
|
|
1504
|
-
errors.push(
|
|
2282
|
+
errors.push({ op: label, message: `unknown task in merge list (already removed, merged, or split by an earlier op)` });
|
|
1505
2283
|
break;
|
|
1506
2284
|
}
|
|
1507
2285
|
const toMerge = [...new Map(resolved.map((t) => [t.id, t])).values()].sort((a, b) => a.order - b.order);
|
|
1508
2286
|
if (toMerge.length < 2) {
|
|
1509
|
-
errors.push(
|
|
2287
|
+
errors.push({ op: label, message: `merge needs at least two tasks` });
|
|
1510
2288
|
break;
|
|
1511
2289
|
}
|
|
1512
|
-
const locked = toMerge.map((t) =>
|
|
2290
|
+
const locked = toMerge.map((t) => reshapeRefusal(t, "merged") ?? loopReshapeRefusal(tasks, t, "merged")).find((refusal) => refusal !== null);
|
|
1513
2291
|
if (locked) {
|
|
1514
|
-
errors.push(
|
|
2292
|
+
errors.push({ op: label, message: `${locked}` });
|
|
1515
2293
|
break;
|
|
1516
2294
|
}
|
|
1517
2295
|
const idSet = new Set(toMerge.map((t) => t.id));
|
|
1518
2296
|
const unionDepsRaw = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));
|
|
1519
2297
|
const { ids, bad } = resolveDeps(unionDepsRaw, i);
|
|
1520
2298
|
if (bad.length) {
|
|
1521
|
-
errors.push(
|
|
2299
|
+
errors.push({ op: label, message: `unknown dependencies: ${bad.join(", ")}` });
|
|
1522
2300
|
break;
|
|
1523
2301
|
}
|
|
1524
2302
|
const spec = op.merged ?? {};
|
|
1525
2303
|
if (!spec.title) {
|
|
1526
|
-
errors.push(
|
|
2304
|
+
errors.push({ op: label, message: `merged task needs a title` });
|
|
1527
2305
|
break;
|
|
1528
2306
|
}
|
|
1529
2307
|
const writes = toMerge.find((t) => !isReadOnly(t, catalog));
|
|
@@ -1536,7 +2314,8 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
|
1536
2314
|
fields: {
|
|
1537
2315
|
description: toMerge.map((t) => t.description).filter(Boolean).join("\n\n"),
|
|
1538
2316
|
prompt: toMerge.map((t) => t.prompt).filter(Boolean).join("\n\n---\n\n") || void 0,
|
|
1539
|
-
|
|
2317
|
+
// A merge that takes in a script runs a body, which the merged task must state: no body is guessed (ADR-0035).
|
|
2318
|
+
type: toMerge.some((t) => t.type === "user") ? "user" : toMerge.some((t) => t.type === "script") ? "script" : "ai",
|
|
1540
2319
|
assignedRunner: toMerge[0].assignedRunner,
|
|
1541
2320
|
assignedModel: toMerge.find((t) => t.assignedModel)?.assignedModel,
|
|
1542
2321
|
taskMode: (writes ?? toMerge[0]).taskMode,
|
|
@@ -1548,15 +2327,15 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
|
1548
2327
|
}
|
|
1549
2328
|
}
|
|
1550
2329
|
});
|
|
1551
|
-
const
|
|
1552
|
-
if (
|
|
1553
|
-
errors.push(
|
|
2330
|
+
const refused2 = refusalOf(planned);
|
|
2331
|
+
if (refused2) {
|
|
2332
|
+
errors.push({ op: label, message: `${refused2}` });
|
|
1554
2333
|
break;
|
|
1555
2334
|
}
|
|
1556
2335
|
adjustments.push(...planned.adjustments);
|
|
1557
2336
|
const built = resolveTree({ ...planned, task: { ...planned.task, dependencies: [] } }, i);
|
|
1558
2337
|
if ("error" in built) {
|
|
1559
|
-
errors.push(
|
|
2338
|
+
errors.push({ op: label, message: `${built.error}` });
|
|
1560
2339
|
break;
|
|
1561
2340
|
}
|
|
1562
2341
|
const mergedTask = { ...built.task, dependencies: ids };
|
|
@@ -1581,24 +2360,24 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
|
1581
2360
|
break;
|
|
1582
2361
|
}
|
|
1583
2362
|
case "split": {
|
|
1584
|
-
const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, i);
|
|
2363
|
+
const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, i, earlierHandles);
|
|
1585
2364
|
if ("error" in ref) {
|
|
1586
|
-
errors.push(
|
|
2365
|
+
errors.push({ op: label, message: `${ref.error}` });
|
|
1587
2366
|
break;
|
|
1588
2367
|
}
|
|
1589
2368
|
const target = findTopLevel(tasks, ref.id);
|
|
1590
2369
|
if (!target) {
|
|
1591
|
-
errors.push(
|
|
2370
|
+
errors.push({ op: label, message: `task "${op.taskId}" not found` });
|
|
1592
2371
|
break;
|
|
1593
2372
|
}
|
|
1594
|
-
const locked =
|
|
2373
|
+
const locked = reshapeRefusal(target, "split") ?? loopReshapeRefusal(tasks, target, "split");
|
|
1595
2374
|
if (locked) {
|
|
1596
|
-
errors.push(
|
|
2375
|
+
errors.push({ op: label, message: `${locked}` });
|
|
1597
2376
|
break;
|
|
1598
2377
|
}
|
|
1599
2378
|
const parts = op.parts ?? [];
|
|
1600
2379
|
if (parts.length < 2) {
|
|
1601
|
-
errors.push(
|
|
2380
|
+
errors.push({ op: label, message: `split needs at least two parts` });
|
|
1602
2381
|
break;
|
|
1603
2382
|
}
|
|
1604
2383
|
const newTasks = [];
|
|
@@ -1607,7 +2386,7 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
|
1607
2386
|
for (let p = 0; p < parts.length; p++) {
|
|
1608
2387
|
const spec = parts[p];
|
|
1609
2388
|
if (!spec || !spec.title) {
|
|
1610
|
-
errors.push(
|
|
2389
|
+
errors.push({ op: label, message: `split part ${p + 1} needs a title` });
|
|
1611
2390
|
buildError = true;
|
|
1612
2391
|
break;
|
|
1613
2392
|
}
|
|
@@ -1615,7 +2394,7 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
|
1615
2394
|
if (p === 0) {
|
|
1616
2395
|
const { ids, bad } = resolveDeps(target.dependencies, i);
|
|
1617
2396
|
if (bad.length) {
|
|
1618
|
-
errors.push(
|
|
2397
|
+
errors.push({ op: label, message: `split part 1 unknown dependencies: ${bad.join(", ")}` });
|
|
1619
2398
|
buildError = true;
|
|
1620
2399
|
break;
|
|
1621
2400
|
}
|
|
@@ -1641,16 +2420,16 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
|
1641
2420
|
}
|
|
1642
2421
|
}
|
|
1643
2422
|
});
|
|
1644
|
-
const
|
|
1645
|
-
if (
|
|
1646
|
-
errors.push(
|
|
2423
|
+
const refused2 = refusalOf(planned);
|
|
2424
|
+
if (refused2) {
|
|
2425
|
+
errors.push({ op: label, message: `split part ${p + 1}: ${refused2}` });
|
|
1647
2426
|
buildError = true;
|
|
1648
2427
|
break;
|
|
1649
2428
|
}
|
|
1650
2429
|
adjustments.push(...planned.adjustments);
|
|
1651
2430
|
const resolved = resolveTree({ ...planned, task: { ...planned.task, dependencies: [] } }, i);
|
|
1652
2431
|
if ("error" in resolved) {
|
|
1653
|
-
errors.push(
|
|
2432
|
+
errors.push({ op: label, message: `split part ${p + 1}: ${resolved.error}` });
|
|
1654
2433
|
buildError = true;
|
|
1655
2434
|
break;
|
|
1656
2435
|
}
|
|
@@ -1674,44 +2453,43 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
|
1674
2453
|
break;
|
|
1675
2454
|
}
|
|
1676
2455
|
case "rearm": {
|
|
1677
|
-
const prep = prepareEdit(op, i,
|
|
2456
|
+
const prep = prepareEdit(op, i, "direct", true);
|
|
1678
2457
|
if ("error" in prep) {
|
|
1679
|
-
errors.push(prep.error);
|
|
2458
|
+
errors.push({ op: label, message: prep.error });
|
|
1680
2459
|
break;
|
|
1681
2460
|
}
|
|
1682
2461
|
const { target, changes, fields, check } = prep;
|
|
1683
|
-
tasks = updateTaskInPlan(tasks, target.id, { ...changes, status: "pending", verdict: void 0, outputSummary: void 0, awaitingReason: void 0, resumeAt: void 0, skipReason: void 0 });
|
|
2462
|
+
tasks = updateTaskInPlan(tasks, target.id, { ...changes, status: "pending", verdict: void 0, outputSummary: void 0, handoff: void 0, awaitingReason: void 0, resumeAt: void 0, skipReason: void 0, loopRound: void 0 });
|
|
1684
2463
|
const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
|
|
1685
2464
|
const changedNote = fields.length ? ` (${fields.join(", ")})` : "";
|
|
1686
2465
|
summary.push(`Re-armed "${target.title}"${changedNote}${clearedNote}`);
|
|
1687
2466
|
break;
|
|
1688
2467
|
}
|
|
1689
2468
|
default:
|
|
1690
|
-
errors.push(
|
|
2469
|
+
errors.push({ op: label, message: `unknown op "${op.op}"` });
|
|
1691
2470
|
}
|
|
1692
2471
|
if (errors.length) break;
|
|
1693
2472
|
lastLabel = label;
|
|
1694
|
-
const now = new Set(
|
|
2473
|
+
const now = new Set(breaches(tasks).map((b) => b.key));
|
|
1695
2474
|
for (const key of brokenBy.keys()) if (!now.has(key)) brokenBy.delete(key);
|
|
1696
2475
|
for (const key of now) if (!standing.has(key) && !brokenBy.has(key)) brokenBy.set(key, label);
|
|
1697
2476
|
}
|
|
1698
2477
|
if (!errors.length) {
|
|
1699
|
-
for (const
|
|
1700
|
-
|
|
1701
|
-
if (!standing.has(key)) errors.push(`${brokenBy.get(key) ?? lastLabel}: ${e.message}`);
|
|
2478
|
+
for (const { error, key } of breaches(tasks)) {
|
|
2479
|
+
if (!standing.has(key)) errors.push({ op: brokenBy.get(key) ?? lastLabel, message: error.message });
|
|
1702
2480
|
}
|
|
1703
2481
|
}
|
|
1704
2482
|
if (!errors.length) {
|
|
1705
2483
|
const repaired = repairOrder(tasks);
|
|
1706
2484
|
if ("error" in repaired) {
|
|
1707
|
-
errors.push(repaired.error);
|
|
2485
|
+
errors.push({ message: repaired.error });
|
|
1708
2486
|
} else {
|
|
1709
2487
|
const moves = describeMoves(tasks, repaired.tasks);
|
|
1710
2488
|
if (moves) summary.push(`Reordered to keep dependencies first: ${moves}`);
|
|
1711
2489
|
tasks = renumberTasks(repaired.tasks);
|
|
1712
2490
|
}
|
|
1713
2491
|
}
|
|
1714
|
-
if (errors.length) return
|
|
2492
|
+
if (errors.length) return refused(currentTasks, errors);
|
|
1715
2493
|
const landed = new Map(flattenTasks(tasks).map((t) => [t.id, t]));
|
|
1716
2494
|
const told = /* @__PURE__ */ new Set();
|
|
1717
2495
|
const standingAdjustments = adjustments.filter((a) => {
|
|
@@ -1722,35 +2500,238 @@ function applyTaskOps(currentTasks, newOps, runners, catalog, held = []) {
|
|
|
1722
2500
|
told.add(key);
|
|
1723
2501
|
return true;
|
|
1724
2502
|
});
|
|
1725
|
-
return { ok: true, tasks, errors: [], summary, handles: handleId, adjustments: standingAdjustments };
|
|
2503
|
+
return { ok: true, tasks, errors: [], refusals: [], summary, handles: handleId, adjustments: standingAdjustments };
|
|
1726
2504
|
}
|
|
1727
2505
|
|
|
1728
|
-
// src/services/
|
|
1729
|
-
function
|
|
1730
|
-
return task.
|
|
2506
|
+
// src/services/AssignmentScope.ts
|
|
2507
|
+
function assignedRunnerOf(task) {
|
|
2508
|
+
return task.type === "ai" ? task.assignedRunner || void 0 : void 0;
|
|
1731
2509
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
if (task.type !== "ai"
|
|
1735
|
-
if (task.
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
if (!task.runnerSessionId) return refuse("its runner left no saved session to resume. Use Retry instead.");
|
|
1741
|
-
const owner = sessionOwner(task);
|
|
1742
|
-
if (owner && owner !== task.assignedRunner) {
|
|
1743
|
-
return refuse(`its saved session belongs to ${owner}; Retry starts fresh on ${task.assignedRunner} and carries over its files.`);
|
|
2510
|
+
var OVER = /* @__PURE__ */ new Set(["completed", "skipped"]);
|
|
2511
|
+
function assignmentIssue(task, scope) {
|
|
2512
|
+
if (task.type !== "ai" || OVER.has(task.status)) return null;
|
|
2513
|
+
if (!scope.enabledRunners.includes(task.assignedRunner)) return { kind: "runner-off", runner: task.assignedRunner };
|
|
2514
|
+
const model = task.assignedModel;
|
|
2515
|
+
const allowed = effectiveAllowlist(scope.allowlist[task.assignedRunner], task.assignedRunner, scope.models);
|
|
2516
|
+
if (model && allowed && !allowed.includes(model.modelId)) {
|
|
2517
|
+
return { kind: "model-off-list", runner: task.assignedRunner, modelId: model.modelId, modelLabel: model.modelLabel || model.modelId };
|
|
1744
2518
|
}
|
|
1745
|
-
return
|
|
2519
|
+
return null;
|
|
1746
2520
|
}
|
|
1747
|
-
function
|
|
1748
|
-
return
|
|
2521
|
+
function assignmentIssueText(issue) {
|
|
2522
|
+
return issue.kind === "runner-off" ? `${issue.runner} is switched off` : `${issue.modelLabel} is not in the allowlist`;
|
|
1749
2523
|
}
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
2524
|
+
function assignmentConsequence({ status, awaitingReason, resumeAt }) {
|
|
2525
|
+
if (status === "failed") return "failed; a retry would run it there unless moved";
|
|
2526
|
+
if (status === "awaiting_user" && awaitingReason === "usage-limit") {
|
|
2527
|
+
return resumeAt ? "paused on a usage limit; resumes there at the reset unless moved" : "paused on a usage limit; a retry would run it there unless moved";
|
|
2528
|
+
}
|
|
2529
|
+
if (status === "awaiting_user" && (awaitingReason === "conflict" || awaitingReason === "files-changed")) return "waiting for the user; what runs next runs there unless moved";
|
|
2530
|
+
if (status === "in_progress" || status === "awaiting_user") return "already running as assigned";
|
|
2531
|
+
return "runs as assigned unless moved";
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
// src/services/conflictFiles.ts
|
|
2535
|
+
function capConflictFiles(files, max = 5) {
|
|
2536
|
+
return files.length <= max ? files.join(", ") : `${files.slice(0, max).join(", ")}, +${files.length - max} more`;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// src/services/mergeResultNotice.ts
|
|
2540
|
+
function blockNotice({ repo, reason, files, expected, actual, rebasing }) {
|
|
2541
|
+
switch (reason) {
|
|
2542
|
+
case "merge-in-progress":
|
|
2543
|
+
return `${repo} has a merge in progress`;
|
|
2544
|
+
case "conflict":
|
|
2545
|
+
return `${repo} would conflict in ${files.join(", ")}`;
|
|
2546
|
+
case "uncommitted-changes":
|
|
2547
|
+
return `${repo} has uncommitted changes to ${files.join(", ")}`;
|
|
2548
|
+
case "partial-landing":
|
|
2549
|
+
return `${repo} holds part of a task whose landing could not be rolled back`;
|
|
2550
|
+
case "wrong-branch":
|
|
2551
|
+
return `${repo === SELF_REPO ? "your checkout" : repo} is ${actual ? `on ${actual}` : rebasing ? "in the middle of a rebase" : "on a detached HEAD"}; this run started on ${expected}`;
|
|
2552
|
+
case "git-error":
|
|
2553
|
+
return `git could not check ${repo}`;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
function blockNotices(blocked) {
|
|
2557
|
+
return blocked.map(blockNotice);
|
|
2558
|
+
}
|
|
2559
|
+
function branchFix(blocked) {
|
|
2560
|
+
if (blocked.length === 0 || blocked.some((b) => b.reason !== "wrong-branch")) return "";
|
|
2561
|
+
const rebasing = blocked.filter((b) => b.rebasing);
|
|
2562
|
+
if (rebasing.length > 0) {
|
|
2563
|
+
const where = rebasing.map((b) => b.repo === SELF_REPO ? "your checkout" : b.repo).join(", ");
|
|
2564
|
+
return ` Finish or abort the rebase in ${where}${rebasing.length < blocked.length ? " and check out the branch each other repository started on" : ""}, then press Merge all again.`;
|
|
2565
|
+
}
|
|
2566
|
+
const started = [...new Set(blocked.map((b) => b.expected))];
|
|
2567
|
+
return started.length === 1 && blocked.length === 1 ? ` Check out ${started[0]} and press Merge all again.` : " Check out the branch each repository started on and press Merge all again.";
|
|
2568
|
+
}
|
|
2569
|
+
function landedNotice(landed) {
|
|
2570
|
+
if (landed.length === 0) return "Nothing was merged.";
|
|
2571
|
+
return landed.length === 1 ? `${landed[0]} was merged already and stays merged.` : `${landed.join(", ")} were merged already and stay merged.`;
|
|
2572
|
+
}
|
|
2573
|
+
function repairedLanded(handoff) {
|
|
2574
|
+
return handoff.landed.filter((t) => t.repairedFiles?.length);
|
|
2575
|
+
}
|
|
2576
|
+
function repairedNotice(handoff) {
|
|
2577
|
+
const repaired = repairedLanded(handoff);
|
|
2578
|
+
if (repaired.length === 0) return "";
|
|
2579
|
+
const named = repaired.map((t) => `${t.title}${t.repairedFiles?.length ? ` (${capConflictFiles(t.repairedFiles)})` : ""}`);
|
|
2580
|
+
const list = named.length === 1 ? named[0] : `${named.slice(0, -1).join(", ")} and ${named[named.length - 1]}`;
|
|
2581
|
+
return ` ${list} landed through ${repaired.length === 1 ? "a conflict repair" : "conflict repairs"}.`;
|
|
2582
|
+
}
|
|
2583
|
+
function describeMergeResult(result, branch, group, repaired = []) {
|
|
2584
|
+
switch (result.outcome) {
|
|
2585
|
+
case "merged": {
|
|
2586
|
+
const base = group ? `Merged ${branch} into the checked-out branch of every repository.` : `Merged ${branch} into your checked-out branch.`;
|
|
2587
|
+
const repairedNoticeText = repairedNotice({ landed: repaired });
|
|
2588
|
+
return { level: "info", message: `${base}${repairedNoticeText}` };
|
|
2589
|
+
}
|
|
2590
|
+
case "blocked":
|
|
2591
|
+
return { level: "warn", message: `Merged nothing, so every tree is as it was: ${blockNotices(result.blocked).join("; ")}.${branchFix(result.blocked)}` };
|
|
2592
|
+
case "conflict":
|
|
2593
|
+
return {
|
|
2594
|
+
level: "warn",
|
|
2595
|
+
message: result.repo === SELF_REPO ? `Merging ${branch} conflicted, so it was aborted: your tree is as it was.` : `Merging ${branch} conflicted in ${result.repo}${result.files?.length ? ` (${result.files.join(", ")})` : ""}, so it was aborted there. ${landedNotice(result.landed ?? [])}`
|
|
2596
|
+
};
|
|
2597
|
+
case "failed": {
|
|
2598
|
+
const lone = result.repo === SELF_REPO;
|
|
2599
|
+
const where = lone ? "" : ` in ${result.repo}`;
|
|
2600
|
+
if (result.error) {
|
|
2601
|
+
return { level: "error", message: `Could not merge ${branch}${where}: git said "${result.error}". ${lone ? "Your tree is as it was." : landedNotice(result.landed ?? [])}` };
|
|
2602
|
+
}
|
|
2603
|
+
return {
|
|
2604
|
+
level: "error",
|
|
2605
|
+
message: lone ? `Could not merge ${branch}: finish or abort the merge already in progress, then try again.` : `Could not merge ${branch}${where}: finish or abort any merge in progress there, then try again. ${landedNotice(result.landed ?? [])}`
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
function mergeOutcome(result, branch, group, repaired = []) {
|
|
2611
|
+
const ok = result.outcome === "merged";
|
|
2612
|
+
if (!group && result.outcome === "conflict") {
|
|
2613
|
+
return { ok, message: `Merging ${branch} conflicted, so it was aborted: your tree is as it was. Merge it with git and resolve the conflict there.` };
|
|
2614
|
+
}
|
|
2615
|
+
const { message } = describeMergeResult(result, branch, group, repaired);
|
|
2616
|
+
return { ok, message: ok || !group ? message : `${message} Each repository's ${branch} is a plain branch you can merge by hand.` };
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
// src/services/isolationPreviewText.ts
|
|
2620
|
+
var SHARED_NAMED = 5;
|
|
2621
|
+
var OPS_TASKS = "Ops and script tasks use your checkout.";
|
|
2622
|
+
function repoName(repo) {
|
|
2623
|
+
if (repo.path !== ".") return repo.path;
|
|
2624
|
+
return repo.top ? `Repository above this workspace (${repo.top})` : "This workspace";
|
|
2625
|
+
}
|
|
2626
|
+
function repoDetail(repo, status) {
|
|
2627
|
+
const head = repo.head;
|
|
2628
|
+
const base = head.kind === "branch" ? `on ${head.branch} (${head.commit})` : head.kind === "detached" ? `detached HEAD at ${head.commit}` : head.kind === "unknown" ? "branch not read" : head.branch ? `no commits yet on ${head.branch}` : "no commits yet";
|
|
2629
|
+
const shared = head.kind === "unborn" && (status === "ready" || status === "dirty") ? "; tasks share it instead of isolating it" : "";
|
|
2630
|
+
return `${base}${shared}${repo.dirty ? " \xB7 uncommitted changes to tracked files" : ""}`;
|
|
2631
|
+
}
|
|
2632
|
+
function sharedLine(shared) {
|
|
2633
|
+
if (shared.length === 0) return void 0;
|
|
2634
|
+
const more = shared.length - SHARED_NAMED;
|
|
2635
|
+
const named = shared.slice(0, SHARED_NAMED).join(", ");
|
|
2636
|
+
return `Shared live between tasks: ${more > 0 ? `${named} and ${more} more` : named}`;
|
|
2637
|
+
}
|
|
2638
|
+
function describeIsolationPreview(preview) {
|
|
2639
|
+
const repos = preview.repos.map((repo) => ({ name: repoName(repo), detail: repoDetail(repo, preview.status) }));
|
|
2640
|
+
const shared = sharedLine(preview.shared);
|
|
2641
|
+
const notice = (summary, note) => ({ summary, repos, ...shared ? { shared } : {}, ...note ? { note } : {} });
|
|
2642
|
+
switch (preview.status) {
|
|
2643
|
+
case "ready":
|
|
2644
|
+
return notice("Code tasks run in isolated git worktrees based on the repositories and branches below.", `${OPS_TASKS} The base branch is read again when a run starts.`);
|
|
2645
|
+
case "dirty":
|
|
2646
|
+
return notice(
|
|
2647
|
+
"Code tasks get isolated worktrees from the repositories and branches below once you decide what to do with uncommitted changes.",
|
|
2648
|
+
`Starting a run asks whether to stash them or run without isolation. ${OPS_TASKS}`
|
|
2649
|
+
);
|
|
2650
|
+
case "disabled":
|
|
2651
|
+
return notice("Worktree isolation is turned off, so tasks run in the workspace without isolated worktrees.");
|
|
2652
|
+
case "git-missing":
|
|
2653
|
+
return notice("Git was not found, so tasks run in the workspace without isolated worktrees.");
|
|
2654
|
+
case "no-commits":
|
|
2655
|
+
return notice("No repository has a commit yet, so tasks run in the workspace without isolated worktrees.");
|
|
2656
|
+
case "no-repo":
|
|
2657
|
+
return notice("No Git repository detected. Tasks run in the workspace without isolated worktrees.");
|
|
2658
|
+
case "failed":
|
|
2659
|
+
return notice(
|
|
2660
|
+
"Could not read the Git repositories just now, so Ordewell can't say yet whether code tasks get isolated worktrees.",
|
|
2661
|
+
`${preview.error ?? "Git did not answer"}. It is checked again when a run starts.`
|
|
2662
|
+
);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
// src/services/mergeWording.ts
|
|
2667
|
+
function isRepoGroup(handoff) {
|
|
2668
|
+
return handoff.repos.some((r) => r.path !== ".");
|
|
2669
|
+
}
|
|
2670
|
+
function reposWithWork(handoff) {
|
|
2671
|
+
const withWork = handoff.repos.filter((r) => r.landed.length > 0);
|
|
2672
|
+
return (withWork.length > 0 ? withWork : handoff.repos).map((r) => r.path);
|
|
2673
|
+
}
|
|
2674
|
+
function mergeTarget(handoff) {
|
|
2675
|
+
if (!isRepoGroup(handoff)) return handoff.repos[0]?.baseBranch ?? "the branch you have checked out";
|
|
2676
|
+
const targets = handoff.repos.filter((r) => reposWithWork(handoff).includes(r.path));
|
|
2677
|
+
const branches = new Set(targets.map((r) => r.baseBranch));
|
|
2678
|
+
const [only] = branches;
|
|
2679
|
+
if (branches.size === 1 && only) return `${only} in each of ${targets.map((r) => r.path).join(", ")}`;
|
|
2680
|
+
return `each of ${targets.map((r) => `${r.path} (${r.baseBranch ?? "as checked out"})`).join(", ")}`;
|
|
2681
|
+
}
|
|
2682
|
+
function checkoutRule(handoff) {
|
|
2683
|
+
const named = handoff.repos.filter((r) => r.baseBranch);
|
|
2684
|
+
if (named.length === 0) return "";
|
|
2685
|
+
if (!isRepoGroup(handoff)) return ` Nothing is merged unless ${named[0].baseBranch} is checked out.`;
|
|
2686
|
+
return " Nothing is merged unless each repository is on the branch it started on.";
|
|
2687
|
+
}
|
|
2688
|
+
function handoffBranchName(handoff) {
|
|
2689
|
+
return [...new Set(handoff.repos.map((r) => r.integrationBranch))].join(", ");
|
|
2690
|
+
}
|
|
2691
|
+
function repoBaseLine(repo, length) {
|
|
2692
|
+
const commit = repo.baseRef.slice(0, length);
|
|
2693
|
+
return repo.baseBranch ? `${repo.baseBranch} @ ${commit}` : `${repo.startedDetached ? "detached at" : "base"} ${commit}`;
|
|
2694
|
+
}
|
|
2695
|
+
function handoffBaseLine(handoff, length) {
|
|
2696
|
+
const group = isRepoGroup(handoff);
|
|
2697
|
+
return handoff.repos.map((repo) => `${group ? `${repo.path}: ` : ""}${repoBaseLine(repo, length)}`).join(", ");
|
|
2698
|
+
}
|
|
2699
|
+
function repoResultLines(handoff) {
|
|
2700
|
+
return handoff.repos.map(({ path, landed }) => `${path}: ${landed.length === 0 ? "nothing to merge" : `${landed.length} task${landed.length === 1 ? "" : "s"} landed`}`);
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// src/services/continuation.ts
|
|
2704
|
+
function sessionOwner(task) {
|
|
2705
|
+
return task.runnerSessionOwner ?? task.attemptUsage?.at(-1)?.runner;
|
|
2706
|
+
}
|
|
2707
|
+
function continuability(task) {
|
|
2708
|
+
const refuse = (reason) => ({ ok: false, reason: `Task "${task.title}" cannot be continued: ${reason}` });
|
|
2709
|
+
if (task.type !== "ai") return refuse("it is not run by an agent.");
|
|
2710
|
+
if (task.status === "awaiting_user" && task.awaitingReason === "conflict") {
|
|
2711
|
+
return refuse("its work is waiting on a merge conflict. Resolve or repair the conflict instead.");
|
|
2712
|
+
}
|
|
2713
|
+
const finished = task.status === "completed" || task.status === "failed" || task.status === "skipped";
|
|
2714
|
+
if (!finished && !pausedOnLimit(task)) return refuse("only a completed, failed or skipped task, or one paused on a usage limit, can be.");
|
|
2715
|
+
if (!task.runnerSessionId) return refuse("its runner left no saved session to resume. Use Retry instead.");
|
|
2716
|
+
const owner = sessionOwner(task);
|
|
2717
|
+
if (owner && owner !== task.assignedRunner) {
|
|
2718
|
+
return refuse(`its saved session belongs to ${owner}; Retry starts fresh on ${task.assignedRunner} and carries over its files.`);
|
|
2719
|
+
}
|
|
2720
|
+
return { ok: true, sessionId: task.runnerSessionId };
|
|
2721
|
+
}
|
|
2722
|
+
function canContinue(task) {
|
|
2723
|
+
return continuability(task).ok;
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// src/services/blocking.ts
|
|
2727
|
+
var UNSTARTED = /* @__PURE__ */ new Set(["pending", "approved"]);
|
|
1753
2728
|
function blockingFailures(tasks) {
|
|
2729
|
+
return planBlocks(tasks).blocked;
|
|
2730
|
+
}
|
|
2731
|
+
function repeatingChecks(tasks) {
|
|
2732
|
+
return planBlocks(tasks).repeating;
|
|
2733
|
+
}
|
|
2734
|
+
function planBlocks(tasks) {
|
|
1754
2735
|
const byId = /* @__PURE__ */ new Map();
|
|
1755
2736
|
const index = (list) => {
|
|
1756
2737
|
for (const t of list) {
|
|
@@ -1760,6 +2741,19 @@ function blockingFailures(tasks) {
|
|
|
1760
2741
|
};
|
|
1761
2742
|
index(tasks);
|
|
1762
2743
|
const memo = /* @__PURE__ */ new Map();
|
|
2744
|
+
const stopped = /* @__PURE__ */ new Map();
|
|
2745
|
+
const repeats = (check) => {
|
|
2746
|
+
if (!betweenRounds(check)) return false;
|
|
2747
|
+
const known = stopped.get(check.id);
|
|
2748
|
+
if (known !== void 0) return !known;
|
|
2749
|
+
stopped.set(check.id, false);
|
|
2750
|
+
const halted = (check.loop?.body ?? []).some((id) => {
|
|
2751
|
+
const body = byId.get(id);
|
|
2752
|
+
return !!body && (body.status === "failed" || body.status === "skipped" || failuresOf(id, /* @__PURE__ */ new Set()).length > 0);
|
|
2753
|
+
});
|
|
2754
|
+
stopped.set(check.id, halted);
|
|
2755
|
+
return !halted;
|
|
2756
|
+
};
|
|
1763
2757
|
const failuresOf = (id, visiting) => {
|
|
1764
2758
|
const known = memo.get(id);
|
|
1765
2759
|
if (known) return known;
|
|
@@ -1770,8 +2764,10 @@ function blockingFailures(tasks) {
|
|
|
1770
2764
|
for (const depId of task.dependencies) {
|
|
1771
2765
|
const dep = byId.get(depId);
|
|
1772
2766
|
if (!dep || dep.status === "completed" || dep.status === "skipped") continue;
|
|
1773
|
-
if (dep.status === "failed")
|
|
1774
|
-
|
|
2767
|
+
if (dep.status === "failed") {
|
|
2768
|
+
if (betweenRounds(dep) && (dep.loop?.body.includes(id) || repeats(dep))) continue;
|
|
2769
|
+
failed.add(depId);
|
|
2770
|
+
} else for (const root of failuresOf(depId, visiting)) failed.add(root);
|
|
1775
2771
|
}
|
|
1776
2772
|
visiting.delete(id);
|
|
1777
2773
|
const result = [...failed];
|
|
@@ -1779,14 +2775,37 @@ function blockingFailures(tasks) {
|
|
|
1779
2775
|
return result;
|
|
1780
2776
|
};
|
|
1781
2777
|
const blocked = /* @__PURE__ */ new Map();
|
|
1782
|
-
|
|
2778
|
+
const repeating = /* @__PURE__ */ new Set();
|
|
2779
|
+
for (const [id, task] of byId) {
|
|
1783
2780
|
const failures = failuresOf(id, /* @__PURE__ */ new Set());
|
|
1784
2781
|
if (failures.length > 0) blocked.set(id, failures);
|
|
2782
|
+
if (repeats(task)) repeating.add(id);
|
|
1785
2783
|
}
|
|
1786
|
-
return blocked;
|
|
2784
|
+
return { blocked, repeating };
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
// src/services/runSettingsText.ts
|
|
2788
|
+
var SUPERVISOR_LEVEL_LABEL = { off: "Off", watch: "Watch", act: "Act" };
|
|
2789
|
+
var plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
2790
|
+
var onOff = (value) => value ? "on" : "off";
|
|
2791
|
+
var supervisorSummary = (run) => `Supervisor: ${SUPERVISOR_LEVEL_LABEL[run.supervisorLevel]}, ${plural(run.supervisorRetryLimit, "retry", "retries")} per task, ${plural(run.wakeUpReadBudget, "read", "reads")} per wake-up`;
|
|
2792
|
+
var automergeSummary = (run) => `Merge automatically (gates, end of run): ${onOff(run.mergeAtGatesAutomatically)}`;
|
|
2793
|
+
var PLANNED_WITH_LABEL = "Planned-with line in commits Ordewell writes";
|
|
2794
|
+
var trailerSummary = (run) => `${PLANNED_WITH_LABEL}: ${onOff(run.plannedWithTrailer)}`;
|
|
2795
|
+
var autorunSummary = (run) => `Autorun: ${onOff(run.autorun)}`;
|
|
2796
|
+
var ACT_RISK = "Tasks it adds run too, ops tasks included, which can act on real systems unattended.";
|
|
2797
|
+
var AUTORUN_RISK = "Plans start without review, ops tasks included, which can act on real systems unattended. In Full, the planner may also start tasks it adds later.";
|
|
2798
|
+
function parseSwitch(arg) {
|
|
2799
|
+
return arg === "on" ? true : arg === "off" ? false : null;
|
|
2800
|
+
}
|
|
2801
|
+
function parseCount(arg, min) {
|
|
2802
|
+
if (arg === void 0 || !/^\d+$/.test(arg)) return null;
|
|
2803
|
+
const n = Number(arg);
|
|
2804
|
+
return n >= min ? n : null;
|
|
1787
2805
|
}
|
|
1788
2806
|
|
|
1789
2807
|
// src/services/SessionMessage.ts
|
|
2808
|
+
var IDLE_ACTIVITY = { starting: false, running: false, scheduled: false, resumable: false, advancesOnItsOwn: false };
|
|
1790
2809
|
function serializeTask(t) {
|
|
1791
2810
|
return {
|
|
1792
2811
|
id: t.id,
|
|
@@ -1809,17 +2828,28 @@ function serializeTask(t) {
|
|
|
1809
2828
|
...t.status === "awaiting_user" && t.awaitingReason === "usage-limit" && t.resumeAt ? { resumeAt: t.resumeAt } : {},
|
|
1810
2829
|
...t.status === "skipped" && t.skipReason ? { skipReason: t.skipReason } : {},
|
|
1811
2830
|
...t.ops ? { ops: true } : {},
|
|
2831
|
+
...t.research !== void 0 ? { research: t.research } : {},
|
|
2832
|
+
...t.handoff ? { handoff: structuredClone(t.handoff) } : {},
|
|
2833
|
+
...t.attempted ? { attempted: true } : {},
|
|
1812
2834
|
...t.attemptUsage ? { attemptUsage: structuredClone(t.attemptUsage) } : {},
|
|
1813
2835
|
...t.skills?.length ? { skills: [...t.skills] } : {},
|
|
1814
|
-
...t.addedBy ? { addedBy: t.addedBy } : {}
|
|
2836
|
+
...t.addedBy ? { addedBy: t.addedBy } : {},
|
|
2837
|
+
...t.commands ? { commands: t.commands.map((argv) => [...argv]) } : {},
|
|
2838
|
+
...t.script !== void 0 ? { script: t.script } : {},
|
|
2839
|
+
...t.timeoutMs !== void 0 ? { timeoutMs: t.timeoutMs } : {},
|
|
2840
|
+
...t.loop ? { loop: { body: [...t.loop.body], maxRounds: t.loop.maxRounds } } : {},
|
|
2841
|
+
...t.loop && t.loopRound !== void 0 ? { loopRound: t.loopRound } : {}
|
|
1815
2842
|
};
|
|
1816
2843
|
}
|
|
1817
|
-
function serializeTaskStatus(t, idleSince = null, isolation = null, queued = [], awaitingApproval = 0, mergeGate = [], checkpoint = "", waitsForApproval = false) {
|
|
2844
|
+
function serializeTaskStatus(t, idleSince = null, isolation = null, queued = [], awaitingApproval = 0, mergeGate = [], checkpoint = "", waitsForApproval = false, holdReason = null, completionReceived = null) {
|
|
1818
2845
|
return {
|
|
1819
2846
|
id: t.id,
|
|
2847
|
+
type: t.type,
|
|
1820
2848
|
status: t.status,
|
|
1821
|
-
verdict: t.verdict ? { outcome: t.verdict.outcome, reason: t.verdict.reason, checks: t.verdict.checks || [] } : null,
|
|
2849
|
+
verdict: t.verdict ? { outcome: t.verdict.outcome, reason: t.verdict.reason, checks: t.verdict.checks || [], ...t.verdict.reported ? { reported: t.verdict.reported } : {} } : null,
|
|
1822
2850
|
idleSince,
|
|
2851
|
+
...completionReceived ? { completionReceived } : {},
|
|
2852
|
+
...t.attempted ? { attempted: true } : {},
|
|
1823
2853
|
...t.attemptUsage ? { attemptUsage: structuredClone(t.attemptUsage) } : {},
|
|
1824
2854
|
...isolation ? { isolation } : {},
|
|
1825
2855
|
...t.status === "awaiting_user" && t.awaitingReason ? { awaitingReason: t.awaitingReason } : {},
|
|
@@ -1831,7 +2861,12 @@ function serializeTaskStatus(t, idleSince = null, isolation = null, queued = [],
|
|
|
1831
2861
|
...awaitingApproval > 0 ? { awaitingApproval } : {},
|
|
1832
2862
|
...mergeGate.length > 0 ? { mergeGate: [...mergeGate] } : {},
|
|
1833
2863
|
...t.forcedPastGate?.length ? { forcedPastGate: [...t.forcedPastGate] } : {},
|
|
1834
|
-
...waitsForApproval ? { waitsForApproval: true } : {}
|
|
2864
|
+
...waitsForApproval ? { waitsForApproval: true } : {},
|
|
2865
|
+
...holdReason ? { holdReason } : {},
|
|
2866
|
+
...t.research ? { research: true } : {},
|
|
2867
|
+
...t.handoff ? { handoff: structuredClone(t.handoff) } : {},
|
|
2868
|
+
...t.loop ? { loop: { body: [...t.loop.body], maxRounds: t.loop.maxRounds } } : {},
|
|
2869
|
+
...t.loop && t.loopRound !== void 0 ? { loopRound: t.loopRound } : {}
|
|
1835
2870
|
};
|
|
1836
2871
|
}
|
|
1837
2872
|
function surfaceConversation(history) {
|
|
@@ -1866,6 +2901,7 @@ function serializePlan(plan) {
|
|
|
1866
2901
|
generatedAt: plan.generatedAt,
|
|
1867
2902
|
runSettings: plan.runSettings,
|
|
1868
2903
|
...plan.plannerResend ? { plannerResend: plan.plannerResend } : {},
|
|
2904
|
+
...plan.plannerContextPause ? { plannerContextPause: plan.plannerContextPause } : {},
|
|
1869
2905
|
conversationHistory: surfaceConversation(plan.conversationHistory),
|
|
1870
2906
|
prdMarkdown: plan.prdMarkdown
|
|
1871
2907
|
};
|
|
@@ -1887,127 +2923,10 @@ function truncateCheckpointSummary(summary) {
|
|
|
1887
2923
|
return firstLine.slice(0, CHECKPOINT_TRUNCATE_LENGTH - 1) + "\u2026";
|
|
1888
2924
|
}
|
|
1889
2925
|
|
|
1890
|
-
// src/services/AssignmentScope.ts
|
|
1891
|
-
var OVER = /* @__PURE__ */ new Set(["completed", "skipped"]);
|
|
1892
|
-
function assignmentIssue(task, scope) {
|
|
1893
|
-
if (task.type !== "ai" || OVER.has(task.status)) return null;
|
|
1894
|
-
if (!scope.enabledRunners.includes(task.assignedRunner)) return { kind: "runner-off", runner: task.assignedRunner };
|
|
1895
|
-
const model = task.assignedModel;
|
|
1896
|
-
const allowed = effectiveAllowlist(scope.allowlist[task.assignedRunner], task.assignedRunner, scope.models);
|
|
1897
|
-
if (model && allowed && !allowed.includes(model.modelId)) {
|
|
1898
|
-
return { kind: "model-off-list", runner: task.assignedRunner, modelId: model.modelId, modelLabel: model.modelLabel || model.modelId };
|
|
1899
|
-
}
|
|
1900
|
-
return null;
|
|
1901
|
-
}
|
|
1902
|
-
function assignmentIssueText(issue) {
|
|
1903
|
-
return issue.kind === "runner-off" ? `${issue.runner} is switched off` : `${issue.modelLabel} is not in the allowlist`;
|
|
1904
|
-
}
|
|
1905
|
-
function assignmentConsequence({ status, awaitingReason, resumeAt }) {
|
|
1906
|
-
if (status === "failed") return "failed; a retry would run it there unless moved";
|
|
1907
|
-
if (status === "awaiting_user" && awaitingReason === "usage-limit") {
|
|
1908
|
-
return resumeAt ? "paused on a usage limit; resumes there at the reset unless moved" : "paused on a usage limit; a retry would run it there unless moved";
|
|
1909
|
-
}
|
|
1910
|
-
if (status === "awaiting_user" && (awaitingReason === "conflict" || awaitingReason === "files-changed")) return "waiting for the user; what runs next runs there unless moved";
|
|
1911
|
-
if (status === "in_progress" || status === "awaiting_user") return "already running as assigned";
|
|
1912
|
-
return "runs as assigned unless moved";
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
// src/services/conflictFiles.ts
|
|
1916
|
-
function capConflictFiles(files, max = 5) {
|
|
1917
|
-
return files.length <= max ? files.join(", ") : `${files.slice(0, max).join(", ")}, +${files.length - max} more`;
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
// src/services/researchStepSummary.ts
|
|
1921
|
-
function ellipsize(text2, max) {
|
|
1922
|
-
const oneLine2 = text2.replace(/\s+/g, " ").trim();
|
|
1923
|
-
return oneLine2.length > max ? `${oneLine2.slice(0, max)}\u2026` : oneLine2;
|
|
1924
|
-
}
|
|
1925
|
-
function summarizeToolCall(tool, argsJson, toolLabel) {
|
|
1926
|
-
const display = toolLabel?.trim() || tool;
|
|
1927
|
-
if (tool === "agent_tool") {
|
|
1928
|
-
try {
|
|
1929
|
-
const args = JSON.parse(argsJson);
|
|
1930
|
-
const hint = args.path ?? args.file_path ?? args.filePath ?? args.pattern ?? args.query ?? args.url ?? args.command;
|
|
1931
|
-
if (hint) return `${display} ${ellipsize(String(hint), 50)}`;
|
|
1932
|
-
} catch {
|
|
1933
|
-
}
|
|
1934
|
-
return display;
|
|
1935
|
-
}
|
|
1936
|
-
if (tool === "spawn_research_agent") {
|
|
1937
|
-
try {
|
|
1938
|
-
const args = JSON.parse(argsJson);
|
|
1939
|
-
const prompt = typeof args.prompt === "string" ? args.prompt : "";
|
|
1940
|
-
const oneLine2 = prompt.replace(/\s+/g, " ").trim();
|
|
1941
|
-
return `spawn: "${oneLine2.slice(0, 60)}${oneLine2.length > 60 ? "\u2026" : ""}"`;
|
|
1942
|
-
} catch {
|
|
1943
|
-
return "spawn_research_agent";
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
try {
|
|
1947
|
-
const args = JSON.parse(argsJson);
|
|
1948
|
-
if (tool === "find_symbol" && args.symbol) {
|
|
1949
|
-
return `find_symbol ${args.symbol}${args.language ? ` [${args.language}]` : ""}`;
|
|
1950
|
-
}
|
|
1951
|
-
if (tool === "web_search" && args.query) {
|
|
1952
|
-
return `web_search "${ellipsize(String(args.query), 50)}"`;
|
|
1953
|
-
}
|
|
1954
|
-
if (tool === "fetch" && args.url) {
|
|
1955
|
-
return `fetch ${ellipsize(String(args.url), 60)}`;
|
|
1956
|
-
}
|
|
1957
|
-
if (tool === "glob" && args.pattern) {
|
|
1958
|
-
return `glob ${ellipsize(String(args.pattern), 50)}`;
|
|
1959
|
-
}
|
|
1960
|
-
if (tool === "grep" && args.pattern) {
|
|
1961
|
-
const mode = args.output_mode && args.output_mode !== "content" ? ` (${args.output_mode})` : "";
|
|
1962
|
-
const scope = args.include ? ` in ${args.include}` : "";
|
|
1963
|
-
return `grep ${ellipsize(String(args.pattern), 40)}${scope}${mode}`;
|
|
1964
|
-
}
|
|
1965
|
-
const path = args.filePath || args.file_path || args.path;
|
|
1966
|
-
if (path) {
|
|
1967
|
-
const basename = String(path).split("/").pop() || String(path);
|
|
1968
|
-
return `${display} ${basename}`;
|
|
1969
|
-
}
|
|
1970
|
-
if (args.pattern) {
|
|
1971
|
-
return `${display} ${args.pattern}`;
|
|
1972
|
-
}
|
|
1973
|
-
if (args.command) {
|
|
1974
|
-
return `bash: ${String(args.command).slice(0, 40)}`;
|
|
1975
|
-
}
|
|
1976
|
-
} catch {
|
|
1977
|
-
}
|
|
1978
|
-
return display;
|
|
1979
|
-
}
|
|
1980
|
-
function classifyOutcome(success, output) {
|
|
1981
|
-
if (!success) {
|
|
1982
|
-
if (/^(Command|Fetch|Web search) refused:/.test(output) || /is not available\. You are a read-only research subagent/.test(output)) return "refused";
|
|
1983
|
-
if (/not approved/i.test(output) || /^Access denied:/.test(output) || /needs user approval/i.test(output)) return "denied";
|
|
1984
|
-
return "failure";
|
|
1985
|
-
}
|
|
1986
|
-
return "success";
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
// src/services/runSettingsText.ts
|
|
1990
|
-
var SUPERVISOR_LEVEL_LABEL = { off: "Off", watch: "Watch", act: "Act" };
|
|
1991
|
-
var plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
1992
|
-
var onOff = (value) => value ? "on" : "off";
|
|
1993
|
-
var supervisorSummary = (run) => `Supervisor: ${SUPERVISOR_LEVEL_LABEL[run.supervisorLevel]}, ${plural(run.supervisorRetryLimit, "retry", "retries")} per task, ${plural(run.wakeUpReadBudget, "read", "reads")} per wake-up`;
|
|
1994
|
-
var automergeSummary = (run) => `Merge automatically (gates, end of run): ${onOff(run.mergeAtGatesAutomatically)}`;
|
|
1995
|
-
var autorunSummary = (run) => `Autorun: ${onOff(run.autorun)}`;
|
|
1996
|
-
var ACT_RISK = "Tasks it adds run too, ops tasks included, which can act on real systems unattended.";
|
|
1997
|
-
var AUTORUN_RISK = "Plans start without review, ops tasks included, which can act on real systems unattended. In Full, the planner may also start tasks it adds later.";
|
|
1998
|
-
function parseSwitch(arg) {
|
|
1999
|
-
return arg === "on" ? true : arg === "off" ? false : null;
|
|
2000
|
-
}
|
|
2001
|
-
function parseCount(arg, min) {
|
|
2002
|
-
if (arg === void 0 || !/^\d+$/.test(arg)) return null;
|
|
2003
|
-
const n = Number(arg);
|
|
2004
|
-
return n >= min ? n : null;
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
2926
|
// src/services/runView.ts
|
|
2008
|
-
var EMPTY_RUN = { tasks: [], runMerge: null, handoff: null, mergeResult: null };
|
|
2009
|
-
function split({ verdict, idleSince, isolation, checkpoint, queued, continuable, awaitingApproval, mergeGate, forcedPastGate, waitsForApproval, ...plan }) {
|
|
2010
|
-
return { plan, status: { verdict, idleSince, isolation, checkpoint, queued, continuable, awaitingApproval, mergeGate, forcedPastGate, waitsForApproval } };
|
|
2927
|
+
var EMPTY_RUN = { tasks: [], runMerge: null, handoff: null, mergeResult: null, activity: IDLE_ACTIVITY };
|
|
2928
|
+
function split({ verdict, idleSince, completionReceived, isolation, checkpoint, queued, continuable, awaitingApproval, mergeGate, forcedPastGate, waitsForApproval, holdReason, handoff, attempted, ...plan }) {
|
|
2929
|
+
return { plan, status: { verdict, idleSince, completionReceived, isolation, checkpoint, queued, continuable, awaitingApproval, mergeGate, forcedPastGate, waitsForApproval, holdReason, handoff, attempted } };
|
|
2011
2930
|
}
|
|
2012
2931
|
function refresh(previous, tasks) {
|
|
2013
2932
|
const held = new Map([...walkPlan(previous)].map(({ task }) => [task.id, task]));
|
|
@@ -2019,7 +2938,7 @@ function refresh(previous, tasks) {
|
|
|
2019
2938
|
};
|
|
2020
2939
|
return tasks.map(fold);
|
|
2021
2940
|
}
|
|
2022
|
-
function withStatus({ awaitingReason: _reason, resumeAt: _resumeAt, skipReason: _skipped, attemptUsage: _usage, ...plan }, { id: _id, ...status }) {
|
|
2941
|
+
function withStatus({ awaitingReason: _reason, resumeAt: _resumeAt, skipReason: _skipped, attemptUsage: _usage, loopRound: _round, ...plan }, { id: _id, ...status }) {
|
|
2023
2942
|
return { ...plan, ...status };
|
|
2024
2943
|
}
|
|
2025
2944
|
function applyStatus(tasks, updates) {
|
|
@@ -2061,7 +2980,13 @@ var PATCHABLE = /* @__PURE__ */ new Set([
|
|
|
2061
2980
|
"ops",
|
|
2062
2981
|
"skills",
|
|
2063
2982
|
"addedBy",
|
|
2064
|
-
"attemptUsage"
|
|
2983
|
+
"attemptUsage",
|
|
2984
|
+
"commands",
|
|
2985
|
+
"script",
|
|
2986
|
+
"timeoutMs",
|
|
2987
|
+
"loop",
|
|
2988
|
+
"loopRound",
|
|
2989
|
+
"research"
|
|
2065
2990
|
]);
|
|
2066
2991
|
function patchTask(task, changes) {
|
|
2067
2992
|
const fields = Object.entries(changes).filter(([key]) => PATCHABLE.has(key));
|
|
@@ -2107,15 +3032,14 @@ function reduceRun(view, input) {
|
|
|
2107
3032
|
tasks: applyStatus(view.tasks, new Map(input.tasks.map((t) => [t.id, t]))),
|
|
2108
3033
|
runMerge: input.runMerge ?? null,
|
|
2109
3034
|
handoff: stillToMerge(view.handoff, input.runMerge),
|
|
2110
|
-
usage: input.usage
|
|
3035
|
+
usage: input.usage,
|
|
3036
|
+
...input.activity ? { activity: input.activity } : {}
|
|
2111
3037
|
};
|
|
2112
3038
|
case "task_started":
|
|
2113
3039
|
case "task_updated": {
|
|
2114
3040
|
const tasks = changeTask(view.tasks, input.taskId, (task) => input.type === "task_started" ? startTask(task) : patchTask(task, input.changes));
|
|
2115
3041
|
return tasks === view.tasks ? view : { ...view, tasks };
|
|
2116
3042
|
}
|
|
2117
|
-
case "plan_edit_held":
|
|
2118
|
-
return { ...view, heldEdit: input.held ?? void 0 };
|
|
2119
3043
|
case "isolation_handoff":
|
|
2120
3044
|
return { ...view, handoff: handoffOf(input) };
|
|
2121
3045
|
case "isolation_merge":
|
|
@@ -2143,6 +3067,7 @@ function reduceRun(view, input) {
|
|
|
2143
3067
|
case "planner_text_retracted":
|
|
2144
3068
|
case "planner_usage":
|
|
2145
3069
|
case "planner_liveness":
|
|
3070
|
+
case "planner_compaction":
|
|
2146
3071
|
case "subagent_started":
|
|
2147
3072
|
case "subagent_finished":
|
|
2148
3073
|
case "research_step":
|
|
@@ -2159,6 +3084,95 @@ function reduceRun(view, input) {
|
|
|
2159
3084
|
}
|
|
2160
3085
|
}
|
|
2161
3086
|
|
|
3087
|
+
// src/taskRow/texts.ts
|
|
3088
|
+
function titledRefs(ids, tasks) {
|
|
3089
|
+
return ids.map((id) => titledTaskRefOf(tasks, id) ?? id);
|
|
3090
|
+
}
|
|
3091
|
+
function dependentsNotice(dependents, tasks = []) {
|
|
3092
|
+
if (dependents.length === 0) return null;
|
|
3093
|
+
const subject = dependents.length === 1 ? "1 task depends" : `${dependents.length} tasks depend`;
|
|
3094
|
+
return `${subject} on it and will lose that dependency: ${dependents.map((t) => (t.id ? titledTaskRefOf(tasks, t.id) : void 0) ?? titledTaskRef(t)).join(", ")}.`;
|
|
3095
|
+
}
|
|
3096
|
+
function loopLeaveNotice(tasks, taskId) {
|
|
3097
|
+
const check = checkOf(tasks, taskId);
|
|
3098
|
+
if (!check?.loop) return null;
|
|
3099
|
+
const [named] = titledRefs([check.id], tasks);
|
|
3100
|
+
return check.loop.body.length > 1 ? `It is in the body of ${named}'s loop, and leaves it.` : `It is the whole body of ${named}'s loop, so the loop is dropped with it.`;
|
|
3101
|
+
}
|
|
3102
|
+
function pastGateConfirmation(unmerged, subject = "This task") {
|
|
3103
|
+
return `${subject} waits for Merge all: the work of ${unmerged.join(", ")} is not merged into your branch yet, so it would act without it. Starting it now is kept on the task.`;
|
|
3104
|
+
}
|
|
3105
|
+
function manualOverrideConfirmation(direction, label) {
|
|
3106
|
+
return direction === "complete" ? `Mark ${label} complete? You vouch for its work although no passing verdict said it was done. Any changes it kept may land in your run, and the tasks that depend on it are released to start on a future run.` : `Mark ${label} not done? This clears its completion and holds it until you retry or start it. The work it did stays where it is, and tasks that already ran on its result are not undone.`;
|
|
3107
|
+
}
|
|
3108
|
+
function manualOverrideStopFirst(direction, label) {
|
|
3109
|
+
return `${label.charAt(0).toUpperCase()}${label.slice(1)} cannot be marked ${direction === "complete" ? "complete" : "not done"} while the plan is starting, running or waiting to resume, because the run could still move it on. Stop the run first, and mark it once everything has stopped.`;
|
|
3110
|
+
}
|
|
3111
|
+
function newTaskFields(title, prompt) {
|
|
3112
|
+
const name = title.trim();
|
|
3113
|
+
if (!name) return null;
|
|
3114
|
+
return { title: name, description: name, prompt: prompt?.trim() ? prompt : name, type: "ai" };
|
|
3115
|
+
}
|
|
3116
|
+
function noExecutorRefusal(type, manual, script) {
|
|
3117
|
+
return type === "script" ? `A script task runs no model, so ${script}` : `Manual tasks do not run an executor, so ${manual}`;
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
// src/services/conversationSummary.ts
|
|
3121
|
+
var KEPT_USER_MESSAGES = 2;
|
|
3122
|
+
var SUMMARY_OPEN = "<conversation_summary>";
|
|
3123
|
+
var SUMMARY_CLOSE = "</conversation_summary>";
|
|
3124
|
+
function summaryRequest(planLines) {
|
|
3125
|
+
return [
|
|
3126
|
+
"The user asked to condense this planning conversation. Write a summary that will replace everything said so far, so the conversation can carry on from it alone.",
|
|
3127
|
+
"Keep: the goal, the decisions made, the constraints, the open questions, the key file and code findings, and where the plan stands now.",
|
|
3128
|
+
"Reply in plain prose. Do not emit task operations, plan JSON or task reads, and do not run tools \u2014 the tasks are not changing in this turn.",
|
|
3129
|
+
`Put the summary between ${SUMMARY_OPEN} and ${SUMMARY_CLOSE}, and nothing outside the tags.`,
|
|
3130
|
+
...planLines ? ["<current_plan>", ...planLines, "</current_plan>"] : []
|
|
3131
|
+
].join("\n");
|
|
3132
|
+
}
|
|
3133
|
+
function extractSummary(reply) {
|
|
3134
|
+
const matches = [...reply.matchAll(new RegExp(`${SUMMARY_OPEN}([\\s\\S]*?)${SUMMARY_CLOSE}`, "g"))];
|
|
3135
|
+
const summary = matches.at(-1)?.[1].trim();
|
|
3136
|
+
return summary || null;
|
|
3137
|
+
}
|
|
3138
|
+
function condensedNotice(summary, trigger = "manual") {
|
|
3139
|
+
return [
|
|
3140
|
+
trigger === "manual" ? "Conversation condensed: the summary below stands in for the earlier messages from here on, and the last two exchanges were kept as they were. The full conversation stays in the history. The task list is unchanged." : "Conversation condensed automatically to fit the model's context: the summary below stands in for the earlier messages from here on, and the most recent exchanges that fit were kept as they were. The full conversation stays in the history. Task and run state are the current plan's, not this summary's.",
|
|
3141
|
+
"",
|
|
3142
|
+
summary
|
|
3143
|
+
].join("\n");
|
|
3144
|
+
}
|
|
3145
|
+
function isCheckpoint(value) {
|
|
3146
|
+
if (!value || typeof value !== "object") return false;
|
|
3147
|
+
const { id, keptFrom, trigger } = value;
|
|
3148
|
+
return typeof id === "string" && id !== "" && Number.isInteger(keptFrom) && (trigger === "manual" || trigger === "auto");
|
|
3149
|
+
}
|
|
3150
|
+
function newestCheckpoint(transcript) {
|
|
3151
|
+
let newest = null;
|
|
3152
|
+
for (const [at, entry] of transcript.entries()) {
|
|
3153
|
+
if (entry.kind !== "compaction") continue;
|
|
3154
|
+
if (at === 0) newest = { at, keptFrom: 1 };
|
|
3155
|
+
else if (isCheckpoint(entry.checkpoint) && entry.checkpoint.keptFrom >= (newest?.keptFrom ?? 0) && entry.checkpoint.keptFrom <= at) {
|
|
3156
|
+
newest = { at, keptFrom: entry.checkpoint.keptFrom };
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
return newest;
|
|
3160
|
+
}
|
|
3161
|
+
function modelContext(transcript) {
|
|
3162
|
+
const checkpoint = newestCheckpoint(transcript);
|
|
3163
|
+
const verbatim = (checkpoint ? transcript.slice(checkpoint.keptFrom) : transcript).filter((m) => m.kind !== "compaction");
|
|
3164
|
+
return checkpoint ? [transcript[checkpoint.at], ...verbatim] : verbatim;
|
|
3165
|
+
}
|
|
3166
|
+
function contextFloor(transcript) {
|
|
3167
|
+
return newestCheckpoint(transcript)?.keptFrom ?? 0;
|
|
3168
|
+
}
|
|
3169
|
+
function compactionCut(transcript) {
|
|
3170
|
+
const floor = contextFloor(transcript);
|
|
3171
|
+
const userAt = transcript.flatMap((m, i) => i >= floor && isUserMessage(m) ? [i] : []);
|
|
3172
|
+
if (userAt.length <= KEPT_USER_MESSAGES) return null;
|
|
3173
|
+
return userAt[userAt.length - KEPT_USER_MESSAGES];
|
|
3174
|
+
}
|
|
3175
|
+
|
|
2162
3176
|
// src/conversation/reduce.ts
|
|
2163
3177
|
var EMPTY_CONVERSATION = { blocks: [], nextId: 1 };
|
|
2164
3178
|
function isUnsettledSegment(block, turnId) {
|
|
@@ -2171,7 +3185,7 @@ function finalSegmentIndex(blocks, turnId, pastPlan = false) {
|
|
|
2171
3185
|
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
2172
3186
|
const block = blocks[i];
|
|
2173
3187
|
if (block.type === "usage" || block.type === "thinking" || block.type === "approval" || pastPlan && block.type === "plan") continue;
|
|
2174
|
-
if (block.type === "command" || block.type === "compaction" || block.type === "runner_loaded") continue;
|
|
3188
|
+
if (block.type === "command" || block.type === "compaction" || block.type === "runner_loaded" || block.type === "handoff") continue;
|
|
2175
3189
|
if (block.turnId !== turnId) continue;
|
|
2176
3190
|
return isUnsettledSegment(block, turnId) ? i : -1;
|
|
2177
3191
|
}
|
|
@@ -2187,10 +3201,11 @@ function openTurn(view, { turnId, prompt = "", skills = [], wakeUps = [], forced
|
|
|
2187
3201
|
const updates = wakeUps.map((text2, n) => settledMessage(`b${said.nextId + n}`, "wake_up", text2, turnId));
|
|
2188
3202
|
return { ...said, blocks: [...said.blocks.slice(0, at), ...updates, ...said.blocks.slice(at)], nextId: said.nextId + updates.length };
|
|
2189
3203
|
}
|
|
2190
|
-
function takePendingInputs(view, prompt, wakeUps, turnId) {
|
|
3204
|
+
function takePendingInputs(view, prompt, wakeUps, turnId, heldAt) {
|
|
2191
3205
|
const pending = view.pendingInputs ?? [];
|
|
2192
|
-
const
|
|
2193
|
-
const
|
|
3206
|
+
const arrived = ({ input }) => heldAt === void 0 || input.timestamp <= heldAt;
|
|
3207
|
+
const users = pending.filter((p) => p.input.source === "user" && arrived(p));
|
|
3208
|
+
const updates = pending.filter((p) => p.input.source === "supervisor" && arrived(p));
|
|
2194
3209
|
const taken = /* @__PURE__ */ new Set();
|
|
2195
3210
|
let matchedPrompt = false;
|
|
2196
3211
|
for (let n = 1; prompt !== void 0 && n <= users.length; n++) {
|
|
@@ -2326,7 +3341,7 @@ function markPlan(view, content, turnId) {
|
|
|
2326
3341
|
}
|
|
2327
3342
|
return append(view, (id) => ({ type: "plan", id, text: "", ...marker }));
|
|
2328
3343
|
}
|
|
2329
|
-
function requestApproval2(view, { id: approvalId, kind, subject, scope, detail, turnId }) {
|
|
3344
|
+
function requestApproval2(view, { id: approvalId, kind, subject, scope, detail, body, turnId }) {
|
|
2330
3345
|
return append(view, (id) => ({
|
|
2331
3346
|
type: "approval",
|
|
2332
3347
|
id,
|
|
@@ -2336,6 +3351,7 @@ function requestApproval2(view, { id: approvalId, kind, subject, scope, detail,
|
|
|
2336
3351
|
scope,
|
|
2337
3352
|
status: "pending",
|
|
2338
3353
|
...detail ? { detail } : {},
|
|
3354
|
+
...body ? { body } : {},
|
|
2339
3355
|
...turnId ? { turnId } : {}
|
|
2340
3356
|
}));
|
|
2341
3357
|
}
|
|
@@ -2366,11 +3382,48 @@ function endTurn2(view, { turnId, outcome }) {
|
|
|
2366
3382
|
if (planText >= 0) next = { ...next, blocks: next.blocks.filter((_, i) => i !== planText) };
|
|
2367
3383
|
return closeOpenBlocks(next, outcome === "stopped" ? "stopped" : outcome === "error" ? "failed" : null);
|
|
2368
3384
|
}
|
|
2369
|
-
function
|
|
3385
|
+
function compactionAt(view, compactionId) {
|
|
3386
|
+
return findLastIndex(view.blocks, (b) => b.type === "compaction" && b.compactionId === compactionId);
|
|
3387
|
+
}
|
|
3388
|
+
function followCompaction(view, { compaction, turnId }) {
|
|
3389
|
+
const i = compactionAt(view, compaction.id);
|
|
3390
|
+
const at = view.blocks[i];
|
|
3391
|
+
const known = at?.type === "compaction" ? at : void 0;
|
|
3392
|
+
if (compaction.phase === "started") {
|
|
3393
|
+
if (known) return view;
|
|
3394
|
+
return append(view, (id) => compactionBlock(id, { compactionId: compaction.id, origin: compaction.origin, trigger: compaction.trigger, status: "running", turnId }));
|
|
3395
|
+
}
|
|
3396
|
+
const { outcome, preTokens, postTokens } = compaction;
|
|
3397
|
+
if (known && known.status !== "running") return view;
|
|
3398
|
+
if (outcome === "unchanged") return known ? { ...view, blocks: view.blocks.filter((_, n) => n !== i) } : view;
|
|
3399
|
+
if (!known && (outcome === "stopped" || outcome === "failed")) return view;
|
|
3400
|
+
const ended = (id) => compactionBlock(id, {
|
|
3401
|
+
compactionId: compaction.id,
|
|
3402
|
+
origin: compaction.origin,
|
|
3403
|
+
trigger: compaction.trigger,
|
|
3404
|
+
status: outcome,
|
|
3405
|
+
preTokens,
|
|
3406
|
+
postTokens,
|
|
3407
|
+
turnId: known?.turnId ?? turnId
|
|
3408
|
+
});
|
|
3409
|
+
return known ? { ...view, blocks: replaceAt(view.blocks, i, ended(known.id)) } : append(view, ended);
|
|
3410
|
+
}
|
|
3411
|
+
function markCompaction(view, entry) {
|
|
3412
|
+
const { content: summary } = entry;
|
|
3413
|
+
const checkpoint = isCheckpoint(entry.checkpoint) ? entry.checkpoint : void 0;
|
|
3414
|
+
if (checkpoint) {
|
|
3415
|
+
const i2 = compactionAt(view, checkpoint.id);
|
|
3416
|
+
const known = view.blocks[i2];
|
|
3417
|
+
if (known?.type !== "compaction") {
|
|
3418
|
+
return append(view, (id) => compactionBlock(id, { compactionId: checkpoint.id, origin: "ordewell", trigger: checkpoint.trigger, status: "condensed", summary }));
|
|
3419
|
+
}
|
|
3420
|
+
if (known.status === "condensed" && known.summary === summary) return view;
|
|
3421
|
+
return { ...view, blocks: replaceAt(view.blocks, i2, { ...known, status: "condensed", summary }) };
|
|
3422
|
+
}
|
|
3423
|
+
const settled = (id) => compactionBlock(id, { origin: "ordewell", trigger: "manual", status: "condensed", summary });
|
|
2370
3424
|
const i = findLastIndex(view.blocks, (b) => b.type === "message" && b.role === "planner" && b.turnId === void 0 && b.text === summary);
|
|
2371
3425
|
const announced = view.blocks[i];
|
|
2372
|
-
|
|
2373
|
-
return append(view, (id) => settledMessage(id, "system", summary));
|
|
3426
|
+
return announced ? { ...view, blocks: replaceAt(view.blocks, i, settled(announced.id)) } : append(view, settled);
|
|
2374
3427
|
}
|
|
2375
3428
|
function syncTranscript(view, { plan, turnId }) {
|
|
2376
3429
|
const history = plan.conversationHistory ?? [];
|
|
@@ -2379,12 +3432,13 @@ function syncTranscript(view, { plan, turnId }) {
|
|
|
2379
3432
|
let latest = view.transcriptAt;
|
|
2380
3433
|
for (const [i, entry] of history.entries()) {
|
|
2381
3434
|
const isNew = view.transcriptAt === void 0 || entry.timestamp > view.transcriptAt;
|
|
2382
|
-
|
|
2383
|
-
if (
|
|
3435
|
+
const heldAt = isNew ? void 0 : entry.timestamp;
|
|
3436
|
+
if (entry.role === "user" && !entry.kind) next = takePendingInputs(next, entry.content, [], void 0, heldAt).view;
|
|
3437
|
+
if (entry.kind === "wake_up") next = takePendingInputs(next, void 0, [entry.content], void 0, heldAt).view;
|
|
2384
3438
|
if (isNew && entry.kind === "plan_generated") next = markPlan(next, entry.content, i === committed ? turnId : void 0);
|
|
2385
3439
|
if (isNew && entry.kind === "system") next = append(next, (id) => settledMessage(id, "system", entry.content));
|
|
2386
3440
|
if (isNew && entry.kind === "error") next = append(next, (id) => settledMessage(id, "error", entry.content));
|
|
2387
|
-
if (isNew && entry.kind === "compaction") next = markCompaction(next, entry
|
|
3441
|
+
if (isNew && entry.kind === "compaction") next = markCompaction(next, entry);
|
|
2388
3442
|
if (latest === void 0 || entry.timestamp > latest) latest = entry.timestamp;
|
|
2389
3443
|
}
|
|
2390
3444
|
return next === view && latest === view.transcriptAt ? view : { ...next, transcriptAt: latest };
|
|
@@ -2436,6 +3490,8 @@ function reduceConversation(view, input) {
|
|
|
2436
3490
|
return append(view, (id) => settledMessage(id, "system", input.text));
|
|
2437
3491
|
case "planner_usage":
|
|
2438
3492
|
return reportUsage2(view, input);
|
|
3493
|
+
case "planner_compaction":
|
|
3494
|
+
return followCompaction(view, input);
|
|
2439
3495
|
default:
|
|
2440
3496
|
return view;
|
|
2441
3497
|
}
|
|
@@ -2451,13 +3507,22 @@ function fromEntry(entry, loaded = []) {
|
|
|
2451
3507
|
const { skill } = entry;
|
|
2452
3508
|
return (id) => skillLoadBlock(id(), skillLoadNotice(skill));
|
|
2453
3509
|
}
|
|
3510
|
+
if (entry.kind === "compaction") {
|
|
3511
|
+
const checkpoint = isCheckpoint(entry.checkpoint) ? entry.checkpoint : void 0;
|
|
3512
|
+
return (id) => compactionBlock(id(), {
|
|
3513
|
+
compactionId: checkpoint?.id,
|
|
3514
|
+
origin: "ordewell",
|
|
3515
|
+
trigger: checkpoint?.trigger ?? "manual",
|
|
3516
|
+
status: "condensed",
|
|
3517
|
+
summary: entry.content
|
|
3518
|
+
});
|
|
3519
|
+
}
|
|
2454
3520
|
if (entry.role === "user" && !entry.kind) return (id) => userMessage(id(), entry.content, loaded.map(skillLoadNotice), { forced: entry.forced });
|
|
2455
3521
|
return (id) => settledMessage(id(), roleOf(entry), entry.content);
|
|
2456
3522
|
}
|
|
2457
3523
|
function roleOf(entry) {
|
|
2458
3524
|
switch (entry.kind) {
|
|
2459
3525
|
case "system":
|
|
2460
|
-
case "compaction":
|
|
2461
3526
|
case "execution":
|
|
2462
3527
|
return "system";
|
|
2463
3528
|
case "wake_up":
|
|
@@ -2568,6 +3633,82 @@ function fromTranscript(conversationHistory, researchLog, plannerUsage, pendingT
|
|
|
2568
3633
|
return { blocks, nextId, ...transcriptAt ? { transcriptAt } : {}, ...pendingInputs.length > 0 ? { pendingInputs } : {} };
|
|
2569
3634
|
}
|
|
2570
3635
|
|
|
3636
|
+
// src/conversation/labels.ts
|
|
3637
|
+
var APPROVAL_KIND_TEXT = {
|
|
3638
|
+
shell_command: "Run a command",
|
|
3639
|
+
url_fetch: "Fetch a URL",
|
|
3640
|
+
web_search: "Search the web",
|
|
3641
|
+
external_path: "Read outside the workspace",
|
|
3642
|
+
mcp_tool: "Use an MCP tool",
|
|
3643
|
+
mcp_server: "Start a project MCP server",
|
|
3644
|
+
runner_tool: "Use a tool",
|
|
3645
|
+
task_start: "Start work"
|
|
3646
|
+
};
|
|
3647
|
+
function approvalKindText(kind) {
|
|
3648
|
+
return APPROVAL_KIND_TEXT[kind];
|
|
3649
|
+
}
|
|
3650
|
+
var APPROVAL_SOURCE_TEXT = {
|
|
3651
|
+
"pre-approved": "pre-approved",
|
|
3652
|
+
remembered: "remembered",
|
|
3653
|
+
mode: "policy",
|
|
3654
|
+
"no-channel": "no approval channel",
|
|
3655
|
+
excluded: "excluded",
|
|
3656
|
+
guard: "blocked address",
|
|
3657
|
+
secret: "secret check",
|
|
3658
|
+
budget: "web-read budget",
|
|
3659
|
+
unsupported: "not usable on this runner version"
|
|
3660
|
+
};
|
|
3661
|
+
function approvalStatusText(block) {
|
|
3662
|
+
const policy = block.decidedBy && block.decidedBy !== "asked" ? APPROVAL_SOURCE_TEXT[block.decidedBy] : void 0;
|
|
3663
|
+
switch (block.status) {
|
|
3664
|
+
case "pending":
|
|
3665
|
+
return "Waiting for you";
|
|
3666
|
+
case "granted":
|
|
3667
|
+
return policy ? `Auto-approved (${policy})` : block.forTask ? "Approved for this task" : "Approved";
|
|
3668
|
+
case "denied":
|
|
3669
|
+
return policy ? `Auto-denied (${policy})` : "Denied";
|
|
3670
|
+
case "withdrawn":
|
|
3671
|
+
return "Withdrawn";
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
function planMarkerParts(block) {
|
|
3675
|
+
if (block.status === "building") return ["Building plan\u2026"];
|
|
3676
|
+
const count = block.taskCount === void 0 ? [] : [`${block.taskCount} task${block.taskCount === 1 ? "" : "s"}`];
|
|
3677
|
+
return [`Plan ${block.status}`, ...count];
|
|
3678
|
+
}
|
|
3679
|
+
function handoffFindingsText(count) {
|
|
3680
|
+
return count === 0 ? "no findings" : `${count} finding${count === 1 ? "" : "s"}`;
|
|
3681
|
+
}
|
|
3682
|
+
function firstNonBlankLine(text2) {
|
|
3683
|
+
return text2.split(/\r?\n/).map((line) => line.trim()).find((line) => line !== "") ?? "";
|
|
3684
|
+
}
|
|
3685
|
+
function handoffPeekParts(handoff) {
|
|
3686
|
+
const severity = handoff.severity ? [severityLabel(handoff.severity)] : [];
|
|
3687
|
+
const commit = handoff.commit ? [`Commit: ${handoff.commit.subject.trim()}`] : [];
|
|
3688
|
+
const summary = firstNonBlankLine(handoff.summary);
|
|
3689
|
+
return [handoffFindingsText(handoff.findingCount), ...severity, ...handoff.patch ? [handoff.patch.peek] : [], ...commit, ...summary ? [summary] : []];
|
|
3690
|
+
}
|
|
3691
|
+
var COMPACTION_STATUS_TEXT = {
|
|
3692
|
+
running: "Compacting context\u2026",
|
|
3693
|
+
stopped: "Compaction stopped \xB7 nothing changed",
|
|
3694
|
+
failed: "Compaction failed \xB7 nothing changed",
|
|
3695
|
+
paused: "Context does not fit \xB7 the planner is paused",
|
|
3696
|
+
pruned: "Older tool output trimmed from context",
|
|
3697
|
+
unchanged: "Context unchanged"
|
|
3698
|
+
};
|
|
3699
|
+
function compactedBy({ origin, trigger }) {
|
|
3700
|
+
if (origin === "native") return trigger === "auto" ? "The planner compacted its context on its own" : "The planner compacted its context";
|
|
3701
|
+
if (origin === "ordewell") return trigger === "auto" ? "Context compacted automatically" : "Context compacted";
|
|
3702
|
+
return trigger === "auto" ? "Context compacted by the runner on its own" : "Context compacted";
|
|
3703
|
+
}
|
|
3704
|
+
function compactionText(block) {
|
|
3705
|
+
const status = block.status ? COMPACTION_STATUS_TEXT[block.status] : void 0;
|
|
3706
|
+
if (status) return status;
|
|
3707
|
+
const { preTokens, postTokens } = block;
|
|
3708
|
+
const sizes = preTokens !== void 0 && postTokens !== void 0 ? ` \xB7 ${formatTokenCount(preTokens)} \u2192 ${formatTokenCount(postTokens)} tokens` : "";
|
|
3709
|
+
return `${compactedBy(block)}${sizes}`;
|
|
3710
|
+
}
|
|
3711
|
+
|
|
2571
3712
|
// src/conversation/promptHold.ts
|
|
2572
3713
|
var EMPTY_HOLD = [];
|
|
2573
3714
|
function holdPrompt(hold, text2) {
|
|
@@ -2636,9 +3777,100 @@ function stopTurn(view, gate) {
|
|
|
2636
3777
|
};
|
|
2637
3778
|
}
|
|
2638
3779
|
|
|
3780
|
+
// src/services/WakePolicy.ts
|
|
3781
|
+
var REPORT_LEAD = ", whose result you asked for, has ended.";
|
|
3782
|
+
function createWakePolicy(options) {
|
|
3783
|
+
const { debounceMs } = options;
|
|
3784
|
+
if (!Number.isFinite(debounceMs) || debounceMs < 0) {
|
|
3785
|
+
throw new RangeError(`debounceMs must be a finite, non-negative number of milliseconds, got ${debounceMs}`);
|
|
3786
|
+
}
|
|
3787
|
+
let attention = [];
|
|
3788
|
+
let sinceLast = [];
|
|
3789
|
+
const dueAt = () => {
|
|
3790
|
+
const first = attention[0];
|
|
3791
|
+
if (!first) return null;
|
|
3792
|
+
return attention.some((e) => e.kind === "run_finished") ? first.at : first.at + debounceMs;
|
|
3793
|
+
};
|
|
3794
|
+
return {
|
|
3795
|
+
push(event) {
|
|
3796
|
+
if (event.kind === "task_passed") {
|
|
3797
|
+
sinceLast.push(event);
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
if (event.kind === "task_idle" && attention.some((e) => e.kind === "task_idle" && e.taskId === event.taskId)) return;
|
|
3801
|
+
if (event.kind === "run_finished" && attention.some((e) => e.kind === "run_finished")) return;
|
|
3802
|
+
if (event.kind === "usage_limit") {
|
|
3803
|
+
const at = attention.findIndex((e) => e.kind === "usage_limit" && e.runner === event.runner);
|
|
3804
|
+
const taskIds = event.taskId === void 0 ? [] : [event.taskId];
|
|
3805
|
+
if (at < 0) {
|
|
3806
|
+
attention.push({ ...event, taskIds });
|
|
3807
|
+
return;
|
|
3808
|
+
}
|
|
3809
|
+
const joined = attention[at];
|
|
3810
|
+
attention = attention.map((e, i) => i !== at ? e : {
|
|
3811
|
+
...joined,
|
|
3812
|
+
taskIds: [.../* @__PURE__ */ new Set([...joined.taskIds ?? [], ...taskIds])],
|
|
3813
|
+
resumeAt: event.resumeAt ?? joined.resumeAt ?? null
|
|
3814
|
+
});
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
attention.push(event);
|
|
3818
|
+
},
|
|
3819
|
+
poll(now) {
|
|
3820
|
+
const due = dueAt();
|
|
3821
|
+
if (due === null || now < due) return null;
|
|
3822
|
+
const batch = { attention, sinceLast };
|
|
3823
|
+
attention = [];
|
|
3824
|
+
sinceLast = [];
|
|
3825
|
+
return batch;
|
|
3826
|
+
},
|
|
3827
|
+
dueAt,
|
|
3828
|
+
snapshot() {
|
|
3829
|
+
return { attention: [...attention], sinceLast: [...sinceLast] };
|
|
3830
|
+
},
|
|
3831
|
+
restore(snapshot) {
|
|
3832
|
+
attention = [...snapshot?.attention ?? []];
|
|
3833
|
+
sinceLast = [...snapshot?.sinceLast ?? []];
|
|
3834
|
+
}
|
|
3835
|
+
};
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
// src/conversation/wakeUp.ts
|
|
3839
|
+
var DIGEST_HEAD = "Run update:";
|
|
3840
|
+
var REPORT_PREVIEW_LINES = 8;
|
|
3841
|
+
function hasLongReport(text2) {
|
|
3842
|
+
return wakeUpItems(text2).some((item) => item.kind === "report" && item.lines.length > REPORT_PREVIEW_LINES);
|
|
3843
|
+
}
|
|
3844
|
+
var REPORTED = /^task (#[\d.]+ ".*")$/;
|
|
3845
|
+
function wakeUpItems(text2) {
|
|
3846
|
+
const items = [];
|
|
3847
|
+
let report = null;
|
|
3848
|
+
for (const line of text2.replace(/\r\n?/g, "\n").split("\n")) {
|
|
3849
|
+
if (report && (line.startsWith(" ") || line === "")) {
|
|
3850
|
+
report.lines.push(line.slice(2));
|
|
3851
|
+
continue;
|
|
3852
|
+
}
|
|
3853
|
+
report = null;
|
|
3854
|
+
if (line === DIGEST_HEAD && items.length === 0) continue;
|
|
3855
|
+
const bullet = line.startsWith("- ") ? line.slice(2) : null;
|
|
3856
|
+
const at = bullet?.indexOf(REPORT_LEAD) ?? -1;
|
|
3857
|
+
const task = bullet === null || at < 0 ? void 0 : REPORTED.exec(bullet.slice(0, at))?.[1];
|
|
3858
|
+
if (bullet !== null && task !== void 0) {
|
|
3859
|
+
report = { kind: "report", task, lines: [bullet.slice(at + REPORT_LEAD.length).trimStart()] };
|
|
3860
|
+
items.push(report);
|
|
3861
|
+
} else items.push({ kind: "line", text: line });
|
|
3862
|
+
}
|
|
3863
|
+
return items.map((item) => item.kind === "report" ? { ...item, lines: trimmed(item.lines) } : item);
|
|
3864
|
+
}
|
|
3865
|
+
function trimmed(lines2) {
|
|
3866
|
+
const kept = [...lines2];
|
|
3867
|
+
while (kept.length > 1 && kept[kept.length - 1].trim() === "") kept.pop();
|
|
3868
|
+
return kept;
|
|
3869
|
+
}
|
|
3870
|
+
|
|
2639
3871
|
// src/conversation/detail.ts
|
|
2640
3872
|
function hasHiddenDetail(blocks) {
|
|
2641
|
-
return blocks.some((b) => b.type === "thinking" || b.type === "tool" || b.type === "subagent" || b.type === "runner_loaded");
|
|
3873
|
+
return blocks.some((b) => b.type === "thinking" || b.type === "tool" || b.type === "subagent" || b.type === "runner_loaded" || b.type === "message" && b.role === "wake_up" && hasLongReport(b.text) || b.type === "handoff" && (b.handoff.report?.replace(/\n+$/, "").split("\n").length ?? 0) > REPORT_PREVIEW_LINES);
|
|
2642
3874
|
}
|
|
2643
3875
|
|
|
2644
3876
|
// src/conversation/notices.ts
|
|
@@ -2730,8 +3962,26 @@ function followTaskLog(state, input) {
|
|
|
2730
3962
|
}
|
|
2731
3963
|
}
|
|
2732
3964
|
|
|
3965
|
+
// src/taskRow/editability.ts
|
|
3966
|
+
function editableWhileRunning(task, run) {
|
|
3967
|
+
if (!NOT_STARTED.has(task.status)) return false;
|
|
3968
|
+
if (!run) return true;
|
|
3969
|
+
if ((run.awaitingApproval ?? 0) > 0) return false;
|
|
3970
|
+
return run.isolation?.state !== "active" && run.isolation?.state !== "repairing";
|
|
3971
|
+
}
|
|
3972
|
+
function runHoldsEditors(isExecuting, activity) {
|
|
3973
|
+
return isExecuting || activity.starting || activity.advancesOnItsOwn;
|
|
3974
|
+
}
|
|
3975
|
+
function cardEditable(runHolds, task, run) {
|
|
3976
|
+
return !runHolds || editableWhileRunning(task, run);
|
|
3977
|
+
}
|
|
3978
|
+
function cardEditNeedsUnstarted(runHolds, task) {
|
|
3979
|
+
return runHolds || NOT_STARTED.has(task.status);
|
|
3980
|
+
}
|
|
3981
|
+
|
|
2733
3982
|
// src/taskRow/actions.ts
|
|
2734
3983
|
var UNSTARTED_AI = (t) => t.type === "ai" && NOT_STARTED.has(t.status);
|
|
3984
|
+
var UNSTARTED_RUN = (t) => t.type !== "user" && NOT_STARTED.has(t.status);
|
|
2735
3985
|
var QUEUED = (t) => t.status === "pending" || t.status === "approved";
|
|
2736
3986
|
var PAUSED = (t) => t.status === "awaiting_user" && t.awaitingReason === "usage-limit";
|
|
2737
3987
|
var RESUMES = (t) => PAUSED(t) && !!t.resumeAt;
|
|
@@ -2742,7 +3992,7 @@ var TASK_RULES = [
|
|
|
2742
3992
|
["resume-now", RESUMABLE],
|
|
2743
3993
|
["cancel-resume", RESUMES],
|
|
2744
3994
|
["skip", HOLDS_UP],
|
|
2745
|
-
["force-start",
|
|
3995
|
+
["force-start", UNSTARTED_RUN],
|
|
2746
3996
|
// A manual task has no runner to report it done, so it can always be marked;
|
|
2747
3997
|
// a skipped one's kept work can still be vouched for.
|
|
2748
3998
|
["complete", (t) => t.status === "awaiting_user" || t.status === "skipped" || t.type === "user"],
|
|
@@ -2765,24 +4015,138 @@ function taskRowActions(task, placement = "task") {
|
|
|
2765
4015
|
function markAction(task) {
|
|
2766
4016
|
return task.status === "completed" ? "uncomplete" : "complete";
|
|
2767
4017
|
}
|
|
2768
|
-
function
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
4018
|
+
function taskActionCommand(action, task, reason) {
|
|
4019
|
+
switch (action) {
|
|
4020
|
+
case "run":
|
|
4021
|
+
return { command: "run_task", task };
|
|
4022
|
+
case "retry":
|
|
4023
|
+
return { command: "retry_task", task, from: "fresh" };
|
|
4024
|
+
case "cancel":
|
|
4025
|
+
return { command: "stop_task", task };
|
|
4026
|
+
// The user says the limit has reset early: the task resumes in its saved session now.
|
|
4027
|
+
case "resume-now":
|
|
4028
|
+
return { command: "retry_task", task, from: "session" };
|
|
4029
|
+
case "cancel-resume":
|
|
4030
|
+
return { command: "cancel_resume", task };
|
|
4031
|
+
case "skip":
|
|
4032
|
+
return { command: "skip_task", task, ...reason ? { reason } : {} };
|
|
4033
|
+
case "force-start":
|
|
4034
|
+
return { command: "force_start", task };
|
|
4035
|
+
case "complete":
|
|
4036
|
+
return { command: "mark_complete", task };
|
|
4037
|
+
case "uncomplete":
|
|
4038
|
+
return { command: "mark_not_done", task };
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
function overrideNeedsConfirmation(task) {
|
|
4042
|
+
return task.type !== "user";
|
|
4043
|
+
}
|
|
4044
|
+
function confirmedOverride(command, seen) {
|
|
4045
|
+
return command.command === "mark_complete" || command.command === "mark_not_done" ? { ...command, confirmed: true, ...seen ? { seen } : {} } : command;
|
|
2772
4046
|
}
|
|
2773
4047
|
function opsEditable(task) {
|
|
2774
4048
|
return UNSTARTED_AI(task);
|
|
2775
4049
|
}
|
|
2776
4050
|
|
|
4051
|
+
// src/taskRow/loop.ts
|
|
4052
|
+
function loopCheckState(check, goesOn) {
|
|
4053
|
+
switch (check.status) {
|
|
4054
|
+
case "completed":
|
|
4055
|
+
return "passed";
|
|
4056
|
+
case "in_progress":
|
|
4057
|
+
case "running":
|
|
4058
|
+
case "awaiting_user":
|
|
4059
|
+
return "running";
|
|
4060
|
+
case "failed":
|
|
4061
|
+
if (betweenRounds(check)) return goesOn ? "repeating" : "stopped";
|
|
4062
|
+
return outOfRounds(check) ? "out-of-rounds" : "failed";
|
|
4063
|
+
case "pending":
|
|
4064
|
+
case "approved":
|
|
4065
|
+
case "blocked":
|
|
4066
|
+
return loopRoundOf(check) > 1 ? "running" : "waiting";
|
|
4067
|
+
default:
|
|
4068
|
+
return "ended";
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
var roundsOf = (count) => `${count} round${count === 1 ? "" : "s"}`;
|
|
4072
|
+
function loopCheckLabel(check, state) {
|
|
4073
|
+
const round = loopRoundOf(check);
|
|
4074
|
+
const of = `round ${round} of ${check.loop.maxRounds}`;
|
|
4075
|
+
switch (state) {
|
|
4076
|
+
case "waiting":
|
|
4077
|
+
case "ended":
|
|
4078
|
+
return `Loop of up to ${roundsOf(check.loop.maxRounds)}`;
|
|
4079
|
+
case "running":
|
|
4080
|
+
return `Round ${round} of ${check.loop.maxRounds}`;
|
|
4081
|
+
case "repeating":
|
|
4082
|
+
return `Round ${round} of ${check.loop.maxRounds}: fixing`;
|
|
4083
|
+
case "stopped":
|
|
4084
|
+
return `Stopped in ${of}`;
|
|
4085
|
+
case "failed":
|
|
4086
|
+
return `Failed in ${of}`;
|
|
4087
|
+
case "passed":
|
|
4088
|
+
return `${check.verdict?.outcome === "pass" ? "Passed" : "Completed"} in ${of}`;
|
|
4089
|
+
case "out-of-rounds":
|
|
4090
|
+
return `Failed after ${roundsOf(round)}`;
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
function loopBodyRound(check, bodyStatus) {
|
|
4094
|
+
if (!["in_progress", "running", "awaiting_user", "completed", "failed"].includes(bodyStatus)) return null;
|
|
4095
|
+
return check.status === "failed" ? loopRoundOf(check) : Math.max(1, loopRoundOf(check) - 1);
|
|
4096
|
+
}
|
|
4097
|
+
function loopBodyLabel(checkRef, round, maxRounds) {
|
|
4098
|
+
return `Runs if ${checkRef} fails${round === null ? "" : ` (round ${round} of ${maxRounds})`}`;
|
|
4099
|
+
}
|
|
4100
|
+
|
|
2777
4101
|
// src/taskRow/view.ts
|
|
4102
|
+
function taskChatAvailable(task) {
|
|
4103
|
+
if (task.type === "user") return false;
|
|
4104
|
+
return task.attempted === true || (task.attemptUsage?.length ?? 0) > 0;
|
|
4105
|
+
}
|
|
2778
4106
|
var refsOf = (ids, tasks) => ids.map((id) => tasks?.length ? taskRefOf(tasks, id) ?? id : id);
|
|
2779
4107
|
var namedRepo = (repo) => repo && repo !== "." ? repo : null;
|
|
2780
4108
|
var namedRepos = (repos) => (repos ?? []).filter((r) => r !== ".");
|
|
2781
4109
|
var filesOf = (files) => files?.length ? capConflictFiles(files) : null;
|
|
2782
|
-
var asBlocking = (tasks) => tasks.map((t) => ({
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
4110
|
+
var asBlocking = (tasks) => tasks.map((t) => ({
|
|
4111
|
+
id: t.id,
|
|
4112
|
+
status: t.status ?? "",
|
|
4113
|
+
dependencies: t.dependencies ?? [],
|
|
4114
|
+
subtasks: asBlocking(t.subtasks ?? []),
|
|
4115
|
+
...t.loop ? { loop: t.loop, loopRound: t.loopRound, ...t.verdict ? { verdict: t.verdict } : {} } : {}
|
|
4116
|
+
}));
|
|
4117
|
+
function blockedBy(task, reading) {
|
|
4118
|
+
if (!reading || !NOT_STARTED.has(task.status)) return [];
|
|
4119
|
+
return reading.blocks.blocked.get(task.id) ?? [];
|
|
4120
|
+
}
|
|
4121
|
+
function loopOf(task, reading) {
|
|
4122
|
+
const tasks = reading?.tasks;
|
|
4123
|
+
if (task.loop) {
|
|
4124
|
+
const check2 = { id: task.id, status: task.status, loop: task.loop, loopRound: task.loopRound, ...task.verdict ? { verdict: task.verdict } : {} };
|
|
4125
|
+
const state = loopCheckState(check2, reading ? reading.blocks.repeating.has(task.id) : true);
|
|
4126
|
+
const body = refsOf(task.loop.body, tasks);
|
|
4127
|
+
const halted = reading ? task.loop.body.filter((id) => {
|
|
4128
|
+
const status = reading.tasks.find((t) => t.id === id)?.status;
|
|
4129
|
+
return status === "failed" || status === "skipped" || reading.blocks.blocked.has(id);
|
|
4130
|
+
}) : [];
|
|
4131
|
+
const detail = state === "repeating" ? `Found problems: runs again once ${body.join(", ")} ${body.length === 1 ? "has" : "have"} landed` : state === "stopped" ? `${refsOf(halted, tasks).join(", ")} did not pass: retry ${halted.length === 1 ? "it" : "them"} and the loop goes on` : null;
|
|
4132
|
+
return { role: "check", state, label: loopCheckLabel(check2, state), detail, body };
|
|
4133
|
+
}
|
|
4134
|
+
const owner = reading && checkOf(reading.tasks, task.id);
|
|
4135
|
+
if (!owner?.loop) return null;
|
|
4136
|
+
const [check] = refsOf([owner.id], tasks);
|
|
4137
|
+
const round = loopBodyRound({ status: owner.status ?? "", loopRound: owner.loopRound }, task.status);
|
|
4138
|
+
return { role: "body", label: loopBodyLabel(check, round, owner.loop.maxRounds), check, round };
|
|
4139
|
+
}
|
|
4140
|
+
function handoffOf2(task) {
|
|
4141
|
+
if (!task.handoff) return null;
|
|
4142
|
+
const patch = savedPatchView(task.handoff);
|
|
4143
|
+
return {
|
|
4144
|
+
...task.handoff.commit ? { commit: task.handoff.commit } : {},
|
|
4145
|
+
summary: task.handoff.summary,
|
|
4146
|
+
findingCount: task.handoff.findingCount,
|
|
4147
|
+
severity: worstSeverity(task.handoff.findings),
|
|
4148
|
+
...patch ? { patch } : {}
|
|
4149
|
+
};
|
|
2786
4150
|
}
|
|
2787
4151
|
function flagOf(task, scope) {
|
|
2788
4152
|
const issue = scope && task.assignedRunner !== void 0 ? assignmentIssue({ ...task, assignedRunner: task.assignedRunner }, scope) : null;
|
|
@@ -2793,17 +4157,26 @@ function taskRowView(task, context = {}) {
|
|
|
2793
4157
|
const isolation = task.isolation && task.isolation.state !== "none" ? task.isolation : null;
|
|
2794
4158
|
const spend = taskUsageSummary(task);
|
|
2795
4159
|
const latestAttempt = task.attemptUsage?.at(-1);
|
|
2796
|
-
const
|
|
4160
|
+
const tasks = task.loop ? context.tasks?.map((t) => t.id === task.id ? { ...t, status: task.status, loopRound: task.loopRound, verdict: task.verdict } : t) : context.tasks;
|
|
4161
|
+
const reading = tasks?.length ? { tasks, blocks: planBlocks(asBlocking(tasks)) } : null;
|
|
4162
|
+
const blocked = blockedBy(task, reading);
|
|
4163
|
+
const loop = loopOf(task, reading);
|
|
2797
4164
|
return {
|
|
2798
4165
|
usage: latestAttempt ? { totals: spend.totals, ...spend.contextFill ? { contextFill: spend.contextFill } : {}, attempt: latestAttempt.attempt } : task.type === "ai" && ["completed", "failed", "in_progress", "awaiting_user"].includes(task.status) ? { totals: {}, attempt: 0 } : null,
|
|
2799
4166
|
orderLabel: taskOrderLabel(task, parent),
|
|
2800
|
-
kind: task.type === "user" ? "user" : task.ops ? "ops" : "ai",
|
|
2801
|
-
|
|
4167
|
+
kind: task.type === "user" ? "user" : task.type === "script" ? "script" : task.ops ? "ops" : "ai",
|
|
4168
|
+
// Between rounds a check waits for its fix as a task waits for its dependencies, and is no failure (ADR-0045 L3).
|
|
4169
|
+
status: blocked.length > 0 ? "blocked" : loop?.role === "check" && loop.state === "repeating" ? "todo" : taskStatusKind(task),
|
|
2802
4170
|
running: isTaskRunning(task),
|
|
4171
|
+
chatAvailable: taskChatAvailable(task),
|
|
2803
4172
|
awaiting: awaitingLabel(task),
|
|
2804
4173
|
resumes: resumeLabel(task, context.now),
|
|
2805
4174
|
approvals: approvalLabel(task.awaitingApproval),
|
|
4175
|
+
receipt: receiptLabel(task),
|
|
2806
4176
|
waitsForApproval: task.waitsForApproval && NOT_STARTED.has(task.status) ? WAITS_FOR_APPROVAL_LABEL : null,
|
|
4177
|
+
holdReason: task.holdReason?.trim() || null,
|
|
4178
|
+
research: task.type === "ai" && task.research === true,
|
|
4179
|
+
runner: assignedRunnerOf(task) ?? null,
|
|
2807
4180
|
...flagOf(task, context.runnerScope),
|
|
2808
4181
|
autonomous: task.type === "ai" && context.modes?.find((m) => m.id === task.taskMode)?.autonomous === true,
|
|
2809
4182
|
repos: namedRepos(isolation?.repos),
|
|
@@ -2815,6 +4188,8 @@ function taskRowView(task, context = {}) {
|
|
|
2815
4188
|
skipReason: task.status === "skipped" ? task.skipReason ?? null : null,
|
|
2816
4189
|
opsChangedFiles: task.status === "awaiting_user" && task.awaitingReason === "files-changed",
|
|
2817
4190
|
forcedPastGate: task.forcedPastGate?.length ? task.forcedPastGate : null,
|
|
4191
|
+
loop,
|
|
4192
|
+
handoff: handoffOf2(task),
|
|
2818
4193
|
dependencies: refsOf(task.dependencies, context.tasks),
|
|
2819
4194
|
// A block read off the plan offers what a stored one does: Skip among them.
|
|
2820
4195
|
actions: taskRowActions(blocked.length > 0 ? { ...task, status: "blocked" } : task, context.placement ?? (parent ? "subtask" : "task")),
|
|
@@ -2822,23 +4197,90 @@ function taskRowView(task, context = {}) {
|
|
|
2822
4197
|
opsEditable: opsEditable(task)
|
|
2823
4198
|
};
|
|
2824
4199
|
}
|
|
4200
|
+
function loopRowParts(loop) {
|
|
4201
|
+
if (loop.role === "body") return [loop.label];
|
|
4202
|
+
const runs = loop.state === "waiting" || loop.state === "running" ? `Runs ${loop.body.join(", ")} if it fails` : null;
|
|
4203
|
+
return [loop.label, loop.detail ?? runs].filter((part) => part !== null);
|
|
4204
|
+
}
|
|
2825
4205
|
|
|
2826
|
-
// src/taskRow/
|
|
2827
|
-
|
|
2828
|
-
|
|
4206
|
+
// src/taskRow/logHeader.ts
|
|
4207
|
+
var STATE_LABEL = {
|
|
4208
|
+
done: "Done",
|
|
4209
|
+
failed: "Failed",
|
|
4210
|
+
running: "Running",
|
|
4211
|
+
quiet: "Stalled",
|
|
4212
|
+
awaiting: "Waiting for your input",
|
|
4213
|
+
blocked: "Blocked",
|
|
4214
|
+
skipped: "Skipped",
|
|
4215
|
+
todo: "To do"
|
|
4216
|
+
};
|
|
4217
|
+
function stateOf(task, follow, approvals) {
|
|
4218
|
+
if (task.status === "completed") return { kind: "done", label: STATE_LABEL.done };
|
|
4219
|
+
if (task.status === "failed") return { kind: "failed", label: STATE_LABEL.failed };
|
|
4220
|
+
if (task.status === "skipped") return { kind: "skipped", label: skippedLabel(task.skipReason ?? null) };
|
|
4221
|
+
const approval = approvalLabel(approvals);
|
|
4222
|
+
if (approval) return { kind: "approval", label: approval };
|
|
4223
|
+
if (follow.view.working) return { kind: "running", label: "Working" };
|
|
4224
|
+
if (task.status === "awaiting_user") {
|
|
4225
|
+
return { kind: task.awaitingReason === "checkpoint" ? "checkpoint" : "awaiting", label: awaitingLabel(task) ?? STATE_LABEL.awaiting };
|
|
4226
|
+
}
|
|
4227
|
+
if (task.status === "blocked") return { kind: "blocked", label: STATE_LABEL.blocked };
|
|
4228
|
+
if (follow.view.lastTurnEnd === "failed") return { kind: "failed", label: "Turn failed" };
|
|
4229
|
+
if (follow.view.lastTurnEnd === "interrupted") return { kind: "todo", label: "Interrupted" };
|
|
4230
|
+
const kind = taskStatusKind(task);
|
|
4231
|
+
return { kind, label: STATE_LABEL[kind] };
|
|
4232
|
+
}
|
|
4233
|
+
function loggedUsage(view) {
|
|
4234
|
+
const last = view.blocks.at(-1);
|
|
4235
|
+
if (last?.type !== "usage") return null;
|
|
4236
|
+
const text2 = usageText(last);
|
|
4237
|
+
return text2 ? { text: text2, ...last.contextFill ? { contextFill: last.contextFill } : {} } : null;
|
|
4238
|
+
}
|
|
4239
|
+
function usageRow(text2, contextFill) {
|
|
4240
|
+
return text2 ? { text: text2, ...contextFill ? { contextFill } : {} } : null;
|
|
4241
|
+
}
|
|
4242
|
+
function usageOf(task, follow, attempt) {
|
|
4243
|
+
const attempts = task.attemptUsage ?? [];
|
|
4244
|
+
const latest = attempts.at(-1);
|
|
4245
|
+
const past = attempt > 0 && attempt !== latest?.attempt;
|
|
4246
|
+
const total = taskUsageSummary(task);
|
|
4247
|
+
const totalText = usageText(total);
|
|
4248
|
+
if (!past) {
|
|
4249
|
+
if (attempts.length > 0) return totalText ? [{ text: totalText, ...total.contextFill ? { contextFill: total.contextFill } : {} }] : [];
|
|
4250
|
+
const logged = loggedUsage(follow.view);
|
|
4251
|
+
return logged ? [logged] : [];
|
|
4252
|
+
}
|
|
4253
|
+
const onScreen = attempts.find((a) => a.attempt === attempt);
|
|
4254
|
+
const own = onScreen ? usageRow(usageText({ totals: onScreen.totals, contextFill: plannerContextFill(onScreen) }), plannerContextFill(onScreen)) : loggedUsage(follow.view);
|
|
4255
|
+
const rows = [];
|
|
4256
|
+
if (own) rows.push(onScreen ? { ...own, text: `attempt ${onScreen.attempt}: ${own.text}` } : own);
|
|
4257
|
+
if (totalText) rows.push({ text: `task total: ${totalText}`, ...total.contextFill ? { contextFill: total.contextFill } : {} });
|
|
4258
|
+
return rows;
|
|
2829
4259
|
}
|
|
2830
|
-
function
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
4260
|
+
function taskLogCommands(view) {
|
|
4261
|
+
for (let i = view.blocks.length - 1; i >= 0; i--) {
|
|
4262
|
+
const block = view.blocks[i];
|
|
4263
|
+
if (block.type === "runner_loaded") return block.commands;
|
|
4264
|
+
}
|
|
4265
|
+
return [];
|
|
2834
4266
|
}
|
|
2835
|
-
function
|
|
2836
|
-
|
|
4267
|
+
function logContinues(task) {
|
|
4268
|
+
const finished = task.status === "completed" || task.status === "failed" || task.status === "skipped";
|
|
4269
|
+
return task.continuable === true && (finished || task.status === "awaiting_user" && task.awaitingReason === "usage-limit");
|
|
2837
4270
|
}
|
|
2838
|
-
function
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
4271
|
+
function logResumes(task) {
|
|
4272
|
+
return task.continuable === true && task.status === "awaiting_user" && task.awaitingReason === "usage-limit";
|
|
4273
|
+
}
|
|
4274
|
+
function taskLogHeaderView(task, follow, approvals, attempt) {
|
|
4275
|
+
return {
|
|
4276
|
+
state: stateOf(task, follow, approvals),
|
|
4277
|
+
resumes: resumeLabel(task),
|
|
4278
|
+
continuable: logContinues(task),
|
|
4279
|
+
resumable: logResumes(task),
|
|
4280
|
+
working: follow.view.working,
|
|
4281
|
+
usage: usageOf(task, follow, attempt),
|
|
4282
|
+
commands: taskLogCommands(follow.view)
|
|
4283
|
+
};
|
|
2842
4284
|
}
|
|
2843
4285
|
|
|
2844
4286
|
export {
|
|
@@ -2866,7 +4308,59 @@ export {
|
|
|
2866
4308
|
modelCostTitle,
|
|
2867
4309
|
subagentCostTitle,
|
|
2868
4310
|
tasksTotalText,
|
|
4311
|
+
hasAccountLimits,
|
|
4312
|
+
accountLimitsOf,
|
|
4313
|
+
accountLimitWindowLabel,
|
|
4314
|
+
accountLimitPercentText,
|
|
4315
|
+
accountLimitsText,
|
|
4316
|
+
accountLimitsTitle,
|
|
4317
|
+
trimToolOutput,
|
|
4318
|
+
toTaskLogEvent,
|
|
4319
|
+
lastServedModel,
|
|
4320
|
+
taskLogForSurface,
|
|
4321
|
+
coalesceTaskLog,
|
|
4322
|
+
scriptRunText,
|
|
4323
|
+
secondsText,
|
|
4324
|
+
scriptExitText,
|
|
4325
|
+
digestTaskLog,
|
|
4326
|
+
SEVERITY_LABEL,
|
|
4327
|
+
severityLabel,
|
|
4328
|
+
worstSeverity,
|
|
4329
|
+
REPORT_MAX_CHARS,
|
|
4330
|
+
HANDOFF_LIMITS,
|
|
4331
|
+
DIGEST_FINDINGS,
|
|
4332
|
+
storedHandoff,
|
|
4333
|
+
withPatch,
|
|
4334
|
+
savedPatchOf,
|
|
4335
|
+
handoffDigest,
|
|
4336
|
+
namedHandoff,
|
|
4337
|
+
handoffReferenceText,
|
|
4338
|
+
byteSize,
|
|
4339
|
+
patchFlagText,
|
|
4340
|
+
patchChangesText,
|
|
4341
|
+
patchLinesText,
|
|
4342
|
+
patchText,
|
|
4343
|
+
patchLines,
|
|
4344
|
+
patchView,
|
|
4345
|
+
savedPatchViews,
|
|
4346
|
+
savedPatchView,
|
|
4347
|
+
findingLines,
|
|
4348
|
+
artifactLines,
|
|
4349
|
+
openingLines,
|
|
4350
|
+
HANDOFF_PUSH_CHARS,
|
|
4351
|
+
boundedHandoffLines,
|
|
4352
|
+
handoffPreviewLines,
|
|
4353
|
+
handoffWakeLines,
|
|
4354
|
+
handoffLines,
|
|
4355
|
+
readableTaskIds,
|
|
4356
|
+
checkHandoff,
|
|
4357
|
+
summarizeToolCall,
|
|
4358
|
+
SEALED_RUN_MARK,
|
|
4359
|
+
ranSealed,
|
|
4360
|
+
withoutSealedMark,
|
|
4361
|
+
classifyOutcome,
|
|
2869
4362
|
sameMessage,
|
|
4363
|
+
isFileEditTool,
|
|
2870
4364
|
mapAgentTool,
|
|
2871
4365
|
normalizeAgentArgs,
|
|
2872
4366
|
toolHeadline,
|
|
@@ -2886,52 +4380,97 @@ export {
|
|
|
2886
4380
|
taskStatusKind,
|
|
2887
4381
|
awaitingLabel,
|
|
2888
4382
|
blockedByLabel,
|
|
4383
|
+
holdLabel,
|
|
2889
4384
|
skippedLabel,
|
|
2890
4385
|
WAITS_FOR_APPROVAL_LABEL,
|
|
4386
|
+
receiptLabel,
|
|
2891
4387
|
approvalLabel,
|
|
2892
4388
|
resumeTime,
|
|
2893
4389
|
resumeLabel,
|
|
4390
|
+
startedEditRefusal,
|
|
2894
4391
|
validateTaskEdit,
|
|
2895
4392
|
UPDATABLE_FIELDS,
|
|
2896
4393
|
taskOpsProtocol,
|
|
2897
4394
|
textHasTaskOps,
|
|
2898
4395
|
parseTaskOpsJson,
|
|
4396
|
+
opLabel,
|
|
4397
|
+
opRefusalText,
|
|
2899
4398
|
canMergeTasks,
|
|
2900
4399
|
canSplitTask,
|
|
2901
4400
|
applyTaskOps,
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
serializePlan,
|
|
2912
|
-
executionSummary,
|
|
2913
|
-
CHECKPOINT_TRUNCATE_LENGTH,
|
|
2914
|
-
truncateCheckpointSummary,
|
|
4401
|
+
REPORT_LEAD,
|
|
4402
|
+
createWakePolicy,
|
|
4403
|
+
summaryRequest,
|
|
4404
|
+
extractSummary,
|
|
4405
|
+
condensedNotice,
|
|
4406
|
+
modelContext,
|
|
4407
|
+
contextFloor,
|
|
4408
|
+
compactionCut,
|
|
4409
|
+
assignedRunnerOf,
|
|
2915
4410
|
assignmentIssue,
|
|
2916
4411
|
assignmentIssueText,
|
|
2917
4412
|
assignmentConsequence,
|
|
2918
4413
|
capConflictFiles,
|
|
2919
|
-
|
|
2920
|
-
|
|
4414
|
+
blockNotice,
|
|
4415
|
+
blockNotices,
|
|
4416
|
+
repairedLanded,
|
|
4417
|
+
repairedNotice,
|
|
4418
|
+
describeMergeResult,
|
|
4419
|
+
mergeOutcome,
|
|
4420
|
+
describeIsolationPreview,
|
|
4421
|
+
isRepoGroup,
|
|
4422
|
+
reposWithWork,
|
|
4423
|
+
mergeTarget,
|
|
4424
|
+
checkoutRule,
|
|
4425
|
+
handoffBranchName,
|
|
4426
|
+
repoBaseLine,
|
|
4427
|
+
handoffBaseLine,
|
|
4428
|
+
repoResultLines,
|
|
4429
|
+
sessionOwner,
|
|
4430
|
+
continuability,
|
|
4431
|
+
canContinue,
|
|
4432
|
+
blockingFailures,
|
|
4433
|
+
repeatingChecks,
|
|
4434
|
+
planBlocks,
|
|
2921
4435
|
SUPERVISOR_LEVEL_LABEL,
|
|
2922
4436
|
supervisorSummary,
|
|
2923
4437
|
automergeSummary,
|
|
4438
|
+
PLANNED_WITH_LABEL,
|
|
4439
|
+
trailerSummary,
|
|
2924
4440
|
autorunSummary,
|
|
2925
4441
|
ACT_RISK,
|
|
2926
4442
|
AUTORUN_RISK,
|
|
2927
4443
|
parseSwitch,
|
|
2928
4444
|
parseCount,
|
|
4445
|
+
IDLE_ACTIVITY,
|
|
4446
|
+
serializeTask,
|
|
4447
|
+
serializeTaskStatus,
|
|
4448
|
+
surfacePlan,
|
|
4449
|
+
surfacePlanState,
|
|
4450
|
+
serializePlan,
|
|
4451
|
+
executionSummary,
|
|
4452
|
+
CHECKPOINT_TRUNCATE_LENGTH,
|
|
4453
|
+
truncateCheckpointSummary,
|
|
2929
4454
|
EMPTY_RUN,
|
|
2930
4455
|
reduceRun,
|
|
4456
|
+
titledRefs,
|
|
4457
|
+
dependentsNotice,
|
|
4458
|
+
loopLeaveNotice,
|
|
4459
|
+
pastGateConfirmation,
|
|
4460
|
+
manualOverrideConfirmation,
|
|
4461
|
+
manualOverrideStopFirst,
|
|
4462
|
+
newTaskFields,
|
|
4463
|
+
noExecutorRefusal,
|
|
2931
4464
|
EMPTY_CONVERSATION,
|
|
2932
4465
|
reconcileApprovals,
|
|
2933
4466
|
reduceConversation,
|
|
2934
4467
|
fromTranscript,
|
|
4468
|
+
approvalKindText,
|
|
4469
|
+
approvalStatusText,
|
|
4470
|
+
planMarkerParts,
|
|
4471
|
+
handoffFindingsText,
|
|
4472
|
+
handoffPeekParts,
|
|
4473
|
+
compactionText,
|
|
2935
4474
|
EMPTY_HOLD,
|
|
2936
4475
|
holdPrompt,
|
|
2937
4476
|
drainNext,
|
|
@@ -2942,21 +4481,36 @@ export {
|
|
|
2942
4481
|
NO_TURN,
|
|
2943
4482
|
followTurn,
|
|
2944
4483
|
stopTurn,
|
|
4484
|
+
REPORT_PREVIEW_LINES,
|
|
4485
|
+
hasLongReport,
|
|
4486
|
+
wakeUpItems,
|
|
2945
4487
|
hasHiddenDetail,
|
|
2946
4488
|
taskStartedNotice,
|
|
2947
4489
|
markdownTokens,
|
|
2948
4490
|
markdownHtml,
|
|
2949
4491
|
EMPTY_TASK_LOG_FOLLOW,
|
|
2950
4492
|
followTaskLog,
|
|
4493
|
+
editableWhileRunning,
|
|
4494
|
+
runHoldsEditors,
|
|
4495
|
+
cardEditable,
|
|
4496
|
+
cardEditNeedsUnstarted,
|
|
2951
4497
|
taskRowActions,
|
|
2952
4498
|
markAction,
|
|
2953
|
-
|
|
4499
|
+
taskActionCommand,
|
|
4500
|
+
overrideNeedsConfirmation,
|
|
4501
|
+
confirmedOverride,
|
|
2954
4502
|
opsEditable,
|
|
4503
|
+
loopCheckState,
|
|
4504
|
+
loopCheckLabel,
|
|
4505
|
+
loopBodyRound,
|
|
4506
|
+
loopBodyLabel,
|
|
4507
|
+
taskChatAvailable,
|
|
2955
4508
|
namedRepos,
|
|
2956
4509
|
taskRowView,
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
4510
|
+
loopRowParts,
|
|
4511
|
+
taskLogCommands,
|
|
4512
|
+
logContinues,
|
|
4513
|
+
logResumes,
|
|
4514
|
+
taskLogHeaderView
|
|
2961
4515
|
};
|
|
2962
|
-
//# sourceMappingURL=chunk-
|
|
4516
|
+
//# sourceMappingURL=chunk-4QXOWEGC.mjs.map
|