@ordewell/cli 0.6.0 → 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.
- package/dist/main.js +205 -50
- 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)
|
|
@@ -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.");
|
|
@@ -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
|
-
|
|
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,9 +2656,6 @@ 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 }) {
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
2873
|
-
|
|
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
|
-
|
|
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
|
|
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);
|
|
@@ -3600,6 +3653,24 @@ Dependencies \xB7 #${task.order} ${task.title}
|
|
|
3600
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.`);
|
|
3601
3654
|
});
|
|
3602
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
|
+
}
|
|
3603
3674
|
|
|
3604
3675
|
// src/tui/index.ts
|
|
3605
3676
|
var import_core33 = require("@ordewell/core");
|
|
@@ -3757,6 +3828,7 @@ var SLASH_COMMANDS = [
|
|
|
3757
3828
|
{ name: "task-effort", usage: "/task-effort <id> [level]", description: "Choose or set a task's thinking effort", category: "tasks" },
|
|
3758
3829
|
{ name: "task-mode", usage: "/task-mode <id> [mode]", description: "Choose or set a task's runner mode", category: "tasks" },
|
|
3759
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" },
|
|
3760
3832
|
// Models & providers
|
|
3761
3833
|
{ name: "planner", usage: "/planner [<provider>]", description: "Choose who plans \u2014 an API provider or a coding agent (no API key)", category: "models" },
|
|
3762
3834
|
{ name: "model", usage: "/model [set <id>]", description: "Show or set the orchestrator model", category: "models" },
|
|
@@ -4271,6 +4343,7 @@ function initialState(overrides = {}) {
|
|
|
4271
4343
|
selection: null,
|
|
4272
4344
|
workspace: process.cwd(),
|
|
4273
4345
|
handoff: null,
|
|
4346
|
+
gate: null,
|
|
4274
4347
|
overlay: null,
|
|
4275
4348
|
pendingApprovals: [],
|
|
4276
4349
|
queuedPrompts: import_core22.EMPTY_HOLD,
|
|
@@ -4346,6 +4419,7 @@ function footerHints(state) {
|
|
|
4346
4419
|
"e effort",
|
|
4347
4420
|
"M mode",
|
|
4348
4421
|
"D deps",
|
|
4422
|
+
"O ops",
|
|
4349
4423
|
"f start",
|
|
4350
4424
|
"E run plan",
|
|
4351
4425
|
"S stop",
|
|
@@ -4577,7 +4651,8 @@ var STATUS_ICON = {
|
|
|
4577
4651
|
var AWAITING_LABEL = {
|
|
4578
4652
|
input: "waiting for your input",
|
|
4579
4653
|
checkpoint: "checkpoint",
|
|
4580
|
-
conflict: "merge conflict"
|
|
4654
|
+
conflict: "merge conflict",
|
|
4655
|
+
"files-changed": "changed tracked files"
|
|
4581
4656
|
};
|
|
4582
4657
|
function approvalLabel(count) {
|
|
4583
4658
|
return count > 1 ? `waiting for approval (${count})` : "waiting for approval";
|
|
@@ -4656,7 +4731,7 @@ function taskLines(state, row, index, cols) {
|
|
|
4656
4731
|
const paint = idle ? style.cyan : STATUS_MARK[task.status] ?? ((t) => t);
|
|
4657
4732
|
const rawIcon = idle ? IDLE_ICON : running ? SPINNER[state.spinnerFrame % SPINNER.length] : STATUS_ICON[task.status] ?? "\xB7";
|
|
4658
4733
|
const icon = paint(rawIcon);
|
|
4659
|
-
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");
|
|
4660
4735
|
const caret = selected ? style.cyan("\u276F") : " ";
|
|
4661
4736
|
const orderLabel = parent ? (0, import_core23.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
|
|
4662
4737
|
const indent = parent ? " " : "";
|
|
@@ -4696,6 +4771,16 @@ function taskLines(state, row, index, cols) {
|
|
|
4696
4771
|
const attempt = task.isolation.repair ? ` (attempt ${task.isolation.repair.attempt}/${task.isolation.repair.limit})` : "";
|
|
4697
4772
|
lines.push(style.yellow(truncate(`${bodyPad}\u21BB repairing conflict${files}${attempt}`, cols)));
|
|
4698
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
|
+
}
|
|
4699
4784
|
if (task.isolation?.state === "integrated" && task.isolation.repairedFiles?.length) {
|
|
4700
4785
|
lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${(0, import_core23.capConflictFiles)(task.isolation.repairedFiles)}`, cols)));
|
|
4701
4786
|
}
|
|
@@ -4710,6 +4795,12 @@ function taskLines(state, row, index, cols) {
|
|
|
4710
4795
|
lines.push(...taskText("Worktree", task.isolation.worktree, cols, bodyPad));
|
|
4711
4796
|
}
|
|
4712
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
|
+
}
|
|
4713
4804
|
if (modeInfo?.autonomous) {
|
|
4714
4805
|
lines.push(...taskText("Autonomy", "Runs without permission prompts (Full auto). Change the level with /auto.", cols, bodyPad));
|
|
4715
4806
|
}
|
|
@@ -5040,7 +5131,7 @@ function renderOverlay(state, rows, cols) {
|
|
|
5040
5131
|
return renderPicker(overlay.picker, rows, cols);
|
|
5041
5132
|
}
|
|
5042
5133
|
function renderHandoff(state, overlay, rows, cols) {
|
|
5043
|
-
const handoff = state
|
|
5134
|
+
const handoff = currentHandoff(state);
|
|
5044
5135
|
if (!handoff) return frame("Handoff", [style.grey("Nothing to hand off.")], rows, cols);
|
|
5045
5136
|
if (overlay.diff) {
|
|
5046
5137
|
const room = diffRoom(rows);
|
|
@@ -5596,6 +5687,34 @@ function confirmRemoveTask(state, task) {
|
|
|
5596
5687
|
}
|
|
5597
5688
|
});
|
|
5598
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
|
+
}
|
|
5599
5718
|
function openTaskDepsPicker(state, task) {
|
|
5600
5719
|
const action = { kind: "set-task-deps", taskId: task.id };
|
|
5601
5720
|
const items = pickerItemsFor(state, action);
|
|
@@ -5752,6 +5871,15 @@ function taskEffortCommand(state, args) {
|
|
|
5752
5871
|
return assignTaskEffort(state, sessionId, task, value);
|
|
5753
5872
|
});
|
|
5754
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
|
+
}
|
|
5755
5883
|
function addTask(state, title) {
|
|
5756
5884
|
if (!state.sessionId) {
|
|
5757
5885
|
return fail3(state, "No active plan to add a task to \u2014 describe a goal first, then /add-task <title>.");
|
|
@@ -5832,9 +5960,11 @@ function handlePlanKey(state, key) {
|
|
|
5832
5960
|
if (key.char === "E") return step(state, [{ type: "execute", sessionId: state.sessionId }]);
|
|
5833
5961
|
if (key.char === "S") return step(state, [{ type: "stopExecution", sessionId: state.sessionId }]);
|
|
5834
5962
|
const action = key.char === "m" ? markAction(task) : PLAN_SHORTCUTS[key.char ?? ""];
|
|
5963
|
+
if (action === "force-start" && task.mergeGate?.length) return confirmForceStartPastGate(state, task);
|
|
5835
5964
|
if (action) {
|
|
5836
5965
|
return step(state, [taskActionEffect(state, state.sessionId, task.id, action)]);
|
|
5837
5966
|
}
|
|
5967
|
+
if (key.char === "O") return toggleTaskOps(state, task, row.parent !== null);
|
|
5838
5968
|
if (key.char === "d") return confirmRemoveTask(state, task);
|
|
5839
5969
|
if (key.char === "t") return openTaskTerminalOrView(state, state.sessionId, task.id);
|
|
5840
5970
|
if (key.char === "x" && task.isolation?.state === "conflict") {
|
|
@@ -6009,6 +6139,8 @@ function runCommand(state, { name, args }) {
|
|
|
6009
6139
|
return taskEffortCommand(state, args);
|
|
6010
6140
|
case "task-mode":
|
|
6011
6141
|
return taskModeCommand(state, args);
|
|
6142
|
+
case "task-ops":
|
|
6143
|
+
return taskOpsCommand(state, args);
|
|
6012
6144
|
case "task-deps":
|
|
6013
6145
|
return taskCommand(
|
|
6014
6146
|
state,
|
|
@@ -6021,11 +6153,11 @@ function runCommand(state, { name, args }) {
|
|
|
6021
6153
|
case "retry":
|
|
6022
6154
|
case "cancel":
|
|
6023
6155
|
case "force-start":
|
|
6024
|
-
return taskCommand(
|
|
6025
|
-
state,
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
);
|
|
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
|
+
});
|
|
6029
6161
|
}
|
|
6030
6162
|
return fail3(state, `/${name} is not wired up yet.`);
|
|
6031
6163
|
}
|
|
@@ -6346,6 +6478,10 @@ function runConfirm(state, overlay) {
|
|
|
6346
6478
|
if (!state.sessionId) return step(closed);
|
|
6347
6479
|
return step(closed, [{ type: "removeTask", sessionId: state.sessionId, taskId: overlay.action.taskId }]);
|
|
6348
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
|
+
}
|
|
6349
6485
|
if (overlay.action.kind === "merge-run" || overlay.action.kind === "discard-run") {
|
|
6350
6486
|
return confirmedHandoff(state, overlay.action.kind);
|
|
6351
6487
|
}
|
|
@@ -6551,7 +6687,9 @@ function toTaskView2(t, index) {
|
|
|
6551
6687
|
thinkingEffort: typeof model.thinkingEffort === "string" ? planLabel(model.thinkingEffort) : typeof t.thinkingEffort === "string" ? planLabel(t.thinkingEffort) : void 0,
|
|
6552
6688
|
availableVariants: Array.isArray(model.availableVariants) ? model.availableVariants.map((variant) => planLabel(variant)) : void 0
|
|
6553
6689
|
} : 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
|
|
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)) } : {}
|
|
6555
6693
|
};
|
|
6556
6694
|
}
|
|
6557
6695
|
var enabledFlag = (value) => typeof value?.enabled === "boolean" ? value.enabled : void 0;
|
|
@@ -6576,7 +6714,7 @@ function applySettings(state, settings) {
|
|
|
6576
6714
|
allowlist: settings.modelAllowlist && typeof settings.modelAllowlist === "object" ? settings.modelAllowlist : state.allowlist
|
|
6577
6715
|
};
|
|
6578
6716
|
}
|
|
6579
|
-
function runLabel(tasks) {
|
|
6717
|
+
function runLabel(tasks, gate = null) {
|
|
6580
6718
|
const running = tasks.filter(isTaskRunning);
|
|
6581
6719
|
if (running.length > 0) {
|
|
6582
6720
|
const [first] = running;
|
|
@@ -6584,8 +6722,8 @@ function runLabel(tasks) {
|
|
|
6584
6722
|
return running.length > 1 ? `${name} (+${running.length - 1} more)` : name;
|
|
6585
6723
|
}
|
|
6586
6724
|
const waiting = tasks.filter((t) => t.status === "awaiting_user").length;
|
|
6587
|
-
if (waiting
|
|
6588
|
-
return
|
|
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" : "";
|
|
6589
6727
|
}
|
|
6590
6728
|
var statusText = (text) => sanitize(text).replace(/\s+/g, " ").trim();
|
|
6591
6729
|
function inFlight(blocks) {
|
|
@@ -6705,7 +6843,7 @@ function reduceAction(state, action) {
|
|
|
6705
6843
|
if (stale(state, action.sessionId) || !state.tasks.some((t) => t.id === action.taskId)) return step(state);
|
|
6706
6844
|
const tasks = state.tasks.map((t) => t.id === action.taskId ? { ...t, status: action.status, awaitingReason: void 0 } : t);
|
|
6707
6845
|
const status = runStatus(state, tasks);
|
|
6708
|
-
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 });
|
|
6709
6847
|
}
|
|
6710
6848
|
case "tasksStatus": {
|
|
6711
6849
|
if (stale(state, action.sessionId)) return step(state);
|
|
@@ -6720,15 +6858,20 @@ function reduceAction(state, action) {
|
|
|
6720
6858
|
const awaitingReason = update.awaitingReason;
|
|
6721
6859
|
const continuable = update.continuable === true;
|
|
6722
6860
|
const awaitingApproval = update.awaitingApproval;
|
|
6723
|
-
|
|
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)) {
|
|
6724
6864
|
changed = true;
|
|
6725
|
-
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 };
|
|
6726
6866
|
}
|
|
6727
6867
|
return t;
|
|
6728
6868
|
});
|
|
6729
|
-
|
|
6730
|
-
const
|
|
6731
|
-
|
|
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 });
|
|
6732
6875
|
}
|
|
6733
6876
|
case "queueReady": {
|
|
6734
6877
|
if (stale(state, action.sessionId)) return step(state);
|
|
@@ -6861,9 +7004,16 @@ function reduceAction(state, action) {
|
|
|
6861
7004
|
function sameTransport(a, b) {
|
|
6862
7005
|
return a?.kind === b?.kind && a?.fallback === b?.fallback;
|
|
6863
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
|
+
}
|
|
6864
7014
|
function runStatus(state, tasks) {
|
|
6865
7015
|
if (tasks.some(isTaskRunning)) return "executing";
|
|
6866
|
-
if (tasks.some((t) => t.status === "awaiting_user")) return "executing";
|
|
7016
|
+
if (tasks.some((t) => t.status === "awaiting_user") || state.gate?.paused) return "executing";
|
|
6867
7017
|
return plannerInFlight(state) ? state.status : "idle";
|
|
6868
7018
|
}
|
|
6869
7019
|
function interrupt(state) {
|
|
@@ -7211,10 +7361,13 @@ function dispatchLifecycle(dispatch, event, sessionId) {
|
|
|
7211
7361
|
transport: task.transport,
|
|
7212
7362
|
awaitingReason: (0, import_core31.isAwaitingReason)(task.awaitingReason) ? task.awaitingReason : void 0,
|
|
7213
7363
|
...task.continuable ? { continuable: true } : {},
|
|
7214
|
-
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) } : {}
|
|
7215
7367
|
};
|
|
7216
7368
|
}
|
|
7217
|
-
|
|
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 });
|
|
7218
7371
|
return;
|
|
7219
7372
|
}
|
|
7220
7373
|
case "task_started":
|
|
@@ -7626,10 +7779,11 @@ async function perform(effect, deps) {
|
|
|
7626
7779
|
return;
|
|
7627
7780
|
// A conflict or a refusal is an answer, not a fault: either way the user's
|
|
7628
7781
|
// 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
|
|
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).
|
|
7630
7784
|
case "isolationMerge": {
|
|
7631
7785
|
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 });
|
|
7786
|
+
if (ok && !effect.midRun) dispatch({ type: "runCleared", sessionId: effect.sessionId });
|
|
7633
7787
|
dispatch({ type: ok ? "notice" : "failed", message });
|
|
7634
7788
|
return;
|
|
7635
7789
|
}
|
|
@@ -8202,6 +8356,7 @@ var COMMANDS = {
|
|
|
8202
8356
|
"task-effort": handleTaskEffort,
|
|
8203
8357
|
"task-mode": handleTaskMode,
|
|
8204
8358
|
"task-deps": handleTaskDeps,
|
|
8359
|
+
"task-ops": handleTaskOps,
|
|
8205
8360
|
terminal: handleTerminal,
|
|
8206
8361
|
"sessions": handleSessions,
|
|
8207
8362
|
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.1",
|
|
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.1",
|
|
53
|
+
"@ordewell/web": "^0.6.1",
|
|
54
54
|
"string-width": "^7.2.0",
|
|
55
55
|
"ws": "^8.18.0"
|
|
56
56
|
},
|