@ordewell/core 0.10.12 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/AUTHORS.md +37 -0
  2. package/NOTICE +71 -0
  3. package/README.md +1 -1
  4. package/dist/{IFileSystem-1PqRCjhu.d.mts → IRunner-B5l9PCTv.d.ts} +926 -102
  5. package/dist/{IFileSystem-lTRL_G9Z.d.ts → IRunner-DBgDx_Mr.d.mts} +926 -102
  6. package/dist/{ModeResolver-CoJAJD-f.d.ts → ModeResolver-BDAgouuE.d.mts} +9 -2
  7. package/dist/{ModeResolver-6TQXVsde.d.mts → ModeResolver-DyXeZ9ib.d.ts} +9 -2
  8. package/dist/{Task-CfBM0wkV.d.mts → Task-BL4Idm9r.d.mts} +656 -26
  9. package/dist/{Task-CfBM0wkV.d.ts → Task-BL4Idm9r.d.ts} +656 -26
  10. package/dist/{chunk-ESKK5I4F.mjs → chunk-4QXOWEGC.mjs} +1888 -334
  11. package/dist/chunk-4QXOWEGC.mjs.map +1 -0
  12. package/dist/{chunk-KWS3VEER.mjs → chunk-MYXMIVWA.mjs} +6 -6
  13. package/dist/chunk-MYXMIVWA.mjs.map +1 -0
  14. package/dist/{chunk-PFH7WFIB.mjs → chunk-QTZ5XTBY.mjs} +228 -15
  15. package/dist/chunk-QTZ5XTBY.mjs.map +1 -0
  16. package/dist/{chunk-24EL4Z2W.mjs → chunk-YAFNYNFX.mjs} +342 -36
  17. package/dist/chunk-YAFNYNFX.mjs.map +1 -0
  18. package/dist/index.d.mts +1488 -769
  19. package/dist/index.d.ts +1488 -769
  20. package/dist/index.js +32003 -20350
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +29380 -19553
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/parsing.d.mts +2 -2
  25. package/dist/parsing.d.ts +2 -2
  26. package/dist/parsing.js +232 -25
  27. package/dist/parsing.js.map +1 -1
  28. package/dist/parsing.mjs +3 -3
  29. package/dist/plan-utils.d.mts +4 -4
  30. package/dist/plan-utils.d.ts +4 -4
  31. package/dist/plan-utils.js +1156 -96
  32. package/dist/plan-utils.js.map +1 -1
  33. package/dist/plan-utils.mjs +144 -8
  34. package/dist/{runSettingsText-Jb_efnI4.d.mts → runSettingsText-BKwWs052.d.mts} +2882 -621
  35. package/dist/{runSettingsText-C5uv2axI.d.ts → runSettingsText-CAXiTpsj.d.ts} +2882 -621
  36. package/dist/testing.d.mts +10 -4
  37. package/dist/testing.d.ts +10 -4
  38. package/dist/testing.js +30 -7
  39. package/dist/testing.js.map +1 -1
  40. package/dist/testing.mjs +26 -7
  41. package/dist/testing.mjs.map +1 -1
  42. package/package.json +7 -3
  43. package/skills/diagnose/SKILL.md +52 -0
  44. package/skills/explore/SKILL.md +65 -0
  45. package/dist/chunk-24EL4Z2W.mjs.map +0 -1
  46. package/dist/chunk-ESKK5I4F.mjs.map +0 -1
  47. package/dist/chunk-KWS3VEER.mjs.map +0 -1
  48. package/dist/chunk-NJGID6BG.mjs +0 -115
  49. package/dist/chunk-NJGID6BG.mjs.map +0 -1
  50. package/dist/chunk-PFH7WFIB.mjs.map +0 -1
