@ordewell/core 0.11.3 → 0.12.1

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 (49) hide show
  1. package/AUTHORS.md +3 -1
  2. package/dist/{ModeResolver-DyXeZ9ib.d.ts → ModeResolver-B64aUZXJ.d.ts} +1 -1
  3. package/dist/{ModeResolver-BDAgouuE.d.mts → ModeResolver-CA2fUU9h.d.mts} +1 -1
  4. package/dist/{Task-BL4Idm9r.d.mts → Task-CxGgN6I0.d.mts} +801 -39
  5. package/dist/{Task-BL4Idm9r.d.ts → Task-CxGgN6I0.d.ts} +801 -39
  6. package/dist/{chunk-YAFNYNFX.mjs → chunk-2IFKPYPO.mjs} +453 -17
  7. package/dist/chunk-2IFKPYPO.mjs.map +1 -0
  8. package/dist/{chunk-MYXMIVWA.mjs → chunk-FX33UKAD.mjs} +8 -6
  9. package/dist/chunk-FX33UKAD.mjs.map +1 -0
  10. package/dist/{chunk-QTZ5XTBY.mjs → chunk-PKJTN2PD.mjs} +458 -13
  11. package/dist/chunk-PKJTN2PD.mjs.map +1 -0
  12. package/dist/{chunk-7WNLN63J.mjs → chunk-RZ23BMVR.mjs} +599 -124
  13. package/dist/chunk-RZ23BMVR.mjs.map +1 -0
  14. package/dist/chunk-UDUATSCJ.mjs +48 -0
  15. package/dist/chunk-UDUATSCJ.mjs.map +1 -0
  16. package/dist/{IRunner-DOMtDTVt.d.ts → eventStore-CTCSKS9E.d.ts} +976 -145
  17. package/dist/{IRunner-ihalYfTo.d.mts → eventStore-CcB8yELZ.d.mts} +976 -145
  18. package/dist/index.d.mts +905 -164
  19. package/dist/index.d.ts +905 -164
  20. package/dist/index.js +12496 -3308
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +10830 -2919
  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 +277 -21
  27. package/dist/parsing.js.map +1 -1
  28. package/dist/parsing.mjs +3 -3
  29. package/dist/plan-utils.d.mts +8 -5
  30. package/dist/plan-utils.d.ts +8 -5
  31. package/dist/plan-utils.js +262 -33
  32. package/dist/plan-utils.js.map +1 -1
  33. package/dist/plan-utils.mjs +39 -3
  34. package/dist/{runSettingsText-DVOd7vKC.d.ts → runSettingsText-DFVpe6aU.d.mts} +1996 -895
  35. package/dist/{runSettingsText-BuLVBlXS.d.mts → runSettingsText-lKLDsyli.d.ts} +1996 -895
  36. package/dist/testing.d.mts +99 -4
  37. package/dist/testing.d.ts +99 -4
  38. package/dist/testing.js +213 -8
  39. package/dist/testing.js.map +1 -1
  40. package/dist/testing.mjs +193 -8
  41. package/dist/testing.mjs.map +1 -1
  42. package/docker/task.Dockerfile +22 -0
  43. package/package.json +3 -1
  44. package/skills/diagnose/SKILL.md +2 -2
  45. package/skills/explore/SKILL.md +1 -1
  46. package/dist/chunk-7WNLN63J.mjs.map +0 -1
  47. package/dist/chunk-MYXMIVWA.mjs.map +0 -1
  48. package/dist/chunk-QTZ5XTBY.mjs.map +0 -1
  49. package/dist/chunk-YAFNYNFX.mjs.map +0 -1
@@ -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,398 @@ 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));
232
331
  }
