@ordewell/core 0.11.2 → 0.12.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 (42) hide show
  1. package/AUTHORS.md +3 -1
  2. package/dist/{IRunner-ihalYfTo.d.mts → IRunner-BwzyUVIL.d.mts} +62 -18
  3. package/dist/{IRunner-DOMtDTVt.d.ts → IRunner-DjJkYwko.d.ts} +62 -18
  4. package/dist/{ModeResolver-DyXeZ9ib.d.ts → ModeResolver-CHsaoV7M.d.ts} +1 -1
  5. package/dist/{ModeResolver-BDAgouuE.d.mts → ModeResolver-b_7bPlBS.d.mts} +1 -1
  6. package/dist/{Task-BL4Idm9r.d.mts → Task-BTEH-ztX.d.mts} +301 -26
  7. package/dist/{Task-BL4Idm9r.d.ts → Task-BTEH-ztX.d.ts} +301 -26
  8. package/dist/{chunk-MYXMIVWA.mjs → chunk-BFLVOYNR.mjs} +3 -3
  9. package/dist/{chunk-YAFNYNFX.mjs → chunk-BKTDYGIP.mjs} +14 -2
  10. package/dist/chunk-BKTDYGIP.mjs.map +1 -0
  11. package/dist/{chunk-QTZ5XTBY.mjs → chunk-F6WS3JLQ.mjs} +446 -10
  12. package/dist/chunk-F6WS3JLQ.mjs.map +1 -0
  13. package/dist/{chunk-7WNLN63J.mjs → chunk-HP2LNKST.mjs} +315 -51
  14. package/dist/chunk-HP2LNKST.mjs.map +1 -0
  15. package/dist/index.d.mts +242 -78
  16. package/dist/index.d.ts +242 -78
  17. package/dist/index.js +5598 -2005
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +4701 -1782
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/parsing.d.mts +2 -2
  22. package/dist/parsing.d.ts +2 -2
  23. package/dist/parsing.js.map +1 -1
  24. package/dist/parsing.mjs +3 -3
  25. package/dist/plan-utils.d.mts +4 -4
  26. package/dist/plan-utils.d.ts +4 -4
  27. package/dist/plan-utils.js +135 -19
  28. package/dist/plan-utils.js.map +1 -1
  29. package/dist/plan-utils.mjs +25 -3
  30. package/dist/{runSettingsText-CYMhTWfw.d.ts → runSettingsText-CvZ40IPv.d.ts} +1086 -190
  31. package/dist/{runSettingsText-tnmvdSfS.d.mts → runSettingsText-x-YML2ay.d.mts} +1086 -190
  32. package/dist/testing.d.mts +18 -2
  33. package/dist/testing.d.ts +18 -2
  34. package/dist/testing.js +71 -7
  35. package/dist/testing.js.map +1 -1
  36. package/dist/testing.mjs +68 -7
  37. package/dist/testing.mjs.map +1 -1
  38. package/package.json +2 -1
  39. package/dist/chunk-7WNLN63J.mjs.map +0 -1
  40. package/dist/chunk-QTZ5XTBY.mjs.map +0 -1
  41. package/dist/chunk-YAFNYNFX.mjs.map +0 -1
  42. /package/dist/{chunk-MYXMIVWA.mjs.map → chunk-BFLVOYNR.mjs.map} +0 -0