@@ -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-24EL4Z2W.mjs";
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-PFH7WFIB.mjs";
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
- if (!fill.windowTokens) return used;
219
- const share = fill.usedTokens / fill.windowTokens * 100;
220
- return `${used} (${share < 1 ? "<1" : Math.round(share)}%)`;
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
- return fill.windowTokens ? `${used} of ${fill.windowTokens.toLocaleString("en-US")} tokens` : `${used} tokens`;
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
- return { ...call, status: STATUS_OF_OUTCOME[outcome], outcome, output, outputLineCount: outputLines(output).length, ...diff ? { diff } : {} };
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) => b.type === "subagent" ? closeSubagent(b, cut) : closeBlock(b, cut !== null));
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 sender = from ? { from } : {};
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, ...sender }, ...rest.slice(placed)] };
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, ...sender }] };
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 } : {}, ...sender } : m) };
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 `"${task.title}" is running and cannot be ${action} now: the user can stop it first, or change it after it finishes`;
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 refused = opsPlacementRefusal(changes.type ?? target.type, isSubtask);
1059
- return refused ? { ok: false, error: `"${target.title}" ${refused}` } : null;
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 not change any file, never on one that must run commands (tests, scripts, builds) or an ops task, and "readOnly": false to lift it; you never set "taskMode". A task you move, split or edit keeps being read-only or not unless you say otherwise, and so do the subtasks of a read-only task you add.',
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 will change, or with the error to fix \u2014 call it again in the same reply. The edit is applied when your reply ends; edits you make in one reply join one batch."] : [],
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 "user" is a content change, not just a label: an update to "user" needs "userSteps" in the SAME op, and an update to "ai" needs "prompt" in the SAME op \u2014 the model/mode/effort/autonomy fields (flipping to "user", which also makes the task HITL) or the userSteps (flipping to "ai") are cleared automatically.',
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 = lockedTaskRefusal(sub, "removed") ?? lockedInSubtree(sub);
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 === "in_progress" ? "running" : "completed";
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
- return { ok: true };
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
- var keptIds = /* @__PURE__ */ new WeakMap();
1280
- function idsOf(op) {
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(`${opLabel(i)}: handle must be a non-empty string`);
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(`${opLabel(i)}: handle "${handle}" is already used earlier in this batch`);
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(`${opLabel(i)}: handle "${handle}" collides with an existing task reference`);
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 { ok: false, tasks: currentTasks, errors, summary: [] };
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, label, audience, refuseRunning = false) => {
1364
- const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, opIndex);
1365
- if ("error" in ref) return { error: `${label}: ${ref.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: `${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)` };
1368
- if (refuseRunning && target.status === "in_progress") return { error: `${label}: ${lockedTaskRefusal(target, "re-armed")}` };
1369
- if (op.changes && "taskMode" in op.changes) return { error: `${label}: ${MODE_NOT_THE_PLANNERS}` };
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: `${label}: "thinkingEffort" is the assigned model's \u2014 set "assignedModel" with it` };
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: `${label}: ${check.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: `${label}: ${MANUAL_NOT_READ_ONLY}` };
1384
- if ((changes.ops ?? target.ops) && (readOnly ?? isReadOnly(target, catalog))) return { error: `${label}: ${OPS_NOT_READ_ONLY}` };
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
- if (written.type === "user" || runners.includes(written.assignedRunner)) {
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: `${label}: ${resolved.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: `${label}: unknown dependencies: ${bad.join(", ")}` };
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 breachKey = (e) => `${e.taskId ?? ""}\0${e.field}`;
1402
- const standing = new Set(checkPlanInvariants(currentTasks, runners, catalog?.runnerModes).map(breachKey));
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 = opLabel(0);
2182
+ let lastLabel = opLabel2(0);
1405
2183
  for (const [i, op] of ops.entries()) {
1406
- const label = opLabel(i);
1407
- const newId = idsOf(op);
2184
+ const label = opLabel2(i);
2185
+ const newId = uuidv4;
1408
2186
  switch (op.op) {
1409
2187
  case "update": {
1410
- const prep = prepareEdit(op, i, label, "planner");
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(`${label}: no valid changes provided`);
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(`${label}: new task needs a title`);
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 refused = refusalOf(planned);
1438
- if (refused) {
1439
- errors.push(`${label}: ${refused}`);
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(`${label}: ${resolved.error}`);
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(`${label}: ${ref.error}`);
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(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
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 = lockedTaskRefusal(target, "removed") ?? lockedInSubtree(target);
2242
+ const locked = reshapeRefusal(target, "removed") ?? lockedInSubtree(target) ?? loopRemovalRefusal(target);
1465
2243
  if (locked) {
1466
- errors.push(`${label}: ${locked}`);
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(`${label}: ${badRef.error}`);
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(`${label}: unknown task in reorder list`);
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(`${label}: reorder must list every top-level task exactly once (${tasks.length} tasks)`);
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(`${label}: ${badRef.error}`);
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(`${label}: unknown task in merge list (already removed, merged, or split by an earlier op)`);
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(`${label}: merge needs at least two tasks`);
2287
+ errors.push({ op: label, message: `merge needs at least two tasks` });
1510
2288
  break;
1511
2289
  }
1512
- const locked = toMerge.map((t) => lockedTaskRefusal(t, "merged")).find((refusal) => refusal !== null);
2290
+ const locked = toMerge.map((t) => reshapeRefusal(t, "merged") ?? loopReshapeRefusal(tasks, t, "merged")).find((refusal) => refusal !== null);
1513
2291
  if (locked) {
1514
- errors.push(`${label}: ${locked}`);
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(`${label}: unknown dependencies: ${bad.join(", ")}`);
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(`${label}: merged task needs a title`);
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
- type: toMerge.some((t) => t.type === "user") ? "user" : "ai",
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 refused = refusalOf(planned);
1552
- if (refused) {
1553
- errors.push(`${label}: ${refused}`);
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(`${label}: ${built.error}`);
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(`${label}: ${ref.error}`);
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(`${label}: task "${op.taskId}" not found`);
2370
+ errors.push({ op: label, message: `task "${op.taskId}" not found` });
1592
2371
  break;
1593
2372
  }
1594
- const locked = lockedTaskRefusal(target, "split");
2373
+ const locked = reshapeRefusal(target, "split") ?? loopReshapeRefusal(tasks, target, "split");
1595
2374
  if (locked) {
1596
- errors.push(`${label}: ${locked}`);
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(`${label}: split needs at least two parts`);
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(`${label}: split part ${p + 1} needs a title`);
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(`${label}: split part 1 unknown dependencies: ${bad.join(", ")}`);
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 refused = refusalOf(planned);
1645
- if (refused) {
1646
- errors.push(`${label}: split part ${p + 1}: ${refused}`);
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(`${label}: split part ${p + 1}: ${resolved.error}`);
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, label, "direct", true);
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(`${label}: unknown op "${op.op}"`);
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(checkPlanInvariants(tasks, runners, catalog?.runnerModes).map(breachKey));
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 e of checkPlanInvariants(tasks, runners, catalog?.runnerModes)) {
1700
- const key = breachKey(e);
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 { ok: false, tasks: currentTasks, errors, summary: [] };
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/continuation.ts
1729
- function sessionOwner(task) {
1730
- return task.runnerSessionOwner ?? task.attemptUsage?.at(-1)?.runner;
2506
+ // src/services/AssignmentScope.ts
2507
+ function assignedRunnerOf(task) {
2508
+ return task.type === "ai" ? task.assignedRunner || void 0 : void 0;
1731
2509
  }
1732
- function continuability(task) {
1733
- const refuse = (reason) => ({ ok: false, reason: `Task "${task.title}" cannot be continued: ${reason}` });
1734
- if (task.type !== "ai") return refuse("it is not run by an agent.");
1735
- if (task.status === "awaiting_user" && task.awaitingReason === "conflict") {
1736
- return refuse("its work is waiting on a merge conflict. Resolve or repair the conflict instead.");
1737
- }
1738
- const finished = task.status === "completed" || task.status === "failed" || task.status === "skipped";
1739
- if (!finished && !pausedOnLimit(task)) return refuse("only a completed, failed or skipped task, or one paused on a usage limit, can be.");
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 { ok: true, sessionId: task.runnerSessionId };
2519
+ return null;
1746
2520
  }
1747
- function canContinue(task) {
1748
- return continuability(task).ok;
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
- // src/services/blocking.ts
1752
- var UNSTARTED = /* @__PURE__ */ new Set(["pending", "approved"]);
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") failed.add(depId);
1774
- else for (const root of failuresOf(depId, visiting)) failed.add(root);
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
- for (const id of byId.keys()) {
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 users = pending.filter(({ input }) => input.source === "user");
2193
- const updates = pending.filter(({ input }) => input.source === "supervisor");
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 markCompaction(view, summary) {
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
- if (announced?.type === "message") return { ...view, blocks: replaceAt(view.blocks, i, { ...announced, role: "system" }) };
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
- if (isNew && entry.role === "user" && !entry.kind) next = takePendingInputs(next, entry.content, []).view;
2383
- if (isNew && entry.kind === "wake_up") next = takePendingInputs(next, void 0, [entry.content]).view;
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.content);
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", UNSTARTED_AI],
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 markRequestFor(action) {
2769
- if (action === "complete") return "complete";
2770
- if (action === "uncomplete") return "uncomplete";
2771
- return null;
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) => ({ id: t.id, status: t.status ?? "", dependencies: t.dependencies ?? [], subtasks: asBlocking(t.subtasks ?? []) }));
2783
- function blockedBy(task, tasks) {
2784
- if (!tasks?.length || !NOT_STARTED.has(task.status)) return [];
2785
- return blockingFailures(asBlocking(tasks)).get(task.id) ?? [];
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 blocked = blockedBy(task, context.tasks);
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
- status: blocked.length > 0 ? "blocked" : taskStatusKind(task),
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/texts.ts
2827
- function titledRefs(ids, tasks) {
2828
- return ids.map((id) => titledTaskRefOf(tasks, id) ?? id);
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 dependentsNotice(dependents, tasks = []) {
2831
- if (dependents.length === 0) return null;
2832
- const subject = dependents.length === 1 ? "1 task depends" : `${dependents.length} tasks depend`;
2833
- return `${subject} on it and will lose that dependency: ${dependents.map((t) => (t.id ? titledTaskRefOf(tasks, t.id) : void 0) ?? titledTaskRef(t)).join(", ")}.`;
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 pastGateConfirmation(unmerged, subject = "This task") {
2836
- 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.`;
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 newTaskFields(title, prompt) {
2839
- const name = title.trim();
2840
- if (!name) return null;
2841
- return { title: name, description: name, prompt: prompt?.trim() ? prompt : name, type: "ai" };
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
- sessionOwner,
2903
- continuability,
2904
- canContinue,
2905
- blockingFailures,
2906
- serializeTask,
2907
- serializeTaskStatus,
2908
- surfaceTask,
2909
- surfacePlan,
2910
- surfacePlanState,
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
- summarizeToolCall,
2920
- classifyOutcome,
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
- markRequestFor,
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
- titledRefs,
2958
- dependentsNotice,
2959
- pastGateConfirmation,
2960
- newTaskFields
4510
+ loopRowParts,
4511
+ taskLogCommands,
4512
+ logContinues,
4513
+ logResumes,
4514
+ taskLogHeaderView
2961
4515
  };
2962
- //# sourceMappingURL=chunk-ESKK5I4F.mjs.map
4516
+ //# sourceMappingURL=chunk-4QXOWEGC.mjs.map