233
- return { tasks, handoff: handoffOf(run) };
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, note) {
348
+ this.checkpointQuestions.delete(taskId);
349
+ this.resumeIdle(taskId);
350
+ this.toolCheckpoints.get(taskId)?.({ kind: "continue", ...note ? { note } : {} });
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, refuted) => this.signalComplete(task.id, gen, report, refuted));
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, refuted) {
419
+ if (this.generations.get(taskId) !== generation) return;
420
+ this.acceptVerdict(taskId, reportedVerdict(report, refuted));
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
+ }
615
+ }
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, refuted) {
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
+ ...refuted ? [refuted] : []
631
+ ];
632
+ const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
633
+ const reported = report.status;
634
+ if (report.status === "done" && refuted) return { outcome: "fail", reason: refuted.detail, checks, decidedAt, reported: "failed" };
635
+ if (report.status === "done") {
636
+ return { outcome: "pass", reason: "Verified: the runner reported completion through task_complete. Task completed successfully.", checks, decidedAt, reported };
637
+ }
638
+ const why = report.reason?.trim() || report.summary?.trim().split(/\n\s*\n/)[0].trim() || "no reason given";
639
+ return { outcome: "fail", reason: `The runner reported the task ${report.status}: ${why}`, checks, decidedAt, reported };
234
640
  }
235
641
 
236
642
  // src/services/TaskRefs.ts
