@higherdev/cli 0.28.0 → 0.30.0
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/README.md +1 -1
- package/dist/api.js +3 -0
- package/dist/index.js +40 -10
- package/dist/out.js +2 -1
- package/dist/roadmap.js +8 -3
- package/dist/tui/App.js +25 -6
- package/dist/tui/Dashboard.js +1 -1
- package/dist/tui/Help.js +2 -2
- package/dist/tui/Panels.js +12 -2
- package/dist/tui/Roadmap.js +1 -1
- package/dist/tui/agent-rows.js +4 -0
- package/dist/tui/chat-view.js +1 -1
- package/dist/tui/chat-wait.js +1 -1
- package/dist/tui/data.js +7 -3
- package/dist/tui/parse.js +7 -2
- package/dist/tui/settings-model.js +7 -0
- package/dist/tui/stream.js +1 -1
- package/dist/workspace-commands.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
30
30
|
| `hd ticket new --title TITLE [--acceptance TEXT] [options]` | Create a ticket non-interactively |
|
|
31
31
|
| `hd ticket queue KEY` | Queue a complete ticket now |
|
|
32
32
|
| `hd ticket cancel KEY` | Cancel a ticket |
|
|
33
|
-
| `hd epic new PATH [--title TITLE]` | Create an epic
|
|
33
|
+
| `hd epic new PATH [--title TITLE] [--draft]` | Create an approved epic, or keep it as a draft |
|
|
34
34
|
| `hd epic list` | List epics and ticket progress |
|
|
35
35
|
| `hd epic approve ID` | Open a draft epic for orchestrator decomposition |
|
|
36
36
|
| `hd epic rm ID` | Remove a draft epic |
|
package/dist/api.js
CHANGED
|
@@ -81,6 +81,9 @@ export async function postMessage(fields, config = loadConfig()) {
|
|
|
81
81
|
export async function getRun(id, config = loadConfig()) {
|
|
82
82
|
return request(config, "GET", `/api/w/${config.slug}/runs/${encodeURIComponent(id)}`);
|
|
83
83
|
}
|
|
84
|
+
export async function cancelRun(id, config = loadConfig()) {
|
|
85
|
+
return request(config, "POST", `/api/w/${config.slug}/runs/${encodeURIComponent(id)}/cancel`);
|
|
86
|
+
}
|
|
84
87
|
export async function answerDecision(id, answer_md, config = loadConfig()) {
|
|
85
88
|
return request(config, "POST", `/api/w/${config.slug}/decisions/${encodeURIComponent(id)}/answer`, { answer_md });
|
|
86
89
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { approveEpic, answerDecision, cancelTicket, mergeTicket, createEpic, deleteAgent, deleteEpic, getStatus, getRoadmap, getWorkspace, listAgents, listEpics, listHostEnv, listMessages, listWorkspaceEnv, listTicketRunEvents, listTickets, listWorkspaces, postMessage, queueTicket, removeWorkspaceEnv, removeHostEnv, setPaused, setWorkspaceEnv, setHostEnv, showTicket, updateAgent, updateEpic, updateCaps, } from "./api.js";
|
|
4
|
+
import { approveEpic, answerDecision, cancelTicket, cancelRun, mergeTicket, createEpic, deleteAgent, deleteEpic, getStatus, getRoadmap, getWorkspace, listAgents, listEpics, listHostEnv, listMessages, listWorkspaceEnv, listTicketRunEvents, listTickets, listWorkspaces, postMessage, queueTicket, removeWorkspaceEnv, removeHostEnv, setPaused, setWorkspaceEnv, setHostEnv, showTicket, updateAgent, updateEpic, updateCaps, } from "./api.js";
|
|
5
5
|
import { formatDrainStatus, hostRoll, initHost, parseHostFlags, parseHostRollFlags } from "./host.js";
|
|
6
6
|
import { login, parseLoginFlags } from "./login.js";
|
|
7
7
|
import { loadConfig } from "./config.js";
|
|
@@ -89,6 +89,25 @@ async function cmdStatus() {
|
|
|
89
89
|
console.log(`\n${c.bold("Chat replies")} median ${seconds}s (last day)`);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
export function runAge(started, created, now = Date.now()) {
|
|
93
|
+
const seconds = Math.max(0, Math.floor((now - Date.parse(started ?? created)) / 1000));
|
|
94
|
+
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
95
|
+
}
|
|
96
|
+
async function cmdRun(argv) {
|
|
97
|
+
if (argv[0] !== "cancel" || !argv[1] || argv.length !== 2)
|
|
98
|
+
fail("usage: hd run cancel ID");
|
|
99
|
+
const { run } = await cancelRun(argv[1]);
|
|
100
|
+
console.log(`${run.id} cancellation requested`);
|
|
101
|
+
}
|
|
102
|
+
async function cmdRuns(argv) {
|
|
103
|
+
if (argv.length)
|
|
104
|
+
fail("usage: hd runs");
|
|
105
|
+
const [{ live_runs: runs }, { agents }] = await Promise.all([getStatus(), listAgents()]);
|
|
106
|
+
const names = new Map(agents.map((agent) => [agent.id, agent.display_name]));
|
|
107
|
+
console.log(table(["ID", "AGE", "KIND", "AGENT"], runs.map((run) => [
|
|
108
|
+
run.id, runAge(run.started_at, run.created_at), run.kind, names.get(run.agent_id ?? "") ?? run.provider,
|
|
109
|
+
])));
|
|
110
|
+
}
|
|
92
111
|
async function cmdTicket(argv, deps = {}) {
|
|
93
112
|
const [action, ...rest] = argv;
|
|
94
113
|
if (action === "list") {
|
|
@@ -167,18 +186,19 @@ async function cmdEpic(argv) {
|
|
|
167
186
|
return;
|
|
168
187
|
}
|
|
169
188
|
console.log(table(["KEY", "STATUS", "PROGRESS", "TITLE"], rows.map((epic) => [
|
|
170
|
-
epic.id,
|
|
189
|
+
epic.id, epic.status === "draft" ? c.yellow(`needs your approval; hd epic approve ${epic.id}`)
|
|
190
|
+
: statusChip(epic.status), `${epic.merged}/${epic.total}`, epic.title,
|
|
171
191
|
])));
|
|
172
192
|
return;
|
|
173
193
|
}
|
|
174
194
|
if (action === "new") {
|
|
175
|
-
const { rest: paths, opts } = flags(rest);
|
|
195
|
+
const { rest: paths, opts, bools } = flags(rest);
|
|
176
196
|
const path = paths.join(" ");
|
|
177
|
-
if (!path)
|
|
178
|
-
fail("usage: hd epic new PATH [--title TITLE]");
|
|
197
|
+
if (!path || [...bools].some((name) => name !== "draft"))
|
|
198
|
+
fail("usage: hd epic new PATH [--title TITLE] [--draft]");
|
|
179
199
|
const input = await readEpicSpec(path, opts.title);
|
|
180
|
-
const { epic } = await createEpic(input);
|
|
181
|
-
console.log(`${c.bold(epic.id)} ${
|
|
200
|
+
const { epic, message } = await createEpic({ ...input, draft: bools.has("draft") });
|
|
201
|
+
console.log(`${c.bold(epic.id)} ${message}`);
|
|
182
202
|
return;
|
|
183
203
|
}
|
|
184
204
|
if (action === "approve" || action === "rm") {
|
|
@@ -214,7 +234,7 @@ async function cmdEpic(argv) {
|
|
|
214
234
|
console.log(`${c.bold(epic.id)} position ${epic.position}`);
|
|
215
235
|
return;
|
|
216
236
|
}
|
|
217
|
-
fail("usage: hd epic new PATH [--title TITLE] | list | approve ID | rm ID | set ID --position N");
|
|
237
|
+
fail("usage: hd epic new PATH [--title TITLE] [--draft] | list | approve ID | rm ID | set ID --position N");
|
|
218
238
|
}
|
|
219
239
|
async function cmdRoadmap(argv) {
|
|
220
240
|
const parsed = flags(argv);
|
|
@@ -445,8 +465,10 @@ async function cmdAgents(argv, deps = {}) {
|
|
|
445
465
|
};
|
|
446
466
|
if (!Object.keys(fields).length)
|
|
447
467
|
fail(agentUsage);
|
|
448
|
-
const { agent } = await updateAgent(current.id, fields);
|
|
468
|
+
const { agent, cancelled_runs = 0 } = await updateAgent(current.id, fields);
|
|
449
469
|
console.log(`${agent.display_name} ${agent.role} ${agent.provider} ${agent.model} ${agent.enabled ? "on" : "off"}`);
|
|
470
|
+
if (cancelled_runs)
|
|
471
|
+
console.log(`cancellation requested for ${cancelled_runs} live run${cancelled_runs === 1 ? "" : "s"}`);
|
|
450
472
|
}
|
|
451
473
|
export function mailerCapsRow(auth) {
|
|
452
474
|
if (auth === "smtp")
|
|
@@ -622,6 +644,14 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
622
644
|
await cmdTicket(rest, deps);
|
|
623
645
|
return;
|
|
624
646
|
}
|
|
647
|
+
if (cmd === "run") {
|
|
648
|
+
await cmdRun(rest);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (cmd === "runs") {
|
|
652
|
+
await cmdRuns(rest);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
625
655
|
if (cmd === "epic") {
|
|
626
656
|
await cmdEpic(rest);
|
|
627
657
|
return;
|
|
@@ -658,7 +688,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
658
688
|
await cmdWorkspace(rest, deps);
|
|
659
689
|
return;
|
|
660
690
|
}
|
|
661
|
-
if (cmd === "agents") {
|
|
691
|
+
if (cmd === "agents" || cmd === "agent") {
|
|
662
692
|
await cmdAgents(rest, deps);
|
|
663
693
|
return;
|
|
664
694
|
}
|
package/dist/out.js
CHANGED
|
@@ -65,7 +65,8 @@ export function usage() {
|
|
|
65
65
|
c.bold("Usage"),
|
|
66
66
|
` ${c.blue("hd status")} workspace overview`,
|
|
67
67
|
` ${c.blue("hd ticket list | show KEY [--json] | new [PATH] | queue | cancel | merge")} ticket operations`,
|
|
68
|
-
` ${c.blue("hd
|
|
68
|
+
` ${c.blue("hd runs | hd run cancel ID")} inspect or cancel live runs`,
|
|
69
|
+
` ${c.blue("hd epic new PATH [--draft] | list | approve | rm")} epic operations`,
|
|
69
70
|
` ${c.blue("hd roadmap [--json]")} ordered workspace roadmap`,
|
|
70
71
|
` ${c.blue("hd plan")} use /architect in the TUI`,
|
|
71
72
|
` ${c.blue("hd workspace ls | use | new | set | rotate-key | grant-runner-access")} workspace operations`,
|
package/dist/roadmap.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export function currentRoadmapEpic(epics) {
|
|
2
2
|
const statuses = new Map(epics.map((epic) => [epic.id, epic.status]));
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
const next = [...epics].sort((a, b) => a.position - b.position).find((epic) => epic.status !== "done" && epic.depends_on.every((id) => statuses.get(id) === "done"));
|
|
4
|
+
return next?.status === "draft" ? null : next ?? null;
|
|
5
|
+
}
|
|
6
|
+
export function orchestratorIdleReason(epics) {
|
|
7
|
+
const statuses = new Map(epics.map((epic) => [epic.id, epic.status]));
|
|
8
|
+
const next = [...epics].sort((a, b) => a.position - b.position).find((epic) => epic.status !== "done" && epic.depends_on.every((id) => statuses.get(id) === "done"));
|
|
9
|
+
return next?.status === "draft" ? `idle because ${next.title} is a draft and needs your approval` : null;
|
|
5
10
|
}
|
|
6
11
|
export function progressBar(merged, total, width = 10) {
|
|
7
12
|
const complete = total > 0 ? Math.round((Math.max(0, Math.min(merged, total)) / total) * width) : 0;
|
|
@@ -12,7 +17,7 @@ export function roadmapText(epics) {
|
|
|
12
17
|
const current = currentRoadmapEpic(epics);
|
|
13
18
|
return [...epics].sort((a, b) => a.position - b.position).flatMap((epic) => {
|
|
14
19
|
const dependencies = epic.depends_on.map((id) => names.get(id) ?? id);
|
|
15
|
-
const status = epic.status === "draft" ?
|
|
20
|
+
const status = epic.status === "draft" ? `needs your approval · /epic approve ${epic.id}` : epic.status;
|
|
16
21
|
return [
|
|
17
22
|
`${epic.id === current?.id ? "▶" : " "} ${epic.position}. ${epic.title} [${status}]`,
|
|
18
23
|
` Outcome: ${epic.outcome_md || "Not described"}`,
|
package/dist/tui/App.js
CHANGED
|
@@ -25,7 +25,7 @@ import { alertOnce } from "./alert.js";
|
|
|
25
25
|
import { bubbleRows } from "./height.js";
|
|
26
26
|
import { planLayout, splitPanels } from "./layout.js";
|
|
27
27
|
import { parseLine } from "./parse.js";
|
|
28
|
-
import { configuredSlugs, acknowledgeInbox, approveEpic, cancelTicket, mergeTicket, createEpicFromFile, decisionOptions, deleteAgent, deleteEpic, followChat, loadChatMessages, loadLiveEvents, listWorkspaceEnv, loadTicketDetail, POLL_MS, pollSnapshot, postAgentMessage, postTicketMessage, queueTicket, resolveDecision, selectDecision, setWorkspacePaused, switchWorkspace, updateAgent, updateProviderCap, updateWorkspace, } from "./data.js";
|
|
28
|
+
import { configuredSlugs, acknowledgeInbox, approveEpic, cancelTicket, cancelRun, mergeTicket, createEpicFromFile, decisionOptions, deleteAgent, deleteEpic, followChat, loadChatMessages, loadLiveEvents, listWorkspaceEnv, loadTicketDetail, POLL_MS, pollSnapshot, postAgentMessage, postTicketMessage, queueTicket, resolveDecision, selectDecision, setWorkspacePaused, switchWorkspace, updateAgent, updateProviderCap, updateWorkspace, } from "./data.js";
|
|
29
29
|
import { inputActive, promptPlaceholder, settleChatReply } from "./chat-wait.js";
|
|
30
30
|
import { answeredLine, decisionHeaderIndex, decisionIdAt, moveDecisionFocus, nextUnanswered, resolveDecisionAnswer, } from "./decide-nav.js";
|
|
31
31
|
import { EARLIER_PAGE } from "./inbox.js";
|
|
@@ -315,16 +315,20 @@ export function App({ initial, availableUpdate: initialUpdate = null, updateDeps
|
|
|
315
315
|
}
|
|
316
316
|
setBusy(true);
|
|
317
317
|
try {
|
|
318
|
+
let message = null;
|
|
318
319
|
if (edit.value.target === "cap") {
|
|
319
320
|
await updateProviderCap(config, edit.value.provider, edit.value.cap);
|
|
320
321
|
}
|
|
321
322
|
else if (edit.value.target === "agent") {
|
|
322
|
-
await updateAgent(config, edit.value.id, edit.value.fields);
|
|
323
|
+
const result = await updateAgent(config, edit.value.id, edit.value.fields);
|
|
324
|
+
const count = result.cancelled_runs ?? 0;
|
|
325
|
+
if (count)
|
|
326
|
+
message = `Saved. Cancellation requested for ${count} live run${count === 1 ? "" : "s"}.`;
|
|
323
327
|
}
|
|
324
328
|
else {
|
|
325
329
|
await updateWorkspace(config, edit.value.fields);
|
|
326
330
|
}
|
|
327
|
-
setNotice(
|
|
331
|
+
setNotice(message);
|
|
328
332
|
await refresh();
|
|
329
333
|
}
|
|
330
334
|
catch (error) {
|
|
@@ -679,8 +683,8 @@ export function App({ initial, availableUpdate: initialUpdate = null, updateDeps
|
|
|
679
683
|
case "epic-new":
|
|
680
684
|
setBusy(true);
|
|
681
685
|
try {
|
|
682
|
-
const { epic } = await createEpicFromFile(config, action.path);
|
|
683
|
-
say("system", `Created epic ${epic.id}: ${
|
|
686
|
+
const { epic, message } = await createEpicFromFile(config, action.path, action.draft);
|
|
687
|
+
say("system", `Created epic ${epic.id}: ${message}`);
|
|
684
688
|
await refresh();
|
|
685
689
|
}
|
|
686
690
|
catch (error) {
|
|
@@ -721,7 +725,8 @@ export function App({ initial, availableUpdate: initialUpdate = null, updateDeps
|
|
|
721
725
|
case "epics": {
|
|
722
726
|
const rows = epicProgressRows(board.epics, board.tickets);
|
|
723
727
|
say("system", rows.length
|
|
724
|
-
? rows.map((epic) => `${epic.id} ${epic.status
|
|
728
|
+
? rows.map((epic) => `${epic.id} ${epic.status === "draft"
|
|
729
|
+
? `needs your approval · /epic approve ${epic.id}` : epic.status} ${epic.merged}/${epic.total} ${epic.title}`).join("\n")
|
|
725
730
|
: "No epics.");
|
|
726
731
|
return;
|
|
727
732
|
}
|
|
@@ -767,6 +772,20 @@ export function App({ initial, availableUpdate: initialUpdate = null, updateDeps
|
|
|
767
772
|
setBusy(false);
|
|
768
773
|
}
|
|
769
774
|
return;
|
|
775
|
+
case "cancel-run":
|
|
776
|
+
setBusy(true);
|
|
777
|
+
try {
|
|
778
|
+
await cancelRun(config, action.id);
|
|
779
|
+
say("system", `Cancellation requested for run ${action.id}.`);
|
|
780
|
+
await refresh();
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
784
|
+
}
|
|
785
|
+
finally {
|
|
786
|
+
setBusy(false);
|
|
787
|
+
}
|
|
788
|
+
return;
|
|
770
789
|
case "merge":
|
|
771
790
|
setBusy(true);
|
|
772
791
|
try {
|
package/dist/tui/Dashboard.js
CHANGED
|
@@ -122,7 +122,7 @@ export function AgentsColumn({ board, width, rows, }) {
|
|
|
122
122
|
? "warning" : "muted";
|
|
123
123
|
return (_jsxs(Box, { flexWrap: "nowrap", children: [_jsxs(Text, { color: inkColor(tone), children: [DOT, " "] }), _jsxs(Text, { color: UI.text, wrap: "truncate", children: [row.name, row.state === "draining" ? null : (_jsx(Text, { color: UI.dim, children: row.run
|
|
124
124
|
? ` · ${row.ticket?.key ?? row.run.kind} ${elapsed(row.run.started_at ?? row.run.created_at)}`
|
|
125
|
-
: ` · ${row.limitedUntil ? `limited until ${row.limitedUntil}` : row.state}` }))] })] }, row.key));
|
|
125
|
+
: ` · ${row.limitedUntil ? `limited until ${row.limitedUntil}` : row.detail ?? row.state}` }))] })] }, row.key));
|
|
126
126
|
}),
|
|
127
127
|
_jsx(More, { count: displayRows.length - shown.length }, "more"),
|
|
128
128
|
] }));
|
package/dist/tui/Help.js
CHANGED
|
@@ -10,12 +10,12 @@ export const COMMANDS = [
|
|
|
10
10
|
{ name: "/inbox", args: "[more]", help: "unread, earlier, and numbered decisions; Enter answers" },
|
|
11
11
|
{ name: "/ticket", args: "HD-12 | new [PATH.md]", help: "open a full ticket view or create one" },
|
|
12
12
|
{ name: "/queue", args: "HD-12", help: "queue a complete ticket now" },
|
|
13
|
-
{ name: "/cancel", args: "HD-12", help: "cancel a ticket" },
|
|
13
|
+
{ name: "/cancel", args: "HD-12 | RUN-ID", help: "cancel a ticket or run" },
|
|
14
14
|
{ name: "/merge", args: "HD-12", help: "approve a reviewed PR over the reviewer's objections" },
|
|
15
15
|
{ name: "/msg", args: "HD-12 TEXT", help: "message a ticket's builder" },
|
|
16
16
|
{ name: "/attach", args: "PATH [HD-12]", help: "attach a file to the open or named ticket" },
|
|
17
17
|
{ name: "/logs", args: "[raw] [HD-12]", help: "filter activity; raw reveals event JSON" },
|
|
18
|
-
{ name: "/epic", args: "new PATH | approve ID | rm ID", help: "create, approve, or remove
|
|
18
|
+
{ name: "/epic", args: "new PATH [--draft] | approve ID | rm ID", help: "create, approve, or remove an epic" },
|
|
19
19
|
{ name: "/epics", help: "list epics and ticket progress" },
|
|
20
20
|
{ name: "/architect", help: "open a chat with the architect" },
|
|
21
21
|
{ name: "/plan", help: "alias for /architect" },
|
package/dist/tui/Panels.js
CHANGED
|
@@ -93,14 +93,15 @@ export function InboxPanel({ board, width = 80, rows = 12, focus = 0, selectedId
|
|
|
93
93
|
const entries = inboxEntries(board, width, answeringId);
|
|
94
94
|
const unread = board.messages ?? [];
|
|
95
95
|
const earlier = board.earlier ?? [];
|
|
96
|
+
const drafts = board.epics.filter((epic) => epic.status === "draft").length;
|
|
96
97
|
const selected = selectedId ?? decisionIdAt(entries, focus);
|
|
97
98
|
const inner = Math.max(0, rows - 1);
|
|
98
99
|
const window = scrollWindow(entries.length, inner, focus);
|
|
99
100
|
const hiddenAbove = window.start;
|
|
100
101
|
const hiddenBelow = entries.length - window.end;
|
|
101
|
-
const note = `${board.decisions.length}d · ${unread.length} unread`
|
|
102
|
+
const note = `${drafts ? `${drafts} epic${drafts === 1 ? "" : "s"} · ` : ""}${board.decisions.length}d · ${unread.length} unread`
|
|
102
103
|
+ `${hiddenAbove ? ` ${hiddenAbove}↑` : ""}${hiddenBelow ? ` ${hiddenBelow}↓` : ""}`;
|
|
103
|
-
const empty = board.decisions.length === 0 && unread.length === 0 && earlier.length === 0;
|
|
104
|
+
const empty = drafts === 0 && board.decisions.length === 0 && unread.length === 0 && earlier.length === 0;
|
|
104
105
|
return (_jsx(Panel, { width: width, rows: rows, children: [
|
|
105
106
|
_jsx(Heading, { text: "Inbox", note: note }, "h"),
|
|
106
107
|
...(empty
|
|
@@ -122,6 +123,15 @@ export function inboxEntries(board, width, answeringId) {
|
|
|
122
123
|
const bodyWidth = Math.max(12, width - 2);
|
|
123
124
|
const unread = board.messages ?? [];
|
|
124
125
|
const earlier = board.earlier ?? [];
|
|
126
|
+
const drafts = board.epics.filter((epic) => epic.status === "draft");
|
|
127
|
+
if (drafts.length) {
|
|
128
|
+
entries.push({ key: "drafts:section", kind: "section", text: "Draft epics" });
|
|
129
|
+
for (const epic of drafts) {
|
|
130
|
+
entries.push({ key: `draft:${epic.id}`, kind: "header", text: epic.title });
|
|
131
|
+
entries.push({ key: `draft:${epic.id}:action`, kind: "body",
|
|
132
|
+
text: `Needs your approval · /epic approve ${epic.id}` });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
125
135
|
board.decisions.forEach((decision, index) => {
|
|
126
136
|
const ticket = board.tickets.find((row) => row.id === decision.ticket_id);
|
|
127
137
|
entries.push({
|
package/dist/tui/Roadmap.js
CHANGED
|
@@ -30,7 +30,7 @@ export function RoadmapPanel({ board, width = 80, rows = 12, offset = 0 }) {
|
|
|
30
30
|
_jsx(Heading, { text: "Roadmap", note: note }, "h"),
|
|
31
31
|
...shown.map((line, index) => {
|
|
32
32
|
const current = line.startsWith("▶");
|
|
33
|
-
const draft = line.includes("
|
|
33
|
+
const draft = line.includes("needs your approval");
|
|
34
34
|
return _jsx(Text, { color: current ? UI.accent
|
|
35
35
|
: draft ? inkColor("warning") : line === "Vision" ? UI.text : UI.dim, bold: current || line === "Vision", inverse: current, wrap: "truncate", children: line || " " }, `${start + index}:${line}`);
|
|
36
36
|
}),
|
package/dist/tui/agent-rows.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatDrainStatus } from "../host.js";
|
|
2
|
+
import { orchestratorIdleReason } from "../roadmap.js";
|
|
2
3
|
const LIMITED_UNTIL = /^(?:Waiting on )?(\w+) (?:limited )?until (\d{1,2}:\d{2})\.?$/;
|
|
3
4
|
export function limitedUntilByProvider(tickets) {
|
|
4
5
|
const limited = new Map();
|
|
@@ -33,6 +34,7 @@ export function agentDisplayRows(board, now = Date.now()) {
|
|
|
33
34
|
ticket: null,
|
|
34
35
|
state: "draining",
|
|
35
36
|
limitedUntil: null,
|
|
37
|
+
detail: null,
|
|
36
38
|
}]
|
|
37
39
|
: [];
|
|
38
40
|
const activeAgents = new Set();
|
|
@@ -61,6 +63,7 @@ export function agentDisplayRows(board, now = Date.now()) {
|
|
|
61
63
|
ticket,
|
|
62
64
|
state: run.status,
|
|
63
65
|
limitedUntil: null,
|
|
66
|
+
detail: null,
|
|
64
67
|
};
|
|
65
68
|
});
|
|
66
69
|
const limited = limitedUntilByProvider(board.tickets);
|
|
@@ -81,6 +84,7 @@ export function agentDisplayRows(board, now = Date.now()) {
|
|
|
81
84
|
? "offline"
|
|
82
85
|
: "idle",
|
|
83
86
|
limitedUntil: until ?? null,
|
|
87
|
+
detail: agent.role === "orchestrator" ? orchestratorIdleReason(board.epics) : null,
|
|
84
88
|
};
|
|
85
89
|
});
|
|
86
90
|
return [...drain, ...live, ...idle];
|
package/dist/tui/chat-view.js
CHANGED
|
@@ -96,7 +96,7 @@ export function followShouldStop(run, reply, tagged = false) {
|
|
|
96
96
|
return true;
|
|
97
97
|
if (!run)
|
|
98
98
|
return false;
|
|
99
|
-
if (["failed", "killed"].includes(run.status))
|
|
99
|
+
if (["failed", "killed", "cancelled"].includes(run.status))
|
|
100
100
|
return true;
|
|
101
101
|
// The role's other runs may finish while ours is still queued; only the run
|
|
102
102
|
// started for this message is definitive when it ends without a reply.
|
package/dist/tui/chat-wait.js
CHANGED
|
@@ -28,7 +28,7 @@ export function settleChatReply(reply, run) {
|
|
|
28
28
|
if (reply) {
|
|
29
29
|
return { body: reply, pending: false, steps: [], done: true, replyMs: run?.reply_ms ?? null };
|
|
30
30
|
}
|
|
31
|
-
if (run && ["failed", "killed"].includes(run.status ?? "")) {
|
|
31
|
+
if (run && ["failed", "killed", "cancelled"].includes(run.status ?? "")) {
|
|
32
32
|
return { body: failedChatNote(run.summary), pending: false, steps: [], done: true, replyMs: run.reply_ms ?? null };
|
|
33
33
|
}
|
|
34
34
|
if (run && run.status && !["queued", "running"].includes(run.status)) {
|
package/dist/tui/data.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { approveEpic as approveEpicNow, answerDecision, cancelTicket as cancelTicketNow, createAgent as postAgent, createEpic as postEpic, deleteEpic as removeEpic, getStatus, getRoadmap, getRun, getWorkspace, listAgents, listFeed, listMessages, markMessagesDelivered, listWorkspaceEnv as getWorkspaceEnv, listTicketRunEvents, listTickets, listWorkspaces, postMessage as sendMessage, mergeTicket as mergeTicketNow, queueTicket as queueTicketNow, setPaused as setPausedNow, showTicket, updateAgent as patchAgent, updateCaps, updateWorkspace as patchWorkspace, deleteAgent as removeAgent, } from "../api.js";
|
|
1
|
+
import { approveEpic as approveEpicNow, answerDecision, cancelTicket as cancelTicketNow, cancelRun as cancelRunNow, createAgent as postAgent, createEpic as postEpic, deleteEpic as removeEpic, getStatus, getRoadmap, getRun, getWorkspace, listAgents, listFeed, listMessages, markMessagesDelivered, listWorkspaceEnv as getWorkspaceEnv, listTicketRunEvents, listTickets, listWorkspaces, postMessage as sendMessage, mergeTicket as mergeTicketNow, queueTicket as queueTicketNow, setPaused as setPausedNow, showTicket, updateAgent as patchAgent, updateCaps, updateWorkspace as patchWorkspace, deleteAgent as removeAgent, } from "../api.js";
|
|
2
2
|
import { loadConfig, switchWorkspace as selectWorkspace, } from "../config.js";
|
|
3
3
|
import { readEpicSpec } from "../epics.js";
|
|
4
4
|
import { EARLIER_PAGE } from "./inbox.js";
|
|
@@ -70,6 +70,7 @@ export async function loadSnapshot(config = loadConfig(), options = {}) {
|
|
|
70
70
|
chat: Number(workspaceData.workspace.settings.max_turns?.chat ?? 8),
|
|
71
71
|
},
|
|
72
72
|
max_attempts: Number(workspaceData.workspace.settings.max_attempts ?? 3),
|
|
73
|
+
chat_timeout_ms: Number(workspaceData.workspace.settings.chat_timeout_ms ?? 600_000),
|
|
73
74
|
},
|
|
74
75
|
board: {
|
|
75
76
|
tickets,
|
|
@@ -129,6 +130,9 @@ export async function postAgentMessage(role, body, config, attachmentIds = []) {
|
|
|
129
130
|
attachment_ids: attachmentIds }, config);
|
|
130
131
|
return message;
|
|
131
132
|
}
|
|
133
|
+
export async function cancelRun(config, id) {
|
|
134
|
+
await cancelRunNow(id, config);
|
|
135
|
+
}
|
|
132
136
|
export async function loadChatMessages(config, role) {
|
|
133
137
|
const { messages } = await listMessages({
|
|
134
138
|
unticketed: true,
|
|
@@ -173,8 +177,8 @@ export async function loadTicketDetail(config, key) {
|
|
|
173
177
|
},
|
|
174
178
|
};
|
|
175
179
|
}
|
|
176
|
-
export async function createEpicFromFile(config, path) {
|
|
177
|
-
return postEpic(await readEpicSpec(path), config);
|
|
180
|
+
export async function createEpicFromFile(config, path, draft = false) {
|
|
181
|
+
return postEpic({ ...await readEpicSpec(path), draft }, config);
|
|
178
182
|
}
|
|
179
183
|
export async function approveEpic(config, id) {
|
|
180
184
|
return approveEpicNow(id, config);
|
package/dist/tui/parse.js
CHANGED
|
@@ -61,7 +61,10 @@ export function parseLine(raw) {
|
|
|
61
61
|
: { kind: "unknown", command: "ticket needs a key" };
|
|
62
62
|
case "epic":
|
|
63
63
|
if (rest[0]?.toLowerCase() === "new" && rest.length > 1) {
|
|
64
|
-
|
|
64
|
+
const draft = rest.includes("--draft");
|
|
65
|
+
const path = rest.slice(1).filter((part) => part !== "--draft").join(" ");
|
|
66
|
+
return path ? { kind: "epic-new", path, draft }
|
|
67
|
+
: { kind: "unknown", command: "epic needs new PATH, approve ID, or rm ID" };
|
|
65
68
|
}
|
|
66
69
|
return rest[0]?.toLowerCase() === "approve" && rest.length === 2
|
|
67
70
|
? { kind: "epic-approve", id: rest[1] }
|
|
@@ -79,7 +82,9 @@ export function parseLine(raw) {
|
|
|
79
82
|
? { kind: "queue", key: argument.toUpperCase() }
|
|
80
83
|
: { kind: "unknown", command: "queue needs a key" };
|
|
81
84
|
case "cancel":
|
|
82
|
-
return argument ?
|
|
85
|
+
return argument ? /^HD-\d+$/i.test(argument)
|
|
86
|
+
? { kind: "cancel", key: argument.toUpperCase() }
|
|
87
|
+
: { kind: "cancel-run", id: argument }
|
|
83
88
|
: { kind: "unknown", command: "cancel needs a key" };
|
|
84
89
|
case "merge":
|
|
85
90
|
return argument ? { kind: "merge", key: argument.toUpperCase() }
|
|
@@ -12,6 +12,7 @@ export function settingsRows(workspace, agents) {
|
|
|
12
12
|
value: String(workspace.max_turns[kind]), hint: "integer >= 1",
|
|
13
13
|
})),
|
|
14
14
|
{ key: "w:max_attempts", kind: "number", label: "max attempts", value: String(workspace.max_attempts) },
|
|
15
|
+
{ key: "w:chat_timeout_ms", kind: "number", label: "chat timeout ms", value: String(workspace.chat_timeout_ms ?? 600_000) },
|
|
15
16
|
];
|
|
16
17
|
for (const provider of providers) {
|
|
17
18
|
rows.push({
|
|
@@ -81,6 +82,12 @@ export function editFor(row, raw) {
|
|
|
81
82
|
return { ok: false, error: "max_attempts must be an integer >= 1." };
|
|
82
83
|
return { ok: true, value: { target: "workspace", fields: { max_attempts: number } } };
|
|
83
84
|
}
|
|
85
|
+
if (id === "chat_timeout_ms") {
|
|
86
|
+
const number = Number(value);
|
|
87
|
+
if (!Number.isInteger(number) || number < 1_000)
|
|
88
|
+
return { ok: false, error: "chat_timeout_ms must be an integer >= 1000." };
|
|
89
|
+
return { ok: true, value: { target: "workspace", fields: { chat_timeout_ms: number } } };
|
|
90
|
+
}
|
|
84
91
|
if (id === "auto_merge")
|
|
85
92
|
return { ok: true, value: { target: "workspace", fields: { auto_merge: value === "yes" } } };
|
|
86
93
|
if (!value)
|
package/dist/tui/stream.js
CHANGED
|
@@ -39,7 +39,7 @@ export function toStreamLines(events, runs, raw = false) {
|
|
|
39
39
|
const target = run.ticket ? ` on ${run.ticket}` : "";
|
|
40
40
|
lines.push({ id: `${run.runId}:end`, sourceIds: [`${run.runId}:end`], runId: run.runId,
|
|
41
41
|
agent: run.agent, at: run.endedAt ?? "", seq: Number.MAX_SAFE_INTEGER,
|
|
42
|
-
kind:
|
|
42
|
+
kind: ["failed", "killed", "cancelled"].includes(run.status) ? "error" : "status",
|
|
43
43
|
title: `${run.agent} finished ${run.kind}${target}: ${run.summary}` });
|
|
44
44
|
}
|
|
45
45
|
return collapse(lines.sort((a, b) => a.at.localeCompare(b.at) || a.seq - b.seq), raw);
|
|
@@ -150,6 +150,12 @@ export async function workspaceSet(argv) {
|
|
|
150
150
|
throw new Error("--max-attempts must be an integer >= 1");
|
|
151
151
|
fields.max_attempts = value;
|
|
152
152
|
}
|
|
153
|
+
if (opts["chat-timeout-ms"]) {
|
|
154
|
+
const value = Number(opts["chat-timeout-ms"]);
|
|
155
|
+
if (!Number.isInteger(value) || value < 1_000)
|
|
156
|
+
throw new Error("--chat-timeout-ms must be an integer >= 1000");
|
|
157
|
+
fields.chat_timeout_ms = value;
|
|
158
|
+
}
|
|
153
159
|
if (!Object.keys(fields).length)
|
|
154
160
|
throw new Error(WORKSPACE_USAGE);
|
|
155
161
|
return (await updateWorkspace(fields)).workspace;
|