@ordewell/cli 0.5.6 → 0.6.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 (3) hide show
  1. package/README.md +4 -2
  2. package/dist/main.js +333 -155
  3. package/package.json +3 -3
package/dist/main.js CHANGED
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/main.ts
27
- var import_core33 = require("@ordewell/core");
27
+ var import_core34 = require("@ordewell/core");
28
28
 
29
29
  // src/utils/env.ts
30
30
  var import_fs = require("fs");
@@ -109,7 +109,7 @@ Planning:
109
109
  ordewell fork Continue in a copy of the conversation and its tasks
110
110
  ordewell compact Condense the conversation into a summary (tasks are kept)
111
111
  ordewell rewind [<n>] Fork the conversation from before message <n> (no <n>: list them)
112
- ordewell handoff [action] Review, merge, discard or clean up a finished isolated run
112
+ ordewell handoff [action] Review, merge, discard or clean up an isolated run
113
113
 
114
114
  Tasks (<id> is an order number or a task ID):
115
115
  ordewell add-task --title "..." Add a task to the current plan
@@ -117,7 +117,7 @@ Tasks (<id> is an order number or a task ID):
117
117
  ordewell complete <id> Mark a task complete (alias: mark-complete)
118
118
  ordewell uncomplete <id> Mark a completed task not done
119
119
  ordewell skip <id> Skip a task (marks it complete so dependents can run)
120
- ordewell force-start <id> Start a task now, ignoring dependencies
120
+ ordewell force-start <id> Start a task now, ignoring dependencies (asks past a merge gate)
121
121
  ordewell run-task <id> Run only one task
122
122
  ordewell retry <id> Re-run a failed task
123
123
  ordewell continue <id> <msg> Continue a finished task in its saved runner session
@@ -130,6 +130,7 @@ Per-task assignment (omit the value to list the options):
130
130
  ordewell task-effort <id> [level] Set a task's thinking effort ("default" to clear)
131
131
  ordewell task-mode <id> [mode] Set a task's runner mode
132
132
  ordewell task-deps <id> [a,b|none] Set which earlier tasks a task waits for
133
+ ordewell task-ops <id> [on|off] Make a task an ops task (runs in your checkout once its dependencies are merged) or a change task
133
134
 
134
135
  Planner, models and runners:
135
136
  ordewell planner [<provider>] Choose who plans \u2014 an API provider or a coding agent (no API key)
@@ -138,8 +139,8 @@ Planner, models and runners:
138
139
  ordewell key [set <p> <key>] Show which providers have a key, or store one
139
140
  ordewell runners [<id> on|off] Enable or disable runners (claude-code, opencode, codex)
140
141
  ordewell allowlist set|clear|show Limit which models a runner may use
141
- ordewell auto [on|off] Autonomous permission mode for new sessions
142
- ordewell transport [terminal|structured] Experimental: run Claude Code tasks over its protocol, from the next run
142
+ ordewell auto [full|auto] Autonomy level for new sessions: Full auto or Auto
143
+ ordewell transport [terminal|structured] Structured (default) or terminal task transport, from the next run
143
144
  ordewell parallel [<n>] How many AI tasks run at once (default 3; applies to a live run)
144
145
  ordewell refresh Re-discover runners and model catalogs
145
146
  ordewell models List every provider's catalog (works without a server)
@@ -212,7 +213,7 @@ Environment:
212
213
  ORDEWELL_PLANNER_EFFORT Thinking effort for a coding-agent planner
213
214
  ORCHESTRATOR_MODEL Default: deepseek/deepseek-v4-flash
214
215
  OPENROUTER_BASE_URL Default: https://openrouter.ai/api/v1
215
- ORDEWELL_AUTONOMOUS_MODE Approval posture for new sessions (see "ordewell auto")
216
+ ORDEWELL_AUTONOMOUS_MODE true: Full auto, false: Auto, for new sessions (see "ordewell auto")
216
217
  ORDEWELL_PORT Daemon port CLI commands target (default: 3742)
217
218
  ORDEWELL_DIRENV false: agents skip the project's direnv .envrc
218
219
  ORDEWELL_RESEARCH_ENABLED true (default) or false
@@ -713,6 +714,17 @@ var ApiClient = class _ApiClient {
713
714
  }
714
715
  return { plan: res.data.plan, summary: res.data.summary, keptMessages: res.data.keptMessages };
715
716
  }