@@ -282,7 +688,7 @@ function resolveUserTaskRef(tasks, ref) {
282
688
  // src/models/Task.ts
283
689
  import { v4 as uuidv4 } from "uuid";
284
690
  function isAwaitingReason(value) {
285
- return value === "input" || value === "checkpoint" || value === "conflict" || value === "files-changed" || value === "usage-limit";
691
+ return value === "input" || value === "checkpoint" || value === "question-lost" || value === "conflict" || value === "files-changed" || value === "usage-limit";
286
692
  }
287
693
  function migrateLoadedTasks(tasks) {
288
694
  for (const task of tasks) {
@@ -301,6 +707,31 @@ function migrateLoadedTasks(tasks) {
301
707
  migrateLoadedTasks(task.subtasks ?? []);
302
708
  }
303
709
  }
710
+ function failInterruptedTasks(tasks, now) {
711
+ for (const task of flattenTasks(tasks)) {
712
+ if (task.type === "delegated" && task.awaitingReason === "checkpoint") task.awaitingReason = "question-lost";
713
+ if (task.status !== "in_progress") continue;
714
+ task.status = "failed";
715
+ task.verdict = interruptedVerdict(now);
716
+ }
717
+ }
718
+ function releaseMadeManual(tasks, live = () => false) {
719
+ for (const task of flattenTasks(tasks)) {
720
+ if (task.type !== "user" || live(task.id) || task.status !== "failed" && task.status !== "awaiting_user") continue;
721
+ task.status = "pending";
722
+ delete task.awaitingReason;
723
+ delete task.resumeAt;
724
+ }
725
+ }
726
+ function settleOwedRepairs(plan) {
727
+ const records = plan.isolation?.run.tasks;
728
+ if (!records) return;
729
+ for (const task of flattenTasks(plan.tasks)) {
730
+ if (task.status !== "pending" && task.status !== "approved" || records[task.id]?.status !== "conflict") continue;
731
+ task.status = "awaiting_user";
732
+ task.awaitingReason = "conflict";
733
+ }
734
+ }
304
735
  function dropRetiredPlanFields(plan) {
305
736
  const raw = plan;
306
737
  delete raw.researchBudget;
@@ -440,7 +871,7 @@ function createTask(overrides = {}) {
440
871
  assignedModel: overrides.assignedModel,
441
872
  assignedRunner: overrides.assignedRunner ?? DEFAULT_RUNNERS[0],
442
873
  // A script task runs in no mode (ADR-0035): one given it would read as a mode it runs in.
443
- taskMode: overrides.taskMode ?? (type === "script" ? void 0 : "build"),
874
+ taskMode: overrides.taskMode ?? (type === "script" || type === "delegated" ? void 0 : "build"),
444
875
  autonomy: overrides.autonomy,
445
876
  sliceType: overrides.sliceType,
446
877
  userStoriesCovered: overrides.userStoriesCovered,
@@ -450,6 +881,12 @@ function createTask(overrides = {}) {
450
881
  ...overrides.commands !== void 0 ? { commands: overrides.commands.map((argv) => [...argv]) } : {},
451
882
  ...overrides.script !== void 0 ? { script: overrides.script } : {},
452
883
  ...overrides.timeoutMs !== void 0 ? { timeoutMs: overrides.timeoutMs } : {},
884
+ ...overrides.goal !== void 0 ? { goal: overrides.goal } : {},
885
+ ...overrides.budget !== void 0 ? { budget: { ...overrides.budget } } : {},
886
+ ...overrides.grants !== void 0 ? { grants: [...overrides.grants] } : {},
887
+ ...overrides.protectedPaths !== void 0 ? { protectedPaths: [...overrides.protectedPaths] } : {},
888
+ ...overrides.scope !== void 0 ? { scope: structuredClone(overrides.scope) } : {},
889
+ ...overrides.readOnly ? { readOnly: true } : {},
453
890
  ...overrides.loop ? { loop: { body: [...overrides.loop.body], maxRounds: overrides.loop.maxRounds } } : {},
454
891
  ...overrides.loopRound !== void 0 ? { loopRound: overrides.loopRound } : {},
455
892
  ...overrides.research !== void 0 ? { research: overrides.research } : {}
@@ -472,7 +909,7 @@ function opsFlag(value, type = "ai") {
472
909
  return value === true && type === "ai" ? true : void 0;
473
910
  }
474
911
  function runsAsOps(task) {
475
- return task.type === "script" || opsFlag(task.ops, task.type) === true;
912
+ return task.type === "script" && !task.readOnly || opsFlag(task.ops, task.type) === true;
476
913
  }
477
914
  function inheritedOps(from, chosen) {
478
915
  return chosen ?? from.every((t) => opsFlag(t.ops) === true);
@@ -599,6 +1036,7 @@ function keepExecutionState(current, rewrite) {
599
1036
  ...t,
600
1037
  status: prior?.status ?? "pending",
601
1038
  skipReason: prior?.skipReason,
1039
+ reverted: prior?.reverted,
602
1040
  verdict: prior?.verdict,
603
1041
  outputSummary: prior?.outputSummary,
604
1042
  handoff: prior?.handoff,
@@ -609,6 +1047,7 @@ function keepExecutionState(current, rewrite) {
609
1047
  attempted: prior?.attempted,
610
1048
  attemptUsage: prior?.attemptUsage,
611
1049
  loopRound: prior?.loopRound,
1050
+ waitsEnded: prior?.waitsEnded,
612
1051
  // Where a task that has run ran is fixed, like its runner session (ADR-0020).
613
1052
  ...prior?.status === "failed" ? { ops: prior.ops } : {},
614
1053
  forcedPastGate: prior?.forcedPastGate,
@@ -647,11 +1086,14 @@ export {
647
1086
  absorbRemoval,
648
1087
  layoutOf,
649
1088
  changedReposOf,
650
- taskIsolationOf,
1089
+ landedInRepo,
1090
+ taskIsolationsOf,
651
1091
  handoffOf,
652
1092
  migratePlanIsolation,
653
1093
  migratePlanStateIsolation,
654
1094
  isolationViewOfPlan,
1095
+ VerdictEngine,
1096
+ endedByExit,
655
1097
  taskOrderLabel,
656
1098
  taskRef,
657
1099
  titledTaskRef,
@@ -661,6 +1103,9 @@ export {
661
1103
  resolveUserTaskRef,
662
1104
  isAwaitingReason,
663
1105
  migrateLoadedTasks,
1106
+ failInterruptedTasks,
1107
+ releaseMadeManual,
1108
+ settleOwedRepairs,
664
1109
  dropRetiredPlanFields,
665
1110
  migrateLoadedPlan,
666
1111
  isUserMessage,
@@ -689,4 +1134,4 @@ export {
689
1134
  stampOrigin,
690
1135
  keepExecutionState
691
1136
  };
692
- //# sourceMappingURL=chunk-QTZ5XTBY.mjs.map
1137
+ //# sourceMappingURL=chunk-PKJTN2PD.mjs.map