@higherdev/cli 0.27.0 → 0.29.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/dist/api.js +3 -0
- package/dist/index.js +32 -3
- package/dist/out.js +1 -0
- package/dist/tui/App.js +22 -4
- package/dist/tui/Help.js +1 -1
- package/dist/tui/chat-view.js +8 -2
- package/dist/tui/chat-wait.js +11 -1
- package/dist/tui/data.js +6 -2
- package/dist/tui/parse.js +3 -1
- 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/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") {
|
|
@@ -445,8 +464,10 @@ async function cmdAgents(argv, deps = {}) {
|
|
|
445
464
|
};
|
|
446
465
|
if (!Object.keys(fields).length)
|
|
447
466
|
fail(agentUsage);
|
|
448
|
-
const { agent } = await updateAgent(current.id, fields);
|
|
467
|
+
const { agent, cancelled_runs = 0 } = await updateAgent(current.id, fields);
|
|
449
468
|
console.log(`${agent.display_name} ${agent.role} ${agent.provider} ${agent.model} ${agent.enabled ? "on" : "off"}`);
|
|
469
|
+
if (cancelled_runs)
|
|
470
|
+
console.log(`cancellation requested for ${cancelled_runs} live run${cancelled_runs === 1 ? "" : "s"}`);
|
|
450
471
|
}
|
|
451
472
|
export function mailerCapsRow(auth) {
|
|
452
473
|
if (auth === "smtp")
|
|
@@ -622,6 +643,14 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
622
643
|
await cmdTicket(rest, deps);
|
|
623
644
|
return;
|
|
624
645
|
}
|
|
646
|
+
if (cmd === "run") {
|
|
647
|
+
await cmdRun(rest);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (cmd === "runs") {
|
|
651
|
+
await cmdRuns(rest);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
625
654
|
if (cmd === "epic") {
|
|
626
655
|
await cmdEpic(rest);
|
|
627
656
|
return;
|
|
@@ -658,7 +687,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
658
687
|
await cmdWorkspace(rest, deps);
|
|
659
688
|
return;
|
|
660
689
|
}
|
|
661
|
-
if (cmd === "agents") {
|
|
690
|
+
if (cmd === "agents" || cmd === "agent") {
|
|
662
691
|
await cmdAgents(rest, deps);
|
|
663
692
|
return;
|
|
664
693
|
}
|
package/dist/out.js
CHANGED
|
@@ -65,6 +65,7 @@ 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 runs | hd run cancel ID")} inspect or cancel live runs`,
|
|
68
69
|
` ${c.blue("hd epic new PATH | 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`,
|
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) {
|
|
@@ -425,7 +429,7 @@ export function App({ initial, availableUpdate: initialUpdate = null, updateDeps
|
|
|
425
429
|
activity: activity.lines,
|
|
426
430
|
},
|
|
427
431
|
}));
|
|
428
|
-
if (followShouldStop(follow.run, follow.reply)) {
|
|
432
|
+
if (followShouldStop(follow.run, follow.reply, follow.tagged)) {
|
|
429
433
|
const settled = settleChatReply(follow.reply, follow.run);
|
|
430
434
|
setThread(key, (current) => ({
|
|
431
435
|
...current,
|
|
@@ -767,6 +771,20 @@ export function App({ initial, availableUpdate: initialUpdate = null, updateDeps
|
|
|
767
771
|
setBusy(false);
|
|
768
772
|
}
|
|
769
773
|
return;
|
|
774
|
+
case "cancel-run":
|
|
775
|
+
setBusy(true);
|
|
776
|
+
try {
|
|
777
|
+
await cancelRun(config, action.id);
|
|
778
|
+
say("system", `Cancellation requested for run ${action.id}.`);
|
|
779
|
+
await refresh();
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
783
|
+
}
|
|
784
|
+
finally {
|
|
785
|
+
setBusy(false);
|
|
786
|
+
}
|
|
787
|
+
return;
|
|
770
788
|
case "merge":
|
|
771
789
|
setBusy(true);
|
|
772
790
|
try {
|
package/dist/tui/Help.js
CHANGED
|
@@ -10,7 +10,7 @@ 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" },
|
package/dist/tui/chat-view.js
CHANGED
|
@@ -91,10 +91,16 @@ export function pendingActivity(events, run, label, now = Date.now()) {
|
|
|
91
91
|
lines: narrateEvents(events).map((line) => line.title),
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
|
-
export function followShouldStop(run, reply) {
|
|
94
|
+
export function followShouldStop(run, reply, tagged = false) {
|
|
95
95
|
if (reply)
|
|
96
96
|
return true;
|
|
97
|
-
if (run
|
|
97
|
+
if (!run)
|
|
98
|
+
return false;
|
|
99
|
+
if (["failed", "killed", "cancelled"].includes(run.status))
|
|
100
|
+
return true;
|
|
101
|
+
// The role's other runs may finish while ours is still queued; only the run
|
|
102
|
+
// started for this message is definitive when it ends without a reply.
|
|
103
|
+
if (tagged && !["queued", "running"].includes(run.status))
|
|
98
104
|
return true;
|
|
99
105
|
return false;
|
|
100
106
|
}
|
package/dist/tui/chat-wait.js
CHANGED
|
@@ -15,6 +15,12 @@ export function failedChatNote(reason) {
|
|
|
15
15
|
const detail = reason?.trim();
|
|
16
16
|
return detail ? `${NO_REPLY_NOTE} ${detail}` : NO_REPLY_NOTE;
|
|
17
17
|
}
|
|
18
|
+
/** A run that finished without writing a reply message: show what it did instead. */
|
|
19
|
+
export const NO_REPLY_WRITTEN = "Finished without a reply.";
|
|
20
|
+
export function finishedWithoutReplyNote(summary) {
|
|
21
|
+
const detail = summary?.trim();
|
|
22
|
+
return detail ? `${NO_REPLY_WRITTEN} ${detail}` : NO_REPLY_WRITTEN;
|
|
23
|
+
}
|
|
18
24
|
export function answeredIn(ms) {
|
|
19
25
|
return `answered in ${Math.max(0, Math.round(ms / 1000))}s`;
|
|
20
26
|
}
|
|
@@ -22,8 +28,12 @@ export function settleChatReply(reply, run) {
|
|
|
22
28
|
if (reply) {
|
|
23
29
|
return { body: reply, pending: false, steps: [], done: true, replyMs: run?.reply_ms ?? null };
|
|
24
30
|
}
|
|
25
|
-
if (run && ["failed", "killed"].includes(run.status ?? "")) {
|
|
31
|
+
if (run && ["failed", "killed", "cancelled"].includes(run.status ?? "")) {
|
|
26
32
|
return { body: failedChatNote(run.summary), pending: false, steps: [], done: true, replyMs: run.reply_ms ?? null };
|
|
27
33
|
}
|
|
34
|
+
if (run && run.status && !["queued", "running"].includes(run.status)) {
|
|
35
|
+
return { body: finishedWithoutReplyNote(run.summary), pending: false, steps: [], done: true,
|
|
36
|
+
replyMs: run.reply_ms ?? null };
|
|
37
|
+
}
|
|
28
38
|
return { body: "", pending: true, steps: [], done: false, replyMs: null };
|
|
29
39
|
}
|
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,
|
|
@@ -156,7 +160,7 @@ export async function followChat(config, role, messageId, since) {
|
|
|
156
160
|
const reply = messages.find((message) => message.from_role === role
|
|
157
161
|
&& ["human", "operator", "all"].includes(message.to_role)
|
|
158
162
|
&& (!run || !message.run_id || message.run_id === run.id));
|
|
159
|
-
return { run, events, reply: reply?.body_md ?? null };
|
|
163
|
+
return { run, events, reply: reply?.body_md ?? null, tagged: Boolean(tagged) };
|
|
160
164
|
}
|
|
161
165
|
export async function loadTicketDetail(config, key) {
|
|
162
166
|
const { ticket, pr, events, runs, messages, decisions, attachments } = await showTicket(key, config);
|
package/dist/tui/parse.js
CHANGED
|
@@ -79,7 +79,9 @@ export function parseLine(raw) {
|
|
|
79
79
|
? { kind: "queue", key: argument.toUpperCase() }
|
|
80
80
|
: { kind: "unknown", command: "queue needs a key" };
|
|
81
81
|
case "cancel":
|
|
82
|
-
return argument ?
|
|
82
|
+
return argument ? /^HD-\d+$/i.test(argument)
|
|
83
|
+
? { kind: "cancel", key: argument.toUpperCase() }
|
|
84
|
+
: { kind: "cancel-run", id: argument }
|
|
83
85
|
: { kind: "unknown", command: "cancel needs a key" };
|
|
84
86
|
case "merge":
|
|
85
87
|
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;
|