@ordewell/cli 0.10.5 → 0.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.js +198 -229
  2. package/package.json +3 -3
package/dist/main.js CHANGED
@@ -631,18 +631,18 @@ var ApiClient = class _ApiClient {
631
631
  return this.call("POST", `/api/plans/${sessionId}/review/approve`, "Approve failed");
632
632
  }
633
633
  markTaskComplete(sessionId, taskId) {
634
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/complete`, "Mark complete failed");
634
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/complete`, "Mark complete failed");
635
635
  }
636
636
  /** Skip: end the task without landing its work, so what depends on it goes ahead once the work it waited on is done; never a mark complete. */
637
637
  skipTask(sessionId, taskId, reason) {
638
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/skip`, "Skip failed", reason ? { reason } : {});
638
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/skip`, "Skip failed", reason ? { reason } : {});
639
639
  }
640
640
  markTaskIncomplete(sessionId, taskId) {
641
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/uncomplete`, "Mark not done failed");
641
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/uncomplete`, "Mark not done failed");
642
642
  }
643
- /** Keep a task paused on a usage limit from resuming on its own; `cancelled` is false when none was scheduled. */
643
+ /** Keep a task paused on a usage limit from resuming on its own; a task with none scheduled is refused with the reason. */
644
644
  cancelResume(sessionId, taskId) {
645
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/resume/cancel`, "Cancel resume failed");
645
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/resume/cancel`, "Cancel resume failed");
646
646
  }
647
647
  /** Keep what waits on the planner's usage limit from being sent at the reset; `cancelled` is false when nothing waited. */
648
648
  cancelPlannerResend(sessionId) {
@@ -650,55 +650,55 @@ var ApiClient = class _ApiClient {
650
650
  }
651
651
  /** Resume a task paused on a usage limit now, in its saved session, once the user says the limit has reset. */
652
652
  resumeNow(sessionId, taskId) {
653
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/resume/now`, "Resume failed");
653
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/resume/now`, "Resume failed");
654
654
  }
655
655
  /** run | force-start | retry | cancel — real orchestrator work, not a status patch. */
656
656
  taskControl(sessionId, taskId, action) {
657
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/${action}`, `${action} failed`);
657
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/${action}`, `${action} failed`);
658
658
  }
659
659
  /** A user message to a structured task: delivered now if it waits for input, else queued behind its turn. */
660
660
  sendTaskMessage(sessionId, taskId, text) {
661
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages`, "Send message failed", { text });
661
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/messages`, "Send message failed", { text });
662
662
  }
663
663
  /** Force send: interrupt the task's running turn and deliver `text` next, ahead of anything queued (ADR-0023, F1). */
664
664
  forceSendTaskMessage(sessionId, taskId, text) {
665
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages/now`, "Send now failed", { text });
665
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/messages/now`, "Send now failed", { text });
666
666
  }
667
667
  /** Run the task runner's own command `/name args` as a turn of its own (#66). */
668
668
  invokeTaskCommand(sessionId, taskId, name, args) {
669
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/commands`, "Command failed", { name, args });
669
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/commands`, "Command failed", { name, args });
670
670
  }
671
- /** Force send a message still queued; `sent` is false once the runner already has it. */
671
+ /** Force send a message still queued; one the runner already has is refused with the reason. */
672
672
  forceSendQueuedTaskMessage(sessionId, taskId, messageId) {
673
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages/${encodeURIComponent(messageId)}/now`, "Send now failed");
673
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/messages/${encodeURIComponent(messageId)}/now`, "Send now failed");
674
674
  }
675
675
  /** Continue a finished structured task in its saved session, with `text` as its next turn. */
676
676
  continueTask(sessionId, taskId, text) {
677
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/continue`, "Continue failed", { text });
677
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/continue`, "Continue failed", { text });
678
678
  }
679
- /** `removed` is false once the message was already delivered. */
679
+ /** Take back a queued message; one already delivered is refused with the reason. */
680
680
  removeQueuedTaskMessage(sessionId, taskId, messageId) {
681
- return this.call("DELETE", `/api/plans/${sessionId}/tasks/${taskId}/messages/${encodeURIComponent(messageId)}`, "Remove message failed");
681
+ return this.call("DELETE", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/messages/${encodeURIComponent(messageId)}`, "Remove message failed");
682
682
  }
683
683
  interruptTask(sessionId, taskId) {
684
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/interrupt`, "Interrupt failed");
684
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/interrupt`, "Interrupt failed");
685
685
  }
686
686
  /** Let a task go on from the checkpoint it waits at; refused, with the reason, when none waits. */
687
687
  approveTaskCheckpoint(sessionId, taskId) {
688
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/checkpoint/approve`, "Approve checkpoint failed");
688
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/checkpoint/approve`, "Approve checkpoint failed");
689
689
  }
690
690
  /** Turn a task back at its checkpoint; `reason` is what the agent is told. */
691
691
  rejectTaskCheckpoint(sessionId, taskId, reason) {
692
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/checkpoint/reject`, "Reject checkpoint failed", reason ? { reason } : {});
692
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/checkpoint/reject`, "Reject checkpoint failed", reason ? { reason } : {});
693
693
  }
694
694
  addTask(sessionId, task) {
695
695
  return this.call("POST", `/api/plans/${sessionId}/tasks`, "Add task failed", task);
696
696
  }
697
697
  updateTask(sessionId, taskId, changes) {
698
- return this.call("PUT", `/api/plans/${sessionId}/tasks/${taskId}`, "Update task failed", changes);
698
+ return this.call("PUT", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}`, "Update task failed", changes);
699
699
  }
700
700
  removeTask(sessionId, taskId) {
701
- return this.call("DELETE", `/api/plans/${sessionId}/tasks/${taskId}`, "Remove task failed");
701
+ return this.call("DELETE", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}`, "Remove task failed");
702
702
  }
703
703
  /**
704
704
  * Make a saved session live on the server. `getSession` only reads the file;
@@ -708,7 +708,7 @@ var ApiClient = class _ApiClient {
708
708
  async adoptSession(sessionId, workspace) {
709
709
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
710
710
  const res = await this.call("POST", `/api/sessions/${sessionId}/load${qs}`, "Failed to load session");
711
- return { plan: res.plan, goal: res.goal ?? "" };
711
+ return { plan: res.plan, goal: res.goal ?? "", ...res.run ? { run: res.run } : {} };
712
712
  }
713
713
  deleteSession(sessionId, workspace) {
714
714
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
@@ -739,7 +739,7 @@ var ApiClient = class _ApiClient {
739
739
  }
740
740
  /** The ancestors a task waits on at its merge gate (ADR-0020); empty when nothing gates it. */
741
741
  async getMergeGate(sessionId, taskId) {
742
- const res = await this.call("GET", `/api/plans/${sessionId}/tasks/${taskId}/merge-gate`, "Could not read the merge gate");
742
+ const res = await this.call("GET", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/merge-gate`, "Could not read the merge gate");
743
743
  return res.mergeGate ?? [];
744
744
  }
745
745
  async reviewRunDiff(sessionId) {
@@ -769,7 +769,7 @@ var ApiClient = class _ApiClient {
769
769
  await this.call("POST", `/api/plans/${sessionId}/isolation/${segment}`, failure);
770
770
  }
771
771
  async resolveConflictAsTask(sessionId, taskId) {
772
- const res = await this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/resolve-conflict`, "Could not add a resolver task");
772
+ const res = await this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/resolve-conflict`, "Could not add a resolver task");
773
773
  return res.plan;
774
774
  }