717
+ /** The dependencies a task waits on at its merge gate (ADR-0020); empty when nothing gates it. */
718
+ async getMergeGate(sessionId, taskId) {
719
+ const res = await this.httpRequest(
720
+ "GET",
721
+ `/api/plans/${sessionId}/tasks/${taskId}/merge-gate`
722
+ );
723
+ if (res.status !== 200) {
724
+ throw new Error(res.data?.error || "Could not read the merge gate");
725
+ }
726
+ return res.data.mergeGate ?? [];
727
+ }
716
728
  async reviewRunDiff(sessionId) {
717
729
  const res = await this.httpRequest("GET", `/api/plans/${sessionId}/isolation/diff`);
718
730
  if (res.status !== 200) {
@@ -1252,7 +1264,7 @@ Plan: ${tasks.length} task${tasks.length === 1 ? "" : "s"} (${aiCount} AI, ${man
1252
1264
  console.log(`Session: ${sessionId}
1253
1265
  `);
1254
1266
  for (const t of tasks) {
1255
- const typeIcon = t.type === "user" ? "[MAN]" : "[ AI]";
1267
+ const typeIcon = t.type === "user" ? "[MAN]" : t.ops ? "[OPS]" : "[ AI]";
1256
1268
  const deps = t.dependencies?.length ? ` \u2190 ${t.dependencies.join(", ")}` : "";
1257
1269
  console.log(` ${(0, import_core7.taskOrderLabel)(t).padStart(2)}. ${typeIcon} ${t.title}${modelSuffix(t)}${deps}`);
1258
1270
  for (const sub of t.subtasks || []) {
@@ -1264,6 +1276,9 @@ Plan: ${tasks.length} task${tasks.length === 1 ? "" : "s"} (${aiCount} AI, ${man
1264
1276
  if (manCount > 0) {
1265
1277
  console.log("\n [MAN] = manual step \u2014 run `ordewell tui` to work through it");
1266
1278
  }
1279
+ if (tasks.some((t) => t.ops)) {
1280
+ console.log(" [OPS] = ops task \u2014 runs in your checkout once the work it depends on is merged (`ordewell handoff merge`)");
1281
+ }
1267
1282
  }
1268
1283
  var PLANNER_ENV = ["AI_PROVIDER", "ORCHESTRATOR_MODEL", "ORDEWELL_PLANNER_EFFORT"];
1269
1284
  async function forwardPlannerEnv(api) {
@@ -1456,7 +1471,7 @@ function isolationOfPlan(plan) {
1456
1471
  // `tasksStatus` update fills it in once the stream catches up.
1457
1472
  ...repairedFiles ? { repairedFiles } : {}
1458
1473
  };
1459
- if (state === "integrated") {
1474
+ if (state === "integrated" && record.inHead !== true) {
1460
1475
  landed.push({ taskId, order: Number(record.order ?? 0), title: String(record.title ?? taskId), changed, ...repairedFiles ? { repairedFiles } : {} });
1461
1476
  }
1462
1477
  }
@@ -1531,6 +1546,9 @@ function iconFor(status) {
1531
1546
  // src/commands/run.ts
1532
1547
  var import_core9 = require("@ordewell/core");
1533
1548
 
1549
+ // src/commands/shared.ts
1550
+ var import_readline2 = require("readline");
1551
+
1534
1552
  // src/catalog.ts
1535
1553
  function normalizeCatalog(result) {
1536
1554
  const runnersByModel = /* @__PURE__ */ new Map();
@@ -1586,6 +1604,16 @@ function fail(...lines) {
1586
1604
  for (const line of lines) console.error(line);
1587
1605
  process.exit(1);
1588
1606
  }
1607
+ async function askYesNo(question) {
1608
+ if (!process.stdin.isTTY) return false;
1609
+ const rl = (0, import_readline2.createInterface)({ input: process.stdin, output: process.stderr });
1610
+ try {
1611
+ const answer = await new Promise((resolve3) => rl.question(`${question} [y/N] `, resolve3));
1612
+ return /^y(es)?$/i.test(answer.trim());
1613
+ } finally {
1614
+ rl.close();
1615
+ }
1616
+ }
1589
1617
  function writeEnv(env) {
1590
1618
  const envFile = findEnvFile();
1591
1619
  for (const [key, value] of Object.entries(env)) {
@@ -1611,7 +1639,8 @@ function toTaskView(task) {
1611
1639
  assignedRunner: task.assignedRunner || void 0,
1612
1640
  taskMode: task.taskMode || void 0,
1613
1641
  assignedModel: task.assignedModel ?? void 0,
1614
- subtasks: [...task.subtasks ?? []].sort((a, b) => a.order - b.order).map(toTaskView)
1642
+ subtasks: [...task.subtasks ?? []].sort((a, b) => a.order - b.order).map(toTaskView),
1643
+ ...task.ops ? { ops: true } : {}
1615
1644
  };
1616
1645
  }
1617
1646
  function taskViews(plan) {
@@ -1658,6 +1687,7 @@ async function followExecution(api, sessionId, start, onBlocked) {
1658
1687
  lastPrinted = output;
1659
1688
  }
1660
1689
  }
1690
+ let pausedAtGate = false;
1661
1691
  let settleReady = () => {
1662
1692
  };
1663
1693
  const streamReady = new Promise((resolve3, reject) => {
@@ -1674,6 +1704,9 @@ async function followExecution(api, sessionId, start, onBlocked) {
1674
1704
  }
1675
1705
  if (event.type === "status_update" && event.tasks) {
1676
1706
  printStatus(event.tasks);
1707
+ const paused = event.gate?.paused === true;
1708
+ if (paused && !pausedAtGate) process.stderr.write("\xB7 Paused for Merge all \u2014 `ordewell handoff merge` merges what has landed, and the waiting tasks start.\n");
1709
+ pausedAtGate = paused;
1677
1710
  }
1678
1711
  if (event.type === "review_needed") {
1679
1712
  console.log("\nPlan needs your sign-off \u2014 run `ordewell approve` to continue.");
@@ -2277,7 +2310,7 @@ async function handleVerify(subArgs, api) {
2277
2310
  }
2278
2311
 
2279
2312
  // src/commands/transport.ts
2280
- var describe = (transport) => `Runner transport (experimental): ${transport === "structured" ? "structured" : "terminal"} \u2014 applies from the next run`;
2313
+ var describe = (transport) => `Runner transport: ${transport === "terminal" ? "terminal" : "structured"} \u2014 applies from the next run`;
2281
2314
  async function handleTransport(subArgs, api) {
2282
2315
  const action = subArgs[0];
2283
2316
  const port = api ? void 0 : await ensureDaemon(resolvePort(subArgs));
@@ -2330,13 +2363,23 @@ async function withResolvedTask(subArgs, usage, injectedApi, run) {
2330
2363
  }
2331
2364
  await run(api, sessionId, taskId, plan);
2332
2365
  }
2366
+ var PASSES_GATE = /* @__PURE__ */ new Set(["run", "force-start"]);
2367
+ async function confirmPastGate(subArgs, api, sessionId, taskId, confirm) {
2368
+ if (hasFlag(subArgs, "--yes")) return;
2369
+ const unmerged = await api.getMergeGate(sessionId, taskId).catch(() => []);
2370
+ if (unmerged.length === 0) return;
2371
+ const question = `This task waits for Merge all: the work of ${unmerged.map((d) => `#${d.order} ${d.title}`).join(", ")} is not merged into your branch yet, so it would act without it. Starting it now is kept on the task. Start it anyway?`;
2372
+ if (!await confirm(question)) fail("Not started \u2014 nothing was changed. Pass --yes to start it without a prompt.");
2373
+ }
2333
2374
  function makeHandler(action, command = action) {
2334
- return async function handle(subArgs, injectedApi) {
2375
+ const yes = PASSES_GATE.has(action) ? " [--yes]" : "";
2376
+ return async function handle(subArgs, injectedApi, confirm = askYesNo) {
2335
2377
  await withResolvedTask(
2336
2378
  subArgs,
2337
- `Usage: ordewell ${command} <task-id-or-order> [--session-id <id>] [--workspace /path]`,
2379
+ `Usage: ordewell ${command} <task-id-or-order> [--session-id <id>] [--workspace /path]${yes}`,
2338
2380
  injectedApi,
2339
2381
  async (api, sessionId, taskId) => {
2382
+ if (PASSES_GATE.has(action)) await confirmPastGate(subArgs, api, sessionId, taskId, confirm);
2340
2383
  try {
2341
2384
  await api.taskControl(sessionId, taskId, action);
2342
2385
  console.log(PAST_TENSE[action]);
@@ -2613,22 +2656,19 @@ async function handleRewind(subArgs, injectedApi) {
2613
2656
  console.log(fork.rewoundMessage);
2614
2657
  }
2615
2658
 
2616
- // src/commands/handoff.ts
2617
- var import_readline2 = require("readline");
2618
-
2619
2659
  // src/tui/transcript.ts
2620
2660
  var import_core15 = require("@ordewell/core");
2621
2661
  function show(state, { view: conversation, gate: turnGate }) {
2622
2662
  const gated = turnGate === state.turnGate ? state : { ...state, turnGate };
2623
2663
  if (conversation === state.conversation) return gated;
2624
- const scroll = conversation.nextId > state.conversation.nextId ? 0 : state.scroll;
2625
- return { ...gated, conversation, scroll };
2664
+ return { ...gated, conversation };
2626
2665
  }
2627
2666
  function fold(state, input) {
2628
2667
  return show(state, { view: (0, import_core15.reduceConversation)(state.conversation, input), gate: state.turnGate });
2629
2668
  }
2630
2669
  function say(state, role, text) {
2631
- return fold(state, { type: "local_entry", role, text });
2670
+ const said = fold(state, { type: "local_entry", role, text });
2671
+ return said.conversation.nextId > state.conversation.nextId ? { ...said, scroll: 0 } : said;
2632
2672
  }
2633
2673
  function hear(state, message) {
2634
2674
  return show(state, (0, import_core15.followTurn)(state.conversation, state.turnGate, message));
@@ -2808,8 +2848,12 @@ var reposOf2 = (handoff) => handoff.repos.map((r) => r.path).join(", ");
2808
2848
  var HANDOFF_USAGE = `/handoff [${HANDOFF_ACTIONS.map((a) => a.id).join("|")}]`;
2809
2849
  var diffRoom = (rows) => Math.max(1, rows - 3);
2810
2850
  var fail2 = (state, content) => ({ state: say(state, "error", content), effects: [] });
2851
+ function currentHandoff(state) {
2852
+ return state.gate?.handoff ?? state.handoff;
2853
+ }
2854
+ var midRun = (state) => state.status === "executing";
2811
2855
  function handoffCommand(state, arg) {
2812
- if (!state.sessionId || !state.handoff) return fail2(state, "No isolated run to hand off \u2014 a plan that ran in worktrees leaves one here.");
2856
+ if (!state.sessionId || !currentHandoff(state)) return fail2(state, "No isolated run to hand off \u2014 a plan that ran in worktrees leaves one here.");
2813
2857
  if (arg === void 0) return { state: { ...state, overlay: { kind: "handoff", index: 0, diff: null } }, effects: [] };
2814
2858
  const action = HANDOFF_ACTIONS.find((a) => a.id === arg.toLowerCase());
2815
2859
  if (!action) return fail2(state, `Usage: ${HANDOFF_USAGE}`);
@@ -2826,7 +2870,8 @@ function handoffArrived(state, handoff) {
2826
2870
  return state.overlay ? told : { ...told, overlay: { kind: "handoff", index: 0, diff: null } };
2827
2871
  }
2828
2872
  function runHandoffAction(state, id) {
2829
- const { sessionId, handoff } = state;
2873
+ const { sessionId } = state;
2874
+ const handoff = currentHandoff(state);
2830
2875
  if (!sessionId || !handoff) return fail2(state, "No isolated run to hand off.");
2831
2876
  const closed = { ...state, overlay: null };
2832
2877
  switch (id) {
@@ -2835,6 +2880,20 @@ function runHandoffAction(state, id) {
2835
2880
  case "cleanup":
2836
2881
  return { state: closed, effects: [{ type: "isolationCleanup", sessionId, branch: handoffBranch(handoff) }] };
2837
2882
  case "merge":
2883
+ if (midRun(state)) {
2884
+ return {
2885
+ state: {
2886
+ ...state,
2887
+ overlay: {
2888
+ kind: "confirm",
2889
+ title: isRepoGroup(handoff) ? "Merge what has landed into your branches?" : "Merge what has landed into your branch?",
2890
+ message: `Merge ${handoffBranch(handoff)} as it stands now into whatever ${isRepoGroup(handoff) ? `each of ${reposWithWork(handoff).join(", ")} has` : "you have"} checked out. Ordewell never does this on its own. The run goes on: tasks waiting for Merge all start once it has merged, and later tasks land on the same branch for the next Merge all. A merge that conflicts is aborted, and your ${isRepoGroup(handoff) ? "trees stay" : "tree stays"} as ${isRepoGroup(handoff) ? "they were" : "it was"}.${repairedNotice(handoff)}`,
2891
+ action: { kind: "merge-run" }
2892
+ }
2893
+ },
2894
+ effects: []
2895
+ };
2896
+ }
2838
2897
  return {
2839
2898
  state: {
2840
2899
  ...state,
@@ -2869,20 +2928,23 @@ function runHandoffAction(state, id) {
2869
2928
  }
2870
2929
  function confirmedHandoff(state, kind) {
2871
2930
  const closed = { ...state, overlay: null };
2872
- if (!state.sessionId || !state.handoff) return { state: closed, effects: [] };
2873
- const { sessionId, handoff } = state;
2931
+ const handoff = currentHandoff(state);
2932
+ if (!state.sessionId || !handoff) return { state: closed, effects: [] };
2933
+ const { sessionId } = state;
2874
2934
  const repaired = repairedLanded(handoff);
2875
2935
  const effect = kind === "merge-run" ? {
2876
2936
  type: "isolationMerge",
2877
2937
  sessionId,
2878
2938
  branch: handoffBranch(handoff),
2879
2939
  ...isRepoGroup(handoff) ? { group: true } : {},
2880
- ...repaired.length > 0 ? { repaired } : {}
2940
+ ...repaired.length > 0 ? { repaired } : {},
2941
+ ...midRun(state) ? { midRun: true } : {}
2881
2942
  } : { type: "isolationDiscard", sessionId, branch: handoffBranch(handoff) };
2882
2943
  return { state: closed, effects: [effect] };
2883
2944
  }
2884
2945
  function showDiff(state, diff) {
2885
- if (diff.trim() === "") return say(state, "system", `Nothing differs from ${state.handoff ? handoffBase(state.handoff, 8) : "the base commit"}.`);
2946
+ const handoff = currentHandoff(state);
2947
+ if (diff.trim() === "") return say(state, "system", `Nothing differs from ${handoff ? handoffBase(handoff, 8) : "the base commit"}.`);
2886
2948
  const index = state.overlay?.kind === "handoff" ? state.overlay.index : 0;
2887
2949
  const lines = sanitize(diff.replace(/\t/g, " ")).split("\n");
2888
2950
  return { ...state, overlay: { kind: "handoff", index, diff: { lines, scroll: 0 } } };
@@ -2955,6 +3017,7 @@ function clearIsolation(state) {
2955
3017
  return {
2956
3018
  ...state,
2957
3019
  handoff: null,
3020
+ gate: null,
2958
3021
  tasks: mapTasks(state.tasks, ({ isolation: _dropped, ...task }) => task)
2959
3022
  };
2960
3023
  }
@@ -2972,16 +3035,6 @@ function isolationForPlan(tasks, record, previous) {
2972
3035
 
2973
3036
  // src/commands/handoff.ts
2974
3037
  var USAGE4 = "Usage: ordewell handoff [review|merge|discard|cleanup] [--session-id <id>] [--workspace /path] [--yes]";
2975
- async function askYesNo(question) {
2976
- if (!process.stdin.isTTY) return false;
2977
- const rl = (0, import_readline2.createInterface)({ input: process.stdin, output: process.stderr });
2978
- try {
2979
- const answer = await new Promise((resolve3) => rl.question(`${question} [y/N] `, resolve3));
2980
- return /^y(es)?$/i.test(answer.trim());
2981
- } finally {
2982
- rl.close();
2983
- }
2984
- }
2985
3038
  async function handleHandoff(subArgs, injectedApi, confirm = askYesNo) {
2986
3039
  const action = positionals(subArgs)[0]?.toLowerCase();
2987
3040
  if (action === void 0) {
@@ -3016,7 +3069,7 @@ async function handleHandoff(subArgs, injectedApi, confirm = askYesNo) {
3016
3069
  return;
3017
3070
  }
3018
3071
  case "merge": {
3019
- await confirmed((group ? `Merge ${branch} into the branch checked out in each of ${reposWithWork(handoff).join(", ")}? Nothing is merged unless every repository can take it; once all have, the run's worktrees and branches are removed.` : `Merge ${branch} into the branch you have checked out? Once merged, the run's worktrees and branches are removed.`) + repairedNotice(handoff));
3072
+ await confirmed((group ? `Merge ${branch} into the branch checked out in each of ${reposWithWork(handoff).join(", ")}? Nothing is merged unless every repository can take it; once a finished run has merged everything, its worktrees and branches are removed.` : `Merge ${branch} into the branch you have checked out? Once a finished run has merged everything, its worktrees and branches are removed.`) + repairedNotice(handoff));
3020
3073
  const { ok, message } = mergeOutcome(await attempt(() => api.mergeRun(sessionId)), branch, group, repairedLanded(handoff));
3021
3074
  if (ok) console.log(message);
3022
3075
  else fail(message);
@@ -3208,6 +3261,7 @@ async function handleKey(subArgs, injectedApi) {
3208
3261
  }
3209
3262
 
3210
3263
  // src/commands/runners.ts
3264
+ var import_core19 = require("@ordewell/core");
3211
3265
  var USAGE7 = "Usage: ordewell runners [<runner-id> on|off]";
3212
3266
  async function handleRunners(subArgs, injectedApi) {
3213
3267
  const [runner, toggle] = positionals(subArgs);
@@ -3232,21 +3286,21 @@ async function handleRunners(subArgs, injectedApi) {
3232
3286
  await api.setRunnerEnabled(runner, enabled);
3233
3287
  console.log(`${runner} ${enabled ? "enabled" : "disabled"}.`);
3234
3288
  }
3235
- var AUTO_USAGE = "Usage: ordewell auto [on|off]";
3289
+ var AUTO_USAGE = "Usage: ordewell auto [full|auto]";
3236
3290
  function handleAuto(subArgs) {
3237
3291
  const [arg] = positionals(subArgs);
3238
3292
  const currentRaw = process.env.ORDEWELL_AUTONOMOUS_MODE;
3239
3293
  const current = currentRaw !== "false" && currentRaw !== "0";
3240
3294
  if (!arg) {
3241
- console.log(`Autonomous mode: ${current ? "ON" : "OFF"}`);
3295
+ console.log(`Autonomy level: ${(0, import_core19.autonomyLevelLabel)(current)}`);
3242
3296
  console.log(` ${AUTO_USAGE}`);
3243
3297
  return;
3244
3298
  }
3245
- const enabled = arg === "on" ? true : arg === "off" ? false : null;
3299
+ const enabled = (0, import_core19.parseAutonomyLevel)(arg);
3246
3300
  if (enabled === null) fail(AUTO_USAGE);
3247
3301
  writeEnvVar(findEnvFile(), "ORDEWELL_AUTONOMOUS_MODE", String(enabled));
3248
3302
  process.env.ORDEWELL_AUTONOMOUS_MODE = String(enabled);
3249
- console.log(`Autonomous mode ${enabled ? "on" : "off"} for new sessions.`);
3303
+ console.log(`Autonomy level: ${(0, import_core19.autonomyLevelLabel)(enabled)} for new sessions.`);
3250
3304
  }
3251
3305
  async function handleRefresh(subArgs, injectedApi) {
3252
3306
  const api = await connect(subArgs, injectedApi);
@@ -3278,7 +3332,7 @@ async function handleApprove(subArgs, injectedApi) {
3278
3332
  // src/tui/terminalLauncher.ts
3279
3333
  var import_child_process3 = require("child_process");
3280
3334
  var import_util = require("util");
3281
- var import_core19 = require("@ordewell/core");
3335
+ var import_core20 = require("@ordewell/core");
3282
3336
  var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
3283
3337
  var defaultExecFile = async (command, args) => {
3284
3338
  const { stdout, stderr } = await execFileAsync(command, args);
@@ -3298,16 +3352,16 @@ var defaultWhich = (bin) => {
3298
3352
  async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
3299
3353
  const platform = deps.platform ?? process.platform;
3300
3354
  const env = deps.env ?? process.env;
3301
- const hasTmuxImpl = deps.hasTmuxImpl ?? import_core19.hasTmux;
3355
+ const hasTmuxImpl = deps.hasTmuxImpl ?? import_core20.hasTmux;
3302
3356
  const execFileImpl = deps.execFileImpl ?? defaultExecFile;
3303
3357
  const spawnImpl = deps.spawnImpl ?? defaultSpawn;
3304
3358
  const which = deps.which ?? defaultWhich;
3305
3359
  if (!hasTmuxImpl()) {
3306
- return { ok: false, message: "tmux is not installed \u2014 install it to get a real terminal per task." };
3360
+ return { ok: false, message: "No terminal window for this task: tmux is not installed. Install tmux to get one for terminal-transport tasks; a structured task has its log view instead." };
3307
3361
  }
3308
- const session = (0, import_core19.tmuxSessionName)(port);
3309
- const socket = (0, import_core19.tmuxSocketName)(port);
3310
- const base = (0, import_core19.tmuxWindowName)(taskId, planSessionId);
3362
+ const session = (0, import_core20.tmuxSessionName)(port);
3363
+ const socket = (0, import_core20.tmuxSocketName)(port);
3364
+ const base = (0, import_core20.tmuxWindowName)(taskId, planSessionId);
3311
3365
  let windows;
3312
3366
  try {
3313
3367
  const { stdout } = await execFileImpl("tmux", ["-L", socket, "list-windows", "-t", session, "-F", "#{window_name}"]);
@@ -3317,7 +3371,7 @@ async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
3317
3371
  }
3318
3372
  const window = latestAttempt(windows, base);
3319
3373
  if (!window) {
3320
- return { ok: false, message: "This task hasn't opened a terminal yet \u2014 it may still be pending." };
3374
+ return { ok: false, message: "This task has no live terminal and no saved log \u2014 it may still be pending." };
3321
3375
  }
3322
3376
  const manualHint = `tmux -L ${socket} attach -t ${session}:${window}`;
3323
3377
  const viewer = `${session}-view-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
@@ -3599,9 +3653,27 @@ Dependencies \xB7 #${task.order} ${task.title}
3599
3653
  console.log(dependencies.length > 0 ? `Task #${task.order} now depends on ${dependencies.length} task${dependencies.length === 1 ? "" : "s"}.` : `Task #${task.order} no longer depends on anything.`);
3600
3654
  });
3601
3655
  }
3656
+ var OPS_USAGE = "Usage: ordewell task-ops <task-id-or-order> [on|off] [--session-id <id>]";
3657
+ async function handleTaskOps(subArgs, injectedApi) {
3658
+ await withTask(subArgs, OPS_USAGE, injectedApi, async (api, sessionId, task, tasks, value) => {
3659
+ if (!value) {
3660
+ console.log(`
3661
+ #${task.order} ${task.title} is ${task.ops ? "an ops task: it runs in your checkout once the work it depends on is merged" : "a change task: it runs in its own worktree"}.`);
3662
+ console.log(`
3663
+ ${OPS_USAGE}`);
3664
+ return;
3665
+ }
3666
+ const to = value.toLowerCase();
3667
+ if (to !== "on" && to !== "off") fail(OPS_USAGE);
3668
+ if (task.type !== "ai") fail("Only an AI task can be an ops task \u2014 a manual task already runs outside any worktree.");
3669
+ if (!tasks.some((t) => t.id === task.id)) fail("A subtask runs with its parent; make the parent an ops task instead.");
3670
+ await api.updateTask(sessionId, task.id, { ops: to === "on" });
3671
+ console.log(to === "on" ? `Task #${task.order} is an ops task: it runs in your checkout once the work it depends on is merged.` : `Task #${task.order} is a change task: it runs in its own worktree.`);
3672
+ });
3673
+ }
3602
3674
 
3603
3675
  // src/tui/index.ts
3604
- var import_core32 = require("@ordewell/core");
3676
+ var import_core33 = require("@ordewell/core");
3605
3677
 
3606
3678
  // src/tui/editor.ts
3607
3679
  function emptyEditor() {
@@ -3756,6 +3828,7 @@ var SLASH_COMMANDS = [
3756
3828
  { name: "task-effort", usage: "/task-effort <id> [level]", description: "Choose or set a task's thinking effort", category: "tasks" },
3757
3829
  { name: "task-mode", usage: "/task-mode <id> [mode]", description: "Choose or set a task's runner mode", category: "tasks" },
3758
3830
  { name: "task-deps", usage: "/task-deps <id>", description: "Edit which earlier tasks a task waits for", category: "tasks" },
3831
+ { name: "task-ops", usage: "/task-ops <id> [on|off]", description: "Make a task an ops task \u2014 run in your checkout once its dependencies are merged \u2014 or a change task", category: "tasks" },
3759
3832
  // Models & providers
3760
3833
  { name: "planner", usage: "/planner [<provider>]", description: "Choose who plans \u2014 an API provider or a coding agent (no API key)", category: "models" },
3761
3834
  { name: "model", usage: "/model [set <id>]", description: "Show or set the orchestrator model", category: "models" },
@@ -3763,8 +3836,8 @@ var SLASH_COMMANDS = [
3763
3836
  { name: "key", usage: "/key [set <provider> <key>]", description: "Configure API provider keys", category: "models" },
3764
3837
  { name: "allowlist", usage: "/allowlist [set <runner> <ids> | clear <runner>]", description: "Limit which models a runner may use", category: "models" },
3765
3838
  { name: "runners", usage: "/runners [<id> on|off]", description: "Enable or disable runners (claude-code, opencode, codex)", category: "models" },
3766
- { name: "auto", usage: "/auto [on|off]", description: "Autonomous permission mode for new plans", category: "models" },
3767
- { name: "transport", usage: "/transport [terminal|structured]", description: "Experimental: run Claude Code tasks over its protocol instead of a terminal, from the next run", category: "models" },
3839
+ { name: "auto", usage: "/auto [full|auto]", description: "Autonomy level for new plans: Full auto or Auto", category: "models" },
3840
+ { name: "transport", usage: "/transport [terminal|structured]", description: "Drive tasks through each runner's protocol (structured) or a terminal, from the next run", category: "models" },
3768
3841
  { name: "parallel", usage: "/parallel [<n>]", description: "How many AI tasks run at once (applies to a live run)", category: "models" },
3769
3842
  { name: "refresh", usage: "/refresh", description: "Re-discover runners and model catalogs", category: "models" },
3770
3843
  // Skills
@@ -3849,7 +3922,7 @@ function tokenCompletions(token) {
3849
3922
  }
3850
3923
 
3851
3924
  // src/tui/blocks.ts
3852
- var import_core20 = require("@ordewell/core");
3925
+ var import_core21 = require("@ordewell/core");
3853
3926
 
3854
3927
  // src/tui/markdown.ts
3855
3928
  function renderMarkdown(source, cols) {
@@ -4057,16 +4130,16 @@ function resultLines(block, room, detailAll) {
4057
4130
  const fit2 = (line) => detailAll ? wrap(line, room) : [truncate(line, room)];
4058
4131
  const outcome = unsettledOutcome(block);
4059
4132
  if (outcome) {
4060
- const [reason, ...rest] = (0, import_core20.outputLines)(block.output).map(sanitize);
4133
+ const [reason, ...rest] = (0, import_core21.outputLines)(block.output).map(sanitize);
4061
4134
  const status = fit2(reason ? `${outcome.label}: ${reason}` : outcome.label).map(outcome.paint);
4062
4135
  return detailAll ? [...status, ...rest.flatMap(fit2).map(style.grey)] : status;
4063
4136
  }
4064
4137
  if (detailAll) {
4065
- const lines2 = (0, import_core20.outputLines)(block.output).map(sanitize);
4138
+ const lines2 = (0, import_core21.outputLines)(block.output).map(sanitize);
4066
4139
  const note = lines2.length > PREVIEW_LINES ? ["\u2026 (ctrl+o to collapse)"] : [];
4067
4140
  return lines2.length > 0 ? [...lines2.flatMap(fit2), ...note].map(style.grey) : [style.grey("(no output)")];
4068
4141
  }
4069
- const { lines, hiddenLineCount } = (0, import_core20.outputPreview)(block.output, PREVIEW_LINES);
4142
+ const { lines, hiddenLineCount } = (0, import_core21.outputPreview)(block.output, PREVIEW_LINES);
4070
4143
  if (lines.length === 0) return [style.grey("(no output)")];
4071
4144
  const more = hiddenLineCount > 0 ? [`\u2026 +${countOf(hiddenLineCount, "line")} (ctrl+o to expand)`] : [];
4072
4145
  return [...lines.map(sanitize), ...more].map((line) => style.grey(truncate(line, room)));
@@ -4187,7 +4260,7 @@ function tokenLine(blocks, cols) {
4187
4260
  }
4188
4261
 
4189
4262
  // src/tui/state.ts
4190
- var import_core21 = require("@ordewell/core");
4263
+ var import_core22 = require("@ordewell/core");
4191
4264
  var SKILL_IDS = ["tdd", "verify"];
4192
4265
  function noSkills() {
4193
4266
  return { tdd: false, verify: false };
@@ -4234,9 +4307,9 @@ function findTask(tasks, id) {
4234
4307
  function initialState(overrides = {}) {
4235
4308
  return {
4236
4309
  editor: emptyEditor(),
4237
- conversation: import_core21.EMPTY_CONVERSATION,
4310
+ conversation: import_core22.EMPTY_CONVERSATION,
4238
4311
  taskView: null,
4239
- turnGate: import_core21.NO_TURN,
4312
+ turnGate: import_core22.NO_TURN,
4240
4313
  detailAll: false,
4241
4314
  status: "idle",
4242
4315
  busyLabel: "",
@@ -4261,18 +4334,19 @@ function initialState(overrides = {}) {
4261
4334
  orchestratorModel: "",
4262
4335
  plannerProvider: "",
4263
4336
  plannerEffort: "",
4264
- maxParallel: import_core21.DEFAULT_MAX_PARALLEL,
4337
+ maxParallel: import_core22.DEFAULT_MAX_PARALLEL,
4265
4338
  configuredProviders: [],
4266
4339
  allowlist: {},
4267
4340
  autonomous: true,
4268
- runnerTransport: "terminal",
4341
+ runnerTransport: "structured",
4269
4342
  mouseCapture: true,
4270
4343
  selection: null,
4271
4344
  workspace: process.cwd(),
4272
4345
  handoff: null,
4346
+ gate: null,
4273
4347
  overlay: null,
4274
4348
  pendingApprovals: [],
4275
- queuedPrompts: import_core21.EMPTY_HOLD,
4349
+ queuedPrompts: import_core22.EMPTY_HOLD,
4276
4350
  stopArmed: false,
4277
4351
  stopArmToken: 0,
4278
4352
  stopRequested: false,
@@ -4320,7 +4394,7 @@ function chatEditorRoomFor(state, active) {
4320
4394
  }
4321
4395
 
4322
4396
  // src/tui/layout.ts
4323
- var import_core22 = require("@ordewell/core");
4397
+ var import_core23 = require("@ordewell/core");
4324
4398
  var CHROME_ROWS = 4;
4325
4399
  function chatInputWrap(state) {
4326
4400
  const active = state.focus === "chat" && !state.overlay;
@@ -4345,6 +4419,7 @@ function footerHints(state) {
4345
4419
  "e effort",
4346
4420
  "M mode",
4347
4421
  "D deps",
4422
+ "O ops",
4348
4423
  "f start",
4349
4424
  "E run plan",
4350
4425
  "S stop",
@@ -4359,7 +4434,7 @@ function footerHints(state) {
4359
4434
  "tab chat"
4360
4435
  ];
4361
4436
  }
4362
- const detailHint = (0, import_core22.hasHiddenDetail)(state.taskView ? state.taskView.view.blocks : state.conversation.blocks) ? [state.detailAll ? "ctrl-o collapse all" : "ctrl-o expand all"] : [];
4437
+ const detailHint = (0, import_core23.hasHiddenDetail)(state.taskView ? state.taskView.view.blocks : state.conversation.blocks) ? [state.detailAll ? "ctrl-o collapse all" : "ctrl-o expand all"] : [];
4363
4438
  return [
4364
4439
  "/help",
4365
4440
  "tab plan",
@@ -4464,9 +4539,9 @@ function chatScrollMax(state) {
4464
4539
  }
4465
4540
  function plannerSetup(state) {
4466
4541
  const provider = state.plannerProvider;
4467
- const registration = import_core22.ALL_PROVIDERS[provider];
4542
+ const registration = import_core23.ALL_PROVIDERS[provider];
4468
4543
  if (!registration) return { label: "not set", ready: false };
4469
- const runner = (0, import_core22.runnerForProvider)(provider);
4544
+ const runner = (0, import_core23.runnerForProvider)(provider);
4470
4545
  const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
4471
4546
  return { label: registration.label, ready };
4472
4547
  }
@@ -4576,7 +4651,8 @@ var STATUS_ICON = {
4576
4651
  var AWAITING_LABEL = {
4577
4652
  input: "waiting for your input",
4578
4653
  checkpoint: "checkpoint",
4579
- conflict: "merge conflict"
4654
+ conflict: "merge conflict",
4655
+ "files-changed": "changed tracked files"
4580
4656
  };
4581
4657
  function approvalLabel(count) {
4582
4658
  return count > 1 ? `waiting for approval (${count})` : "waiting for approval";
@@ -4655,9 +4731,9 @@ function taskLines(state, row, index, cols) {
4655
4731
  const paint = idle ? style.cyan : STATUS_MARK[task.status] ?? ((t) => t);
4656
4732
  const rawIcon = idle ? IDLE_ICON : running ? SPINNER[state.spinnerFrame % SPINNER.length] : STATUS_ICON[task.status] ?? "\xB7";
4657
4733
  const icon = paint(rawIcon);
4658
- const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : style.grey(" AI");
4734
+ const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : task.ops ? style.magenta("OPS") : style.grey(" AI");
4659
4735
  const caret = selected ? style.cyan("\u276F") : " ";
4660
- const orderLabel = parent ? (0, import_core22.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
4736
+ const orderLabel = parent ? (0, import_core23.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
4661
4737
  const indent = parent ? " " : "";
4662
4738
  const head = `${indent}${caret} ${icon} ${orderLabel} ${kind} `;
4663
4739
  const titleRoom = Math.max(1, cols - width(head));
@@ -4687,16 +4763,26 @@ function taskLines(state, row, index, cols) {
4687
4763
  if (effort || mode) lines.push(style.grey(truncate(`${bodyPad}${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
4688
4764
  if (task.isolation?.state === "conflict") {
4689
4765
  const where = task.isolation.conflictRepo && task.isolation.conflictRepo !== "." ? ` in ${task.isolation.conflictRepo}` : "";
4690
- const files = task.isolation.conflictFiles?.length ? ` (${(0, import_core22.capConflictFiles)(task.isolation.conflictFiles)})` : "";
4766
+ const files = task.isolation.conflictFiles?.length ? ` (${(0, import_core23.capConflictFiles)(task.isolation.conflictFiles)})` : "";
4691
4767
  lines.push(style.red(truncate(`${bodyPad}\u26A0 merge conflict${where}${files} \u2014 its work is kept on its own branch`, cols)));
4692
4768
  }
4693
4769
  if (task.isolation?.state === "repairing") {
4694
- const files = task.isolation.conflictFiles?.length ? ` in ${(0, import_core22.capConflictFiles)(task.isolation.conflictFiles)}` : "";
4770
+ const files = task.isolation.conflictFiles?.length ? ` in ${(0, import_core23.capConflictFiles)(task.isolation.conflictFiles)}` : "";
4695
4771
  const attempt = task.isolation.repair ? ` (attempt ${task.isolation.repair.attempt}/${task.isolation.repair.limit})` : "";
4696
4772
  lines.push(style.yellow(truncate(`${bodyPad}\u21BB repairing conflict${files}${attempt}`, cols)));
4697
4773
  }
4774
+ if (task.mergeGate?.length) {
4775
+ const deps = task.mergeGate.map((id) => {
4776
+ const dep = state.tasks.find((t) => t.id === id);
4777
+ return dep ? `#${dep.order}` : id;
4778
+ });
4779
+ lines.push(style.yellow(truncate(`${bodyPad}\u23F8 waits for Merge all \u2014 ${deps.join(", ")} not merged into your branch yet`, cols)));
4780
+ }
4781
+ if (task.status === "awaiting_user" && task.awaitingReason === "files-changed") {
4782
+ lines.push(style.yellow(truncate(`${bodyPad}\u26A0 an ops task changed tracked files \u2014 check them, then m done or /retry`, cols)));
4783
+ }
4698
4784
  if (task.isolation?.state === "integrated" && task.isolation.repairedFiles?.length) {
4699
- lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${(0, import_core22.capConflictFiles)(task.isolation.repairedFiles)}`, cols)));
4785
+ lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${(0, import_core23.capConflictFiles)(task.isolation.repairedFiles)}`, cols)));
4700
4786
  }
4701
4787
  let editorLine;
4702
4788
  if (expanded) {
@@ -4709,8 +4795,14 @@ function taskLines(state, row, index, cols) {
4709
4795
  lines.push(...taskText("Worktree", task.isolation.worktree, cols, bodyPad));
4710
4796
  }
4711
4797
  }
4798
+ if (task.ops) {
4799
+ lines.push(...taskText("Ops", "Runs in your checkout, not a worktree, once the work it depends on is merged. O makes it a change task.", cols, bodyPad));
4800
+ }
4801
+ if (task.forcedPastGate?.length) {
4802
+ lines.push(...taskText("Forced", `Started before the work of ${task.forcedPastGate.join(", ")} was merged into your branch.`, cols, bodyPad));
4803
+ }
4712
4804
  if (modeInfo?.autonomous) {
4713
- lines.push(...taskText("Autonomy", "Runs without permission prompts. Toggle with /auto.", cols, bodyPad));
4805
+ lines.push(...taskText("Autonomy", "Runs without permission prompts (Full auto). Change the level with /auto.", cols, bodyPad));
4714
4806
  }
4715
4807
  const editableSeed = task.prompt ?? task.description ?? task.title;
4716
4808
  if (task.description && task.description !== task.title && task.description !== editableSeed) {
@@ -4784,7 +4876,7 @@ function helpScrollMax(state) {
4784
4876
  }
4785
4877
 
4786
4878
  // src/tui/render.ts
4787
- var import_core23 = require("@ordewell/core");
4879
+ var import_core24 = require("@ordewell/core");
4788
4880
  function render(state) {
4789
4881
  const { rows, cols } = state;
4790
4882
  const rowsForBody = bodyRows(state);
@@ -4879,9 +4971,9 @@ function renderSkills(state, cols) {
4879
4971
  const badges = SKILL_IDS.map(
4880
4972
  (id) => state.skills[id] ? style.green(`\u25CF ${id}`) : style.grey(`\u25CB ${id}`)
4881
4973
  );
4882
- const auto = state.autonomous ? style.yellow("\u25CF auto") : style.grey("\u25CB auto");
4883
- const structured = state.runnerTransport === "structured" ? [style.yellow("\u25CF structured")] : [];
4884
- return truncate([...badges, auto, ...structured].join(" "), cols);
4974
+ const auto = state.autonomous ? style.yellow("\u25CF Full auto") : style.green("\u25CF Auto");
4975
+ const terminal = state.runnerTransport === "terminal" ? [style.yellow("\u25CF terminal")] : [];
4976
+ return truncate([...badges, auto, ...terminal].join(" "), cols);
4885
4977
  }
4886
4978
  function scrolledBackMark(state) {
4887
4979
  return state.focus !== "plan" && state.scroll > 0 ? "\u2191 scrolled back \xB7 pgdn for live" : "";
@@ -5039,7 +5131,7 @@ function renderOverlay(state, rows, cols) {
5039
5131
  return renderPicker(overlay.picker, rows, cols);
5040
5132
  }
5041
5133
  function renderHandoff(state, overlay, rows, cols) {
5042
- const handoff = state.handoff;
5134
+ const handoff = currentHandoff(state);
5043
5135
  if (!handoff) return frame("Handoff", [style.grey("Nothing to hand off.")], rows, cols);
5044
5136
  if (overlay.diff) {
5045
5137
  const room = diffRoom(rows);
@@ -5049,7 +5141,7 @@ function renderHandoff(state, overlay, rows, cols) {
5049
5141
  return frame(`Diff \u2014 ${handoffBranch(handoff)}`, [...shown, "", style.grey(`${more}enter or esc goes back`)], rows, cols);
5050
5142
  }
5051
5143
  const landed = handoff.landed.length === 0 ? [style.grey("Nothing landed on it.")] : handoff.landed.map((task) => {
5052
- const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core23.capConflictFiles)(task.repairedFiles)})`) : "";
5144
+ const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core24.capConflictFiles)(task.repairedFiles)})`) : "";
5053
5145
  return ` ${style.green("\u2713")} #${task.order} ${task.title}${repaired}`;
5054
5146
  });
5055
5147
  const actions = handoffActions(handoff).map((action, index) => {
@@ -5139,7 +5231,7 @@ function frame(title, body, rows, cols) {
5139
5231
  }
5140
5232
 
5141
5233
  // src/tui/reducer.ts
5142
- var import_core29 = require("@ordewell/core");
5234
+ var import_core30 = require("@ordewell/core");
5143
5235
 
5144
5236
  // src/tui/reducers/shared.ts
5145
5237
  function step(state, effects = []) {
@@ -5188,10 +5280,10 @@ function handleApprovalKey(state, overlay, key) {
5188
5280
  }
5189
5281
 
5190
5282
  // src/tui/reducers/commands.ts
5191
- var import_core27 = require("@ordewell/core");
5283
+ var import_core28 = require("@ordewell/core");
5192
5284
 
5193
5285
  // src/tui/reducers/pickers.ts
5194
- var import_core24 = require("@ordewell/core");
5286
+ var import_core25 = require("@ordewell/core");
5195
5287
  var import_plan_utils2 = require("@ordewell/core/plan-utils");
5196
5288
  var DEFAULT_EFFORT = "__runner_default__";
5197
5289
  function picker(title, items, action, extra = {}) {
@@ -5210,11 +5302,11 @@ function modelDetail(m) {
5210
5302
  function providerErrorHint(state) {
5211
5303
  const failed = Object.keys(state.providerErrors ?? {});
5212
5304
  if (failed.length === 0) return void 0;
5213
- const names = failed.map((p) => import_core24.ALL_PROVIDERS[p]?.label ?? p);
5305
+ const names = failed.map((p) => import_core25.ALL_PROVIDERS[p]?.label ?? p);
5214
5306
  return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
5215
5307
  }
5216
5308
  function plannerModelItems(state) {
5217
- const runner = (0, import_core24.runnerForProvider)(state.plannerProvider);
5309
+ const runner = (0, import_core25.runnerForProvider)(state.plannerProvider);
5218
5310
  if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
5219
5311
  const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
5220
5312
  id: m.id,
@@ -5228,7 +5320,7 @@ function plannerModelItems(state) {
5228
5320
  return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
5229
5321
  }
5230
5322
  function plannerEffortItems(state) {
5231
- const runner = (0, import_core24.runnerForProvider)(state.plannerProvider);
5323
+ const runner = (0, import_core25.runnerForProvider)(state.plannerProvider);
5232
5324
  if (!runner) {
5233
5325
  return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
5234
5326
  }
@@ -5337,21 +5429,21 @@ function pickerItemsFor(state, action) {
5337
5429
  }
5338
5430
  function plannerItems(state) {
5339
5431
  const installed = new Set(state.runners.map((r) => r.id));
5340
- const agents = import_core24.CLI_PROVIDERS.map((id) => {
5341
- const runner = (0, import_core24.runnerForProvider)(id);
5432
+ const agents = import_core25.CLI_PROVIDERS.map((id) => {
5433
+ const runner = (0, import_core25.runnerForProvider)(id);
5342
5434
  const ready = installed.has(runner);
5343
5435
  return {
5344
5436
  id,
5345
- label: import_core24.ALL_PROVIDERS[id].label,
5437
+ label: import_core25.ALL_PROVIDERS[id].label,
5346
5438
  detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
5347
5439
  selected: state.plannerProvider === id,
5348
5440
  disabled: !ready
5349
5441
  };
5350
5442
  });
5351
- const vendors = import_core24.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
5443
+ const vendors = import_core25.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
5352
5444
  id,
5353
- label: import_core24.ALL_PROVIDERS[id].label,
5354
- detail: import_core24.ALL_PROVIDERS[id].apiKeyEnvVar,
5445
+ label: import_core25.ALL_PROVIDERS[id].label,
5446
+ detail: import_core25.ALL_PROVIDERS[id].apiKeyEnvVar,
5355
5447
  selected: state.plannerProvider === id
5356
5448
  }));
5357
5449
  return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
@@ -5433,13 +5525,13 @@ function handleMouseSelect(state, key) {
5433
5525
  }
5434
5526
 
5435
5527
  // src/tui/reducers/taskView.ts
5436
- var import_core25 = require("@ordewell/core");
5528
+ var import_core26 = require("@ordewell/core");
5437
5529
  function openTaskView(state, sessionId, taskId) {
5438
5530
  const task = findTask(state.tasks, taskId);
5439
5531
  if (!task) return step(say(state, "system", `No task ${taskId} in this plan.`));
5440
5532
  const taskView = {
5441
5533
  taskId,
5442
- view: import_core25.EMPTY_TASK_LOG,
5534
+ view: import_core26.EMPTY_TASK_LOG,
5443
5535
  attempts: [],
5444
5536
  attempt: 0,
5445
5537
  pending: [],
@@ -5472,13 +5564,13 @@ function applyLive(tv, attempt, events) {
5472
5564
  }
5473
5565
  next = {
5474
5566
  ...next,
5475
- view: import_core25.EMPTY_TASK_LOG,
5567
+ view: import_core26.EMPTY_TASK_LOG,
5476
5568
  attempt,
5477
5569
  attempts: next.attempts.includes(attempt) ? next.attempts : [...next.attempts, attempt]
5478
5570
  };
5479
5571
  }
5480
5572
  if (attempt < next.attempt) return next;
5481
- return clampQueueIndex({ ...next, view: events.reduce(import_core25.reduceTaskLog, next.view) });
5573
+ return clampQueueIndex({ ...next, view: events.reduce(import_core26.reduceTaskLog, next.view) });
5482
5574
  }
5483
5575
  function taskLogAbandoned(state) {
5484
5576
  const tv = state.taskView;
@@ -5498,15 +5590,15 @@ function taskLogArrived(state, action) {
5498
5590
  function announceApprovals(state, action) {
5499
5591
  if (state.taskView?.taskId === action.taskId) return state;
5500
5592
  const task = findTask(state.tasks, action.taskId);
5501
- const spoken = action.events.reduce((s, event) => event.type === "approval_requested" ? say(s, "system", `\xB7 Task ${task?.order ?? "?"} waits for approval: ${(0, import_core25.runnerToolSubject)(event.tool, event.args)} \u2014 t on it to answer`) : s, state);
5502
- return state.taskView && spoken !== state ? { ...spoken, scroll: state.scroll } : spoken;
5593
+ const spoken = action.events.reduce((s, event) => event.type === "approval_requested" ? say(s, "system", `\xB7 Task ${task?.order ?? "?"} waits for approval: ${(0, import_core26.runnerToolSubject)(event.tool, event.args)} \u2014 t on it to answer`) : s, state);
5594
+ return spoken !== state ? { ...spoken, scroll: state.scroll } : spoken;
5503
5595
  }
5504
5596
  function taskLogLoaded(state, action) {
5505
5597
  const tv = state.taskView;
5506
5598
  if (!tv || tv.taskId !== action.taskId) return state;
5507
5599
  const loaded = action.attempt >= 1 ? {
5508
5600
  ...tv,
5509
- view: (0, import_core25.replayTaskLog)(action.events),
5601
+ view: (0, import_core26.replayTaskLog)(action.events),
5510
5602
  attempts: action.attempts ?? tv.attempts,
5511
5603
  attempt: action.attempt
5512
5604
  } : { ...tv, attempts: action.attempts ?? tv.attempts };
@@ -5595,6 +5687,34 @@ function confirmRemoveTask(state, task) {
5595
5687
  }
5596
5688
  });
5597
5689
  }
5690
+ function toggleTaskOps(state, task, subtask, to = !task.ops) {
5691
+ if (!state.sessionId) return fail3(state, "No active plan.");
5692
+ if (task.type !== "ai") return fail3(state, "Only an AI task can be an ops task \u2014 a manual task already runs outside any worktree.");
5693
+ if (subtask) return fail3(state, "A subtask runs with its parent; make the parent an ops task instead.");
5694
+ if (!!task.ops === to) return fail3(state, `#${task.order} is already ${to ? "an ops" : "a change"} task.`);
5695
+ return step(state, [{
5696
+ type: "updateTask",
5697
+ sessionId: state.sessionId,
5698
+ taskId: task.id,
5699
+ changes: { ops: to },
5700
+ message: to ? `Task #${task.order} is an ops task: it runs in your checkout once the work it depends on is merged.` : `Task #${task.order} is a change task: it runs in its own worktree.`
5701
+ }]);
5702
+ }
5703
+ function confirmForceStartPastGate(state, task) {
5704
+ const named = (task.mergeGate ?? []).map((id) => {
5705
+ const dep = findTask(state.tasks, id);
5706
+ return dep ? `#${dep.order} ${dep.title}` : id;
5707
+ });
5708
+ return step({
5709
+ ...state,
5710
+ overlay: {
5711
+ kind: "confirm",
5712
+ title: `Force start #${task.order} ${task.title}?`,
5713
+ message: `It waits for Merge all: the work of ${named.join(", ")} is not merged into your branch yet, so it would act without it. Starting it now is kept on the task.`,
5714
+ action: { kind: "force-start-gated", taskId: task.id }
5715
+ }
5716
+ });
5717
+ }
5598
5718
  function openTaskDepsPicker(state, task) {
5599
5719
  const action = { kind: "set-task-deps", taskId: task.id };
5600
5720
  const items = pickerItemsFor(state, action);
@@ -5751,6 +5871,15 @@ function taskEffortCommand(state, args) {
5751
5871
  return assignTaskEffort(state, sessionId, task, value);
5752
5872
  });
5753
5873
  }
5874
+ function taskOpsCommand(state, args) {
5875
+ return taskCommand(state, args[0], (_sessionId, taskId) => {
5876
+ const task = findTask(state.tasks, taskId);
5877
+ const subtask = !state.tasks.some((t) => t.id === taskId);
5878
+ const value = args[1]?.toLowerCase();
5879
+ if (value !== void 0 && value !== "on" && value !== "off") return fail3(state, "Usage: /task-ops <id> [on|off]");
5880
+ return toggleTaskOps(state, task, subtask, value === void 0 ? !task.ops : value === "on");
5881
+ });
5882
+ }
5754
5883
  function addTask(state, title) {
5755
5884
  if (!state.sessionId) {
5756
5885
  return fail3(state, "No active plan to add a task to \u2014 describe a goal first, then /add-task <title>.");
@@ -5831,9 +5960,11 @@ function handlePlanKey(state, key) {
5831
5960
  if (key.char === "E") return step(state, [{ type: "execute", sessionId: state.sessionId }]);
5832
5961
  if (key.char === "S") return step(state, [{ type: "stopExecution", sessionId: state.sessionId }]);
5833
5962
  const action = key.char === "m" ? markAction(task) : PLAN_SHORTCUTS[key.char ?? ""];
5963
+ if (action === "force-start" && task.mergeGate?.length) return confirmForceStartPastGate(state, task);
5834
5964
  if (action) {
5835
5965
  return step(state, [taskActionEffect(state, state.sessionId, task.id, action)]);
5836
5966
  }
5967
+ if (key.char === "O") return toggleTaskOps(state, task, row.parent !== null);
5837
5968
  if (key.char === "d") return confirmRemoveTask(state, task);
5838
5969
  if (key.char === "t") return openTaskTerminalOrView(state, state.sessionId, task.id);
5839
5970
  if (key.char === "x" && task.isolation?.state === "conflict") {
@@ -5868,9 +5999,9 @@ function commitTaskEdit(state, task, editor) {
5868
5999
  }
5869
6000
 
5870
6001
  // src/tui/reducers/turnQueue.ts
5871
- var import_core26 = require("@ordewell/core");
6002
+ var import_core27 = require("@ordewell/core");
5872
6003
  function drainQueue(state, settled) {
5873
- const next = (0, import_core26.drainNext)(state.queuedPrompts);
6004
+ const next = (0, import_core27.drainNext)(state.queuedPrompts);
5874
6005
  const ended = disarmStop({ ...settled, stopRequested: false });
5875
6006
  if (!next) return step(ended);
5876
6007
  const spoken = say(ended, "user", next.text);
@@ -5889,14 +6020,14 @@ function stopPlanning(state, sessionId) {
5889
6020
  return step(queueToEditor(cutTurn({ ...dismissed, stopArmed: false, stopRequested: true })), [{ type: "cancelPlanning", sessionId }]);
5890
6021
  }
5891
6022
  function queueToEditor(state) {
5892
- const all = (0, import_core26.unsendAll)(state.queuedPrompts);
6023
+ const all = (0, import_core27.unsendAll)(state.queuedPrompts);
5893
6024
  if (!all) return state;
5894
6025
  return { ...state, queuedPrompts: all.rest, editor: { ...state.editor, text: all.text, cursor: all.text.length } };
5895
6026
  }
5896
6027
  function unsendLatest(state) {
5897
- const latest = (0, import_core26.unsendLatest)(state.queuedPrompts);
6028
+ const latest = (0, import_core27.unsendLatest)(state.queuedPrompts);
5898
6029
  if (!latest) return step(state);
5899
- const text = (0, import_core26.aheadOfDraft)(latest.text, state.editor.text);
6030
+ const text = (0, import_core27.aheadOfDraft)(latest.text, state.editor.text);
5900
6031
  return step({
5901
6032
  ...state,
5902
6033
  queuedPrompts: latest.rest,
@@ -5913,7 +6044,7 @@ function plannerEscape(state) {
5913
6044
  }
5914
6045
 
5915
6046
  // src/tui/reducers/commands.ts
5916
- var KNOWN_PROVIDERS2 = Object.keys(import_core27.ALL_PROVIDERS);
6047
+ var KNOWN_PROVIDERS2 = Object.keys(import_core28.ALL_PROVIDERS);
5917
6048
  function runCommand(state, { name, args }) {
5918
6049
  if (!findCommand(name)) {
5919
6050
  return fail3(state, `Unknown command: /${name} \u2014 type /help to see what's available.`);
@@ -6008,6 +6139,8 @@ function runCommand(state, { name, args }) {
6008
6139
  return taskEffortCommand(state, args);
6009
6140
  case "task-mode":
6010
6141
  return taskModeCommand(state, args);
6142
+ case "task-ops":
6143
+ return taskOpsCommand(state, args);
6011
6144
  case "task-deps":
6012
6145
  return taskCommand(
6013
6146
  state,
@@ -6020,11 +6153,11 @@ function runCommand(state, { name, args }) {
6020
6153
  case "retry":
6021
6154
  case "cancel":
6022
6155
  case "force-start":
6023
- return taskCommand(
6024
- state,
6025
- args[0],
6026
- (sessionId, taskId) => step(state, [taskActionEffect(state, sessionId, taskId, name)])
6027
- );
6156
+ return taskCommand(state, args[0], (sessionId, taskId) => {
6157
+ const task = findTask(state.tasks, taskId);
6158
+ if (name === "force-start" && task?.mergeGate?.length) return confirmForceStartPastGate(state, task);
6159
+ return step(state, [taskActionEffect(state, sessionId, taskId, name)]);
6160
+ });
6028
6161
  }
6029
6162
  return fail3(state, `/${name} is not wired up yet.`);
6030
6163
  }
@@ -6069,8 +6202,8 @@ function newSession(state) {
6069
6202
  taskView: null,
6070
6203
  tasks: [],
6071
6204
  planApproved: false,
6072
- conversation: import_core27.EMPTY_CONVERSATION,
6073
- turnGate: import_core27.NO_TURN,
6205
+ conversation: import_core28.EMPTY_CONVERSATION,
6206
+ turnGate: import_core28.NO_TURN,
6074
6207
  selectedTask: 0,
6075
6208
  expandedTaskId: null,
6076
6209
  taskEditor: null,
@@ -6081,14 +6214,14 @@ function newSession(state) {
6081
6214
  // Prompts belong to the session that raised them; the old planner is gone
6082
6215
  // and its pending requests deny on their own timeout.
6083
6216
  pendingApprovals: [],
6084
- queuedPrompts: import_core27.EMPTY_HOLD,
6217
+ queuedPrompts: import_core28.EMPTY_HOLD,
6085
6218
  stopArmed: false,
6086
6219
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
6087
6220
  }, closeEffects);
6088
6221
  }
6089
6222
  function setModel(state, args) {
6090
6223
  if (args[0] === "set" && args[1]) {
6091
- const runner = (0, import_core27.runnerForProvider)(state.plannerProvider);
6224
+ const runner = (0, import_core28.runnerForProvider)(state.plannerProvider);
6092
6225
  const known = runner ? modelsForRunner(state.models, runner) : [];
6093
6226
  if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
6094
6227
  return fail3(state, `${args[1]} was not discovered for ${runner}.`);
@@ -6161,16 +6294,16 @@ function setKey(state, args) {
6161
6294
  }
6162
6295
  return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
6163
6296
  }
6164
- const items = import_core27.PROVIDER_PRIORITY.map((id) => ({
6297
+ const items = import_core28.PROVIDER_PRIORITY.map((id) => ({
6165
6298
  id,
6166
- label: import_core27.ALL_PROVIDERS[id].label,
6167
- detail: import_core27.ALL_PROVIDERS[id].apiKeyEnvVar,
6299
+ label: import_core28.ALL_PROVIDERS[id].label,
6300
+ detail: import_core28.ALL_PROVIDERS[id].apiKeyEnvVar,
6168
6301
  selected: state.configuredProviders.includes(id)
6169
6302
  }));
6170
6303
  return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
6171
6304
  }
6172
6305
  function keyPrompt(provider) {
6173
- const meta = import_core27.ALL_PROVIDERS[provider];
6306
+ const meta = import_core28.ALL_PROVIDERS[provider];
6174
6307
  return {
6175
6308
  kind: "prompt",
6176
6309
  title: `${meta.label} API key`,
@@ -6247,18 +6380,19 @@ function runners(state, args) {
6247
6380
  }
6248
6381
  function setMaxParallel(state, arg) {
6249
6382
  if (!arg) return step(say(state, "system", `Up to ${state.maxParallel} AI task${state.maxParallel === 1 ? "" : "s"} run at once \u2014 /parallel <n> changes it.`));
6250
- const limit = (0, import_core27.parseMaxParallel)(arg);
6383
+ const limit = (0, import_core28.parseMaxParallel)(arg);
6251
6384
  if (limit === null) return fail3(state, "Usage: /parallel [<number of tasks, 1 or more>]");
6252
6385
  return step({ ...state, maxParallel: limit }, [{ type: "setMaxParallel", limit }]);
6253
6386
  }
6254
6387
  function setAutonomous(state, arg) {
6255
- const enabled = resolveToggle(arg, state.autonomous);
6256
- if (enabled === null) return fail3(state, "Usage: /auto [on|off]");
6388
+ if (arg === void 0) return step(say(state, "system", `Autonomy level: ${(0, import_core28.autonomyLevelLabel)(state.autonomous)} \u2014 /auto full or /auto auto changes it for new plans.`));
6389
+ const enabled = (0, import_core28.parseAutonomyLevel)(arg);
6390
+ if (enabled === null) return fail3(state, "Usage: /auto [full|auto]");
6257
6391
  return step({ ...state, autonomous: enabled }, [{ type: "setAutonomous", enabled }]);
6258
6392
  }
6259
6393
  function setTransport(state, arg) {
6260
6394
  const value = arg?.toLowerCase() ?? (state.runnerTransport === "structured" ? "terminal" : "structured");
6261
- if (!(0, import_core27.isRunnerTransport)(value)) return fail3(state, "Usage: /transport [terminal|structured]");
6395
+ if (!(0, import_core28.isRunnerTransport)(value)) return fail3(state, "Usage: /transport [terminal|structured]");
6262
6396
  return step(state, [{ type: "setTransport", transport: value }]);
6263
6397
  }
6264
6398
  function setMouseCapture(state, arg) {
@@ -6344,6 +6478,10 @@ function runConfirm(state, overlay) {
6344
6478
  if (!state.sessionId) return step(closed);
6345
6479
  return step(closed, [{ type: "removeTask", sessionId: state.sessionId, taskId: overlay.action.taskId }]);
6346
6480
  }
6481
+ if (overlay.action.kind === "force-start-gated") {
6482
+ if (!state.sessionId) return step(closed);
6483
+ return step(closed, [taskActionEffect(closed, state.sessionId, overlay.action.taskId, "force-start")]);
6484
+ }
6347
6485
  if (overlay.action.kind === "merge-run" || overlay.action.kind === "discard-run") {
6348
6486
  return confirmedHandoff(state, overlay.action.kind);
6349
6487
  }
@@ -6508,7 +6646,7 @@ function choose(state, picker2, item) {
6508
6646
  }
6509
6647
 
6510
6648
  // src/tui/reducers/incoming.ts
6511
- var import_core28 = require("@ordewell/core");
6649
+ var import_core29 = require("@ordewell/core");
6512
6650
  var planText = (value, fallback = "") => sanitize(String(value ?? fallback));
6513
6651
  var planLabel = (value, fallback = "") => planText(value, fallback).replace(/\n+/g, " ");
6514
6652
  var asRecord = (value) => typeof value === "object" && value !== null ? value : {};
@@ -6539,7 +6677,7 @@ function toTaskView2(t, index) {
6539
6677
  prompt: typeof t.prompt === "string" ? sanitize(t.prompt) : void 0,
6540
6678
  type: t.type === "user" ? "user" : "ai",
6541
6679
  status: planLabel(t.status, "pending"),
6542
- awaitingReason: t.status === "awaiting_user" && (0, import_core28.isAwaitingReason)(t.awaitingReason) ? t.awaitingReason : void 0,
6680
+ awaitingReason: t.status === "awaiting_user" && (0, import_core29.isAwaitingReason)(t.awaitingReason) ? t.awaitingReason : void 0,
6543
6681
  dependencies: Array.isArray(t.dependencies) ? t.dependencies.map((id) => planLabel(id)) : [],
6544
6682
  assignedRunner: typeof t.assignedRunner === "string" ? planLabel(t.assignedRunner) : void 0,
6545
6683
  taskMode: typeof t.taskMode === "string" ? planLabel(t.taskMode) : void 0,
@@ -6549,7 +6687,9 @@ function toTaskView2(t, index) {
6549
6687
  thinkingEffort: typeof model.thinkingEffort === "string" ? planLabel(model.thinkingEffort) : typeof t.thinkingEffort === "string" ? planLabel(t.thinkingEffort) : void 0,
6550
6688
  availableVariants: Array.isArray(model.availableVariants) ? model.availableVariants.map((variant) => planLabel(variant)) : void 0
6551
6689
  } : void 0,
6552
- subtasks: Array.isArray(t.subtasks) ? [...t.subtasks].sort((a, b) => wireOrder(a) - wireOrder(b)).map((s, i) => toTaskView2(asRecord(s), i)) : void 0
6690
+ subtasks: Array.isArray(t.subtasks) ? [...t.subtasks].sort((a, b) => wireOrder(a) - wireOrder(b)).map((s, i) => toTaskView2(asRecord(s), i)) : void 0,
6691
+ ...t.ops === true ? { ops: true } : {},
6692
+ ...Array.isArray(t.forcedPastGate) && t.forcedPastGate.length > 0 ? { forcedPastGate: t.forcedPastGate.map((title) => planLabel(title)) } : {}
6553
6693
  };
6554
6694
  }
6555
6695
  var enabledFlag = (value) => typeof value?.enabled === "boolean" ? value.enabled : void 0;
@@ -6570,11 +6710,11 @@ function applySettings(state, settings) {
6570
6710
  plannerProvider: typeof settings.aiProvider === "string" ? settings.aiProvider : state.plannerProvider,
6571
6711
  plannerEffort: typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : state.plannerEffort,
6572
6712
  maxParallel: typeof settings.maxParallel === "number" ? settings.maxParallel : state.maxParallel,
6573
- runnerTransport: (0, import_core28.isRunnerTransport)(settings.runnerTransport) ? settings.runnerTransport : state.runnerTransport,
6713
+ runnerTransport: (0, import_core29.isRunnerTransport)(settings.runnerTransport) ? settings.runnerTransport : state.runnerTransport,
6574
6714
  allowlist: settings.modelAllowlist && typeof settings.modelAllowlist === "object" ? settings.modelAllowlist : state.allowlist
6575
6715
  };
6576
6716
  }
6577
- function runLabel(tasks) {
6717
+ function runLabel(tasks, gate = null) {
6578
6718
  const running = tasks.filter(isTaskRunning);
6579
6719
  if (running.length > 0) {
6580
6720
  const [first] = running;
@@ -6582,8 +6722,8 @@ function runLabel(tasks) {
6582
6722
  return running.length > 1 ? `${name} (+${running.length - 1} more)` : name;
6583
6723
  }
6584
6724
  const waiting = tasks.filter((t) => t.status === "awaiting_user").length;
6585
- if (waiting === 0) return "";
6586
- return waiting === 1 ? "1 task waits for you" : `${waiting} tasks wait for you`;
6725
+ if (waiting > 0) return waiting === 1 ? "1 task waits for you" : `${waiting} tasks wait for you`;
6726
+ return gate?.paused ? "paused for Merge all \u2014 /handoff merge" : "";
6587
6727
  }
6588
6728
  var statusText = (text) => sanitize(text).replace(/\s+/g, " ").trim();
6589
6729
  function inFlight(blocks) {
@@ -6612,7 +6752,18 @@ function followSession(state, message) {
6612
6752
  }
6613
6753
 
6614
6754
  // src/tui/reducer.ts
6755
+ function holdReadingPlace(before, after) {
6756
+ if (before.scroll <= 0 || after.scroll !== before.scroll) return after;
6757
+ if (before.taskView?.taskId !== after.taskView?.taskId) return after;
6758
+ const grown = chatScrollMax(after) - chatScrollMax(before);
6759
+ return grown > 0 ? { ...after, scroll: clamp(after.scroll + grown, chatScrollMax(after)) } : after;
6760
+ }
6615
6761
  function reduce(state, action) {
6762
+ const result = reduceAction(state, action);
6763
+ if (action.type !== "sessionMessage" && action.type !== "taskLog") return result;
6764
+ return { ...result, state: holdReadingPlace(state, result.state) };
6765
+ }
6766
+ function reduceAction(state, action) {
6616
6767
  switch (action.type) {
6617
6768
  case "key":
6618
6769
  return handleKey2(state, action.key);
@@ -6630,13 +6781,13 @@ function reduce(state, action) {
6630
6781
  goal: "",
6631
6782
  taskView: null,
6632
6783
  pendingApprovals: [],
6633
- queuedPrompts: import_core29.EMPTY_HOLD,
6784
+ queuedPrompts: import_core30.EMPTY_HOLD,
6634
6785
  stopArmed: false,
6635
6786
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
6636
6787
  });
6637
6788
  case "chatRestored":
6638
6789
  if (stale(state, action.sessionId)) return step(state);
6639
- return step({ ...state, conversation: (0, import_core29.fromTranscript)(action.history, action.researchLog, action.plannerUsage), scroll: 0 });
6790
+ return step({ ...state, conversation: (0, import_core30.fromTranscript)(action.history, action.researchLog, action.plannerUsage), scroll: 0 });
6640
6791
  case "planUpdated": {
6641
6792
  if (stale(state, action.sessionId)) return step(state);
6642
6793
  const isolation = isolationOfPlan(action.plan);
@@ -6671,6 +6822,9 @@ function reduce(state, action) {
6671
6822
  case "taskLog":
6672
6823
  if (stale(state, action.sessionId)) return step(state);
6673
6824
  return step(announceApprovals(taskLogArrived(state, action), action));
6825
+ case "taskViewRequested":
6826
+ if (stale(state, action.sessionId)) return step(state);
6827
+ return openTaskView(state, action.sessionId, action.taskId);
6674
6828
  case "taskLogLoaded":
6675
6829
  if (stale(state, action.sessionId)) return step(state);
6676
6830
  return step(taskLogLoaded(state, action));
@@ -6682,14 +6836,14 @@ function reduce(state, action) {
6682
6836
  const existing = findTask(state.tasks, action.taskId);
6683
6837
  if (existing && isTaskRunning(existing)) return step(state);
6684
6838
  const tasks = state.tasks.map((t) => t.id === action.taskId && !isTaskRunning(t) ? { ...t, status: "in_progress" } : t);
6685
- const spoken = say(state, "system", (0, import_core29.taskStartedNotice)(action.title, action.runner));
6839
+ const spoken = say(state, "system", (0, import_core30.taskStartedNotice)(action.title, action.runner));
6686
6840
  return step({ ...spoken, tasks, status: "executing", busyLabel: runLabel(tasks) });
6687
6841
  }
6688
6842
  case "taskStatus": {
6689
6843
  if (stale(state, action.sessionId) || !state.tasks.some((t) => t.id === action.taskId)) return step(state);
6690
6844
  const tasks = state.tasks.map((t) => t.id === action.taskId ? { ...t, status: action.status, awaitingReason: void 0 } : t);
6691
6845
  const status = runStatus(state, tasks);
6692
- return step({ ...state, status, tasks, busyLabel: status === "executing" ? runLabel(tasks) : state.busyLabel });
6846
+ return step({ ...state, status, tasks, busyLabel: status === "executing" ? runLabel(tasks, state.gate) : state.busyLabel });
6693
6847
  }
6694
6848
  case "tasksStatus": {
6695
6849
  if (stale(state, action.sessionId)) return step(state);
@@ -6704,15 +6858,20 @@ function reduce(state, action) {
6704
6858
  const awaitingReason = update.awaitingReason;
6705
6859
  const continuable = update.continuable === true;
6706
6860
  const awaitingApproval = update.awaitingApproval;
6707
- if (update.status !== t.status || idleSince !== (t.idleSince ?? null) || !sameIsolation(isolation, t.isolation) || !sameTransport(transport, t.transport) || awaitingReason !== t.awaitingReason || continuable !== (t.continuable ?? false) || awaitingApproval !== t.awaitingApproval) {
6861
+ const mergeGate = update.mergeGate;
6862
+ const forcedPastGate = update.forcedPastGate;
6863
+ if (update.status !== t.status || idleSince !== (t.idleSince ?? null) || !sameIsolation(isolation, t.isolation) || !sameTransport(transport, t.transport) || awaitingReason !== t.awaitingReason || continuable !== (t.continuable ?? false) || awaitingApproval !== t.awaitingApproval || !sameList(mergeGate, t.mergeGate) || !sameList(forcedPastGate, t.forcedPastGate)) {
6708
6864
  changed = true;
6709
- return { ...t, status: update.status, idleSince, isolation, transport, awaitingReason, continuable, awaitingApproval };
6865
+ return { ...t, status: update.status, idleSince, isolation, transport, awaitingReason, continuable, awaitingApproval, mergeGate, forcedPastGate };
6710
6866
  }
6711
6867
  return t;
6712
6868
  });
6713
- if (!changed) return step(state);
6714
- const status = runStatus(state, tasks);
6715
- return step({ ...state, status, tasks, busyLabel: status === "executing" ? runLabel(tasks) : state.busyLabel });
6869
+ const gate = action.gate === void 0 ? state.gate : action.gate;
6870
+ const gateChanged = !sameGate(gate, state.gate);
6871
+ if (!changed && !gateChanged) return step(state);
6872
+ const next = { ...state, tasks, gate };
6873
+ const status = runStatus(next, tasks);
6874
+ return step({ ...next, status, busyLabel: status === "executing" ? runLabel(tasks, gate) : state.busyLabel });
6716
6875
  }
6717
6876
  case "queueReady": {
6718
6877
  if (stale(state, action.sessionId)) return step(state);
@@ -6786,7 +6945,7 @@ function reduce(state, action) {
6786
6945
  busyLabel: "",
6787
6946
  planApproved: false,
6788
6947
  pendingApprovals: [],
6789
- queuedPrompts: import_core29.EMPTY_HOLD,
6948
+ queuedPrompts: import_core30.EMPTY_HOLD,
6790
6949
  stopArmed: false,
6791
6950
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
6792
6951
  });
@@ -6845,9 +7004,16 @@ function reduce(state, action) {
6845
7004
  function sameTransport(a, b) {
6846
7005
  return a?.kind === b?.kind && a?.fallback === b?.fallback;
6847
7006
  }
7007
+ function sameList(a, b) {
7008
+ return (a ?? []).join("\0") === (b ?? []).join("\0");
7009
+ }
7010
+ function sameGate(a, b) {
7011
+ if (!a || !b) return a === b;
7012
+ return a.paused === b.paused && JSON.stringify(a.handoff) === JSON.stringify(b.handoff);
7013
+ }
6848
7014
  function runStatus(state, tasks) {
6849
7015
  if (tasks.some(isTaskRunning)) return "executing";
6850
- if (tasks.some((t) => t.status === "awaiting_user")) return "executing";
7016
+ if (tasks.some((t) => t.status === "awaiting_user") || state.gate?.paused) return "executing";
6851
7017
  return plannerInFlight(state) ? state.status : "idle";
6852
7018
  }
6853
7019
  function interrupt(state) {
@@ -6915,7 +7081,7 @@ function submit(state) {
6915
7081
  return step(cleared, [{ type: "sendTaskMessage", sessionId: state.sessionId, taskId, text }]);
6916
7082
  }
6917
7083
  if (plannerInFlight(state)) {
6918
- return step({ ...cleared, queuedPrompts: (0, import_core29.holdPrompt)(state.queuedPrompts, text) });
7084
+ return step({ ...cleared, queuedPrompts: (0, import_core30.holdPrompt)(state.queuedPrompts, text) });
6919
7085
  }
6920
7086
  const spoken = say(cleared, "user", text);
6921
7087
  const effect = state.sessionId ? { type: "sendMessage", sessionId: state.sessionId, message: text } : { type: "startConversation", goal: text };
@@ -6975,10 +7141,10 @@ function createApp(deps) {
6975
7141
 
6976
7142
  // src/tui/effects.ts
6977
7143
  var import_child_process4 = require("child_process");
6978
- var import_core31 = require("@ordewell/core");
7144
+ var import_core32 = require("@ordewell/core");
6979
7145
 
6980
7146
  // src/tui/inbound.ts
6981
- var import_core30 = require("@ordewell/core");
7147
+ var import_core31 = require("@ordewell/core");
6982
7148
  var CONVERSATION_TYPES = /* @__PURE__ */ new Set([
6983
7149
  "planner_message",
6984
7150
  "planner_turn_started",
@@ -7193,12 +7359,15 @@ function dispatchLifecycle(dispatch, event, sessionId) {
7193
7359
  idleSince: task.idleSince ?? null,
7194
7360
  isolation: task.isolation,
7195
7361
  transport: task.transport,
7196
- awaitingReason: (0, import_core30.isAwaitingReason)(task.awaitingReason) ? task.awaitingReason : void 0,
7362
+ awaitingReason: (0, import_core31.isAwaitingReason)(task.awaitingReason) ? task.awaitingReason : void 0,
7197
7363
  ...task.continuable ? { continuable: true } : {},
7198
- awaitingApproval: typeof task.awaitingApproval === "number" && task.awaitingApproval > 0 ? task.awaitingApproval : void 0
7364
+ awaitingApproval: typeof task.awaitingApproval === "number" && task.awaitingApproval > 0 ? task.awaitingApproval : void 0,
7365
+ ...task.mergeGate?.length ? { mergeGate: task.mergeGate.map(String) } : {},
7366
+ ...task.forcedPastGate?.length ? { forcedPastGate: task.forcedPastGate.map(String) } : {}
7199
7367
  };
7200
7368
  }
7201
- dispatch({ type: "tasksStatus", updates, sessionId });
7369
+ const gate = event.gate ? { paused: event.gate.paused === true, handoff: { repos: event.gate.repos, landed: event.gate.landed } } : null;
7370
+ dispatch({ type: "tasksStatus", updates, gate, sessionId });
7202
7371
  return;
7203
7372
  }
7204
7373
  case "task_started":
@@ -7207,7 +7376,7 @@ function dispatchLifecycle(dispatch, event, sessionId) {
7207
7376
  // The extension shows these in its checkpoint panel; here they are
7208
7377
  // transcript lines, which is the TUI's equivalent surface.
7209
7378
  case "checkpoint":
7210
- dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core30.truncateCheckpointSummary)(event.summary)}` });
7379
+ dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core31.truncateCheckpointSummary)(event.summary)}` });
7211
7380
  return;
7212
7381
  case "review_needed":
7213
7382
  dispatch({ type: "notice", message: "Plan needs your sign-off \u2014 /approve to continue." });
@@ -7405,7 +7574,14 @@ async function perform(effect, deps) {
7405
7574
  }
7406
7575
  return;
7407
7576
  }
7577
+ // A task with a saved log ran structured, whatever its `transport` says
7578
+ // after a reload; it has no tmux window, so open the log rather than fail.
7408
7579
  case "openTaskTerminal": {
7580
+ const saved = await api.getTaskLogAttempts(effect.sessionId, effect.taskId, workspace).catch(() => []);
7581
+ if (saved.length > 0) {
7582
+ dispatch({ type: "taskViewRequested", sessionId: effect.sessionId, taskId: effect.taskId });
7583
+ return;
7584
+ }
7409
7585
  const result = await deps.openTerminal(effect.sessionId, effect.taskId);
7410
7586
  dispatch(result.ok ? { type: "notice", message: result.message } : { type: "failed", message: result.message });
7411
7587
  return;
@@ -7460,7 +7636,7 @@ async function perform(effect, deps) {
7460
7636
  case "setTransport": {
7461
7637
  const result = await api.sendCommand("transport", { action: effect.transport });
7462
7638
  if (result.settings) dispatch({ type: "settingsLoaded", settings: result.settings });
7463
- dispatch({ type: "notice", message: `Runner transport is ${effect.transport} (experimental) \u2014 it applies from the next run.` });
7639
+ dispatch({ type: "notice", message: `Runner transport is ${effect.transport} \u2014 it applies from the next run.` });
7464
7640
  return;
7465
7641
  }
7466
7642
  case "setModel":
@@ -7470,7 +7646,7 @@ async function perform(effect, deps) {
7470
7646
  dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
7471
7647
  return;
7472
7648
  case "setPlanner": {
7473
- const meta = import_core31.ALL_PROVIDERS[effect.provider];
7649
+ const meta = import_core32.ALL_PROVIDERS[effect.provider];
7474
7650
  if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
7475
7651
  const env = { AI_PROVIDER: effect.provider };
7476
7652
  const settings = await api.updateSettings({ env });
@@ -7482,7 +7658,7 @@ async function perform(effect, deps) {
7482
7658
  });
7483
7659
  dispatch({
7484
7660
  type: "notice",
7485
- message: `${(0, import_core31.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
7661
+ message: `${(0, import_core32.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
7486
7662
  });
7487
7663
  await loadModels(deps);
7488
7664
  return;
@@ -7494,7 +7670,7 @@ async function perform(effect, deps) {
7494
7670
  dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
7495
7671
  return;
7496
7672
  case "setApiKey": {
7497
- const meta = import_core31.ALL_PROVIDERS[effect.provider];
7673
+ const meta = import_core32.ALL_PROVIDERS[effect.provider];
7498
7674
  if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
7499
7675
  await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
7500
7676
  deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
@@ -7532,7 +7708,7 @@ async function perform(effect, deps) {
7532
7708
  return;
7533
7709
  case "setAutonomous":
7534
7710
  deps.setEnvVar("ORDEWELL_AUTONOMOUS_MODE", String(effect.enabled));
7535
- dispatch({ type: "notice", message: `Autonomous mode ${effect.enabled ? "on" : "off"}.` });
7711
+ dispatch({ type: "notice", message: `Autonomy level: ${(0, import_core32.autonomyLevelLabel)(effect.enabled)} for new plans.` });
7536
7712
  return;
7537
7713
  case "setMouseCapture":
7538
7714
  deps.setMouseCapture(effect.enabled);
@@ -7603,10 +7779,11 @@ async function perform(effect, deps) {
7603
7779
  return;
7604
7780
  // A conflict or a refusal is an answer, not a fault: either way the user's
7605
7781
  // tree is exactly as it was, and the words say what to do next. A full
7606
- // merge leaves nothing to hand over — the daemon has cleared the run up.
7782
+ // merge of a settled run leaves nothing to hand over — the daemon has
7783
+ // cleared the run up; one mid-run leaves the run going (ADR-0020).
7607
7784
  case "isolationMerge": {
7608
7785
  const { ok, message } = mergeOutcome(await api.mergeRun(effect.sessionId), effect.branch, effect.group === true, effect.repaired ?? []);
7609
- if (ok) dispatch({ type: "runCleared", sessionId: effect.sessionId });
7786
+ if (ok && !effect.midRun) dispatch({ type: "runCleared", sessionId: effect.sessionId });
7610
7787
  dispatch({ type: ok ? "notice" : "failed", message });
7611
7788
  return;
7612
7789
  }
@@ -7665,7 +7842,7 @@ function osc52(text) {
7665
7842
  function copySelection(deps, text) {
7666
7843
  if (!text) return;
7667
7844
  const lines = text.split("\n").length;
7668
- const command = (0, import_core31.clipboardCopyCommand)(deps.hasBin);
7845
+ const command = (0, import_core32.clipboardCopyCommand)(deps.hasBin);
7669
7846
  if (command) {
7670
7847
  try {
7671
7848
  (deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
@@ -8055,7 +8232,7 @@ async function handleTui(subArgs) {
8055
8232
  }
8056
8233
  const workspace = flag(subArgs, "--workspace") || process.cwd();
8057
8234
  try {
8058
- (0, import_core32.assertWorkspaceExists)(workspace);
8235
+ (0, import_core33.assertWorkspaceExists)(workspace);
8059
8236
  } catch {
8060
8237
  console.error(
8061
8238
  `Workspace "${workspace}" does not exist.
@@ -8064,7 +8241,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
8064
8241
  );
8065
8242
  process.exit(1);
8066
8243
  }
8067
- registerSkillCommands((0, import_core32.createSkillsService)(workspace).listSkills());
8244
+ registerSkillCommands((0, import_core33.createSkillsService)(workspace).listSkills());
8068
8245
  const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
8069
8246
  const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
8070
8247
  const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
@@ -8081,7 +8258,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
8081
8258
  workspace,
8082
8259
  port,
8083
8260
  dispatch: (action) => app.dispatch(action),
8084
- newSessionId: () => (0, import_core32.mintSessionId)(),
8261
+ newSessionId: () => (0, import_core33.mintSessionId)(),
8085
8262
  setEnvVar: (key, value) => {
8086
8263
  writeEnvVar(findEnvFile(), key, value);
8087
8264
  process.env[key] = value;
@@ -8179,6 +8356,7 @@ var COMMANDS = {
8179
8356
  "task-effort": handleTaskEffort,
8180
8357
  "task-mode": handleTaskMode,
8181
8358
  "task-deps": handleTaskDeps,
8359
+ "task-ops": handleTaskOps,
8182
8360
  terminal: handleTerminal,
8183
8361
  "sessions": handleSessions,
8184
8362
  fork: handleFork,
@@ -8204,7 +8382,7 @@ function cliVersion() {
8204
8382
  // src/main.ts
8205
8383
  var TUI_FLAGS = /* @__PURE__ */ new Set(["--workspace", "--port"]);
8206
8384
  async function main() {
8207
- (0, import_core33.migrateOldConfigDir)();
8385
+ (0, import_core34.migrateOldConfigDir)();
8208
8386
  loadEnvFile();
8209
8387
  const argv = process.argv.slice(2);
8210
8388
  if (TUI_FLAGS.has(argv[0] ?? "")) argv.unshift("tui");