@ordewell/cli 0.6.0 → 0.6.2
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.
- package/dist/main.js +232 -58
- package/package.json +3 -3
package/dist/main.js
CHANGED
|
@@ -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
|
|
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)
|
|
@@ -412,6 +413,8 @@ var ApiClient = class _ApiClient {
|
|
|
412
413
|
static defaultWorkspace;
|
|
413
414
|
port;
|
|
414
415
|
workspace;
|
|
416
|
+
/** A session's open execution stream, so a new one can end it: one run is reported once. */
|
|
417
|
+
executionStreams = /* @__PURE__ */ new Map();
|
|
415
418
|
constructor(port, workspace) {
|
|
416
419
|
this.port = port || DEFAULT_PORT;
|
|
417
420
|
this.workspace = workspace ?? _ApiClient.defaultWorkspace ?? process.cwd();
|
|
@@ -713,6 +716,17 @@ var ApiClient = class _ApiClient {
|
|
|
713
716
|
}
|
|
714
717
|
return { plan: res.data.plan, summary: res.data.summary, keptMessages: res.data.keptMessages };
|
|
715
718
|
}
|
|
719
|
+
/** The dependencies a task waits on at its merge gate (ADR-0020); empty when nothing gates it. */
|
|
720
|
+
async getMergeGate(sessionId, taskId) {
|
|
721
|
+
const res = await this.httpRequest(
|
|
722
|
+
"GET",
|
|
723
|
+
`/api/plans/${sessionId}/tasks/${taskId}/merge-gate`
|
|
724
|
+
);
|
|
725
|
+
if (res.status !== 200) {
|
|
726
|
+
throw new Error(res.data?.error || "Could not read the merge gate");
|
|
727
|
+
}
|
|
728
|
+
return res.data.mergeGate ?? [];
|
|
729
|
+
}
|
|
716
730
|
async reviewRunDiff(sessionId) {
|
|
717
731
|
const res = await this.httpRequest("GET", `/api/plans/${sessionId}/isolation/diff`);
|
|
718
732
|
if (res.status !== 200) {
|
|
@@ -837,26 +851,36 @@ var ApiClient = class _ApiClient {
|
|
|
837
851
|
}
|
|
838
852
|
streamExecution(sessionId, onEvent, onReady) {
|
|
839
853
|
return new Promise((resolve3, reject) => {
|
|
854
|
+
this.executionStreams.get(sessionId)?.();
|
|
840
855
|
const socket = this.openSessionSocket(sessionId);
|
|
841
856
|
let resolved = false;
|
|
842
857
|
let opened = false;
|
|
858
|
+
const forget = () => {
|
|
859
|
+
if (this.executionStreams.get(sessionId) === end) this.executionStreams.delete(sessionId);
|
|
860
|
+
};
|
|
861
|
+
const end = () => {
|
|
862
|
+
forget();
|
|
863
|
+
if (resolved) return;
|
|
864
|
+
resolved = true;
|
|
865
|
+
socket.close();
|
|
866
|
+
resolve3();
|
|
867
|
+
};
|
|
868
|
+
this.executionStreams.set(sessionId, end);
|
|
843
869
|
socket.on("open", () => {
|
|
844
870
|
opened = true;
|
|
845
871
|
onReady?.();
|
|
846
872
|
});
|
|
847
873
|
socket.on("message", (data) => {
|
|
874
|
+
if (resolved) return;
|
|
848
875
|
try {
|
|
849
876
|
const event = JSON.parse(data.toString());
|
|
850
877
|
onEvent(event);
|
|
851
|
-
if (
|
|
852
|
-
resolved = true;
|
|
853
|
-
socket.close();
|
|
854
|
-
resolve3();
|
|
855
|
-
}
|
|
878
|
+
if (event.type === "execution_complete" || event.type === "execution_stopped") end();
|
|
856
879
|
} catch {
|
|
857
880
|
}
|
|
858
881
|
});
|
|
859
882
|
socket.on("error", (err) => {
|
|
883
|
+
forget();
|
|
860
884
|
if (!opened) onReady?.(err);
|
|
861
885
|
if (!resolved) {
|
|
862
886
|
resolved = true;
|
|
@@ -864,6 +888,7 @@ var ApiClient = class _ApiClient {
|
|
|
864
888
|
}
|
|
865
889
|
});
|
|
866
890
|
socket.on("close", () => {
|
|
891
|
+
forget();
|
|
867
892
|
if (!opened) onReady?.(new Error("Execution stream closed before connecting"));
|
|
868
893
|
if (!resolved) {
|
|
869
894
|
resolved = true;
|
|
@@ -1252,7 +1277,7 @@ Plan: ${tasks.length} task${tasks.length === 1 ? "" : "s"} (${aiCount} AI, ${man
|
|
|
1252
1277
|
console.log(`Session: ${sessionId}
|
|
1253
1278
|
`);
|
|
1254
1279
|
for (const t of tasks) {
|
|
1255
|
-
const typeIcon = t.type === "user" ? "[MAN]" : "[ AI]";
|
|
1280
|
+
const typeIcon = t.type === "user" ? "[MAN]" : t.ops ? "[OPS]" : "[ AI]";
|
|
1256
1281
|
const deps = t.dependencies?.length ? ` \u2190 ${t.dependencies.join(", ")}` : "";
|
|
1257
1282
|
console.log(` ${(0, import_core7.taskOrderLabel)(t).padStart(2)}. ${typeIcon} ${t.title}${modelSuffix(t)}${deps}`);
|
|
1258
1283
|
for (const sub of t.subtasks || []) {
|
|
@@ -1264,6 +1289,9 @@ Plan: ${tasks.length} task${tasks.length === 1 ? "" : "s"} (${aiCount} AI, ${man
|
|
|
1264
1289
|
if (manCount > 0) {
|
|
1265
1290
|
console.log("\n [MAN] = manual step \u2014 run `ordewell tui` to work through it");
|
|
1266
1291
|
}
|
|
1292
|
+
if (tasks.some((t) => t.ops)) {
|
|
1293
|
+
console.log(" [OPS] = ops task \u2014 runs in your checkout once the work it depends on is merged (`ordewell handoff merge`)");
|
|
1294
|
+
}
|
|
1267
1295
|
}
|
|
1268
1296
|
var PLANNER_ENV = ["AI_PROVIDER", "ORCHESTRATOR_MODEL", "ORDEWELL_PLANNER_EFFORT"];
|
|
1269
1297
|
async function forwardPlannerEnv(api) {
|
|
@@ -1456,7 +1484,7 @@ function isolationOfPlan(plan) {
|
|
|
1456
1484
|
// `tasksStatus` update fills it in once the stream catches up.
|
|
1457
1485
|
...repairedFiles ? { repairedFiles } : {}
|
|
1458
1486
|
};
|
|
1459
|
-
if (state === "integrated") {
|
|
1487
|
+
if (state === "integrated" && record.inHead !== true) {
|
|
1460
1488
|
landed.push({ taskId, order: Number(record.order ?? 0), title: String(record.title ?? taskId), changed, ...repairedFiles ? { repairedFiles } : {} });
|
|
1461
1489
|
}
|
|
1462
1490
|
}
|
|
@@ -1531,6 +1559,9 @@ function iconFor(status) {
|
|
|
1531
1559
|
// src/commands/run.ts
|
|
1532
1560
|
var import_core9 = require("@ordewell/core");
|
|
1533
1561
|
|
|
1562
|
+
// src/commands/shared.ts
|
|
1563
|
+
var import_readline2 = require("readline");
|
|
1564
|
+
|
|
1534
1565
|
// src/catalog.ts
|
|
1535
1566
|
function normalizeCatalog(result) {
|
|
1536
1567
|
const runnersByModel = /* @__PURE__ */ new Map();
|
|
@@ -1586,6 +1617,16 @@ function fail(...lines) {
|
|
|
1586
1617
|
for (const line of lines) console.error(line);
|
|
1587
1618
|
process.exit(1);
|
|
1588
1619
|
}
|
|
1620
|
+
async function askYesNo(question) {
|
|
1621
|
+
if (!process.stdin.isTTY) return false;
|
|
1622
|
+
const rl = (0, import_readline2.createInterface)({ input: process.stdin, output: process.stderr });
|
|
1623
|
+
try {
|
|
1624
|
+
const answer = await new Promise((resolve3) => rl.question(`${question} [y/N] `, resolve3));
|
|
1625
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1626
|
+
} finally {
|
|
1627
|
+
rl.close();
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1589
1630
|
function writeEnv(env) {
|
|
1590
1631
|
const envFile = findEnvFile();
|
|
1591
1632
|
for (const [key, value] of Object.entries(env)) {
|
|
@@ -1611,7 +1652,8 @@ function toTaskView(task) {
|
|
|
1611
1652
|
assignedRunner: task.assignedRunner || void 0,
|
|
1612
1653
|
taskMode: task.taskMode || void 0,
|
|
1613
1654
|
assignedModel: task.assignedModel ?? void 0,
|
|
1614
|
-
subtasks: [...task.subtasks ?? []].sort((a, b) => a.order - b.order).map(toTaskView)
|
|
1655
|
+
subtasks: [...task.subtasks ?? []].sort((a, b) => a.order - b.order).map(toTaskView),
|
|
1656
|
+
...task.ops ? { ops: true } : {}
|
|
1615
1657
|
};
|
|
1616
1658
|
}
|
|
1617
1659
|
function taskViews(plan) {
|
|
@@ -1658,6 +1700,12 @@ async function followExecution(api, sessionId, start, onBlocked) {
|
|
|
1658
1700
|
lastPrinted = output;
|
|
1659
1701
|
}
|
|
1660
1702
|
}
|
|
1703
|
+
let pausedAtGate = false;
|
|
1704
|
+
let noteBlocked = () => {
|
|
1705
|
+
};
|
|
1706
|
+
const blockedSeen = new Promise((resolve3) => {
|
|
1707
|
+
noteBlocked = resolve3;
|
|
1708
|
+
});
|
|
1661
1709
|
let settleReady = () => {
|
|
1662
1710
|
};
|
|
1663
1711
|
const streamReady = new Promise((resolve3, reject) => {
|
|
@@ -1674,6 +1722,9 @@ async function followExecution(api, sessionId, start, onBlocked) {
|
|
|
1674
1722
|
}
|
|
1675
1723
|
if (event.type === "status_update" && event.tasks) {
|
|
1676
1724
|
printStatus(event.tasks);
|
|
1725
|
+
const paused = event.gate?.paused === true;
|
|
1726
|
+
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");
|
|
1727
|
+
pausedAtGate = paused;
|
|
1677
1728
|
}
|
|
1678
1729
|
if (event.type === "review_needed") {
|
|
1679
1730
|
console.log("\nPlan needs your sign-off \u2014 run `ordewell approve` to continue.");
|
|
@@ -1693,6 +1744,7 @@ Done. ${summary.completed || 0} completed, ${summary.failed || 0} failed, ${summ
|
|
|
1693
1744
|
if (event.type === "isolation_blocked") {
|
|
1694
1745
|
blocked = event.message;
|
|
1695
1746
|
dirtyRepos = event.repos ?? [];
|
|
1747
|
+
noteBlocked();
|
|
1696
1748
|
}
|
|
1697
1749
|
if (event.type === "isolation_handoff") {
|
|
1698
1750
|
const n = event.landed.length;
|
|
@@ -1710,7 +1762,7 @@ Run finished on ${handoffBranch(event)}${where} \u2014 ${n} task${n === 1 ? "" :
|
|
|
1710
1762
|
void stream.catch(settleReady);
|
|
1711
1763
|
await streamReady.catch(streamFailed);
|
|
1712
1764
|
await start();
|
|
1713
|
-
await stream.catch(streamFailed);
|
|
1765
|
+
await Promise.race([stream, blockedSeen]).catch(streamFailed);
|
|
1714
1766
|
if (blocked === null) return;
|
|
1715
1767
|
if (!onBlocked) {
|
|
1716
1768
|
await api.stopExecution(sessionId);
|
|
@@ -2330,13 +2382,23 @@ async function withResolvedTask(subArgs, usage, injectedApi, run) {
|
|
|
2330
2382
|
}
|
|
2331
2383
|
await run(api, sessionId, taskId, plan);
|
|
2332
2384
|
}
|
|
2385
|
+
var PASSES_GATE = /* @__PURE__ */ new Set(["run", "force-start"]);
|
|
2386
|
+
async function confirmPastGate(subArgs, api, sessionId, taskId, confirm) {
|
|
2387
|
+
if (hasFlag(subArgs, "--yes")) return;
|
|
2388
|
+
const unmerged = await api.getMergeGate(sessionId, taskId).catch(() => []);
|
|
2389
|
+
if (unmerged.length === 0) return;
|
|
2390
|
+
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?`;
|
|
2391
|
+
if (!await confirm(question)) fail("Not started \u2014 nothing was changed. Pass --yes to start it without a prompt.");
|
|
2392
|
+
}
|
|
2333
2393
|
function makeHandler(action, command = action) {
|
|
2334
|
-
|
|
2394
|
+
const yes = PASSES_GATE.has(action) ? " [--yes]" : "";
|
|
2395
|
+
return async function handle(subArgs, injectedApi, confirm = askYesNo) {
|
|
2335
2396
|
await withResolvedTask(
|
|
2336
2397
|
subArgs,
|
|
2337
|
-
`Usage: ordewell ${command} <task-id-or-order> [--session-id <id>] [--workspace /path]`,
|
|
2398
|
+
`Usage: ordewell ${command} <task-id-or-order> [--session-id <id>] [--workspace /path]${yes}`,
|
|
2338
2399
|
injectedApi,
|
|
2339
2400
|
async (api, sessionId, taskId) => {
|
|
2401
|
+
if (PASSES_GATE.has(action)) await confirmPastGate(subArgs, api, sessionId, taskId, confirm);
|
|
2340
2402
|
try {
|
|
2341
2403
|
await api.taskControl(sessionId, taskId, action);
|
|
2342
2404
|
console.log(PAST_TENSE[action]);
|
|
@@ -2613,9 +2675,6 @@ async function handleRewind(subArgs, injectedApi) {
|
|
|
2613
2675
|
console.log(fork.rewoundMessage);
|
|
2614
2676
|
}
|
|
2615
2677
|
|
|
2616
|
-
// src/commands/handoff.ts
|
|
2617
|
-
var import_readline2 = require("readline");
|
|
2618
|
-
|
|
2619
2678
|
// src/tui/transcript.ts
|
|
2620
2679
|
var import_core15 = require("@ordewell/core");
|
|
2621
2680
|
function show(state, { view: conversation, gate: turnGate }) {
|
|
@@ -2808,8 +2867,12 @@ var reposOf2 = (handoff) => handoff.repos.map((r) => r.path).join(", ");
|
|
|
2808
2867
|
var HANDOFF_USAGE = `/handoff [${HANDOFF_ACTIONS.map((a) => a.id).join("|")}]`;
|
|
2809
2868
|
var diffRoom = (rows) => Math.max(1, rows - 3);
|
|
2810
2869
|
var fail2 = (state, content) => ({ state: say(state, "error", content), effects: [] });
|
|
2870
|
+
function currentHandoff(state) {
|
|
2871
|
+
return state.gate?.handoff ?? state.handoff;
|
|
2872
|
+
}
|
|
2873
|
+
var midRun = (state) => state.status === "executing";
|
|
2811
2874
|
function handoffCommand(state, arg) {
|
|
2812
|
-
if (!state.sessionId || !state
|
|
2875
|
+
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
2876
|
if (arg === void 0) return { state: { ...state, overlay: { kind: "handoff", index: 0, diff: null } }, effects: [] };
|
|
2814
2877
|
const action = HANDOFF_ACTIONS.find((a) => a.id === arg.toLowerCase());
|
|
2815
2878
|
if (!action) return fail2(state, `Usage: ${HANDOFF_USAGE}`);
|
|
@@ -2826,7 +2889,8 @@ function handoffArrived(state, handoff) {
|
|
|
2826
2889
|
return state.overlay ? told : { ...told, overlay: { kind: "handoff", index: 0, diff: null } };
|
|
2827
2890
|
}
|
|
2828
2891
|
function runHandoffAction(state, id) {
|
|
2829
|
-
const { sessionId
|
|
2892
|
+
const { sessionId } = state;
|
|
2893
|
+
const handoff = currentHandoff(state);
|
|
2830
2894
|
if (!sessionId || !handoff) return fail2(state, "No isolated run to hand off.");
|
|
2831
2895
|
const closed = { ...state, overlay: null };
|
|
2832
2896
|
switch (id) {
|
|
@@ -2835,6 +2899,20 @@ function runHandoffAction(state, id) {
|
|
|
2835
2899
|
case "cleanup":
|
|
2836
2900
|
return { state: closed, effects: [{ type: "isolationCleanup", sessionId, branch: handoffBranch(handoff) }] };
|
|
2837
2901
|
case "merge":
|
|
2902
|
+
if (midRun(state)) {
|
|
2903
|
+
return {
|
|
2904
|
+
state: {
|
|
2905
|
+
...state,
|
|
2906
|
+
overlay: {
|
|
2907
|
+
kind: "confirm",
|
|
2908
|
+
title: isRepoGroup(handoff) ? "Merge what has landed into your branches?" : "Merge what has landed into your branch?",
|
|
2909
|
+
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)}`,
|
|
2910
|
+
action: { kind: "merge-run" }
|
|
2911
|
+
}
|
|
2912
|
+
},
|
|
2913
|
+
effects: []
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2838
2916
|
return {
|
|
2839
2917
|
state: {
|
|
2840
2918
|
...state,
|
|
@@ -2869,20 +2947,23 @@ function runHandoffAction(state, id) {
|
|
|
2869
2947
|
}
|
|
2870
2948
|
function confirmedHandoff(state, kind) {
|
|
2871
2949
|
const closed = { ...state, overlay: null };
|
|
2872
|
-
|
|
2873
|
-
|
|
2950
|
+
const handoff = currentHandoff(state);
|
|
2951
|
+
if (!state.sessionId || !handoff) return { state: closed, effects: [] };
|
|
2952
|
+
const { sessionId } = state;
|
|
2874
2953
|
const repaired = repairedLanded(handoff);
|
|
2875
2954
|
const effect = kind === "merge-run" ? {
|
|
2876
2955
|
type: "isolationMerge",
|
|
2877
2956
|
sessionId,
|
|
2878
2957
|
branch: handoffBranch(handoff),
|
|
2879
2958
|
...isRepoGroup(handoff) ? { group: true } : {},
|
|
2880
|
-
...repaired.length > 0 ? { repaired } : {}
|
|
2959
|
+
...repaired.length > 0 ? { repaired } : {},
|
|
2960
|
+
...midRun(state) ? { midRun: true } : {}
|
|
2881
2961
|
} : { type: "isolationDiscard", sessionId, branch: handoffBranch(handoff) };
|
|
2882
2962
|
return { state: closed, effects: [effect] };
|
|
2883
2963
|
}
|
|
2884
2964
|
function showDiff(state, diff) {
|
|
2885
|
-
|
|
2965
|
+
const handoff = currentHandoff(state);
|
|
2966
|
+
if (diff.trim() === "") return say(state, "system", `Nothing differs from ${handoff ? handoffBase(handoff, 8) : "the base commit"}.`);
|
|
2886
2967
|
const index = state.overlay?.kind === "handoff" ? state.overlay.index : 0;
|
|
2887
2968
|
const lines = sanitize(diff.replace(/\t/g, " ")).split("\n");
|
|
2888
2969
|
return { ...state, overlay: { kind: "handoff", index, diff: { lines, scroll: 0 } } };
|
|
@@ -2955,6 +3036,7 @@ function clearIsolation(state) {
|
|
|
2955
3036
|
return {
|
|
2956
3037
|
...state,
|
|
2957
3038
|
handoff: null,
|
|
3039
|
+
gate: null,
|
|
2958
3040
|
tasks: mapTasks(state.tasks, ({ isolation: _dropped, ...task }) => task)
|
|
2959
3041
|
};
|
|
2960
3042
|
}
|
|
@@ -2972,16 +3054,6 @@ function isolationForPlan(tasks, record, previous) {
|
|
|
2972
3054
|
|
|
2973
3055
|
// src/commands/handoff.ts
|
|
2974
3056
|
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
3057
|
async function handleHandoff(subArgs, injectedApi, confirm = askYesNo) {
|
|
2986
3058
|
const action = positionals(subArgs)[0]?.toLowerCase();
|
|
2987
3059
|
if (action === void 0) {
|
|
@@ -3016,7 +3088,7 @@ async function handleHandoff(subArgs, injectedApi, confirm = askYesNo) {
|
|
|
3016
3088
|
return;
|
|
3017
3089
|
}
|
|
3018
3090
|
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
|
|
3091
|
+
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
3092
|
const { ok, message } = mergeOutcome(await attempt(() => api.mergeRun(sessionId)), branch, group, repairedLanded(handoff));
|
|
3021
3093
|
if (ok) console.log(message);
|
|
3022
3094
|
else fail(message);
|
|
@@ -3600,6 +3672,24 @@ Dependencies \xB7 #${task.order} ${task.title}
|
|
|
3600
3672
|
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.`);
|
|
3601
3673
|
});
|
|
3602
3674
|
}
|
|
3675
|
+
var OPS_USAGE = "Usage: ordewell task-ops <task-id-or-order> [on|off] [--session-id <id>]";
|
|
3676
|
+
async function handleTaskOps(subArgs, injectedApi) {
|
|
3677
|
+
await withTask(subArgs, OPS_USAGE, injectedApi, async (api, sessionId, task, tasks, value) => {
|
|
3678
|
+
if (!value) {
|
|
3679
|
+
console.log(`
|
|
3680
|
+
#${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"}.`);
|
|
3681
|
+
console.log(`
|
|
3682
|
+
${OPS_USAGE}`);
|
|
3683
|
+
return;
|
|
3684
|
+
}
|
|
3685
|
+
const to = value.toLowerCase();
|
|
3686
|
+
if (to !== "on" && to !== "off") fail(OPS_USAGE);
|
|
3687
|
+
if (task.type !== "ai") fail("Only an AI task can be an ops task \u2014 a manual task already runs outside any worktree.");
|
|
3688
|
+
if (!tasks.some((t) => t.id === task.id)) fail("A subtask runs with its parent; make the parent an ops task instead.");
|
|
3689
|
+
await api.updateTask(sessionId, task.id, { ops: to === "on" });
|
|
3690
|
+
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.`);
|
|
3691
|
+
});
|
|
3692
|
+
}
|
|
3603
3693
|
|
|
3604
3694
|
// src/tui/index.ts
|
|
3605
3695
|
var import_core33 = require("@ordewell/core");
|
|
@@ -3757,6 +3847,7 @@ var SLASH_COMMANDS = [
|
|
|
3757
3847
|
{ name: "task-effort", usage: "/task-effort <id> [level]", description: "Choose or set a task's thinking effort", category: "tasks" },
|
|
3758
3848
|
{ name: "task-mode", usage: "/task-mode <id> [mode]", description: "Choose or set a task's runner mode", category: "tasks" },
|
|
3759
3849
|
{ name: "task-deps", usage: "/task-deps <id>", description: "Edit which earlier tasks a task waits for", category: "tasks" },
|
|
3850
|
+
{ 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" },
|
|
3760
3851
|
// Models & providers
|
|
3761
3852
|
{ name: "planner", usage: "/planner [<provider>]", description: "Choose who plans \u2014 an API provider or a coding agent (no API key)", category: "models" },
|
|
3762
3853
|
{ name: "model", usage: "/model [set <id>]", description: "Show or set the orchestrator model", category: "models" },
|
|
@@ -4271,6 +4362,7 @@ function initialState(overrides = {}) {
|
|
|
4271
4362
|
selection: null,
|
|
4272
4363
|
workspace: process.cwd(),
|
|
4273
4364
|
handoff: null,
|
|
4365
|
+
gate: null,
|
|
4274
4366
|
overlay: null,
|
|
4275
4367
|
pendingApprovals: [],
|
|
4276
4368
|
queuedPrompts: import_core22.EMPTY_HOLD,
|
|
@@ -4346,6 +4438,7 @@ function footerHints(state) {
|
|
|
4346
4438
|
"e effort",
|
|
4347
4439
|
"M mode",
|
|
4348
4440
|
"D deps",
|
|
4441
|
+
"O ops",
|
|
4349
4442
|
"f start",
|
|
4350
4443
|
"E run plan",
|
|
4351
4444
|
"S stop",
|
|
@@ -4577,7 +4670,8 @@ var STATUS_ICON = {
|
|
|
4577
4670
|
var AWAITING_LABEL = {
|
|
4578
4671
|
input: "waiting for your input",
|
|
4579
4672
|
checkpoint: "checkpoint",
|
|
4580
|
-
conflict: "merge conflict"
|
|
4673
|
+
conflict: "merge conflict",
|
|
4674
|
+
"files-changed": "changed tracked files"
|
|
4581
4675
|
};
|
|
4582
4676
|
function approvalLabel(count) {
|
|
4583
4677
|
return count > 1 ? `waiting for approval (${count})` : "waiting for approval";
|
|
@@ -4656,7 +4750,7 @@ function taskLines(state, row, index, cols) {
|
|
|
4656
4750
|
const paint = idle ? style.cyan : STATUS_MARK[task.status] ?? ((t) => t);
|
|
4657
4751
|
const rawIcon = idle ? IDLE_ICON : running ? SPINNER[state.spinnerFrame % SPINNER.length] : STATUS_ICON[task.status] ?? "\xB7";
|
|
4658
4752
|
const icon = paint(rawIcon);
|
|
4659
|
-
const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : style.grey(" AI");
|
|
4753
|
+
const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : task.ops ? style.magenta("OPS") : style.grey(" AI");
|
|
4660
4754
|
const caret = selected ? style.cyan("\u276F") : " ";
|
|
4661
4755
|
const orderLabel = parent ? (0, import_core23.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
|
|
4662
4756
|
const indent = parent ? " " : "";
|
|
@@ -4696,6 +4790,16 @@ function taskLines(state, row, index, cols) {
|
|
|
4696
4790
|
const attempt = task.isolation.repair ? ` (attempt ${task.isolation.repair.attempt}/${task.isolation.repair.limit})` : "";
|
|
4697
4791
|
lines.push(style.yellow(truncate(`${bodyPad}\u21BB repairing conflict${files}${attempt}`, cols)));
|
|
4698
4792
|
}
|
|
4793
|
+
if (task.mergeGate?.length) {
|
|
4794
|
+
const deps = task.mergeGate.map((id) => {
|
|
4795
|
+
const dep = state.tasks.find((t) => t.id === id);
|
|
4796
|
+
return dep ? `#${dep.order}` : id;
|
|
4797
|
+
});
|
|
4798
|
+
lines.push(style.yellow(truncate(`${bodyPad}\u23F8 waits for Merge all \u2014 ${deps.join(", ")} not merged into your branch yet`, cols)));
|
|
4799
|
+
}
|
|
4800
|
+
if (task.status === "awaiting_user" && task.awaitingReason === "files-changed") {
|
|
4801
|
+
lines.push(style.yellow(truncate(`${bodyPad}\u26A0 an ops task changed tracked files \u2014 check them, then m done or /retry`, cols)));
|
|
4802
|
+
}
|
|
4699
4803
|
if (task.isolation?.state === "integrated" && task.isolation.repairedFiles?.length) {
|
|
4700
4804
|
lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${(0, import_core23.capConflictFiles)(task.isolation.repairedFiles)}`, cols)));
|
|
4701
4805
|
}
|
|
@@ -4710,6 +4814,12 @@ function taskLines(state, row, index, cols) {
|
|
|
4710
4814
|
lines.push(...taskText("Worktree", task.isolation.worktree, cols, bodyPad));
|
|
4711
4815
|
}
|
|
4712
4816
|
}
|
|
4817
|
+
if (task.ops) {
|
|
4818
|
+
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));
|
|
4819
|
+
}
|
|
4820
|
+
if (task.forcedPastGate?.length) {
|
|
4821
|
+
lines.push(...taskText("Forced", `Started before the work of ${task.forcedPastGate.join(", ")} was merged into your branch.`, cols, bodyPad));
|
|
4822
|
+
}
|
|
4713
4823
|
if (modeInfo?.autonomous) {
|
|
4714
4824
|
lines.push(...taskText("Autonomy", "Runs without permission prompts (Full auto). Change the level with /auto.", cols, bodyPad));
|
|
4715
4825
|
}
|
|
@@ -5040,7 +5150,7 @@ function renderOverlay(state, rows, cols) {
|
|
|
5040
5150
|
return renderPicker(overlay.picker, rows, cols);
|
|
5041
5151
|
}
|
|
5042
5152
|
function renderHandoff(state, overlay, rows, cols) {
|
|
5043
|
-
const handoff = state
|
|
5153
|
+
const handoff = currentHandoff(state);
|
|
5044
5154
|
if (!handoff) return frame("Handoff", [style.grey("Nothing to hand off.")], rows, cols);
|
|
5045
5155
|
if (overlay.diff) {
|
|
5046
5156
|
const room = diffRoom(rows);
|
|
@@ -5596,6 +5706,34 @@ function confirmRemoveTask(state, task) {
|
|
|
5596
5706
|
}
|
|
5597
5707
|
});
|
|
5598
5708
|
}
|
|
5709
|
+
function toggleTaskOps(state, task, subtask, to = !task.ops) {
|
|
5710
|
+
if (!state.sessionId) return fail3(state, "No active plan.");
|
|
5711
|
+
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.");
|
|
5712
|
+
if (subtask) return fail3(state, "A subtask runs with its parent; make the parent an ops task instead.");
|
|
5713
|
+
if (!!task.ops === to) return fail3(state, `#${task.order} is already ${to ? "an ops" : "a change"} task.`);
|
|
5714
|
+
return step(state, [{
|
|
5715
|
+
type: "updateTask",
|
|
5716
|
+
sessionId: state.sessionId,
|
|
5717
|
+
taskId: task.id,
|
|
5718
|
+
changes: { ops: to },
|
|
5719
|
+
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.`
|
|
5720
|
+
}]);
|
|
5721
|
+
}
|
|
5722
|
+
function confirmForceStartPastGate(state, task) {
|
|
5723
|
+
const named = (task.mergeGate ?? []).map((id) => {
|
|
5724
|
+
const dep = findTask(state.tasks, id);
|
|
5725
|
+
return dep ? `#${dep.order} ${dep.title}` : id;
|
|
5726
|
+
});
|
|
5727
|
+
return step({
|
|
5728
|
+
...state,
|
|
5729
|
+
overlay: {
|
|
5730
|
+
kind: "confirm",
|
|
5731
|
+
title: `Force start #${task.order} ${task.title}?`,
|
|
5732
|
+
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.`,
|
|
5733
|
+
action: { kind: "force-start-gated", taskId: task.id }
|
|
5734
|
+
}
|
|
5735
|
+
});
|
|
5736
|
+
}
|
|
5599
5737
|
function openTaskDepsPicker(state, task) {
|
|
5600
5738
|
const action = { kind: "set-task-deps", taskId: task.id };
|
|
5601
5739
|
const items = pickerItemsFor(state, action);
|
|
@@ -5752,6 +5890,15 @@ function taskEffortCommand(state, args) {
|
|
|
5752
5890
|
return assignTaskEffort(state, sessionId, task, value);
|
|
5753
5891
|
});
|
|
5754
5892
|
}
|
|
5893
|
+
function taskOpsCommand(state, args) {
|
|
5894
|
+
return taskCommand(state, args[0], (_sessionId, taskId) => {
|
|
5895
|
+
const task = findTask(state.tasks, taskId);
|
|
5896
|
+
const subtask = !state.tasks.some((t) => t.id === taskId);
|
|
5897
|
+
const value = args[1]?.toLowerCase();
|
|
5898
|
+
if (value !== void 0 && value !== "on" && value !== "off") return fail3(state, "Usage: /task-ops <id> [on|off]");
|
|
5899
|
+
return toggleTaskOps(state, task, subtask, value === void 0 ? !task.ops : value === "on");
|
|
5900
|
+
});
|
|
5901
|
+
}
|
|
5755
5902
|
function addTask(state, title) {
|
|
5756
5903
|
if (!state.sessionId) {
|
|
5757
5904
|
return fail3(state, "No active plan to add a task to \u2014 describe a goal first, then /add-task <title>.");
|
|
@@ -5832,9 +5979,11 @@ function handlePlanKey(state, key) {
|
|
|
5832
5979
|
if (key.char === "E") return step(state, [{ type: "execute", sessionId: state.sessionId }]);
|
|
5833
5980
|
if (key.char === "S") return step(state, [{ type: "stopExecution", sessionId: state.sessionId }]);
|
|
5834
5981
|
const action = key.char === "m" ? markAction(task) : PLAN_SHORTCUTS[key.char ?? ""];
|
|
5982
|
+
if (action === "force-start" && task.mergeGate?.length) return confirmForceStartPastGate(state, task);
|
|
5835
5983
|
if (action) {
|
|
5836
5984
|
return step(state, [taskActionEffect(state, state.sessionId, task.id, action)]);
|
|
5837
5985
|
}
|
|
5986
|
+
if (key.char === "O") return toggleTaskOps(state, task, row.parent !== null);
|
|
5838
5987
|
if (key.char === "d") return confirmRemoveTask(state, task);
|
|
5839
5988
|
if (key.char === "t") return openTaskTerminalOrView(state, state.sessionId, task.id);
|
|
5840
5989
|
if (key.char === "x" && task.isolation?.state === "conflict") {
|
|
@@ -6009,6 +6158,8 @@ function runCommand(state, { name, args }) {
|
|
|
6009
6158
|
return taskEffortCommand(state, args);
|
|
6010
6159
|
case "task-mode":
|
|
6011
6160
|
return taskModeCommand(state, args);
|
|
6161
|
+
case "task-ops":
|
|
6162
|
+
return taskOpsCommand(state, args);
|
|
6012
6163
|
case "task-deps":
|
|
6013
6164
|
return taskCommand(
|
|
6014
6165
|
state,
|
|
@@ -6021,11 +6172,11 @@ function runCommand(state, { name, args }) {
|
|
|
6021
6172
|
case "retry":
|
|
6022
6173
|
case "cancel":
|
|
6023
6174
|
case "force-start":
|
|
6024
|
-
return taskCommand(
|
|
6025
|
-
state,
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
);
|
|
6175
|
+
return taskCommand(state, args[0], (sessionId, taskId) => {
|
|
6176
|
+
const task = findTask(state.tasks, taskId);
|
|
6177
|
+
if (name === "force-start" && task?.mergeGate?.length) return confirmForceStartPastGate(state, task);
|
|
6178
|
+
return step(state, [taskActionEffect(state, sessionId, taskId, name)]);
|
|
6179
|
+
});
|
|
6029
6180
|
}
|
|
6030
6181
|
return fail3(state, `/${name} is not wired up yet.`);
|
|
6031
6182
|
}
|
|
@@ -6346,6 +6497,10 @@ function runConfirm(state, overlay) {
|
|
|
6346
6497
|
if (!state.sessionId) return step(closed);
|
|
6347
6498
|
return step(closed, [{ type: "removeTask", sessionId: state.sessionId, taskId: overlay.action.taskId }]);
|
|
6348
6499
|
}
|
|
6500
|
+
if (overlay.action.kind === "force-start-gated") {
|
|
6501
|
+
if (!state.sessionId) return step(closed);
|
|
6502
|
+
return step(closed, [taskActionEffect(closed, state.sessionId, overlay.action.taskId, "force-start")]);
|
|
6503
|
+
}
|
|
6349
6504
|
if (overlay.action.kind === "merge-run" || overlay.action.kind === "discard-run") {
|
|
6350
6505
|
return confirmedHandoff(state, overlay.action.kind);
|
|
6351
6506
|
}
|
|
@@ -6551,7 +6706,9 @@ function toTaskView2(t, index) {
|
|
|
6551
6706
|
thinkingEffort: typeof model.thinkingEffort === "string" ? planLabel(model.thinkingEffort) : typeof t.thinkingEffort === "string" ? planLabel(t.thinkingEffort) : void 0,
|
|
6552
6707
|
availableVariants: Array.isArray(model.availableVariants) ? model.availableVariants.map((variant) => planLabel(variant)) : void 0
|
|
6553
6708
|
} : void 0,
|
|
6554
|
-
subtasks: Array.isArray(t.subtasks) ? [...t.subtasks].sort((a, b) => wireOrder(a) - wireOrder(b)).map((s, i) => toTaskView2(asRecord(s), i)) : void 0
|
|
6709
|
+
subtasks: Array.isArray(t.subtasks) ? [...t.subtasks].sort((a, b) => wireOrder(a) - wireOrder(b)).map((s, i) => toTaskView2(asRecord(s), i)) : void 0,
|
|
6710
|
+
...t.ops === true ? { ops: true } : {},
|
|
6711
|
+
...Array.isArray(t.forcedPastGate) && t.forcedPastGate.length > 0 ? { forcedPastGate: t.forcedPastGate.map((title) => planLabel(title)) } : {}
|
|
6555
6712
|
};
|
|
6556
6713
|
}
|
|
6557
6714
|
var enabledFlag = (value) => typeof value?.enabled === "boolean" ? value.enabled : void 0;
|
|
@@ -6576,7 +6733,7 @@ function applySettings(state, settings) {
|
|
|
6576
6733
|
allowlist: settings.modelAllowlist && typeof settings.modelAllowlist === "object" ? settings.modelAllowlist : state.allowlist
|
|
6577
6734
|
};
|
|
6578
6735
|
}
|
|
6579
|
-
function runLabel(tasks) {
|
|
6736
|
+
function runLabel(tasks, gate = null) {
|
|
6580
6737
|
const running = tasks.filter(isTaskRunning);
|
|
6581
6738
|
if (running.length > 0) {
|
|
6582
6739
|
const [first] = running;
|
|
@@ -6584,8 +6741,8 @@ function runLabel(tasks) {
|
|
|
6584
6741
|
return running.length > 1 ? `${name} (+${running.length - 1} more)` : name;
|
|
6585
6742
|
}
|
|
6586
6743
|
const waiting = tasks.filter((t) => t.status === "awaiting_user").length;
|
|
6587
|
-
if (waiting
|
|
6588
|
-
return
|
|
6744
|
+
if (waiting > 0) return waiting === 1 ? "1 task waits for you" : `${waiting} tasks wait for you`;
|
|
6745
|
+
return gate?.paused ? "paused for Merge all \u2014 /handoff merge" : "";
|
|
6589
6746
|
}
|
|
6590
6747
|
var statusText = (text) => sanitize(text).replace(/\s+/g, " ").trim();
|
|
6591
6748
|
function inFlight(blocks) {
|
|
@@ -6705,7 +6862,7 @@ function reduceAction(state, action) {
|
|
|
6705
6862
|
if (stale(state, action.sessionId) || !state.tasks.some((t) => t.id === action.taskId)) return step(state);
|
|
6706
6863
|
const tasks = state.tasks.map((t) => t.id === action.taskId ? { ...t, status: action.status, awaitingReason: void 0 } : t);
|
|
6707
6864
|
const status = runStatus(state, tasks);
|
|
6708
|
-
return step({ ...state, status, tasks, busyLabel: status === "executing" ? runLabel(tasks) : state.busyLabel });
|
|
6865
|
+
return step({ ...state, status, tasks, busyLabel: status === "executing" ? runLabel(tasks, state.gate) : state.busyLabel });
|
|
6709
6866
|
}
|
|
6710
6867
|
case "tasksStatus": {
|
|
6711
6868
|
if (stale(state, action.sessionId)) return step(state);
|
|
@@ -6720,15 +6877,20 @@ function reduceAction(state, action) {
|
|
|
6720
6877
|
const awaitingReason = update.awaitingReason;
|
|
6721
6878
|
const continuable = update.continuable === true;
|
|
6722
6879
|
const awaitingApproval = update.awaitingApproval;
|
|
6723
|
-
|
|
6880
|
+
const mergeGate = update.mergeGate;
|
|
6881
|
+
const forcedPastGate = update.forcedPastGate;
|
|
6882
|
+
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)) {
|
|
6724
6883
|
changed = true;
|
|
6725
|
-
return { ...t, status: update.status, idleSince, isolation, transport, awaitingReason, continuable, awaitingApproval };
|
|
6884
|
+
return { ...t, status: update.status, idleSince, isolation, transport, awaitingReason, continuable, awaitingApproval, mergeGate, forcedPastGate };
|
|
6726
6885
|
}
|
|
6727
6886
|
return t;
|
|
6728
6887
|
});
|
|
6729
|
-
|
|
6730
|
-
const
|
|
6731
|
-
|
|
6888
|
+
const gate = action.gate === void 0 ? state.gate : action.gate;
|
|
6889
|
+
const gateChanged = !sameGate(gate, state.gate);
|
|
6890
|
+
if (!changed && !gateChanged) return step(state);
|
|
6891
|
+
const next = { ...state, tasks, gate };
|
|
6892
|
+
const status = runStatus(next, tasks);
|
|
6893
|
+
return step({ ...next, status, busyLabel: status === "executing" ? runLabel(tasks, gate) : state.busyLabel });
|
|
6732
6894
|
}
|
|
6733
6895
|
case "queueReady": {
|
|
6734
6896
|
if (stale(state, action.sessionId)) return step(state);
|
|
@@ -6861,9 +7023,16 @@ function reduceAction(state, action) {
|
|
|
6861
7023
|
function sameTransport(a, b) {
|
|
6862
7024
|
return a?.kind === b?.kind && a?.fallback === b?.fallback;
|
|
6863
7025
|
}
|
|
7026
|
+
function sameList(a, b) {
|
|
7027
|
+
return (a ?? []).join("\0") === (b ?? []).join("\0");
|
|
7028
|
+
}
|
|
7029
|
+
function sameGate(a, b) {
|
|
7030
|
+
if (!a || !b) return a === b;
|
|
7031
|
+
return a.paused === b.paused && JSON.stringify(a.handoff) === JSON.stringify(b.handoff);
|
|
7032
|
+
}
|
|
6864
7033
|
function runStatus(state, tasks) {
|
|
6865
7034
|
if (tasks.some(isTaskRunning)) return "executing";
|
|
6866
|
-
if (tasks.some((t) => t.status === "awaiting_user")) return "executing";
|
|
7035
|
+
if (tasks.some((t) => t.status === "awaiting_user") || state.gate?.paused) return "executing";
|
|
6867
7036
|
return plannerInFlight(state) ? state.status : "idle";
|
|
6868
7037
|
}
|
|
6869
7038
|
function interrupt(state) {
|
|
@@ -7211,10 +7380,13 @@ function dispatchLifecycle(dispatch, event, sessionId) {
|
|
|
7211
7380
|
transport: task.transport,
|
|
7212
7381
|
awaitingReason: (0, import_core31.isAwaitingReason)(task.awaitingReason) ? task.awaitingReason : void 0,
|
|
7213
7382
|
...task.continuable ? { continuable: true } : {},
|
|
7214
|
-
awaitingApproval: typeof task.awaitingApproval === "number" && task.awaitingApproval > 0 ? task.awaitingApproval : void 0
|
|
7383
|
+
awaitingApproval: typeof task.awaitingApproval === "number" && task.awaitingApproval > 0 ? task.awaitingApproval : void 0,
|
|
7384
|
+
...task.mergeGate?.length ? { mergeGate: task.mergeGate.map(String) } : {},
|
|
7385
|
+
...task.forcedPastGate?.length ? { forcedPastGate: task.forcedPastGate.map(String) } : {}
|
|
7215
7386
|
};
|
|
7216
7387
|
}
|
|
7217
|
-
|
|
7388
|
+
const gate = event.gate ? { paused: event.gate.paused === true, handoff: { repos: event.gate.repos, landed: event.gate.landed } } : null;
|
|
7389
|
+
dispatch({ type: "tasksStatus", updates, gate, sessionId });
|
|
7218
7390
|
return;
|
|
7219
7391
|
}
|
|
7220
7392
|
case "task_started":
|
|
@@ -7626,10 +7798,11 @@ async function perform(effect, deps) {
|
|
|
7626
7798
|
return;
|
|
7627
7799
|
// A conflict or a refusal is an answer, not a fault: either way the user's
|
|
7628
7800
|
// tree is exactly as it was, and the words say what to do next. A full
|
|
7629
|
-
// merge leaves nothing to hand over — the daemon has
|
|
7801
|
+
// merge of a settled run leaves nothing to hand over — the daemon has
|
|
7802
|
+
// cleared the run up; one mid-run leaves the run going (ADR-0020).
|
|
7630
7803
|
case "isolationMerge": {
|
|
7631
7804
|
const { ok, message } = mergeOutcome(await api.mergeRun(effect.sessionId), effect.branch, effect.group === true, effect.repaired ?? []);
|
|
7632
|
-
if (ok) dispatch({ type: "runCleared", sessionId: effect.sessionId });
|
|
7805
|
+
if (ok && !effect.midRun) dispatch({ type: "runCleared", sessionId: effect.sessionId });
|
|
7633
7806
|
dispatch({ type: ok ? "notice" : "failed", message });
|
|
7634
7807
|
return;
|
|
7635
7808
|
}
|
|
@@ -7643,8 +7816,8 @@ async function perform(effect, deps) {
|
|
|
7643
7816
|
await refreshPlan(deps, effect.sessionId);
|
|
7644
7817
|
dispatch({ type: "notice", message: `Removed the run's worktrees and task branches; ${effect.branch} is kept.` });
|
|
7645
7818
|
return;
|
|
7646
|
-
// Spawns runners again, so it needs the stream a run needs
|
|
7647
|
-
//
|
|
7819
|
+
// Spawns runners again, so it needs the stream a run needs. Opening it ends
|
|
7820
|
+
// the blocked run's, which stayed open to show the ops tasks still running.
|
|
7648
7821
|
case "isolationContinue": {
|
|
7649
7822
|
const { sessionId } = effect;
|
|
7650
7823
|
await withExecutionStream(deps, sessionId, () => effect.mode === "stash" ? api.continueWithStash(sessionId) : api.continueWithoutIsolation(sessionId));
|
|
@@ -8202,6 +8375,7 @@ var COMMANDS = {
|
|
|
8202
8375
|
"task-effort": handleTaskEffort,
|
|
8203
8376
|
"task-mode": handleTaskMode,
|
|
8204
8377
|
"task-deps": handleTaskDeps,
|
|
8378
|
+
"task-ops": handleTaskOps,
|
|
8205
8379
|
terminal: handleTerminal,
|
|
8206
8380
|
"sessions": handleSessions,
|
|
8207
8381
|
fork: handleFork,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ordewell/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
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",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"typecheck": "tsc --noEmit"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@ordewell/core": "^0.6.
|
|
53
|
-
"@ordewell/web": "^0.6.
|
|
52
|
+
"@ordewell/core": "^0.6.2",
|
|
53
|
+
"@ordewell/web": "^0.6.2",
|
|
54
54
|
"string-width": "^7.2.0",
|
|
55
55
|
"ws": "^8.18.0"
|
|
56
56
|
},
|