775
775
  async closeSession(sessionId) {
@@ -798,13 +798,13 @@ var ApiClient = class _ApiClient {
798
798
  async getTaskLogAttempts(sessionId, taskId, workspace) {
799
799
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
800
800
  const res = await this.call("GET", `/api/sessions/${sessionId}/tasks/${encodeURIComponent(taskId)}/log${qs}`, "Could not read the task log");
801
- return res.attempts;
801
+ return res;
802
802
  }
803
803
  /** One attempt's saved log, for `replayTaskLog`. */
804
804
  async getTaskLog(sessionId, taskId, attempt, workspace) {
805
805
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
806
806
  const res = await this.call("GET", `/api/sessions/${sessionId}/tasks/${encodeURIComponent(taskId)}/log/${attempt}${qs}`, "Could not read the task log");
807
- return res.events;
807
+ return res;
808
808
  }
809
809
  /**
810
810
  * The one place a session socket is built, so the token travels on both
@@ -1045,12 +1045,7 @@ function allTasksOf(plan) {
1045
1045
  return (0, import_core5.rejoinPlanTree)(tasks, executionLog);
1046
1046
  }
1047
1047
  function resolveTaskId(plan, identifier) {
1048
- const allTasks = allTasksOf(plan);
1049
- const byRef = (0, import_core5.resolveTaskRef)(allTasks, identifier);
1050
- if (byRef) return byRef.id;
1051
- const prefixMatches = allTasks.filter((t) => typeof t.id === "string" && t.id.startsWith(identifier));
1052
- if (prefixMatches.length === 1) return prefixMatches[0].id;
1053
- return void 0;
1048
+ return (0, import_core5.resolveUserTaskRef)(allTasksOf(plan), identifier)?.id;
1054
1049
  }
1055
1050
 
1056
1051
  // src/utils/session.ts
@@ -1863,7 +1858,8 @@ async function handleRun(subArgs, injectedApi) {
1863
1858
  const onBlocked = hasFlag(subArgs, "--stash") ? "stash" : hasFlag(subArgs, "--without-isolation") ? "shared" : void 0;
1864
1859
  await followExecution(api, sessionId, async () => {
1865
1860
  try {
1866
- await api.executePlan(sessionId);
1861
+ const started = await api.executePlan(sessionId);
1862
+ if (started.message) console.error(started.message);
1867
1863
  } catch (err) {
1868
1864
  console.error(`Failed to start execution: ${err.message}`);
1869
1865
  process.exit(1);
@@ -2808,7 +2804,7 @@ function servedApprovalRecord(plan) {
2808
2804
  }
2809
2805
 
2810
2806
  // src/commands/stop.ts
2811
- async function handleStop(subArgs) {
2807
+ async function handleStop(subArgs, injectedApi) {
2812
2808
  if (subArgs.includes("--server") || subArgs[0] === "server") {
2813
2809
  const stopped = await stopDaemon(resolvePort(subArgs));
2814
2810
  if (!stopped) process.exit(1);
@@ -2825,10 +2821,9 @@ async function handleStop(subArgs) {
2825
2821
  sessionId = last.sessionId;
2826
2822
  console.error(`Stopping last session: ${sessionId}`);
2827
2823
  }
2828
- const port = await ensureDaemon(resolvePort(subArgs));
2829
- const api = new ApiClient(port);
2830
- await api.stopExecution(sessionId);
2831
- console.log("Execution stopped.");
2824
+ const api = await connect(subArgs, injectedApi);
2825
+ const { message } = await api.stopExecution(sessionId);
2826
+ console.log(message ?? "Execution stopped.");
2832
2827
  }
2833
2828
 
2834
2829
  // src/commands/web.ts
@@ -3243,7 +3238,7 @@ var PAST_TENSE = {
3243
3238
  retry: "Task retried.",
3244
3239
  cancel: "Task cancelled."
3245
3240
  };
3246
- async function withResolvedTask(subArgs, usage, injectedApi, run) {
3241
+ async function sessionAndRef(subArgs, usage, injectedApi) {
3247
3242
  const workspace = flag(subArgs, "--workspace") || process.cwd();
3248
3243
  let sessionId = flag(subArgs, "--session-id");
3249
3244
  if (!sessionId) {
@@ -3254,12 +3249,20 @@ async function withResolvedTask(subArgs, usage, injectedApi, run) {
3254
3249
  }
3255
3250
  sessionId = last.sessionId;
3256
3251
  }
3257
- const identifier = positionals(subArgs)[0];
3258
- if (!identifier) {
3252
+ const ref = positionals(subArgs)[0];
3253
+ if (!ref) {
3259
3254
  console.error(usage);
3260
3255
  process.exit(1);
3261
3256
  }
3262
3257
  const api = injectedApi || new ApiClient(await ensureDaemon(resolvePort(subArgs)));
3258
+ return { api, sessionId, ref, workspace };
3259
+ }
3260
+ async function withTaskRef(subArgs, usage, injectedApi, run) {
3261
+ const { api, sessionId, ref } = await sessionAndRef(subArgs, usage, injectedApi);
3262
+ await run(api, sessionId, ref);
3263
+ }
3264
+ async function withResolvedTask(subArgs, usage, injectedApi, run) {
3265
+ const { api, sessionId, ref, workspace } = await sessionAndRef(subArgs, usage, injectedApi);
3263
3266
  let plan;
3264
3267
  try {
3265
3268
  plan = (await api.getSession(sessionId, workspace)).plan;
@@ -3267,35 +3270,37 @@ async function withResolvedTask(subArgs, usage, injectedApi, run) {
3267
3270
  console.error(`Failed to load session: ${err.message}`);
3268
3271
  process.exit(1);
3269
3272
  }
3270
- const taskId = resolveTaskId(plan, identifier);
3273
+ const taskId = resolveTaskId(plan, ref);
3271
3274
  if (!taskId) {
3272
- console.error(`Task not found: "${identifier}"`);
3275
+ console.error(`Task not found: "${ref}"`);
3273
3276
  process.exit(1);
3274
3277
  }
3275
3278
  await run(api, sessionId, taskId, plan);
3276
3279
  }
3277
3280
  var PASSES_GATE = /* @__PURE__ */ new Set(["run", "force-start"]);
3278
- async function confirmPastGate(subArgs, api, sessionId, taskId, confirm2) {
3279
- if (hasFlag(subArgs, "--yes")) return;
3280
- const unmerged = await api.getMergeGate(sessionId, taskId).catch(() => []);
3281
- if (unmerged.length === 0) return;
3281
+ async function confirmPastGate(subArgs, api, sessionId, ref, confirm2) {
3282
+ const unmerged = await api.getMergeGate(sessionId, ref).catch(() => []);
3283
+ if (unmerged.length === 0) return false;
3284
+ if (hasFlag(subArgs, "--yes")) return true;
3282
3285
  const question = `${(0, import_core21.pastGateConfirmation)(unmerged.map((d) => (0, import_core21.titledTaskRef)(d)))} Start it anyway?`;
3283
3286
  if (!await confirm2(question)) fail("Not started: nothing was changed. Pass --yes to start it without a prompt.");
3287
+ return true;
3284
3288
  }
3285
3289
  function makeHandler(action, command = action) {
3286
3290
  const yes = PASSES_GATE.has(action) ? " [--yes]" : "";
3287
3291
  return async function handle(subArgs, injectedApi, confirm2 = askYesNo) {
3288
- await withResolvedTask(
3292
+ await withTaskRef(
3289
3293
  subArgs,
3290
3294
  `Usage: ordewell ${command} <task-id-or-order> [--session-id <id>] [--workspace /path]${yes}`,
3291
3295
  injectedApi,
3292
- async (api, sessionId, taskId) => {
3293
- if (PASSES_GATE.has(action)) await confirmPastGate(subArgs, api, sessionId, taskId, confirm2);
3296
+ async (api, sessionId, ref) => {
3297
+ const pastGate = PASSES_GATE.has(action) ? await confirmPastGate(subArgs, api, sessionId, ref, confirm2) : false;
3298
+ const control = pastGate && action === "run" ? "force-start" : action;
3294
3299
  try {
3295
- const { queued } = await api.taskControl(sessionId, taskId, action);
3296
- console.log(queued ?? PAST_TENSE[action]);
3300
+ const { message, queued } = await api.taskControl(sessionId, ref, control);
3301
+ console.log(message ?? queued ?? PAST_TENSE[control]);
3297
3302
  } catch (err) {
3298
- console.error(`Failed to ${action} task: ${err.message}`);
3303
+ console.error(`Failed to ${control} task: ${err.message}`);
3299
3304
  process.exit(1);
3300
3305
  }
3301
3306
  }
@@ -3309,10 +3314,10 @@ async function handleContinue(subArgs, injectedApi) {
3309
3314
  console.error(usage);
3310
3315
  process.exit(1);
3311
3316
  }
3312
- await withResolvedTask(subArgs, usage, injectedApi, async (api, sessionId, taskId) => {
3317
+ await withTaskRef(subArgs, usage, injectedApi, async (api, sessionId, ref) => {
3313
3318
  try {
3314
- await api.continueTask(sessionId, taskId, message);
3315
- console.log("Task continued.");
3319
+ const sent = await api.continueTask(sessionId, ref, message);
3320
+ console.log(sent.message ?? "Task continued.");
3316
3321
  } catch (err) {
3317
3322
  console.error(`Failed to continue task: ${err.message}`);
3318
3323
  process.exit(1);
@@ -3332,11 +3337,10 @@ async function handleCheckpoint(subArgs, injectedApi) {
3332
3337
  ${usage}`);
3333
3338
  process.exit(1);
3334
3339
  }
3335
- await withResolvedTask(subArgs, usage, injectedApi, async (api, sessionId, taskId) => {
3340
+ await withTaskRef(subArgs, usage, injectedApi, async (api, sessionId, ref) => {
3336
3341
  try {
3337
- if (answer === "approve") await api.approveTaskCheckpoint(sessionId, taskId);
3338
- else await api.rejectTaskCheckpoint(sessionId, taskId, reason || void 0);
3339
- console.log(answer === "approve" ? "Checkpoint approved." : "Checkpoint rejected.");
3342
+ const sent = answer === "approve" ? await api.approveTaskCheckpoint(sessionId, ref) : await api.rejectTaskCheckpoint(sessionId, ref, reason || void 0);
3343
+ console.log(sent.message ?? (answer === "approve" ? "Checkpoint approved." : "Checkpoint rejected."));
3340
3344
  } catch (err) {
3341
3345
  console.error(`Failed to ${answer} checkpoint: ${err.message}`);
3342
3346
  process.exit(1);
@@ -3350,14 +3354,14 @@ var handleCancel = makeHandler("cancel");
3350
3354
 
3351
3355
  // src/commands/mark-complete.ts
3352
3356
  async function handleMarkComplete(subArgs, api) {
3353
- await withResolvedTask(
3357
+ await withTaskRef(
3354
3358
  subArgs,
3355
3359
  "Usage: ordewell mark-complete <task-id-or-order> [--session-id <id>]",
3356
3360
  api,
3357
- async (client, sessionId, taskId) => {
3361
+ async (client, sessionId, ref) => {
3358
3362
  try {
3359
- await client.markTaskComplete(sessionId, taskId);
3360
- console.log("Task marked complete.");
3363
+ const done = await client.markTaskComplete(sessionId, ref);
3364
+ console.log(done.message ?? "Task marked complete.");
3361
3365
  } catch (err) {
3362
3366
  console.error(`Failed to mark task complete: ${err.message}`);
3363
3367
  process.exit(1);
@@ -3366,14 +3370,14 @@ async function handleMarkComplete(subArgs, api) {
3366
3370
  );
3367
3371
  }
3368
3372
  async function handleUncomplete(subArgs, api) {
3369
- await withResolvedTask(
3373
+ await withTaskRef(
3370
3374
  subArgs,
3371
3375
  "Usage: ordewell uncomplete <task-id-or-order> [--session-id <id>]",
3372
3376
  api,
3373
- async (client, sessionId, taskId) => {
3377
+ async (client, sessionId, ref) => {
3374
3378
  try {
3375
- await client.markTaskIncomplete(sessionId, taskId);
3376
- console.log("Task marked not done.");
3379
+ const done = await client.markTaskIncomplete(sessionId, ref);
3380
+ console.log(done.message ?? "Task marked not done.");
3377
3381
  } catch (err) {
3378
3382
  console.error(`Failed to mark task not done: ${err.message}`);
3379
3383
  process.exit(1);
@@ -3382,14 +3386,14 @@ async function handleUncomplete(subArgs, api) {
3382
3386
  );
3383
3387
  }
3384
3388
  async function handleSkip(subArgs, api) {
3385
- await withResolvedTask(
3389
+ await withTaskRef(
3386
3390
  subArgs,
3387
3391
  "Usage: ordewell skip <task-id-or-order> [--reason <text>] [--session-id <id>]",
3388
3392
  api,
3389
- async (client, sessionId, taskId) => {
3393
+ async (client, sessionId, ref) => {
3390
3394
  try {
3391
- await client.skipTask(sessionId, taskId, flag(subArgs, "--reason"));
3392
- console.log("Task skipped: none of its work lands, and the tasks that depend on it go ahead without it once the work it waited on is done.");
3395
+ const done = await client.skipTask(sessionId, ref, flag(subArgs, "--reason"));
3396
+ console.log(done.message ?? "Task skipped: none of its work lands, and the tasks that depend on it go ahead without it once the work it waited on is done.");
3393
3397
  } catch (err) {
3394
3398
  console.error(`Failed to skip task: ${err.message}`);
3395
3399
  process.exit(1);
@@ -3674,9 +3678,6 @@ function handoffArrived(state, handoff) {
3674
3678
  );
3675
3679
  return state.overlay ? told : { ...told, overlay: { kind: "handoff", index: 0, diff: null } };
3676
3680
  }
3677
- function withDeferral(fresh, known) {
3678
- return known?.deferredTo && handoffBranch(known) === handoffBranch(fresh) ? { ...fresh, deferredTo: known.deferredTo } : fresh;
3679
- }
3680
3681
  function runHandoffAction(state, id) {
3681
3682
  const { sessionId } = state;
3682
3683
  const handoff = currentHandoff(state);
@@ -3745,8 +3746,7 @@ function confirmedHandoff(state, kind) {
3745
3746
  sessionId,
3746
3747
  branch: handoffBranch(handoff),
3747
3748
  ...(0, import_core10.isRepoGroup)(handoff) ? { group: true } : {},
3748
- ...repaired.length > 0 ? { repaired } : {},
3749
- ...midRun(state) ? { midRun: true } : {}
3749
+ ...repaired.length > 0 ? { repaired } : {}
3750
3750
  } : { type: "isolationDiscard", sessionId, branch: handoffBranch(handoff) };
3751
3751
  return { state: closed, effects: [effect] };
3752
3752
  }
@@ -4147,21 +4147,23 @@ async function handleApprove(subArgs, injectedApi) {
4147
4147
  const sessionId = resolveSessionId(subArgs);
4148
4148
  const api = await connect(subArgs, injectedApi);
4149
4149
  await followExecution(api, sessionId, async () => {
4150
+ let approved;
4150
4151
  try {
4151
- await api.approveReview(sessionId);
4152
+ approved = await api.approveReview(sessionId);
4152
4153
  } catch (err) {
4153
4154
  console.error(`Failed to approve: ${err.message}`);
4154
4155
  process.exit(1);
4155
4156
  }
4156
- console.error("Plan approved: continuing execution.");
4157
+ console.error(approved.message ?? "Plan approved: continuing execution.");
4157
4158
  });
4158
4159
  }
4159
4160
 
4160
4161
  // src/commands/task-assign.ts
4161
4162
  var import_core29 = require("@ordewell/core");
4162
- var import_plan_utils = require("@ordewell/core/plan-utils");
4163
+ var import_plan_utils3 = require("@ordewell/core/plan-utils");
4163
4164
 
4164
4165
  // src/tui/taskAssignment.ts
4166
+ var import_plan_utils = require("@ordewell/core/plan-utils");
4165
4167
  function runnerServes(runner, model) {
4166
4168
  return !runner || !model.runners || model.runners.length === 0 || model.runners.includes(runner);
4167
4169
  }
@@ -4184,7 +4186,7 @@ function effortsForTask(models, task) {
4184
4186
  }
4185
4187
  function assignedModelFor(model, currentEffort) {
4186
4188
  const variants = model.variants ?? [];
4187
- const thinkingEffort = variants.some((variant) => variant.id === currentEffort) ? currentEffort : void 0;
4189
+ const thinkingEffort = (0, import_plan_utils.clampThinkingEffort)(currentEffort, variants);
4188
4190
  return {
4189
4191
  modelId: model.id,
4190
4192
  modelLabel: model.label,
@@ -4198,6 +4200,7 @@ function modesForTask(modesByRunner, task) {
4198
4200
  }
4199
4201
 
4200
4202
  // src/tui/state.ts
4203
+ var import_plan_utils2 = require("@ordewell/core/plan-utils");
4201
4204
  var import_core28 = require("@ordewell/core");
4202
4205
 
4203
4206
  // src/tui/editor.ts
@@ -4394,12 +4397,7 @@ function resumesTask(task) {
4394
4397
  return task?.status === "awaiting_user" && task.awaitingReason === "usage-limit";
4395
4398
  }
4396
4399
  function findTask(tasks, id) {
4397
- for (const task of tasks) {
4398
- if (task.id === id) return task;
4399
- const found = findTask(task.subtasks ?? [], id);
4400
- if (found) return found;
4401
- }
4402
- return void 0;
4400
+ return (0, import_plan_utils2.taskPlacement)(tasks, id)?.task;
4403
4401
  }
4404
4402
  function refFor(state, task) {
4405
4403
  return (0, import_core28.taskRefOf)(state.tasks, task.id) ?? (0, import_core28.taskRef)(task);
@@ -4485,8 +4483,8 @@ function initialState(overrides = {}) {
4485
4483
  }
4486
4484
 
4487
4485
  // src/commands/task-assign.ts
4488
- var refOf = (tasks, task) => (0, import_plan_utils.taskRefOf)(tasks, task.id) ?? (0, import_plan_utils.taskRef)(task);
4489
- var namedOf = (tasks, task) => (0, import_plan_utils.titledTaskRefOf)(tasks, task.id) ?? (0, import_plan_utils.titledTaskRef)(task);
4486
+ var refOf = (tasks, task) => (0, import_plan_utils3.taskRefOf)(tasks, task.id) ?? (0, import_plan_utils3.taskRef)(task);
4487
+ var namedOf = (tasks, task) => (0, import_plan_utils3.titledTaskRefOf)(tasks, task.id) ?? (0, import_plan_utils3.titledTaskRef)(task);
4490
4488
  async function withTask(subArgs, usage, injectedApi, run, { joinValue = false } = {}) {
4491
4489
  await withResolvedTask(subArgs, usage, injectedApi, async (api, sessionId, taskId, plan) => {
4492
4490
  const tasks = taskViews(plan);
@@ -4548,8 +4546,9 @@ Model \xB7 ${namedOf(tasks, task)}${task.assignedRunner ? ` (${task.assignedRunn
4548
4546
  const assignedModel = assignedModelFor(model, task.assignedModel?.thinkingEffort);
4549
4547
  await api.updateTask(sessionId, task.id, { assignedModel });
4550
4548
  console.log(`Task ${refOf(tasks, task)} model set to ${model.label}.`);
4551
- if (task.assignedModel?.thinkingEffort && !assignedModel.thinkingEffort) {
4552
- console.log(` ${model.label} does not expose "${task.assignedModel.thinkingEffort}", so the effort went back to the runner default.`);
4549
+ const was = task.assignedModel?.thinkingEffort;
4550
+ if (was && assignedModel.thinkingEffort !== was) {
4551
+ console.log(` ${model.label} does not expose "${was}", so the effort is now ${assignedModel.thinkingEffort ? `"${assignedModel.thinkingEffort}", its nearest level` : "the runner default"}.`);
4553
4552
  }
4554
4553
  });
4555
4554
  }
@@ -4625,15 +4624,16 @@ Mode \xB7 ${namedOf(tasks, task)}${task.assignedRunner ? ` (${task.assignedRunne
4625
4624
  var DEPS_USAGE = "Usage: ordewell task-deps <task-id-or-order> [<id,id,\u2026>|none] [--session-id <id>]";
4626
4625
  async function handleTaskDeps(subArgs, injectedApi) {
4627
4626
  await withTask(subArgs, DEPS_USAGE, injectedApi, async (api, sessionId, task, tasks, value) => {
4628
- const candidates = (0, import_plan_utils.dependencyCandidates)(tasks, task.id);
4627
+ const candidates = (0, import_plan_utils3.dependencyCandidates)(tasks, task.id);
4628
+ const held = task.dependencies.filter((id) => !candidates.some((c) => c.id === id)).flatMap((id) => (0, import_plan_utils3.resolveTaskRef)(tasks, id) ?? []);
4629
4629
  if (!value) {
4630
4630
  console.log(`
4631
4631
  Dependencies \xB7 ${namedOf(tasks, task)}
4632
4632
  `);
4633
4633
  if (candidates.length === 0) {
4634
- console.log(` Nothing runs before ${refOf(tasks, task)}, so it has no possible dependencies.`);
4634
+ console.log(` No earlier siblings to offer as dependencies for ${refOf(tasks, task)}.`);
4635
4635
  }
4636
- for (const c of candidates) {
4636
+ for (const c of [...candidates, ...held]) {
4637
4637
  const chosen = task.dependencies.includes(c.id);
4638
4638
  console.log(` ${chosen ? "*" : " "} ${c.id} ${namedOf(tasks, c)}${c.status === "completed" ? " (already completed)" : ""}`);
4639
4639
  }
@@ -4641,8 +4641,8 @@ Dependencies \xB7 ${namedOf(tasks, task)}
4641
4641
  * = current. ${DEPS_USAGE}`);
4642
4642
  return;
4643
4643
  }
4644
- const dependencies = value.toLowerCase() === "none" ? [] : value.split(",").map((s) => s.trim()).filter(Boolean).map((token) => tasks.find((t) => t.id === token || String(t.order) === token)?.id ?? token);
4645
- const check = (0, import_plan_utils.canSetDependencies)(tasks, task.id, dependencies);
4644
+ const dependencies = value.toLowerCase() === "none" ? [] : value.split(",").map((s) => s.trim()).filter(Boolean).map((token) => (0, import_plan_utils3.resolveUserTaskRef)(tasks, token)?.id ?? token);
4645
+ const check = (0, import_plan_utils3.canSetDependencies)(tasks, task.id, dependencies);
4646
4646
  if (!check.ok) fail(check.error ?? "Invalid dependencies.");
4647
4647
  await api.updateTask(sessionId, task.id, { dependencies });
4648
4648
  console.log(dependencies.length > 0 ? `Task ${refOf(tasks, task)} now depends on ${dependencies.length} task${dependencies.length === 1 ? "" : "s"}.` : `Task ${refOf(tasks, task)} no longer depends on anything.`);
@@ -4871,7 +4871,7 @@ var import_core33 = require("@ordewell/core");
4871
4871
 
4872
4872
  // src/tui/reducers/pickers.ts
4873
4873
  var import_core31 = require("@ordewell/core");
4874
- var import_plan_utils2 = require("@ordewell/core/plan-utils");
4874
+ var import_plan_utils4 = require("@ordewell/core/plan-utils");
4875
4875
  var DEFAULT_EFFORT = "__runner_default__";
4876
4876
  function picker(title, items, action, extra = {}) {
4877
4877
  return { title, items, filter: "", index: 0, multi: false, chosen: [], filterable: false, action, ...extra };
@@ -4980,7 +4980,7 @@ function pickerItemsFor(state, action) {
4980
4980
  }));
4981
4981
  }
4982
4982
  if (action.kind === "set-task-deps") {
4983
- return (0, import_plan_utils2.dependencyCandidates)(state.tasks, action.taskId).map((candidate) => ({
4983
+ return (0, import_plan_utils4.dependencyCandidates)(state.tasks, action.taskId).map((candidate) => ({
4984
4984
  id: candidate.id,
4985
4985
  label: titledRefFor(state, candidate),
4986
4986
  detail: candidate.status === "completed" ? "already completed" : void 0
@@ -6651,12 +6651,7 @@ function openTaskView(state, sessionId, taskId) {
6651
6651
  if (!task) return step(say(state, "system", `No task ${taskId} in this plan.`));
6652
6652
  const taskView = {
6653
6653
  taskId,
6654
- view: import_core40.EMPTY_TASK_LOG,
6655
- attempts: [],
6656
- attempt: 0,
6657
- pending: [],
6658
- loaded: false,
6659
- followLatest: true,
6654
+ ...import_core40.EMPTY_TASK_LOG_FOLLOW,
6660
6655
  queuedIndex: null
6661
6656
  };
6662
6657
  return step({ ...state, taskView, focus: "chat", scroll: 0 }, [{ type: "openTaskLog", sessionId, taskId }]);
@@ -6672,67 +6667,35 @@ function clampQueueIndex(tv) {
6672
6667
  const max = tv.view.queued.length - 1;
6673
6668
  return tv.queuedIndex <= max ? tv : { ...tv, queuedIndex: max };
6674
6669
  }
6675
- function applyLive(tv, attempt, events) {
6676
- if (attempt <= 0) return tv;
6677
- let next = tv;
6678
- if (attempt > next.attempt) {
6679
- if (!next.followLatest) {
6680
- return next.attempts.includes(attempt) ? next : { ...next, attempts: [...next.attempts, attempt] };
6681
- }
6682
- next = {
6683
- ...next,
6684
- view: import_core40.EMPTY_TASK_LOG,
6685
- attempt,
6686
- attempts: next.attempts.includes(attempt) ? next.attempts : [...next.attempts, attempt]
6687
- };
6688
- }
6689
- if (attempt < next.attempt) return next;
6690
- return clampQueueIndex({ ...next, view: events.reduce(import_core40.reduceTaskLog, next.view) });
6691
- }
6692
6670
  function taskLogAbandoned(state) {
6693
6671
  const tv = state.taskView;
6694
6672
  if (!tv || tv.loaded) return state;
6695
- let next = { ...tv, pending: [], loaded: true };
6696
- for (const batch of tv.pending) next = applyLive(next, batch.attempt, batch.events);
6697
- return { ...state, taskView: clampQueueIndex(next) };
6673
+ return { ...state, taskView: clampQueueIndex({ ...tv, ...(0, import_core40.followTaskLog)(tv, { type: "loaded", attempt: tv.attempt, events: [], abandoned: true }) }) };
6698
6674
  }
6699
6675
  function taskLogArrived(state, action) {
6700
6676
  const tv = state.taskView;
6701
6677
  if (!tv || tv.taskId !== action.taskId) return state;
6702
- if (!tv.loaded) {
6703
- return { ...state, taskView: { ...tv, pending: [...tv.pending, { attempt: action.attempt, events: action.events }] } };
6704
- }
6705
- return { ...state, taskView: applyLive(tv, action.attempt, action.events) };
6678
+ const next = (0, import_core40.followTaskLog)(tv, { ...action, type: "live" });
6679
+ return { ...state, taskView: next === tv ? tv : clampQueueIndex({ ...tv, ...next }) };
6706
6680
  }
6707
6681
  function announceApprovals(state, action) {
6708
6682
  if (state.taskView?.taskId === action.taskId) return state;
6709
6683
  const task = findTask(state.tasks, action.taskId);
6710
- const way = task ? `tab to the plan, select task ${task.order} and press enter (or /open ${task.order})` : "tab to the plan, select it and press enter";
6711
- const spoken = action.events.reduce((s, event) => event.type === "approval_requested" && !event.decided ? say(s, "system", `Task ${task?.order ?? "?"} waits for approval: ${(0, import_core40.runnerToolSubject)(event.tool, event.args)}: ${way}, then ctrl-y allows or ctrl-g denies`) : s, state);
6684
+ const way = task ? `tab to the plan, select task ${refFor(state, task)} and press enter (or /open ${refFor(state, task)})` : "tab to the plan, select it and press enter";
6685
+ const spoken = action.events.reduce((s, event) => event.type === "approval_requested" && !event.decided ? say(s, "system", `Task ${task ? refFor(state, task) : "?"} waits for approval: ${(0, import_core40.runnerToolSubject)(event.tool, event.args)}: ${way}, then ctrl-y allows or ctrl-g denies`) : s, state);
6712
6686
  return spoken !== state ? { ...spoken, scroll: state.scroll } : spoken;
6713
6687
  }
6714
6688
  function announceCheckpoint(state, action) {
6715
6689
  const task = findTask(state.tasks, action.taskId) ?? state.tasks.find((t) => t.title === action.title);
6716
- const label = task ? `Task ${task.order}` : action.title;
6717
- const where = task ? `tab to the plan, select task ${task.order} and press enter (or /open ${task.order}) to answer` : "tab to the plan, select it and press enter to answer";
6690
+ const label = task ? `Task ${refFor(state, task)}` : action.title;
6691
+ const where = task ? `tab to the plan, select task ${refFor(state, task)} and press enter (or /open ${refFor(state, task)}) to answer` : "tab to the plan, select it and press enter to answer";
6718
6692
  const spoken = say(state, "system", `${label} asks: ${(0, import_core40.truncateCheckpointSummary)(action.summary)}: ${where}`);
6719
6693
  return { ...spoken, scroll: state.scroll };
6720
6694
  }
6721
6695
  function taskLogLoaded(state, action) {
6722
6696
  const tv = state.taskView;
6723
6697
  if (!tv || tv.taskId !== action.taskId) return state;
6724
- const loaded = action.attempt >= 1 ? {
6725
- ...tv,
6726
- view: (0, import_core40.replayTaskLog)(action.events),
6727
- attempts: action.attempts ?? tv.attempts,
6728
- attempt: action.attempt
6729
- } : { ...tv, attempts: action.attempts ?? tv.attempts };
6730
- let next = { ...loaded, pending: [], loaded: true };
6731
- for (const batch of tv.pending) {
6732
- if (batch.attempt === action.attempt) continue;
6733
- next = applyLive(next, batch.attempt, batch.events);
6734
- }
6735
- return { ...state, taskView: clampQueueIndex(next) };
6698
+ return { ...state, taskView: clampQueueIndex({ ...tv, ...(0, import_core40.followTaskLog)(tv, { ...action, type: "loaded" }) }) };
6736
6699
  }
6737
6700
  function answerApproval(state, tv, sessionId, decision) {
6738
6701
  const approval = waitingApproval(tv);
@@ -6776,7 +6739,7 @@ function handleTaskViewKey(state, key) {
6776
6739
  const tv = state.taskView;
6777
6740
  if (!tv || !state.sessionId) return null;
6778
6741
  if (key.name === "alt-left" || key.name === "alt-right") {
6779
- const index = tv.attempts.indexOf(tv.attempt);
6742
+ const index = tv.attempts.indexOf(tv.loadingAttempt ?? tv.attempt);
6780
6743
  if (index < 0) return null;
6781
6744
  const target = key.name === "alt-left" ? index - 1 : index + 1;
6782
6745
  const attempt = tv.attempts[target];
@@ -6784,7 +6747,7 @@ function handleTaskViewKey(state, key) {
6784
6747
  return step(say(state, "system", target < 0 ? "This is the first attempt." : "This is the latest attempt."));
6785
6748
  }
6786
6749
  return step(
6787
- { ...state, taskView: { ...tv, followLatest: false } },
6750
+ { ...state, taskView: { ...tv, ...(0, import_core40.followTaskLog)(tv, { type: "picked", attempt }) } },
6788
6751
  [{ type: "loadTaskAttempt", sessionId: state.sessionId, taskId: tv.taskId, attempt }]
6789
6752
  );
6790
6753
  }
@@ -6816,7 +6779,7 @@ function handleTaskViewKey(state, key) {
6816
6779
 
6817
6780
  // src/tui/reducers/taskEdits.ts
6818
6781
  var import_core41 = require("@ordewell/core");
6819
- var import_plan_utils3 = require("@ordewell/core/plan-utils");
6782
+ var import_plan_utils5 = require("@ordewell/core/plan-utils");
6820
6783
  function openTaskRunnerPicker(state, task) {
6821
6784
  if (task.type !== "ai") return fail3(state, "Manual tasks do not run on an executor, so they have no runner.");
6822
6785
  const action = { kind: "set-task-runner", taskId: task.id };
@@ -6838,7 +6801,7 @@ function confirmRemoveTask(state, task) {
6838
6801
  overlay: {
6839
6802
  kind: "confirm",
6840
6803
  title: `Remove ${titledRefFor(state, task)}?`,
6841
- message: (0, import_plan_utils3.dependentsNotice)((0, import_plan_utils3.dependentsOf)(state.tasks, task.id)) ?? "This cannot be undone.",
6804
+ message: (0, import_plan_utils5.dependentsNotice)((0, import_plan_utils5.dependentsOf)(state.tasks, task.id), state.tasks) ?? "This cannot be undone.",
6842
6805
  action: { kind: "remove-task", taskId: task.id }
6843
6806
  }
6844
6807
  });
@@ -6862,7 +6825,7 @@ function confirmForceStartPastGate(state, task) {
6862
6825
  overlay: {
6863
6826
  kind: "confirm",
6864
6827
  title: `Force start ${titledRefFor(state, task)}?`,
6865
- message: (0, import_plan_utils3.pastGateConfirmation)((0, import_plan_utils3.titledRefs)(task.mergeGate ?? [], state.tasks), "It"),
6828
+ message: (0, import_plan_utils5.pastGateConfirmation)((0, import_plan_utils5.titledRefs)(task.mergeGate ?? [], state.tasks), "It"),
6866
6829
  action: { kind: "force-start-gated", taskId: task.id }
6867
6830
  }
6868
6831
  });
@@ -6870,16 +6833,16 @@ function confirmForceStartPastGate(state, task) {
6870
6833
  function openTaskDepsPicker(state, task) {
6871
6834
  const action = { kind: "set-task-deps", taskId: task.id };
6872
6835
  const items = pickerItemsFor(state, action);
6873
- if (items.length === 0) return fail3(state, `Nothing runs before ${refFor(state, task)}, so it has no possible dependencies.`);
6836
+ if (items.length === 0) return fail3(state, `No earlier siblings to offer as dependencies for ${refFor(state, task)}.`);
6874
6837
  return step({
6875
6838
  ...state,
6876
6839
  overlay: {
6877
6840
  kind: "picker",
6878
6841
  picker: picker(`Depends on \xB7 ${titledRefFor(state, task)}`, items, action, {
6879
- hint: "Only tasks earlier in the plan can be dependencies.",
6842
+ hint: "Offers earlier siblings; existing dependencies are kept.",
6880
6843
  multi: true,
6881
6844
  filterable: true,
6882
- chosen: task.dependencies.filter((id) => items.some((i) => i.id === id))
6845
+ chosen: task.dependencies
6883
6846
  })
6884
6847
  }
6885
6848
  });
@@ -7097,14 +7060,7 @@ function taskCommand(state, token, run) {
7097
7060
  });
7098
7061
  }
7099
7062
  function resolveTaskId2(state, token) {
7100
- const byId = findTask(state.tasks, token);
7101
- if (byId) return byId.id;
7102
- const order = Number(token);
7103
- if (Number.isInteger(order)) {
7104
- const byOrder = state.tasks.find((t) => t.order === order);
7105
- if (byOrder) return byOrder.id;
7106
- }
7107
- return null;
7063
+ return (0, import_plan_utils5.resolveUserTaskRef)(state.tasks, token)?.id ?? null;
7108
7064
  }
7109
7065
 
7110
7066
  // src/tui/reducers/planPane.ts
@@ -7349,28 +7305,6 @@ function foldRun(state, input) {
7349
7305
  const run = (0, import_core44.reduceRun)(state.run, input);
7350
7306
  return run === state.run ? state : withRun(state, run);
7351
7307
  }
7352
- function mapRunTasks(tasks, map) {
7353
- return tasks.map((task) => ({ ...map(task), subtasks: mapRunTasks(task.subtasks, map) }));
7354
- }
7355
- function applyIsolationRecord(state, plan) {
7356
- const record = isolationOfPlan(plan);
7357
- if (!record) return state;
7358
- const { run } = state;
7359
- return withRun(state, {
7360
- ...run,
7361
- tasks: mapRunTasks(run.tasks, (task) => ({ ...task, isolation: record.tasks[task.id] ?? { state: "none" } })),
7362
- handoff: withDeferral(record.handoff, run.handoff)
7363
- });
7364
- }
7365
- function clearRunIsolation(run) {
7366
- return {
7367
- ...run,
7368
- handoff: null,
7369
- runMerge: null,
7370
- mergeResult: null,
7371
- tasks: mapRunTasks(run.tasks, ({ isolation: _dropped, ...task }) => task)
7372
- };
7373
- }
7374
7308
  function applySettings(state, settings) {
7375
7309
  return {
7376
7310
  ...state,
@@ -8241,6 +8175,24 @@ function reduceAction(state, action) {
8241
8175
  // tasks — may carry over into this.
8242
8176
  case "sessionStarted":
8243
8177
  return step(withRun({ ...state, sessionId: action.sessionId, goal: action.goal, taskView: null }, import_core49.EMPTY_RUN));
8178
+ // A loaded session is a different one: the turn the old one had out, the
8179
+ // prompts held behind it and a stop armed against it are not this one's.
8180
+ // (`sessionStarted` cannot reset them: `startConversation` dispatches it
8181
+ // as its own turn begins.)
8182
+ case "sessionLoaded":
8183
+ return step(withRun({
8184
+ ...state,
8185
+ sessionId: action.sessionId,
8186
+ goal: action.goal,
8187
+ taskView: null,
8188
+ planner: "idle",
8189
+ turnGate: import_core49.NO_TURN,
8190
+ pendingApprovals: [],
8191
+ queuedPrompts: import_core49.EMPTY_HOLD,
8192
+ stopArmed: false,
8193
+ forceSendPending: null,
8194
+ overlay: state.overlay?.kind === "approval" ? null : state.overlay
8195
+ }, import_core49.EMPTY_RUN));
8244
8196
  // The daemon registers a session only once planning succeeds; when it does
8245
8197
  // not, holding on to the id would send every next message to a session the
8246
8198
  // server never had.
@@ -8250,6 +8202,7 @@ function reduceAction(state, action) {
8250
8202
  sessionId: null,
8251
8203
  goal: "",
8252
8204
  taskView: null,
8205
+ turnGate: import_core49.NO_TURN,
8253
8206
  pendingApprovals: [],
8254
8207
  queuedPrompts: import_core49.EMPTY_HOLD,
8255
8208
  stopArmed: false,
@@ -8263,6 +8216,9 @@ function reduceAction(state, action) {
8263
8216
  case "chatRestored":
8264
8217
  if (stale(state, action.sessionId)) return step(state);
8265
8218
  return step({ ...state, conversation: (0, import_core49.fromTranscript)(action.history, action.researchLog, action.plannerUsage, action.pendingTurns), scroll: 0 });
8219
+ case "planRefreshed":
8220
+ if (stale(state, action.sessionId)) return step(state);
8221
+ return step(showPlan(state, action.plan));
8266
8222
  case "planUpdated": {
8267
8223
  if (stale(state, action.sessionId)) return step(state);
8268
8224
  const settled = { ...showPlan(state, action.plan), planner: "idle" };
@@ -8349,10 +8305,16 @@ function reduceAction(state, action) {
8349
8305
  if (stale(state, action.sessionId)) return step(state);
8350
8306
  return step(showDiff(state, action.diff));
8351
8307
  }
8352
- case "runCleared": {
8308
+ // What Merge all did, folded the way the Session says it: a merge that
8309
+ // leaves the run going keeps the marks (ADR-0020), and only `run_closed`
8310
+ // takes them (below).
8311
+ case "isolationMerged": {
8353
8312
  if (stale(state, action.sessionId)) return step(state);
8354
- const overlay = state.overlay?.kind === "handoff" ? null : state.overlay;
8355
- return step({ ...withRun(state, clearRunIsolation(state.run)), overlay });
8313
+ return step(withoutHandoffOverlay(foldRun(state, { type: "isolation_merge", result: action.result })));
8314
+ }
8315
+ case "runClosed": {
8316
+ if (stale(state, action.sessionId)) return step(state);
8317
+ return step(withoutHandoffOverlay(foldRun(state, { type: "run_closed" })));
8356
8318
  }
8357
8319
  case "executionComplete": {
8358
8320
  if (stale(state, action.sessionId)) return step(state);
@@ -8412,9 +8374,9 @@ function reduceAction(state, action) {
8412
8374
  if (stale(state, action.sessionId)) return step(state);
8413
8375
  if (action.pick !== void 0) return pickRewindTarget({ ...state, rewindTargets: action.targets }, action.pick);
8414
8376
  return step(refillPicker({ ...state, rewindTargets: action.targets }, ["rewind"]));
8415
- // A fork holds no run, whatever the session it came from was doing — the
8416
- // original keeps its run, and its events are stale from here on. Its plan
8417
- // arrives next, onto an empty run view.
8377
+ // A fork holds no run and no turn, whatever the session it came from was
8378
+ // doing — the original keeps both, and its events are stale from here on.
8379
+ // Its plan arrives next, onto an empty run view.
8418
8380
  case "sessionForked":
8419
8381
  return step(withRun({
8420
8382
  ...state,
@@ -8423,6 +8385,7 @@ function reduceAction(state, action) {
8423
8385
  taskView: null,
8424
8386
  status: "idle",
8425
8387
  planner: "idle",
8388
+ turnGate: import_core49.NO_TURN,
8426
8389
  planApproved: false,
8427
8390
  pendingApprovals: [],
8428
8391
  queuedPrompts: import_core49.EMPTY_HOLD,
@@ -8488,8 +8451,11 @@ function reduceAction(state, action) {
8488
8451
  return step(disarmStop(state));
8489
8452
  }
8490
8453
  }
8454
+ function withoutHandoffOverlay(state) {
8455
+ return state.handoff || state.overlay?.kind !== "handoff" ? state : { ...state, overlay: null };
8456
+ }
8491
8457
  function showPlan(state, plan) {
8492
- const folded = applyIsolationRecord(foldRun(state, planSnapshot(plan)), plan);
8458
+ const folded = foldRun(state, planSnapshot(plan));
8493
8459
  const { tasks } = folded;
8494
8460
  const expandedTaskId = state.expandedTaskId && findTask(tasks, state.expandedTaskId) ? state.expandedTaskId : null;
8495
8461
  const next = {
@@ -8847,6 +8813,7 @@ function createInbound(dispatch, sessionId) {
8847
8813
  const delivered = new Delivered(256);
8848
8814
  const copies = new Copies(64);
8849
8815
  const shown = new WindowSet(64);
8816
+ const shownTexts = new WindowSet(64);
8850
8817
  const pending = new WindowSet(16);
8851
8818
  const DELTA_DEBOUNCE_MS = 75;
8852
8819
  let held = null;
@@ -8875,7 +8842,8 @@ function createInbound(dispatch, sessionId) {
8875
8842
  timer = setTimeout(flush, DELTA_DEBOUNCE_MS);
8876
8843
  };
8877
8844
  const showReply = (message) => {
8878
- shown.add(message.content);
8845
+ shown.add(replyKey(message.content, message.timestamp));
8846
+ shownTexts.add(message.content);
8879
8847
  dispatch({ type: "sessionMessage", message, sessionId });
8880
8848
  };
8881
8849
  const fresh = (id, event) => {
@@ -8947,14 +8915,15 @@ function createInbound(dispatch, sessionId) {
8947
8915
  const id = `e${++subscription}`;
8948
8916
  return (event) => receive("execution", id, event);
8949
8917
  },
8950
- backfill: (content, timestamp) => {
8951
- if (shown.has(content)) return;
8952
- const message = { type: "planner_message", content, timestamp: timestamp ?? (/* @__PURE__ */ new Date()).toISOString() };
8918
+ backfill: (content, timestamp, runs) => {
8919
+ if (timestamp ? shown.has(replyKey(content, timestamp)) : shownTexts.has(content)) return;
8920
+ const message = { type: "planner_message", content, timestamp: timestamp ?? (/* @__PURE__ */ new Date()).toISOString(), ...runs ? { runs } : {} };
8953
8921
  showReply(message);
8954
8922
  if (timestamp) pending.add(replyKey(content, timestamp));
8955
8923
  },
8956
8924
  redrawn: (content, timestamp) => {
8957
- shown.add(content);
8925
+ shown.add(replyKey(content, timestamp));
8926
+ shownTexts.add(content);
8958
8927
  pending.add(replyKey(content, timestamp));
8959
8928
  },
8960
8929
  flush,
@@ -9026,7 +8995,7 @@ function dispatchLifecycle(dispatch, event, sessionId) {
9026
8995
  // its only reader, and the reducer drops it unless that view is open on
9027
8996
  // this task. The run's watcher itself does nothing with it.
9028
8997
  case "task_log":
9029
- dispatch({ type: "taskLog", taskId: String(event.taskId), attempt: event.attempt, events: event.events, sessionId });
8998
+ dispatch({ type: "taskLog", taskId: String(event.taskId), attempt: event.attempt, events: event.events, position: stampOf(event) ?? void 0, sessionId });
9030
8999
  return;
9031
9000
  case "task_updated":
9032
9001
  dispatch({ type: "taskUpdated", taskId: String(event.taskId), changes: event.changes, sessionId });
@@ -9036,10 +9005,13 @@ function dispatchLifecycle(dispatch, event, sessionId) {
9036
9005
  // does with it.
9037
9006
  case "task_output":
9038
9007
  return;
9039
- // Its asker already has the words, from the merge request itself; a
9040
- // viewer that did not ask still has to drop a run that is gone.
9008
+ // Its asker already has the words, from the merge request itself. Whether
9009
+ // the merge ended the run is the Session's to say, in `run_closed`.
9041
9010
  case "isolation_merge":
9042
- if (event.result.outcome === "merged") dispatch({ type: "runCleared", sessionId });
9011
+ dispatch({ type: "isolationMerged", result: event.result, sessionId });
9012
+ return;
9013
+ case "run_closed":
9014
+ dispatch({ type: "runClosed", sessionId });
9043
9015
  return;
9044
9016
  default: {
9045
9017
  const unhandled = event;
@@ -9136,9 +9108,10 @@ async function perform(effect, deps) {
9136
9108
  case "execute":
9137
9109
  await withExecutionStream(deps, effect.sessionId, () => api.executePlan(effect.sessionId));
9138
9110
  return;
9111
+ // The seam keeps its own line of a Stop in the timeline and the stream
9112
+ // announces the run's end, so this adds none (ADR-0029).
9139
9113
  case "stopExecution":
9140
9114
  await api.stopExecution(effect.sessionId);
9141
- dispatch({ type: "notice", message: "Execution stopped." });
9142
9115
  return;
9143
9116
  case "cancelPlanning": {
9144
9117
  const { cancelled } = await api.cancelPlanning(effect.sessionId);
@@ -9156,8 +9129,7 @@ async function perform(effect, deps) {
9156
9129
  case "taskAction": {
9157
9130
  const { sessionId, taskId, action } = effect;
9158
9131
  if (action === "cancel-resume") {
9159
- const { cancelled } = await api.cancelResume(sessionId, taskId);
9160
- dispatch({ type: "notice", message: cancelled ? "Resume cancelled: retry the task once the limit resets." : "That task had no resume scheduled." });
9132
+ await api.cancelResume(sessionId, taskId);
9161
9133
  await refreshPlan(deps, sessionId);
9162
9134
  return;
9163
9135
  }
@@ -9215,19 +9187,19 @@ async function perform(effect, deps) {
9215
9187
  // The saved log is read off disk (ADR-0018, P1) so the view opens with the
9216
9188
  // task's history; the live `task_log` stream then catches it up in place.
9217
9189
  case "openTaskLog": {
9218
- const attempts = await api.getTaskLogAttempts(effect.sessionId, effect.taskId, workspace);
9190
+ const { attempts, position } = await api.getTaskLogAttempts(effect.sessionId, effect.taskId, workspace);
9219
9191
  if (attempts.length === 0) {
9220
- dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempts: [], attempt: 0, events: [], sessionId: effect.sessionId });
9192
+ dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempts: [], attempt: 0, events: [], position, sessionId: effect.sessionId });
9221
9193
  return;
9222
9194
  }
9223
9195
  const attempt = attempts[attempts.length - 1];
9224
- const events = await api.getTaskLog(effect.sessionId, effect.taskId, attempt, workspace);
9225
- dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempts, attempt, events, sessionId: effect.sessionId });
9196
+ const log = await api.getTaskLog(effect.sessionId, effect.taskId, attempt, workspace);
9197
+ dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempts, ...log, sessionId: effect.sessionId });
9226
9198
  return;
9227
9199
  }
9228
9200
  case "loadTaskAttempt": {
9229
- const events = await api.getTaskLog(effect.sessionId, effect.taskId, effect.attempt, workspace);
9230
- dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempt: effect.attempt, events, sessionId: effect.sessionId });
9201
+ const log = await api.getTaskLog(effect.sessionId, effect.taskId, effect.attempt, workspace);
9202
+ dispatch({ type: "taskLogLoaded", taskId: effect.taskId, ...log, sessionId: effect.sessionId });
9231
9203
  return;
9232
9204
  }
9233
9205
  // The daemon queues the message (or delivers it to a waiting task) and
@@ -9247,24 +9219,20 @@ async function perform(effect, deps) {
9247
9219
  await refreshPlan(deps, sessionId);
9248
9220
  return;
9249
9221
  }
9250
- case "removeTaskMessage": {
9251
- const { removed } = await api.removeQueuedTaskMessage(effect.sessionId, effect.taskId, effect.messageId);
9252
- if (!removed) dispatch({ type: "notice", message: "That message was already delivered to the task." });
9222
+ // One already delivered is refused in the daemon's words.
9223
+ case "removeTaskMessage":
9224
+ await api.removeQueuedTaskMessage(effect.sessionId, effect.taskId, effect.messageId);
9253
9225
  return;
9254
- }
9255
9226
  // The task log shows the forced message at the head of the queue, then
9256
9227
  // the interrupt, then the turn it opens; nothing to dispatch here.
9257
9228
  case "forceSendTaskMessage":
9258
9229
  await api.forceSendTaskMessage(effect.sessionId, effect.taskId, effect.text);
9259
9230
  return;
9260
- case "forceSendQueuedTaskMessage": {
9261
- const { sent } = await api.forceSendQueuedTaskMessage(effect.sessionId, effect.taskId, effect.messageId);
9262
- if (!sent) dispatch({ type: "notice", message: "That message has already reached the runner." });
9231
+ case "forceSendQueuedTaskMessage":
9232
+ await api.forceSendQueuedTaskMessage(effect.sessionId, effect.taskId, effect.messageId);
9263
9233
  return;
9264
- }
9265
9234
  case "interruptTask":
9266
9235
  await api.interruptTask(effect.sessionId, effect.taskId);
9267
- dispatch({ type: "notice", message: "Interrupting the task\u2026" });
9268
9236
  return;
9269
9237
  case "setModel": {
9270
9238
  const settled = await api.updateSettings({ orchestratorModel: effect.modelId });
@@ -9404,11 +9372,12 @@ async function perform(effect, deps) {
9404
9372
  else unfollow(deps, effect.sessionId);
9405
9373
  throw err;
9406
9374
  }
9407
- const { plan, goal } = adopted2;
9408
- dispatch({ type: "sessionStarted", sessionId: effect.sessionId, goal });
9375
+ const { plan, goal, run } = adopted2;
9376
+ dispatch({ type: "sessionLoaded", sessionId: effect.sessionId, goal });
9409
9377
  dispatch(restoredChat(plan, effect.sessionId));
9410
9378
  inbound.releaseSnapshot(true);
9411
9379
  dispatch({ type: "planUpdated", plan, sessionId: effect.sessionId });
9380
+ if (run) dispatch({ type: "runSnapshot", view: run, sessionId: effect.sessionId });
9412
9381
  dispatch({ type: "notice", message: `Loaded "${goal || effect.sessionId}".` });
9413
9382
  return;
9414
9383
  }
@@ -9469,16 +9438,15 @@ async function perform(effect, deps) {
9469
9438
  // A conflict or a refusal is an answer, not a fault: either way the user's
9470
9439
  // tree is exactly as it was, and the words say what to do next. A full
9471
9440
  // merge of a settled run leaves nothing to hand over — the daemon has
9472
- // cleared the run up; one mid-run leaves the run going (ADR-0020).
9441
+ // cleared the run up and says so on the stream (`run_closed`); one
9442
+ // mid-run leaves the run going (ADR-0020).
9473
9443
  case "isolationMerge": {
9474
9444
  const { ok, message } = mergeOutcome(await api.mergeRun(effect.sessionId), effect.branch, effect.group === true, effect.repaired ?? []);
9475
- if (ok && !effect.midRun) dispatch({ type: "runCleared", sessionId: effect.sessionId });
9476
9445
  dispatch({ type: ok ? "notice" : "failed", message });
9477
9446
  return;
9478
9447
  }
9479
9448
  case "isolationDiscard":
9480
9449
  await api.discardRun(effect.sessionId);
9481
- dispatch({ type: "runCleared", sessionId: effect.sessionId });
9482
9450
  dispatch({ type: "notice", message: `Discarded the run and ${effect.branch}.` });
9483
9451
  return;
9484
9452
  case "isolationCleanup":
@@ -9626,8 +9594,7 @@ async function converse(deps, sessionId, call) {
9626
9594
  await Promise.all([stream.ready, follow(deps, sessionId)]);
9627
9595
  const plan = await call();
9628
9596
  inbound.flush();
9629
- const reply = lastAssistantMessage(plan);
9630
- if (reply) inbound.backfill(reply.content, reply.timestamp);
9597
+ for (const reply of turnReplies(plan)) inbound.backfill(reply.content, reply.timestamp, reply.runs);
9631
9598
  deps.dispatch({ type: "planUpdated", plan, sessionId });
9632
9599
  } finally {
9633
9600
  inbound.flush();
@@ -9645,17 +9612,18 @@ function restoredChat(plan, sessionId) {
9645
9612
  sessionId
9646
9613
  };
9647
9614
  }
9648
- function lastAssistantMessage(plan) {
9615
+ function turnReplies(plan) {
9649
9616
  const history = plan?.conversationHistory ?? [];
9617
+ const replies = [];
9650
9618
  for (let i = history.length - 1; i >= 0; i--) {
9651
9619
  const entry = history[i];
9652
- if ((0, import_core50.isUserMessage)(entry)) return null;
9620
+ if ((0, import_core50.isUserMessage)(entry)) break;
9653
9621
  if (entry.role === "assistant" && entry.kind === void 0) {
9654
- const { content, timestamp } = entry;
9655
- return { content, ...timestamp ? { timestamp } : {} };
9622
+ const { content, timestamp, runs } = entry;
9623
+ replies.unshift({ content, ...timestamp ? { timestamp } : {}, ...runs ? { runs } : {} });
9656
9624
  }
9657
9625
  }
9658
- return null;
9626
+ return replies;
9659
9627
  }
9660
9628
  function compactionSummary(plan) {
9661
9629
  const history = plan?.conversationHistory ?? [];
@@ -9663,8 +9631,9 @@ function compactionSummary(plan) {
9663
9631
  return entry ? { content: entry.content, ...entry.timestamp ? { timestamp: entry.timestamp } : {} } : null;
9664
9632
  }
9665
9633
  async function refreshPlan(deps, sessionId) {
9666
- const { plan } = await deps.api.getSession(sessionId, deps.workspace);
9667
- deps.dispatch({ type: "planUpdated", plan, sessionId });
9634
+ const { plan, run } = await deps.api.getSession(sessionId, deps.workspace);
9635
+ deps.dispatch({ type: "planRefreshed", plan, sessionId });
9636
+ if (run) deps.dispatch({ type: "runSnapshot", view: run, sessionId });
9668
9637
  }
9669
9638
  async function loadModels(deps) {
9670
9639
  deps.dispatch({ type: "modelsLoaded", ...normalizeCatalog(await deps.api.getModels()) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ordewell/cli",
3
- "version": "0.10.5",
3
+ "version": "0.10.6",
4
4
  "description": "Ordewell CLI and TUI — plan, run and verify coding-agent tasks from the terminal.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Alessandro Costanzo Ciano",
@@ -51,8 +51,8 @@
51
51
  "typecheck": "tsc --noEmit"
52
52
  },
53
53
  "dependencies": {
54
- "@ordewell/core": "^0.10.5",
55
- "@ordewell/web": "^0.10.5",
54
+ "@ordewell/core": "^0.10.6",
55
+ "@ordewell/web": "^0.10.6",
56
56
  "string-width": "^7.2.0",
57
57
  "ws": "^8.18.0"
58
58
  },