@higherdev/cli 0.8.0 → 0.10.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 +14 -4
- package/dist/api.js +18 -0
- package/dist/index.js +127 -25
- package/dist/out.js +4 -3
- package/dist/tui/App.js +54 -2
- package/dist/tui/Help.js +2 -1
- package/dist/tui/data.js +28 -3
- package/dist/tui/parse.js +18 -1
- package/dist/tui/settings-model.js +30 -2
- package/dist/workspace-preflight.js +77 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,13 +32,19 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
32
32
|
| `hd ticket show KEY` | Show one ticket |
|
|
33
33
|
| `hd ticket new --title TITLE [--acceptance TEXT] [options]` | Create a ticket |
|
|
34
34
|
| `hd ticket queue KEY` | Queue a complete ticket now |
|
|
35
|
+
| `hd ticket cancel KEY` | Cancel a ticket |
|
|
35
36
|
| `hd epic new PATH [--title TITLE]` | Create an epic from a Markdown spec |
|
|
36
37
|
| `hd epic list` | List epics and ticket progress |
|
|
37
38
|
| `hd plan [--repo DIR]` | Hand the terminal to Codex to author an epic spec |
|
|
38
39
|
| `hd workspace ls` | List every workspace available to the configured key |
|
|
39
|
-
| `hd workspace new --name NAME --repo OWNER/NAME [options]` |
|
|
40
|
+
| `hd workspace new --name NAME --repo OWNER/NAME [options]` | Preflight GitHub, wire the runner, and create a paused workspace |
|
|
41
|
+
| `hd workspace set [options]` | Update settings; max turns uses `--max-turns KIND=N[,KIND=N...]` |
|
|
42
|
+
| `hd workspace rotate-key` | Rotate the shared API key and save it locally |
|
|
40
43
|
| `hd agents` | List agents |
|
|
41
|
-
| `hd agents
|
|
44
|
+
| `hd agents add ROLE --provider P --model M [options]` | Add an agent |
|
|
45
|
+
| `hd agents rm ROLE\|ID` | Remove an unambiguous agent |
|
|
46
|
+
| `hd agents set ROLE\|ID [options]` | Rename, configure, enable, or disable an agent |
|
|
47
|
+
| `hd caps [set PROVIDER N]` | Show or update provider concurrency caps |
|
|
42
48
|
| `hd logs KEY [-f]` | Show or follow run events |
|
|
43
49
|
| `hd msg KEY "message" [--interrupt]` | Message a builder |
|
|
44
50
|
| `hd decide` | List open decisions |
|
|
@@ -48,7 +54,11 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
48
54
|
| `hd init [options]` | Configure this host and runner service |
|
|
49
55
|
| `hd upgrade [options]` | Refresh this host configuration |
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
`hd workspace new` uses the operator's authenticated `gh`, defaults to the repository's real default
|
|
58
|
+
branch, bootstraps an empty repository unless `--no-bootstrap` is set, and invites `mel-ilotus` unless
|
|
59
|
+
`--runner-user USER` overrides it.
|
|
60
|
+
|
|
61
|
+
Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/queue`, `/cancel`, `/epic new`,
|
|
62
|
+
`/epics`, `/plan`, `/decide`, `/agents add`, `/agents rm`, `/settings`, `/workspace`, `/feed`,
|
|
53
63
|
`/orchestrator`, `/refresh`, `/help`, or `/exit`. The display refreshes from
|
|
54
64
|
the HDX API every five seconds.
|
package/dist/api.js
CHANGED
|
@@ -55,6 +55,9 @@ export async function createTicket(fields, config = loadConfig()) {
|
|
|
55
55
|
export async function queueTicket(key, config = loadConfig()) {
|
|
56
56
|
return request(config, "POST", `/api/w/${config.slug}/tickets/${encodeURIComponent(key)}/queue`, undefined, true);
|
|
57
57
|
}
|
|
58
|
+
export async function cancelTicket(key, config = loadConfig()) {
|
|
59
|
+
return request(config, "POST", `/api/w/${config.slug}/tickets/${encodeURIComponent(key)}/cancel`);
|
|
60
|
+
}
|
|
58
61
|
export async function listTicketEvents(key, afterAt, afterId, config = loadConfig()) {
|
|
59
62
|
const query = new URLSearchParams();
|
|
60
63
|
if (afterAt)
|
|
@@ -81,12 +84,27 @@ export async function createWorkspace(fields, config = loadConfig()) {
|
|
|
81
84
|
export async function listWorkspaces(config = loadConfig()) {
|
|
82
85
|
return request(config, "GET", "/api/workspaces");
|
|
83
86
|
}
|
|
87
|
+
export async function getWorkspace(config = loadConfig()) {
|
|
88
|
+
return request(config, "GET", `/api/w/${config.slug}`);
|
|
89
|
+
}
|
|
90
|
+
export async function updateWorkspace(fields, config = loadConfig()) {
|
|
91
|
+
return request(config, "PATCH", `/api/w/${config.slug}`, fields);
|
|
92
|
+
}
|
|
93
|
+
export async function rotateWorkspaceApiKey(config = loadConfig()) {
|
|
94
|
+
return request(config, "POST", `/api/w/${config.slug}/api-key`);
|
|
95
|
+
}
|
|
84
96
|
export async function listAgents(config = loadConfig()) {
|
|
85
97
|
return request(config, "GET", `/api/w/${config.slug}/agents`);
|
|
86
98
|
}
|
|
99
|
+
export async function createAgent(fields, config = loadConfig()) {
|
|
100
|
+
return request(config, "POST", `/api/w/${config.slug}/agents`, fields);
|
|
101
|
+
}
|
|
87
102
|
export async function updateAgent(id, fields, config = loadConfig()) {
|
|
88
103
|
return request(config, "PATCH", `/api/w/${config.slug}/agents`, { id, ...fields });
|
|
89
104
|
}
|
|
105
|
+
export async function deleteAgent(id, config = loadConfig()) {
|
|
106
|
+
return request(config, "DELETE", `/api/w/${config.slug}/agents`, { id });
|
|
107
|
+
}
|
|
90
108
|
export async function listEpics(config = loadConfig()) {
|
|
91
109
|
return request(config, "GET", `/api/w/${config.slug}/epics`);
|
|
92
110
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { answerDecision, createEpic, createTicket, createWorkspace, getStatus, listAgents, listEpics, listTicketEvents, listTickets, listWorkspaces, postMessage, queueTicket, setPaused, showTicket, updateAgent, } from "./api.js";
|
|
4
|
+
import { answerDecision, cancelTicket, createAgent, createEpic, createTicket, createWorkspace, deleteAgent, getStatus, listAgents, listEpics, listTicketEvents, listTickets, listWorkspaces, postMessage, queueTicket, rotateWorkspaceApiKey, setPaused, showTicket, updateAgent, updateCaps, updateWorkspace, } from "./api.js";
|
|
5
5
|
import { initHost, parseHostFlags } from "./host.js";
|
|
6
6
|
import { loadConfig, writeHdConfig } from "./config.js";
|
|
7
7
|
import { epicProgressRows, readEpicSpec } from "./epics.js";
|
|
8
8
|
import { banner, c, statusChip, table, truncate, usage } from "./out.js";
|
|
9
9
|
import { launchArchitect } from "./plan.js";
|
|
10
|
+
import { HDX_RUNNER_GH_USER, preflightWorkspace } from "./workspace-preflight.js";
|
|
10
11
|
function fail(message) {
|
|
11
12
|
console.error(message);
|
|
12
13
|
process.exit(1);
|
|
@@ -118,7 +119,15 @@ async function cmdTicket(argv) {
|
|
|
118
119
|
console.log(`${c.bold(ticket.key)} ${statusChip(ticket.status)}`);
|
|
119
120
|
return;
|
|
120
121
|
}
|
|
121
|
-
|
|
122
|
+
if (action === "cancel") {
|
|
123
|
+
const key = rest[0];
|
|
124
|
+
if (!key)
|
|
125
|
+
fail("usage: hd ticket cancel KEY");
|
|
126
|
+
const { ticket } = await cancelTicket(key.toUpperCase());
|
|
127
|
+
console.log(`${c.bold(ticket.key)} ${statusChip(ticket.status)}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
fail("usage: hd ticket list | show KEY | new --title TITLE | queue KEY | cancel KEY");
|
|
122
131
|
}
|
|
123
132
|
async function cmdEpic(argv) {
|
|
124
133
|
const [action, ...rest] = argv;
|
|
@@ -218,7 +227,7 @@ async function cmdDecide(argv) {
|
|
|
218
227
|
await answerDecision(id, opts.answer);
|
|
219
228
|
console.log("answered");
|
|
220
229
|
}
|
|
221
|
-
async function cmdWorkspace(argv) {
|
|
230
|
+
async function cmdWorkspace(argv, deps = {}) {
|
|
222
231
|
const [action, ...rest] = argv;
|
|
223
232
|
if (action === "ls") {
|
|
224
233
|
const current = loadConfig().slug;
|
|
@@ -232,24 +241,80 @@ async function cmdWorkspace(argv) {
|
|
|
232
241
|
])));
|
|
233
242
|
return;
|
|
234
243
|
}
|
|
244
|
+
const workspaceUsage = "usage: hd workspace ls | new ... | set [flags] | rotate-key";
|
|
245
|
+
if (action === "rotate-key") {
|
|
246
|
+
const config = loadConfig();
|
|
247
|
+
const { api_key } = await rotateWorkspaceApiKey(config);
|
|
248
|
+
writeHdConfig({ ...config, api_key });
|
|
249
|
+
console.log(`api key: ${api_key}`);
|
|
250
|
+
console.log("saved locally; update Mel's hd init configuration on the box.");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (action === "set") {
|
|
254
|
+
const { rest: args, opts, bools } = flags(rest);
|
|
255
|
+
if (args.length || bools.size)
|
|
256
|
+
fail(workspaceUsage);
|
|
257
|
+
const fields = {};
|
|
258
|
+
for (const [flag, field] of [["name", "name"], ["repo", "repo"], ["branch", "default_branch"], ["host", "default_host"]]) {
|
|
259
|
+
if (opts[flag])
|
|
260
|
+
fields[field] = opts[flag];
|
|
261
|
+
}
|
|
262
|
+
if (opts["auto-merge"]) {
|
|
263
|
+
if (!["on", "off"].includes(opts["auto-merge"]))
|
|
264
|
+
fail("--auto-merge must be on or off");
|
|
265
|
+
fields.auto_merge = opts["auto-merge"] === "on";
|
|
266
|
+
}
|
|
267
|
+
if (opts["max-turns"]) {
|
|
268
|
+
const maxTurns = {};
|
|
269
|
+
for (const entry of opts["max-turns"].split(",")) {
|
|
270
|
+
const [kind, raw, ...extra] = entry.split("=");
|
|
271
|
+
const value = Number(raw);
|
|
272
|
+
if (extra.length || !["build", "followup", "review", "orchestrate"].includes(kind)
|
|
273
|
+
|| !Number.isInteger(value) || value < 1) {
|
|
274
|
+
fail("--max-turns must be KIND=N[,KIND=N...] for build, followup, review, or orchestrate");
|
|
275
|
+
}
|
|
276
|
+
maxTurns[kind] = value;
|
|
277
|
+
}
|
|
278
|
+
fields.max_turns = maxTurns;
|
|
279
|
+
}
|
|
280
|
+
if (opts["max-attempts"]) {
|
|
281
|
+
const value = Number(opts["max-attempts"]);
|
|
282
|
+
if (!Number.isInteger(value) || value < 1)
|
|
283
|
+
fail("--max-attempts must be an integer >= 1");
|
|
284
|
+
fields.max_attempts = value;
|
|
285
|
+
}
|
|
286
|
+
if (!Object.keys(fields).length)
|
|
287
|
+
fail(workspaceUsage);
|
|
288
|
+
const { workspace } = await updateWorkspace(fields);
|
|
289
|
+
console.log(`${workspace.slug} ${workspace.name} ${workspace.repo}`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
235
292
|
if (action !== "new")
|
|
236
|
-
fail(
|
|
237
|
-
const { opts } = flags(rest);
|
|
293
|
+
fail(workspaceUsage);
|
|
294
|
+
const { opts, bools } = flags(rest);
|
|
238
295
|
if (!opts.name || !opts.repo) {
|
|
239
|
-
fail(
|
|
296
|
+
fail(workspaceUsage);
|
|
240
297
|
}
|
|
298
|
+
const preflight = await (deps.preflightWorkspace ?? preflightWorkspace)({ name: opts.name, repo: opts.repo,
|
|
299
|
+
branch: opts.branch, noBootstrap: bools.has("no-bootstrap"),
|
|
300
|
+
runnerUser: opts["runner-user"] ?? HDX_RUNNER_GH_USER });
|
|
301
|
+
if (preflight.invitationPending)
|
|
302
|
+
console.log(`invitation pending for ${opts["runner-user"] ?? HDX_RUNNER_GH_USER}`);
|
|
241
303
|
const result = await createWorkspace({ name: opts.name, repo: opts.repo, slug: opts.slug,
|
|
242
|
-
default_branch:
|
|
304
|
+
default_branch: preflight.branch, default_host: opts.host ?? "box" });
|
|
243
305
|
console.log(`workspace: ${result.workspace.slug}`);
|
|
244
306
|
writeHdConfig({ url: result.url, api_key: result.api_key, slug: result.workspace.slug });
|
|
245
307
|
console.log("saved and switched; configure another machine with:");
|
|
246
308
|
console.log(`hd init --url ${result.url} --api-key ${result.api_key} --slug ${result.workspace.slug}`);
|
|
247
309
|
}
|
|
248
|
-
function selectAgent(agents,
|
|
249
|
-
const
|
|
310
|
+
function selectAgent(agents, target, provider) {
|
|
311
|
+
const exact = agents.find((agent) => agent.id === target);
|
|
312
|
+
if (exact)
|
|
313
|
+
return exact;
|
|
314
|
+
const matches = agents.filter((agent) => agent.role === target);
|
|
250
315
|
const selected = matches.length === 1 ? matches[0] : matches.find((agent) => agent.provider === provider);
|
|
251
316
|
if (!selected)
|
|
252
|
-
fail(`No unambiguous ${
|
|
317
|
+
fail(`No unambiguous agent ${target}${provider ? ` for provider ${provider}` : ""}.`);
|
|
253
318
|
return selected;
|
|
254
319
|
}
|
|
255
320
|
async function cmdAgents(argv) {
|
|
@@ -260,26 +325,59 @@ async function cmdAgents(argv) {
|
|
|
260
325
|
console.log("No agents.");
|
|
261
326
|
return;
|
|
262
327
|
}
|
|
263
|
-
console.log(table(["ROLE", "PROVIDER", "MODEL", "EFFORT", "STATE"], agents.map((agent) => [
|
|
264
|
-
agent.
|
|
328
|
+
console.log(table(["ID", "NAME", "ROLE", "PROVIDER", "MODEL", "EFFORT", "STATE"], agents.map((agent) => [
|
|
329
|
+
agent.id, agent.display_name, agent.role, agent.provider, agent.model, agent.effort,
|
|
330
|
+
agent.enabled ? c.green("on") : c.dim("off"),
|
|
265
331
|
])));
|
|
266
332
|
return;
|
|
267
333
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
334
|
+
const agentUsage = "usage: hd agents add ROLE --provider P --model M [--effort E] [--name N] | rm ROLE|ID | set ROLE|ID [flags]";
|
|
335
|
+
const { rest: args, opts, bools } = flags(rest);
|
|
336
|
+
const target = args[0];
|
|
337
|
+
if (action === "add") {
|
|
338
|
+
if (!target || !opts.provider || !opts.model)
|
|
339
|
+
fail(agentUsage);
|
|
340
|
+
const { agent } = await createAgent({ role: target, provider: opts.provider, model: opts.model,
|
|
341
|
+
effort: opts.effort, display_name: opts.name ?? opts.provider });
|
|
342
|
+
console.log(`${agent.id} ${agent.display_name} ${agent.role} ${agent.provider}`);
|
|
343
|
+
return;
|
|
274
344
|
}
|
|
345
|
+
if (!target)
|
|
346
|
+
fail(agentUsage);
|
|
275
347
|
const { agents } = await listAgents();
|
|
276
|
-
const current = selectAgent(agents,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
348
|
+
const current = selectAgent(agents, target, opts.provider);
|
|
349
|
+
if (action === "rm") {
|
|
350
|
+
await deleteAgent(current.id);
|
|
351
|
+
console.log(`removed ${current.display_name} (${current.id})`);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (action !== "set" || (bools.has("on") && bools.has("off")))
|
|
355
|
+
fail(agentUsage);
|
|
356
|
+
const fields = {
|
|
357
|
+
...(opts.provider ? { provider: opts.provider } : {}), ...(opts.model ? { model: opts.model } : {}),
|
|
358
|
+
...(opts.effort ? { effort: opts.effort } : {}), ...(opts.name ? { display_name: opts.name } : {}),
|
|
359
|
+
...(bools.has("on") || bools.has("off") ? { enabled: bools.has("on") } : {}),
|
|
360
|
+
};
|
|
361
|
+
if (!Object.keys(fields).length)
|
|
362
|
+
fail(agentUsage);
|
|
363
|
+
const { agent } = await updateAgent(current.id, fields);
|
|
364
|
+
console.log(`${agent.display_name} ${agent.role} ${agent.provider} ${agent.model} ${agent.enabled ? "on" : "off"}`);
|
|
365
|
+
}
|
|
366
|
+
async function cmdCaps(argv) {
|
|
367
|
+
const [action, provider, raw, ...extra] = argv;
|
|
368
|
+
if (!action) {
|
|
369
|
+
const caps = (await getStatus()).workspace.provider_caps;
|
|
370
|
+
console.log(table(["PROVIDER", "CAP"], Object.entries(caps).sort().map(([name, cap]) => [name, String(cap)])));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const cap = Number(raw);
|
|
374
|
+
if (action !== "set" || !provider || extra.length || !Number.isInteger(cap) || cap < 0) {
|
|
375
|
+
fail("usage: hd caps | hd caps set PROVIDER N");
|
|
376
|
+
}
|
|
377
|
+
const result = await updateCaps({ [provider]: cap });
|
|
378
|
+
console.log(`${provider} ${result.provider_caps[provider]}`);
|
|
281
379
|
}
|
|
282
|
-
export async function main(argv = process.argv.slice(2)) {
|
|
380
|
+
export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
283
381
|
const [cmd, ...rest] = argv;
|
|
284
382
|
try {
|
|
285
383
|
if (!cmd) {
|
|
@@ -329,13 +427,17 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
329
427
|
return;
|
|
330
428
|
}
|
|
331
429
|
if (cmd === "workspace") {
|
|
332
|
-
await cmdWorkspace(rest);
|
|
430
|
+
await cmdWorkspace(rest, deps);
|
|
333
431
|
return;
|
|
334
432
|
}
|
|
335
433
|
if (cmd === "agents") {
|
|
336
434
|
await cmdAgents(rest);
|
|
337
435
|
return;
|
|
338
436
|
}
|
|
437
|
+
if (cmd === "caps") {
|
|
438
|
+
await cmdCaps(rest);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
339
441
|
if (cmd === "pause" || cmd === "off") {
|
|
340
442
|
await setPaused(true);
|
|
341
443
|
console.log(cmd === "off" ? "off" : "paused");
|
package/dist/out.js
CHANGED
|
@@ -64,11 +64,12 @@ export function usage() {
|
|
|
64
64
|
return [
|
|
65
65
|
c.bold("Usage"),
|
|
66
66
|
` ${c.blue("hd status")} workspace overview`,
|
|
67
|
-
` ${c.blue("hd ticket list | show | new | queue")} ticket operations`,
|
|
67
|
+
` ${c.blue("hd ticket list | show | new | queue | cancel")} ticket operations`,
|
|
68
68
|
` ${c.blue("hd epic new PATH | list")} epic operations`,
|
|
69
69
|
` ${c.blue("hd plan [--repo DIR]")} author an epic with Codex`,
|
|
70
|
-
` ${c.blue("hd workspace ls | new")}
|
|
71
|
-
` ${c.blue("hd agents [set]")}
|
|
70
|
+
` ${c.blue("hd workspace ls | new | set | rotate-key")} workspace operations`,
|
|
71
|
+
` ${c.blue("hd agents [add | rm | set]")} manage agents`,
|
|
72
|
+
` ${c.blue("hd caps [set PROVIDER N]")} provider concurrency`,
|
|
72
73
|
` ${c.blue("hd logs KEY [-f]")} run events`,
|
|
73
74
|
` ${c.blue("hd msg KEY TEXT")} message a builder`,
|
|
74
75
|
` ${c.blue("hd decide [ID --answer TEXT]")} decisions`,
|
package/dist/tui/App.js
CHANGED
|
@@ -16,7 +16,7 @@ import { alertOnce } from "./alert.js";
|
|
|
16
16
|
import { bubbleRows } from "./height.js";
|
|
17
17
|
import { planLayout, splitPanels } from "./layout.js";
|
|
18
18
|
import { parseLine } from "./parse.js";
|
|
19
|
-
import { configuredSlugs, createEpicFromFile, decisionOptions, loadLiveEvents, loadTicketDetail, pollSnapshot, postOrchestrator, queueTicket, resolveDecision, switchWorkspace, updateAgent, updateProviderCap, } from "./data.js";
|
|
19
|
+
import { configuredSlugs, cancelTicket, createAgent, createEpicFromFile, decisionOptions, deleteAgent, loadLiveEvents, loadTicketDetail, pollSnapshot, postOrchestrator, queueTicket, resolveDecision, switchWorkspace, updateAgent, updateProviderCap, updateWorkspace, } from "./data.js";
|
|
20
20
|
import { editFor, editableKeys, nextValue, seedFor, settingsRows } from "./settings-model.js";
|
|
21
21
|
import { appendLines, runLabels, toStreamLines } from "./stream.js";
|
|
22
22
|
import { UI } from "./theme.js";
|
|
@@ -149,9 +149,12 @@ export function App({ initial }) {
|
|
|
149
149
|
if (edit.value.target === "cap") {
|
|
150
150
|
await updateProviderCap(config, edit.value.provider, edit.value.cap);
|
|
151
151
|
}
|
|
152
|
-
else {
|
|
152
|
+
else if (edit.value.target === "agent") {
|
|
153
153
|
await updateAgent(config, edit.value.id, edit.value.fields);
|
|
154
154
|
}
|
|
155
|
+
else {
|
|
156
|
+
await updateWorkspace(config, edit.value.fields);
|
|
157
|
+
}
|
|
155
158
|
setNotice(null);
|
|
156
159
|
await refresh();
|
|
157
160
|
}
|
|
@@ -348,6 +351,55 @@ export function App({ initial }) {
|
|
|
348
351
|
setBusy(false);
|
|
349
352
|
}
|
|
350
353
|
return;
|
|
354
|
+
case "cancel":
|
|
355
|
+
setBusy(true);
|
|
356
|
+
try {
|
|
357
|
+
const { ticket: cancelled } = await cancelTicket(config, action.key);
|
|
358
|
+
say("system", `${cancelled.key} cancelled.`);
|
|
359
|
+
await refresh();
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
setBusy(false);
|
|
366
|
+
}
|
|
367
|
+
return;
|
|
368
|
+
case "agent-add":
|
|
369
|
+
setBusy(true);
|
|
370
|
+
try {
|
|
371
|
+
const { agent } = await createAgent(config, { role: action.role, provider: action.provider,
|
|
372
|
+
model: action.model, effort: action.effort, display_name: action.name ?? action.provider });
|
|
373
|
+
say("system", `Added ${agent.display_name} (${agent.id}).`);
|
|
374
|
+
await refresh();
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
setBusy(false);
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
case "agent-rm": {
|
|
384
|
+
const matches = board.agents.filter((agent) => agent.id === action.target || agent.role === action.target);
|
|
385
|
+
if (matches.length !== 1) {
|
|
386
|
+
setNotice(`Agent ${action.target} is missing or ambiguous; use its ID.`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
setBusy(true);
|
|
390
|
+
try {
|
|
391
|
+
await deleteAgent(config, matches[0].id);
|
|
392
|
+
say("system", `Removed ${matches[0].display_name}.`);
|
|
393
|
+
await refresh();
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
397
|
+
}
|
|
398
|
+
finally {
|
|
399
|
+
setBusy(false);
|
|
400
|
+
}
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
351
403
|
case "plan":
|
|
352
404
|
setBusy(true);
|
|
353
405
|
try {
|
package/dist/tui/Help.js
CHANGED
|
@@ -9,11 +9,12 @@ export const COMMANDS = [
|
|
|
9
9
|
{ name: "/inbox", help: "decisions and messages waiting on you" },
|
|
10
10
|
{ name: "/ticket", args: "HD-12", help: "open one ticket" },
|
|
11
11
|
{ name: "/queue", args: "HD-12", help: "queue a complete ticket now" },
|
|
12
|
+
{ name: "/cancel", args: "HD-12", help: "cancel a ticket" },
|
|
12
13
|
{ name: "/epic", args: "new PATH", help: "create an epic from a Markdown spec" },
|
|
13
14
|
{ name: "/epics", help: "list epics and ticket progress" },
|
|
14
15
|
{ name: "/plan", help: "author an epic with your Codex CLI" },
|
|
15
16
|
{ name: "/decide", args: "2 | text", help: "answer the decision on screen" },
|
|
16
|
-
{ name: "/agents",
|
|
17
|
+
{ name: "/agents", args: "[add ... | rm ROLE|ID]", help: "view or manage agents" },
|
|
17
18
|
{ name: "/settings", help: "change provider caps and agent settings" },
|
|
18
19
|
{ name: "/workspace", args: "[slug]", help: "switch workspace, or list the ones you can reach" },
|
|
19
20
|
{ name: "/feed", help: "what just happened" },
|
package/dist/tui/data.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { answerDecision, createEpic as postEpic, getStatus, listAgents, listEpics, listFeed, listMessages, listTicketEvents, listTickets, listWorkspaces, postMessage as sendMessage, queueTicket as queueTicketNow, showTicket, updateAgent as patchAgent, updateCaps, } from "../api.js";
|
|
1
|
+
import { answerDecision, cancelTicket as cancelTicketNow, createAgent as postAgent, createEpic as postEpic, getStatus, getWorkspace, listAgents, listEpics, listFeed, listMessages, listTicketEvents, listTickets, listWorkspaces, postMessage as sendMessage, queueTicket as queueTicketNow, 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
|
export const POLL_MS = 5_000;
|
|
@@ -22,8 +22,9 @@ export async function configuredSlugs(config = loadConfig()) {
|
|
|
22
22
|
return (await listWorkspaces(config)).map((workspace) => workspace.slug).sort();
|
|
23
23
|
}
|
|
24
24
|
export async function loadSnapshot(config = loadConfig()) {
|
|
25
|
-
const [status, ticketData, agentData, epicData, feedData] = await Promise.all([
|
|
25
|
+
const [status, workspaceData, ticketData, agentData, epicData, feedData] = await Promise.all([
|
|
26
26
|
getStatus(config),
|
|
27
|
+
getWorkspace(config),
|
|
27
28
|
listTickets(config),
|
|
28
29
|
listAgents(config),
|
|
29
30
|
listEpics(config),
|
|
@@ -44,7 +45,19 @@ export async function loadSnapshot(config = loadConfig()) {
|
|
|
44
45
|
});
|
|
45
46
|
return {
|
|
46
47
|
config,
|
|
47
|
-
workspace:
|
|
48
|
+
workspace: {
|
|
49
|
+
...status.workspace,
|
|
50
|
+
...workspaceData.workspace,
|
|
51
|
+
paused: status.workspace.paused,
|
|
52
|
+
provider_caps: status.workspace.provider_caps,
|
|
53
|
+
max_turns: {
|
|
54
|
+
build: Number(workspaceData.workspace.settings.max_turns?.build ?? 40),
|
|
55
|
+
followup: Number(workspaceData.workspace.settings.max_turns?.followup ?? 15),
|
|
56
|
+
review: Number(workspaceData.workspace.settings.max_turns?.review ?? 50),
|
|
57
|
+
orchestrate: Number(workspaceData.workspace.settings.max_turns?.orchestrate ?? 30),
|
|
58
|
+
},
|
|
59
|
+
max_attempts: Number(workspaceData.workspace.settings.max_attempts ?? 3),
|
|
60
|
+
},
|
|
48
61
|
board: {
|
|
49
62
|
tickets,
|
|
50
63
|
agents: agentData.agents,
|
|
@@ -105,6 +118,15 @@ export async function createEpicFromFile(config, path) {
|
|
|
105
118
|
export async function queueTicket(config, key) {
|
|
106
119
|
return queueTicketNow(key, config);
|
|
107
120
|
}
|
|
121
|
+
export async function cancelTicket(config, key) {
|
|
122
|
+
return cancelTicketNow(key, config);
|
|
123
|
+
}
|
|
124
|
+
export async function createAgent(config, fields) {
|
|
125
|
+
return postAgent(fields, config);
|
|
126
|
+
}
|
|
127
|
+
export async function deleteAgent(config, id) {
|
|
128
|
+
return removeAgent(id, config);
|
|
129
|
+
}
|
|
108
130
|
export async function waitForReply(config, since, timeoutMs) {
|
|
109
131
|
const deadline = Date.now() + timeoutMs;
|
|
110
132
|
while (Date.now() <= deadline) {
|
|
@@ -126,6 +148,9 @@ export async function updateAgent(config, id, fields) {
|
|
|
126
148
|
export async function updateProviderCap(config, provider, cap) {
|
|
127
149
|
return updateCaps({ [provider]: cap }, config);
|
|
128
150
|
}
|
|
151
|
+
export async function updateWorkspace(config, fields) {
|
|
152
|
+
return patchWorkspace(fields, config);
|
|
153
|
+
}
|
|
129
154
|
export async function loadLiveEvents(config, board) {
|
|
130
155
|
const liveIds = new Set(board.runs.filter((run) => run.status === "running").map((run) => run.id));
|
|
131
156
|
const keys = new Set(board.runs
|
package/dist/tui/parse.js
CHANGED
|
@@ -14,11 +14,25 @@ export function parseLine(raw) {
|
|
|
14
14
|
case "orchestrator":
|
|
15
15
|
return { kind: "mode", mode: "orchestrator" };
|
|
16
16
|
case "board":
|
|
17
|
-
case "agents":
|
|
18
17
|
case "feed":
|
|
19
18
|
case "inbox":
|
|
20
19
|
case "settings":
|
|
21
20
|
return { kind: "view", view: word.toLowerCase() };
|
|
21
|
+
case "agents": {
|
|
22
|
+
if (!argument)
|
|
23
|
+
return { kind: "view", view: "agents" };
|
|
24
|
+
const [verb, target, ...tail] = rest;
|
|
25
|
+
if (verb === "rm" && target && !tail.length)
|
|
26
|
+
return { kind: "agent-rm", target };
|
|
27
|
+
const opts = {};
|
|
28
|
+
for (let i = 0; i < tail.length; i += 2)
|
|
29
|
+
if (tail[i]?.startsWith("--") && tail[i + 1])
|
|
30
|
+
opts[tail[i].slice(2)] = tail[i + 1];
|
|
31
|
+
return verb === "add" && target && opts.provider && opts.model
|
|
32
|
+
? { kind: "agent-add", role: target, provider: opts.provider, model: opts.model,
|
|
33
|
+
...(opts.effort ? { effort: opts.effort } : {}), ...(opts.name ? { name: opts.name } : {}) }
|
|
34
|
+
: { kind: "unknown", command: "agents needs add ROLE --provider P --model M or rm ROLE|ID" };
|
|
35
|
+
}
|
|
22
36
|
case "help":
|
|
23
37
|
return { kind: "help" };
|
|
24
38
|
case "workspace":
|
|
@@ -39,6 +53,9 @@ export function parseLine(raw) {
|
|
|
39
53
|
return argument
|
|
40
54
|
? { kind: "queue", key: argument.toUpperCase() }
|
|
41
55
|
: { kind: "unknown", command: "queue needs a key" };
|
|
56
|
+
case "cancel":
|
|
57
|
+
return argument ? { kind: "cancel", key: argument.toUpperCase() }
|
|
58
|
+
: { kind: "unknown", command: "cancel needs a key" };
|
|
42
59
|
case "plan":
|
|
43
60
|
return argument ? { kind: "unknown", command: "plan takes no arguments" } : { kind: "plan" };
|
|
44
61
|
case "decide": {
|
|
@@ -2,8 +2,16 @@ import { efforts, providers } from "./data.js";
|
|
|
2
2
|
export function settingsRows(workspace, agents) {
|
|
3
3
|
const rows = [
|
|
4
4
|
{ key: "h:workspace", kind: "heading", label: workspace.slug, value: workspace.repo },
|
|
5
|
-
{ key: "w:
|
|
6
|
-
{ key: "w:
|
|
5
|
+
{ key: "w:name", kind: "text", label: "name", value: workspace.name },
|
|
6
|
+
{ key: "w:repo", kind: "text", label: "repo", value: workspace.repo, hint: "owner/name" },
|
|
7
|
+
{ key: "w:default_branch", kind: "text", label: "branch", value: workspace.default_branch },
|
|
8
|
+
{ key: "w:default_host", kind: "text", label: "host", value: workspace.default_host },
|
|
9
|
+
{ key: "w:auto_merge", kind: "toggle", label: "auto merge", value: workspace.auto_merge ? "yes" : "no" },
|
|
10
|
+
...["build", "followup", "review", "orchestrate"].map((kind) => ({
|
|
11
|
+
key: `w:max_turns:${kind}`, kind: "number", label: `turns ${kind}`,
|
|
12
|
+
value: String(workspace.max_turns[kind]), hint: "integer >= 1",
|
|
13
|
+
})),
|
|
14
|
+
{ key: "w:max_attempts", kind: "number", label: "max attempts", value: String(workspace.max_attempts) },
|
|
7
15
|
];
|
|
8
16
|
for (const provider of providers) {
|
|
9
17
|
rows.push({
|
|
@@ -16,6 +24,7 @@ export function settingsRows(workspace, agents) {
|
|
|
16
24
|
}
|
|
17
25
|
for (const agent of agents) {
|
|
18
26
|
rows.push({ key: `h:${agent.id}`, kind: "heading", label: agent.display_name, value: agent.role });
|
|
27
|
+
rows.push({ key: `a:${agent.id}:display_name`, kind: "text", label: "name", value: agent.display_name });
|
|
19
28
|
rows.push({
|
|
20
29
|
key: `a:${agent.id}:enabled`, kind: "toggle", label: "enabled",
|
|
21
30
|
value: agent.enabled ? "yes" : "no", agent: agent.display_name,
|
|
@@ -59,6 +68,25 @@ export function editFor(row, raw) {
|
|
|
59
68
|
return { ok: false, error: `${field} cap must be an integer >= 0.` };
|
|
60
69
|
return { ok: true, value: { target: "cap", provider: field, cap } };
|
|
61
70
|
}
|
|
71
|
+
if (scope === "w" && id) {
|
|
72
|
+
if (id === "max_turns" && field) {
|
|
73
|
+
const number = Number(value);
|
|
74
|
+
if (!Number.isInteger(number) || number < 1)
|
|
75
|
+
return { ok: false, error: `max_turns.${field} must be an integer >= 1.` };
|
|
76
|
+
return { ok: true, value: { target: "workspace", fields: { max_turns: { [field]: number } } } };
|
|
77
|
+
}
|
|
78
|
+
if (id === "max_attempts") {
|
|
79
|
+
const number = Number(value);
|
|
80
|
+
if (!Number.isInteger(number) || number < 1)
|
|
81
|
+
return { ok: false, error: "max_attempts must be an integer >= 1." };
|
|
82
|
+
return { ok: true, value: { target: "workspace", fields: { max_attempts: number } } };
|
|
83
|
+
}
|
|
84
|
+
if (id === "auto_merge")
|
|
85
|
+
return { ok: true, value: { target: "workspace", fields: { auto_merge: value === "yes" } } };
|
|
86
|
+
if (!value)
|
|
87
|
+
return { ok: false, error: `${row.label} is required.` };
|
|
88
|
+
return { ok: true, value: { target: "workspace", fields: { [id]: value } } };
|
|
89
|
+
}
|
|
62
90
|
if (scope !== "a" || !id || !field)
|
|
63
91
|
return { ok: false, error: `Nothing to change on ${row.label}.` };
|
|
64
92
|
if (field === "enabled") {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const exec = promisify(execFile);
|
|
4
|
+
export const HDX_RUNNER_GH_USER = "mel-ilotus";
|
|
5
|
+
export function preflightDecision(input) {
|
|
6
|
+
if (!input.found)
|
|
7
|
+
return { ok: false, error: "Repository was not found or is inaccessible." };
|
|
8
|
+
if (input.viewerPermission.toUpperCase() !== "ADMIN") {
|
|
9
|
+
return { ok: false, error: `Repository admin permission is required; viewer has ${input.viewerPermission || "none"}.` };
|
|
10
|
+
}
|
|
11
|
+
if (input.empty && input.noBootstrap) {
|
|
12
|
+
return { ok: false, error: "Repository is empty and --no-bootstrap was set." };
|
|
13
|
+
}
|
|
14
|
+
return { ok: true, bootstrap: input.empty };
|
|
15
|
+
}
|
|
16
|
+
function errorMessage(error) {
|
|
17
|
+
if (!error || typeof error !== "object")
|
|
18
|
+
return String(error);
|
|
19
|
+
const value = error;
|
|
20
|
+
return value.stderr?.trim() || value.message || String(error);
|
|
21
|
+
}
|
|
22
|
+
async function defaultGh(args) {
|
|
23
|
+
try {
|
|
24
|
+
return (await exec("gh", args, { encoding: "utf8", maxBuffer: 1024 * 1024 })).stdout;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new Error(errorMessage(error));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function repoView(repo, gh) {
|
|
31
|
+
return JSON.parse(await gh(["repo", "view", repo, "--json", "defaultBranchRef,isEmpty,viewerPermission"]));
|
|
32
|
+
}
|
|
33
|
+
function canPush(permission) {
|
|
34
|
+
return ["admin", "maintain", "write", "push"].includes(permission.toLowerCase());
|
|
35
|
+
}
|
|
36
|
+
export async function preflightWorkspace(input, gh = defaultGh) {
|
|
37
|
+
try {
|
|
38
|
+
await gh(["--version"]);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error("GitHub CLI is unavailable. Install it if needed, then run `gh auth login`.");
|
|
42
|
+
}
|
|
43
|
+
let view;
|
|
44
|
+
try {
|
|
45
|
+
view = await repoView(input.repo, gh);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
throw new Error(`GitHub repository ${input.repo} was not found or is inaccessible: ${errorMessage(error)}`);
|
|
49
|
+
}
|
|
50
|
+
const decision = preflightDecision({ found: true, empty: view.isEmpty,
|
|
51
|
+
viewerPermission: view.viewerPermission, noBootstrap: input.noBootstrap });
|
|
52
|
+
if (!decision.ok)
|
|
53
|
+
throw new Error(decision.error);
|
|
54
|
+
if (decision.bootstrap) {
|
|
55
|
+
await gh(["api", "-X", "PUT", `repos/${input.repo}/contents/README.md`,
|
|
56
|
+
"-f", "message=Initial commit", "-f", `content=${Buffer.from(`# ${input.name}`).toString("base64")}`]);
|
|
57
|
+
view = await repoView(input.repo, gh);
|
|
58
|
+
}
|
|
59
|
+
const branch = input.branch || view.defaultBranchRef?.name;
|
|
60
|
+
if (!branch)
|
|
61
|
+
throw new Error(`GitHub repository ${input.repo} has no default branch.`);
|
|
62
|
+
const runnerUser = input.runnerUser || HDX_RUNNER_GH_USER;
|
|
63
|
+
let permission = "none";
|
|
64
|
+
try {
|
|
65
|
+
const result = JSON.parse(await gh(["api", `repos/${input.repo}/collaborators/${runnerUser}/permission`]));
|
|
66
|
+
permission = result.permission ?? "none";
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (!/404|not found/i.test(errorMessage(error)))
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
const invitationPending = !canPush(permission);
|
|
73
|
+
if (invitationPending) {
|
|
74
|
+
await gh(["api", "-X", "PUT", `repos/${input.repo}/collaborators/${runnerUser}`, "-f", "permission=push"]);
|
|
75
|
+
}
|
|
76
|
+
return { branch, invitationPending };
|
|
77
|
+
}
|