@@ -146,7 +146,16 @@ var isKnownStatus = (status) => Object.prototype.hasOwnProperty.call(ISOLATION_S
146
146
  function changedReposOf(record) {
147
147
  return Object.entries(record.repos).filter(([, r]) => r.changed).map(([repoPath]) => repoPath);
148
148
  }
149
- function taskIsolationOf(record, repairLimit) {
149
+ function landedInRepo(run, record, repoPath) {
150
+ return !!record.repos[repoPath]?.changed || run.landings?.[record.taskId]?.[repoPath] !== void 0;
151
+ }
152
+ function revertible(record, run) {
153
+ if (record.inHead || record.status === "active" || record.status === "repairing" || !run?.carried || run.carried[record.taskId]) return false;
154
+ const landings = run?.landings?.[record.taskId];
155
+ const changed = record.status === "merged" ? changedReposOf(record) : [];
156
+ return (changed.length > 0 || landings !== void 0) && changed.every((repo) => record.repos[repo].landedTip || landings?.[repo]);
157
+ }
158
+ function taskIsolationOf(record, repairLimit, run) {
150
159
  return {
151
160
  state: ISOLATION_STATE[record.status],
152
161
  branch: record.branch,
@@ -155,9 +164,20 @@ function taskIsolationOf(record, repairLimit) {
155
164
  ...record.conflictRepo ? { conflictRepo: record.conflictRepo } : {},
156
165
  ...record.conflictFiles?.length ? { conflictFiles: record.conflictFiles } : {},
157
166
  ...record.repairs && repairLimit !== void 0 ? { repair: { attempt: record.repairs, limit: repairLimit } } : {},
158
- ...record.repairedFiles?.length ? { repairedFiles: record.repairedFiles } : {}
167
+ ...record.repairedFiles?.length ? { repairedFiles: record.repairedFiles } : {},
168
+ ...revertible(record, run) ? { revertible: true } : {}
159
169
  };
160
170
  }
171
+ function taskIsolationsOf(run, repairLimit) {
172
+ const tasks = {};
173
+ for (const taskId of Object.keys(run.landings ?? {})) {
174
+ if (run.carried && !run.carried[taskId]) tasks[taskId] = { state: "none", revertible: true };
175
+ }
176
+ for (const [taskId, record] of Object.entries(run.tasks)) {
177
+ if (isKnownStatus(record.status)) tasks[taskId] = taskIsolationOf(record, repairLimit, run);
178
+ }
179
+ return tasks;
180
+ }
161
181
  function handoffOf(run) {
162
182
  const merged = Object.values(run.tasks).filter((r) => r.status === "merged" && !r.inHead).sort((a, b) => a.order - b.order);
163
183
  const entry = (r) => ({
@@ -173,7 +193,7 @@ function handoffOf(run) {
173
193
  baseRef: repo.baseRef,
174
194
  ...repo.baseBranch ? { baseBranch: repo.baseBranch } : {},
175
195
  ...repo.startedDetached ? { startedDetached: true } : {},
176
- landed: merged.filter((r) => r.repos[repo.path]?.changed).map(entry)
196
+ landed: merged.filter((r) => landedInRepo(run, r, repo.path)).map(entry)
177
197
  })),
178
198
  landed: merged.map(entry)
179
199
  };
@@ -225,12 +245,396 @@ function carriesIsolation(plan) {
225
245
  function isolationViewOfPlan(plan, repairLimit) {
226
246
  if (!carriesIsolation(plan)) return null;
227
247
  const { run } = migratePlanIsolation(plan.isolation);
228
- const tasks = {};
229
- for (const [taskId, record] of Object.entries(run.tasks)) {
230
- if (!isKnownStatus(record.status)) continue;
231
- tasks[taskId] = taskIsolationOf(record, repairLimit);
248
+ return { tasks: taskIsolationsOf(run, repairLimit), handoff: handoffOf(run) };
249
+ }
250
+
251
+ // src/services/VerdictEngine.ts
252
+ var IDLE_TIMEOUT_MS = 10 * 6e4;
253
+ var VerdictEngine = class {
254
+ pendingVerdicts = /* @__PURE__ */ new Map();
255
+ sessions = /* @__PURE__ */ new Map();
256
+ listeners = [];
257
+ checkpointListeners = [];
258
+ withdrawnListeners = [];
259
+ /** The open `checkpoint` tool call per task (ADR-0022, V5): settling it is how an answer reaches the runner. */
260
+ toolCheckpoints = /* @__PURE__ */ new Map();
261
+ /** The question a task's checkpoint asks, whole, for as long as it waits. */
262
+ checkpointQuestions = /* @__PURE__ */ new Map();
263
+ idleListeners = [];
264
+ /**
265
+ * Per-task generation. Replaced on every watch(), clear() and verdict.
266
+ * Stale callbacks (from a prior session whose generation doesn't match
267
+ * the current one) bail out instead of delivering a verdict for the
268
+ * wrong session.
269
+ */
270
+ generations = /* @__PURE__ */ new Map();
271
+ /**
272
+ * Generations come from one counter that {@link reset} never rewinds: a
273
+ * per-task count restarted at 1 after a reset, and the next watch handed a
274
+ * session that outlived the reset the same generation as its successor.
275
+ */
276
+ lastGeneration = 0;
277
+ idleTimers = /* @__PURE__ */ new Map();
278
+ idleSince = /* @__PURE__ */ new Map();
279
+ /** Tasks waiting on the user, whose silence is expected rather than a sign of a stuck runner. */
280
+ idlePaused = /* @__PURE__ */ new Set();
281
+ /**
282
+ * Open tool approvals per structured task. They arrive mid-turn and leave the
283
+ * status alone (ADR-0018, W1), but a task waiting on one is waiting on the
284
+ * user, so it is not idle either.
285
+ */
286
+ openApprovals = /* @__PURE__ */ new Map();
287
+ onVerdict(listener) {
288
+ this.listeners.push(listener);
289
+ }
290
+ onCheckpoint(listener) {
291
+ this.checkpointListeners.push(listener);
292
+ }
293
+ onCheckpointWithdrawn(listener) {
294
+ this.withdrawnListeners.push(listener);
295
+ }
296
+ onIdleChange(listener) {
297
+ this.idleListeners.push(listener);
298
+ }
299
+ /** Advisory silence timestamp for a task, or null if it isn't idle. */
300
+ getIdleSince(taskId) {
301
+ return this.idleSince.get(taskId) ?? null;
302
+ }
303
+ /**
304
+ * Stop watching a task's silence while it waits on the user (ADR-0018, W1).
305
+ * Watching resumes when its next turn starts, or when a checkpoint is answered.
306
+ */
307
+ pauseIdle(taskId) {
308
+ this.idlePaused.add(taskId);
309
+ this.clearIdle(taskId);
310
+ }
311
+ resumeIdle(taskId) {
312
+ const gen = this.generations.get(taskId);
313
+ if (!this.idlePaused.delete(taskId) || gen === void 0) return;
314
+ this.touchIdle(taskId, gen);
315
+ }
316
+ /** Restart the silence timer on fresh output; broadcasts the null transition if it was idle. */
317
+ touchIdle(taskId, gen) {
318
+ if (this.idlePaused.has(taskId) || this.openApprovals.get(taskId)?.size) return;
319
+ const existing = this.idleTimers.get(taskId);
320
+ if (existing) clearTimeout(existing);
321
+ if (this.idleSince.get(taskId)) {
322
+ this.idleSince.set(taskId, null);
323
+ for (const l of this.idleListeners) l(taskId, null);
324
+ }
325
+ this.idleTimers.set(taskId, setTimeout(() => {
326
+ if (this.generations.get(taskId) !== gen) return;
327
+ const now = (/* @__PURE__ */ new Date()).toISOString();
328
+ this.idleSince.set(taskId, now);
329
+ for (const l of this.idleListeners) l(taskId, now);
330
+ }, IDLE_TIMEOUT_MS));
331
+ }
332
+ /** Tear down idle tracking for a task; broadcasts the null transition if it was idle. */
333
+ clearIdle(taskId) {
334
+ const timer = this.idleTimers.get(taskId);
335
+ if (timer) clearTimeout(timer);
336
+ this.idleTimers.delete(taskId);
337
+ const wasIdle = this.idleSince.get(taskId);
338
+ this.idleSince.delete(taskId);
339
+ if (wasIdle) {
340
+ for (const l of this.idleListeners) l(taskId, null);
341
+ }
342
+ }
343
+ /** What a task's waiting checkpoint asks, untruncated; undefined when none waits. */
344
+ getCheckpointQuestion(taskId) {
345
+ return this.checkpointQuestions.get(taskId);
346
+ }
347
+ approveCheckpoint(taskId) {
348
+ this.checkpointQuestions.delete(taskId);
349
+ this.resumeIdle(taskId);
350
+ this.toolCheckpoints.get(taskId)?.({ kind: "continue" });
351
+ }
352
+ rejectCheckpoint(taskId, reason) {
353
+ this.checkpointQuestions.delete(taskId);
354
+ this.resumeIdle(taskId);
355
+ this.toolCheckpoints.get(taskId)?.({ kind: "rejected", reason });
356
+ }
357
+ /**
358
+ * An attempt's runner is being started, with no session to watch yet. Its
359
+ * silence counts from here, so one whose start never resolves — stuck
360
+ * launching, or waiting for its tools to attach — is flagged quiet as well.
361
+ * {@link watch} starts the count again once the runner is up.
362
+ */
363
+ starting(taskId) {
364
+ const gen = this.bumpGeneration(taskId);
365
+ this.idlePaused.delete(taskId);
366
+ this.openApprovals.delete(taskId);
367
+ this.touchIdle(taskId, gen);
368
+ }
369
+ /**
370
+ * Attach to a spawned session. The runner's `task_complete` call on the
371
+ * attempt's token is the only completion evidence (ADR-0022, V2); a session
372
+ * that exits before one gets a failed verdict. A script task's session has
373
+ * no runner to call it: its exit status is the evidence instead (ADR-0035).
374
+ *
375
+ * Returns the attempt's generation, what {@link signalComplete} is checked against.
376
+ */
377
+ watch(task, session) {
378
+ const gen = this.bumpGeneration(task.id);
379
+ this.idlePaused.delete(task.id);
380
+ this.openApprovals.delete(task.id);
381
+ this.touchIdle(task.id, gen);
382
+ session.onOutput(() => {
383
+ if (this.generations.get(task.id) === gen) this.touchIdle(task.id, gen);
384
+ });
385
+ this.sessions.set(task.id, session);
386
+ session.onTurnEnd(() => {
387
+ if (this.generations.get(task.id) !== gen) return;
388
+ this.callWentAway(task.id, "the turn it was asked in has ended.");
389
+ const pending = this.pendingVerdicts.get(task.id);
390
+ this.pendingVerdicts.delete(task.id);
391
+ if (session.turnState() === "idle" && pending) this.publishVerdict(task.id, pending);
392
+ });
393
+ session.onEvent((event) => {
394
+ if (this.generations.get(task.id) !== gen) return;
395
+ if (event.type === "turn_start") {
396
+ this.supersede(task.id);
397
+ this.resumeIdle(task.id);
398
+ } else if (event.type === "message_delivered") this.supersede(task.id);
399
+ else if (event.type === "permission_request" && !event.decided) this.approvalOpened(task.id, event.id);
400
+ else if (event.type === "permission_decided" || event.type === "permission_withdrawn") this.approvalClosed(task.id, event.id, gen);
401
+ });
402
+ session.onTaskComplete((report) => this.signalComplete(task.id, gen, report));
403
+ session.onScriptExit((exit) => this.signalScriptExit(task.id, gen, exit));
404
+ session.onToolCheckpoint((question, signal) => this.raiseCheckpoint(task.id, gen, question, signal));
405
+ session.onExit((exitCode) => {
406
+ if (this.generations.get(task.id) !== gen) return;
407
+ this.forget(task.id);
408
+ const verdict = exitVerdict(exitCode);
409
+ for (const l of this.listeners) l(task.id, verdict);
410
+ });
411
+ return gen;
412
+ }
413
+ /**
414
+ * The runner's own `task_complete` call (ADR-0022, V1/V3): settles the
415
+ * attempt, unless that attempt is no longer the task's current one or an
416
+ * earlier call already settled it.
417
+ */
418
+ signalComplete(taskId, generation, report) {
419
+ if (this.generations.get(taskId) !== generation) return;
420
+ this.acceptVerdict(taskId, reportedVerdict(report));
421
+ }
422
+ /**
423
+ * A script task's exit status (ADR-0035): its one evidence, as a runner's
424
+ * `task_complete` call is a runner's. It settles the attempt unless that
425
+ * attempt is no longer the task's current one.
426
+ */
427
+ signalScriptExit(taskId, generation, exit) {
428
+ if (this.generations.get(taskId) !== generation) return;
429
+ this.acceptVerdict(taskId, scriptVerdict(exit));
430
+ }
431
+ /** The runner was told something after its evidence so far, so only what it reports from here counts. */
432
+ supersede(taskId) {
433
+ this.pendingVerdicts.delete(taskId);
434
+ }
435
+ acceptVerdict(taskId, verdict) {
436
+ if (this.pendingVerdicts.has(taskId)) return;
437
+ if (this.sessions.get(taskId)?.queued().length) {
438
+ this.pendingVerdicts.set(taskId, verdict);
439
+ return;
440
+ }
441
+ this.publishVerdict(taskId, verdict);
442
+ }
443
+ publishVerdict(taskId, verdict) {
444
+ this.forget(taskId);
445
+ this.bumpGeneration(taskId);
446
+ for (const l of this.listeners) l(taskId, verdict);
447
+ }
448
+ /**
449
+ * The runner's `checkpoint` call (ADR-0022, V5): the task waits on the
450
+ * user until it is settled by {@link approveCheckpoint} or {@link rejectCheckpoint}. It is
451
+ * withdrawn, never left hanging, once the attempt is over or the call goes.
452
+ */
453
+ raiseCheckpoint(taskId, generation, question, signal) {
454
+ if (this.generations.get(taskId) !== generation || signal.aborted) {
455
+ return Promise.resolve({ kind: "withdrawn", why: "this attempt has ended." });
456
+ }
457
+ if (this.toolCheckpoints.has(taskId)) {
458
+ return Promise.resolve({ kind: "withdrawn", why: "another checkpoint is still waiting for an answer. Wait for it before asking again." });
459
+ }
460
+ return new Promise((resolve) => {
461
+ const settle = (answer) => {
462
+ signal.removeEventListener("abort", onAbort);
463
+ if (this.toolCheckpoints.get(taskId) === settle) {
464
+ this.toolCheckpoints.delete(taskId);
465
+ this.checkpointQuestions.delete(taskId);
466
+ }
467
+ resolve(answer);
468
+ };
469
+ const onAbort = () => {
470
+ settle({ kind: "withdrawn", why: "the call was cancelled." });
471
+ if (this.generations.get(taskId) !== generation) return;
472
+ this.resumeIdle(taskId);
473
+ for (const l of this.withdrawnListeners) l(taskId);
474
+ };
475
+ signal.addEventListener("abort", onAbort, { once: true });
476
+ this.toolCheckpoints.set(taskId, settle);
477
+ this.checkpointQuestions.set(taskId, question.trim());
478
+ for (const l of this.checkpointListeners) l(taskId, question.trim());
479
+ });
480
+ }
481
+ /** The open `checkpoint` call can no longer be answered: refuse it, and let the task out of the wait. */
482
+ callWentAway(taskId, why) {
483
+ const open = this.toolCheckpoints.get(taskId);
484
+ if (!open) return;
485
+ open({ kind: "withdrawn", why });
486
+ this.resumeIdle(taskId);
487
+ for (const l of this.withdrawnListeners) l(taskId);
488
+ }
489
+ withdrawToolCheckpoint(taskId) {
490
+ this.toolCheckpoints.get(taskId)?.({ kind: "withdrawn", why: "this attempt has ended." });
491
+ }
492
+ approvalOpened(taskId, approvalId) {
493
+ const open = this.openApprovals.get(taskId) ?? /* @__PURE__ */ new Set();
494
+ open.add(approvalId);
495
+ this.openApprovals.set(taskId, open);
496
+ this.clearIdle(taskId);
497
+ }
498
+ approvalClosed(taskId, approvalId, gen) {
499
+ const open = this.openApprovals.get(taskId);
500
+ if (!open?.delete(approvalId) || open.size) return;
501
+ this.openApprovals.delete(taskId);
502
+ this.touchIdle(taskId, gen);
503
+ }
504
+ bumpGeneration(taskId) {
505
+ this.lastGeneration += 1;
506
+ this.generations.set(taskId, this.lastGeneration);
507
+ return this.lastGeneration;
508
+ }
509
+ forget(taskId) {
510
+ this.pendingVerdicts.delete(taskId);
511
+ this.sessions.delete(taskId);
512
+ this.withdrawToolCheckpoint(taskId);
513
+ this.checkpointQuestions.delete(taskId);
514
+ this.idlePaused.delete(taskId);
515
+ this.openApprovals.delete(taskId);
516
+ this.clearIdle(taskId);
517
+ }
518
+ /** Manual "Mark complete" override: a pass verdict that bypasses evidence. */
519
+ markComplete(task) {
520
+ this.ceaseTracking(task.id);
521
+ return {
522
+ outcome: "pass",
523
+ reason: "Manually marked complete by user.",
524
+ checks: [
525
+ {
526
+ name: "manual",
527
+ passed: true,
528
+ skipped: false,
529
+ detail: "Task was manually marked complete by the user; no automatic verification was performed."
530
+ }
531
+ ],
532
+ decidedAt: (/* @__PURE__ */ new Date()).toISOString()
533
+ };
534
+ }
535
+ /** Clear verification state for a task (used on retry). */
536
+ clear(task) {
537
+ this.ceaseTracking(task.id);
538
+ }
539
+ /** Drop a task's verification state and invalidate any callbacks still holding its generation. */
540
+ ceaseTracking(taskId) {
541
+ this.forget(taskId);
542
+ this.bumpGeneration(taskId);
543
+ }
544
+ /** Drop all tracking state (used on stop / loadPlan). */
545
+ reset() {
546
+ this.pendingVerdicts.clear();
547
+ this.sessions.clear();
548
+ for (const taskId of [...this.toolCheckpoints.keys()]) this.withdrawToolCheckpoint(taskId);
549
+ this.checkpointQuestions.clear();
550
+ for (const timer of this.idleTimers.values()) clearTimeout(timer);
551
+ this.idleTimers.clear();
552
+ this.idleSince.clear();
553
+ this.idlePaused.clear();
554
+ this.openApprovals.clear();
555
+ this.generations.clear();
556
+ }
557
+ };
558
+ var EXIT_CHECK = "exit_code";
559
+ function endedByExit(verdict) {
560
+ return verdict.checks.some((check) => check.name === EXIT_CHECK);
561
+ }
562
+ function exitVerdict(exitCode) {
563
+ const normalized = exitCode == null ? 0 : exitCode;
564
+ const exitOk = normalized === 0;
565
+ return {
566
+ outcome: "fail",
567
+ reason: exitOk ? "Failed verification: the runner exited cleanly but never called task_complete." : `Failed verification: the runner exited with code ${normalized} without calling task_complete.`,
568
+ checks: [
569
+ { name: "task_complete", passed: false, skipped: false, detail: "the runner exited before calling task_complete" },
570
+ { name: EXIT_CHECK, passed: exitOk, skipped: false, detail: exitOk ? "the runner exited cleanly (code 0)" : `the runner exited with code ${normalized}` }
571
+ ],
572
+ decidedAt: (/* @__PURE__ */ new Date()).toISOString()
573
+ };
574
+ }
575
+ function interruptedVerdict(decidedAt) {
576
+ return { outcome: "fail", reason: "It was interrupted when Ordewell stopped, and nothing is running it now.", checks: [], decidedAt };
577
+ }
578
+ function statusText(status) {
579
+ if (status.error !== void 0) return `could not start: ${status.error}`;
580
+ if (status.signal) return `was ended by ${status.signal}`;
581
+ return `exited with code ${status.code ?? "unknown"}`;
582
+ }
583
+ function withTail(reason, errorTail, fullOutput) {
584
+ const tail = errorTail.trim();
585
+ const quoted = tail ? `${reason}
586
+
587
+ The end of its error output:
588
+ ${tail}` : reason;
589
+ if (!fullOutput) return quoted;
590
+ const cut = fullOutput.droppedChars > 0 ? `, except its last ${fullOutput.droppedChars} characters, which were not saved.` : "";
591
+ return `${quoted}
592
+
593
+ All of its output is in ${fullOutput.file}${cut}`;
594
+ }
595
+ function scriptVerdict(exit) {
596
+ const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
597
+ const check = (passed, detail) => [{ name: "script_exit", passed, skipped: false, detail }];
598
+ const command = exit.outcome !== "passed" && exit.command !== void 0 ? `\`${exit.command}\`` : void 0;
599
+ switch (exit.outcome) {
600
+ case "passed":
601
+ return { outcome: "pass", reason: "Verified: every step of the script exited 0.", checks: check(true, "every step exited 0"), decidedAt, reported: "done" };
602
+ case "failed": {
603
+ const said = `${command ?? "it"} ${statusText(exit.status)}`;
604
+ const reported = exit.status.error !== void 0 ? "blocked" : "failed";
605
+ return { outcome: "fail", reason: withTail(`The script failed: ${said}.`, exit.errorTail, exit.fullOutput), checks: check(false, said), decidedAt, reported };
606
+ }
607
+ case "timed-out": {
608
+ const said = `the time limit of ${durationText(exit.timeoutMs)} ran out${command ? ` while ${command} ran` : ""}, so it was ended`;
609
+ return { outcome: "fail", reason: withTail(`The script failed: ${said}.`, exit.errorTail, exit.fullOutput), checks: check(false, said), decidedAt, reported: "failed" };
610
+ }
611
+ case "stopped": {
612
+ const said = `it was stopped${command ? ` while ${command} ran` : ""}`;
613
+ return { outcome: "fail", reason: withTail(`The script did not finish: ${said}.`, "", exit.fullOutput), checks: check(false, said), decidedAt };
614
+ }
232
615
  }
233
- return { tasks, handoff: handoffOf(run) };
616
+ }
617
+ function durationText(ms) {
618
+ if (ms % 6e4 === 0) return `${ms / 6e4} minute${ms === 6e4 ? "" : "s"}`;
619
+ const seconds = Math.round(ms / 100) / 10;
620
+ return `${seconds} second${seconds === 1 ? "" : "s"}`;
621
+ }
622
+ function reportedVerdict(report) {
623
+ const checks = [
624
+ {
625
+ name: "task_complete",
626
+ passed: report.status === "done",
627
+ skipped: false,
628
+ detail: `the runner called task_complete with status "${report.status}"`
629
+ }
630
+ ];
631
+ const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
632
+ const reported = report.status;
633
+ if (report.status === "done") {
634
+ return { outcome: "pass", reason: "Verified: the runner reported completion through task_complete. Task completed successfully.", checks, decidedAt, reported };
635
+ }
636
+ const why = report.reason?.trim() || report.summary?.trim().split(/\n\s*\n/)[0].trim() || "no reason given";
637
+ return { outcome: "fail", reason: `The runner reported the task ${report.status}: ${why}`, checks, decidedAt, reported };
234
638
  }
235
639
 
236
640
  // src/services/TaskRefs.ts
@@ -301,6 +705,30 @@ function migrateLoadedTasks(tasks) {
301
705
  migrateLoadedTasks(task.subtasks ?? []);
302
706
  }
303
707
  }
708
+ function failInterruptedTasks(tasks, now) {
709
+ for (const task of flattenTasks(tasks)) {
710
+ if (task.status !== "in_progress") continue;
711
+ task.status = "failed";
712
+ task.verdict = interruptedVerdict(now);
713
+ }
714
+ }
715
+ function releaseMadeManual(tasks, live = () => false) {
716
+ for (const task of flattenTasks(tasks)) {
717
+ if (task.type !== "user" || live(task.id) || task.status !== "failed" && task.status !== "awaiting_user") continue;
718
+ task.status = "pending";
719
+ delete task.awaitingReason;
720
+ delete task.resumeAt;
721
+ }
722
+ }
723
+ function settleOwedRepairs(plan) {
724
+ const records = plan.isolation?.run.tasks;
725
+ if (!records) return;
726
+ for (const task of flattenTasks(plan.tasks)) {
727
+ if (task.status !== "pending" && task.status !== "approved" || records[task.id]?.status !== "conflict") continue;
728
+ task.status = "awaiting_user";
729
+ task.awaitingReason = "conflict";
730
+ }
731
+ }
304
732
  function dropRetiredPlanFields(plan) {
305
733
  const raw = plan;
306
734
  delete raw.researchBudget;
@@ -599,6 +1027,7 @@ function keepExecutionState(current, rewrite) {
599
1027
  ...t,
600
1028
  status: prior?.status ?? "pending",
601
1029
  skipReason: prior?.skipReason,
1030
+ reverted: prior?.reverted,
602
1031
  verdict: prior?.verdict,
603
1032
  outputSummary: prior?.outputSummary,
604
1033
  handoff: prior?.handoff,
@@ -609,6 +1038,7 @@ function keepExecutionState(current, rewrite) {
609
1038
  attempted: prior?.attempted,
610
1039
  attemptUsage: prior?.attemptUsage,
611
1040
  loopRound: prior?.loopRound,
1041
+ waitsEnded: prior?.waitsEnded,
612
1042
  // Where a task that has run ran is fixed, like its runner session (ADR-0020).
613
1043
  ...prior?.status === "failed" ? { ops: prior.ops } : {},
614
1044
  forcedPastGate: prior?.forcedPastGate,
@@ -647,11 +1077,14 @@ export {
647
1077
  absorbRemoval,
648
1078
  layoutOf,
649
1079
  changedReposOf,
650
- taskIsolationOf,
1080
+ landedInRepo,
1081
+ taskIsolationsOf,
651
1082
  handoffOf,
652
1083
  migratePlanIsolation,
653
1084
  migratePlanStateIsolation,
654
1085
  isolationViewOfPlan,
1086
+ VerdictEngine,
1087
+ endedByExit,
655
1088
  taskOrderLabel,
656
1089
  taskRef,
657
1090
  titledTaskRef,
@@ -661,6 +1094,9 @@ export {
661
1094
  resolveUserTaskRef,
662
1095
  isAwaitingReason,
663
1096
  migrateLoadedTasks,
1097
+ failInterruptedTasks,
1098
+ releaseMadeManual,
1099
+ settleOwedRepairs,
664
1100
  dropRetiredPlanFields,
665
1101
  migrateLoadedPlan,
666
1102
  isUserMessage,
@@ -689,4 +1125,4 @@ export {
689
1125
  stampOrigin,
690
1126
  keepExecutionState
691
1127
  };
692
- //# sourceMappingURL=chunk-QTZ5XTBY.mjs.map
1128
+ //# sourceMappingURL=chunk-F6WS3JLQ.mjs.map