@higherdev/cli 0.11.0 → 0.12.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 +7 -4
- package/dist/api.js +7 -1
- package/dist/config.js +19 -3
- package/dist/index.js +19 -9
- package/dist/out.js +2 -2
- package/dist/prompt.js +13 -0
- package/dist/tui/App.js +46 -29
- package/dist/tui/Help.js +5 -2
- package/dist/tui/data.js +11 -5
- package/dist/tui/parse.js +14 -5
- package/dist/tui/theme.js +2 -1
- package/dist/workspace-commands.js +9 -17
- package/package.json +3 -3
- package/dist/plan.js +0 -94
- package/prompts/architect.md +0 -23
package/README.md
CHANGED
|
@@ -14,7 +14,8 @@ Create `~/.config/hd/config.json` with an existing workspace API key:
|
|
|
14
14
|
{
|
|
15
15
|
"url": "https://hdx-higher-ops.vercel.app",
|
|
16
16
|
"api_key": "hdx_...",
|
|
17
|
-
"current": "workspace"
|
|
17
|
+
"current": "workspace",
|
|
18
|
+
"repos": {}
|
|
18
19
|
}
|
|
19
20
|
```
|
|
20
21
|
|
|
@@ -35,7 +36,9 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
35
36
|
| `hd ticket cancel KEY` | Cancel a ticket |
|
|
36
37
|
| `hd epic new PATH [--title TITLE]` | Create an epic from a Markdown spec |
|
|
37
38
|
| `hd epic list` | List epics and ticket progress |
|
|
38
|
-
| `hd
|
|
39
|
+
| `hd epic approve ID` | Open a draft epic for orchestrator decomposition |
|
|
40
|
+
| `hd epic rm ID` | Remove a draft epic |
|
|
41
|
+
| `hd plan` | Point to the TUI `/architect` conversation |
|
|
39
42
|
| `hd workspace ls` | List every workspace available to the configured key |
|
|
40
43
|
| `hd workspace new --name NAME --repo OWNER/NAME [options]` | Preflight GitHub, wire the runner, and create a paused workspace |
|
|
41
44
|
| `hd workspace set [options]` | Update settings; max turns uses `--max-turns KIND=N[,KIND=N...]` |
|
|
@@ -61,7 +64,7 @@ the repository is missing, approve private creation interactively, use `--create
|
|
|
61
64
|
`--no-create` to fail. Fully flagged calls remain non-interactive for scripts and Mel.
|
|
62
65
|
|
|
63
66
|
Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/queue`, `/cancel`, `/epic new`,
|
|
64
|
-
`/epics`, `/plan
|
|
67
|
+
`/epic approve`, `/epics`, `/architect` (or `/plan`), `/decide`, `/agents add`, `/agents rm`, `/settings`, `/workspace`,
|
|
65
68
|
`/workspace new`, `/workspace set`, `/workspace rotate-key`, `/feed`,
|
|
66
|
-
`/orchestrator`, `/refresh`, `/help`, or `/exit`. The display refreshes from
|
|
69
|
+
`/on`, `/off`, `/orchestrator`, `/refresh`, `/help`, or `/exit`. The display refreshes from
|
|
67
70
|
the HDX API every five seconds.
|
package/dist/api.js
CHANGED
|
@@ -4,7 +4,7 @@ export function apiErrorMessage(status, parsed, fallback) {
|
|
|
4
4
|
const raw = parsed && typeof parsed === "object" && "error" in parsed
|
|
5
5
|
&& typeof parsed.error === "string"
|
|
6
6
|
? parsed.error : fallback;
|
|
7
|
-
const duplicate = raw.match(/agents_one_enabled_(orchestrator|reviewer)_idx/i)?.[1]?.toLowerCase();
|
|
7
|
+
const duplicate = raw.match(/agents_one_enabled_(architect|orchestrator|reviewer)_idx/i)?.[1]?.toLowerCase();
|
|
8
8
|
if (duplicate) {
|
|
9
9
|
return `hd: ${status} Only one enabled ${duplicate} is allowed per workspace. Disable the current ${duplicate} before enabling another.`;
|
|
10
10
|
}
|
|
@@ -111,6 +111,12 @@ export async function listEpics(config = loadConfig()) {
|
|
|
111
111
|
export async function createEpic(fields, config = loadConfig()) {
|
|
112
112
|
return request(config, "POST", `/api/w/${config.slug}/epics`, fields);
|
|
113
113
|
}
|
|
114
|
+
export async function approveEpic(id, config = loadConfig()) {
|
|
115
|
+
return request(config, "PATCH", `/api/w/${config.slug}/epics`, { id });
|
|
116
|
+
}
|
|
117
|
+
export async function deleteEpic(id, config = loadConfig()) {
|
|
118
|
+
return request(config, "DELETE", `/api/w/${config.slug}/epics`, { id });
|
|
119
|
+
}
|
|
114
120
|
export async function listMessages(options = {}, config = loadConfig()) {
|
|
115
121
|
const query = new URLSearchParams();
|
|
116
122
|
if (options.ticketId)
|
package/dist/config.js
CHANGED
|
@@ -16,6 +16,11 @@ function normalizeConnection(value) {
|
|
|
16
16
|
function missing(path) {
|
|
17
17
|
return new Error(`missing ${path}\nWrite { "url": "https://hdx-higher-ops.vercel.app", "api_key": "hdx_...", "current": "workspace" }`);
|
|
18
18
|
}
|
|
19
|
+
function normalizeRepos(value) {
|
|
20
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
21
|
+
return {};
|
|
22
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => Boolean(entry[0]) && typeof entry[1] === "string" && Boolean(entry[1])));
|
|
23
|
+
}
|
|
19
24
|
export function loadStoredConfig(path = configPath()) {
|
|
20
25
|
let raw;
|
|
21
26
|
try {
|
|
@@ -27,13 +32,14 @@ export function loadStoredConfig(path = configPath()) {
|
|
|
27
32
|
const parsed = JSON.parse(raw);
|
|
28
33
|
const connection = normalizeConnection(parsed);
|
|
29
34
|
const current = typeof parsed.current === "string" ? parsed.current : "";
|
|
35
|
+
const repos = normalizeRepos(parsed.repos);
|
|
30
36
|
if (connection && current)
|
|
31
|
-
return { ...connection, current };
|
|
37
|
+
return { ...connection, current, repos };
|
|
32
38
|
// 0.4 stored one workspace at the top level with `slug` rather than
|
|
33
39
|
// `current`. One key can now reach every workspace, so only the selected
|
|
34
40
|
// slug needs to survive.
|
|
35
41
|
if (connection && typeof parsed.slug === "string" && parsed.slug) {
|
|
36
|
-
const migrated = { ...connection, current: parsed.slug };
|
|
42
|
+
const migrated = { ...connection, current: parsed.slug, repos };
|
|
37
43
|
writeStoredConfig(migrated, path);
|
|
38
44
|
return migrated;
|
|
39
45
|
}
|
|
@@ -46,7 +52,7 @@ export function loadStoredConfig(path = configPath()) {
|
|
|
46
52
|
const selected = normalizeConnection(source[current]);
|
|
47
53
|
if (!selected)
|
|
48
54
|
throw new Error(`${path} current workspace ${current} is not configured`);
|
|
49
|
-
const migrated = { ...selected, current };
|
|
55
|
+
const migrated = { ...selected, current, repos };
|
|
50
56
|
writeStoredConfig(migrated, path);
|
|
51
57
|
return migrated;
|
|
52
58
|
}
|
|
@@ -62,10 +68,16 @@ export function writeStoredConfig(config, path = configPath()) {
|
|
|
62
68
|
}
|
|
63
69
|
/** Store one operator connection and make its selected workspace current. */
|
|
64
70
|
export function writeHdConfig(config, path = configPath()) {
|
|
71
|
+
let repos = {};
|
|
72
|
+
try {
|
|
73
|
+
repos = loadStoredConfig(path).repos;
|
|
74
|
+
}
|
|
75
|
+
catch { }
|
|
65
76
|
writeStoredConfig({
|
|
66
77
|
url: config.url.replace(/\/$/, ""),
|
|
67
78
|
api_key: config.api_key,
|
|
68
79
|
current: config.slug,
|
|
80
|
+
repos,
|
|
69
81
|
}, path);
|
|
70
82
|
}
|
|
71
83
|
export function switchWorkspace(slug, path = configPath()) {
|
|
@@ -73,3 +85,7 @@ export function switchWorkspace(slug, path = configPath()) {
|
|
|
73
85
|
writeStoredConfig({ ...stored, current: slug }, path);
|
|
74
86
|
return { url: stored.url, api_key: stored.api_key, slug };
|
|
75
87
|
}
|
|
88
|
+
export function rememberWorkspaceRepo(slug, directory, path = configPath()) {
|
|
89
|
+
const stored = loadStoredConfig(path);
|
|
90
|
+
writeStoredConfig({ ...stored, repos: { ...stored.repos, [slug]: directory } }, path);
|
|
91
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { answerDecision, cancelTicket, createAgent, createEpic, createTicket, deleteAgent, getStatus, listAgents, listEpics, listTicketEvents, listTickets, listWorkspaces, postMessage, queueTicket, setPaused, showTicket, updateAgent, updateCaps, } from "./api.js";
|
|
4
|
+
import { approveEpic, answerDecision, cancelTicket, createAgent, createEpic, createTicket, deleteAgent, deleteEpic, getStatus, listAgents, listEpics, listTicketEvents, listTickets, listWorkspaces, postMessage, queueTicket, setPaused, showTicket, updateAgent, updateCaps, } from "./api.js";
|
|
5
5
|
import { initHost, parseHostFlags } from "./host.js";
|
|
6
6
|
import { loadConfig } from "./config.js";
|
|
7
7
|
import { epicProgressRows, readEpicSpec } from "./epics.js";
|
|
8
8
|
import { banner, c, statusChip, table, truncate, usage } from "./out.js";
|
|
9
|
-
import { launchArchitect } from "./plan.js";
|
|
10
9
|
import { WORKSPACE_USAGE, workspaceNew, workspaceRotateKey, workspaceSet } from "./workspace-commands.js";
|
|
11
10
|
function fail(message) {
|
|
12
11
|
console.error(message);
|
|
@@ -153,15 +152,26 @@ async function cmdEpic(argv) {
|
|
|
153
152
|
console.log(`${c.bold(epic.id)} ${statusChip(epic.status)} ${epic.title}`);
|
|
154
153
|
return;
|
|
155
154
|
}
|
|
156
|
-
|
|
155
|
+
if (action === "approve" || action === "rm") {
|
|
156
|
+
const id = rest[0];
|
|
157
|
+
if (!id || rest.length !== 1)
|
|
158
|
+
fail(`usage: hd epic ${action} ID`);
|
|
159
|
+
if (action === "approve") {
|
|
160
|
+
const { epic } = await approveEpic(id);
|
|
161
|
+
console.log(`${c.bold(epic.id)} ${statusChip(epic.status)} ${epic.title}`);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
await deleteEpic(id);
|
|
165
|
+
console.log(`removed ${id}`);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
fail("usage: hd epic new PATH [--title TITLE] | list | approve ID | rm ID");
|
|
157
170
|
}
|
|
158
171
|
async function cmdPlan(argv) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
const { workspace } = await getStatus();
|
|
164
|
-
await launchArchitect({ workspaceRepo: workspace.repo, repo: opts.repo });
|
|
172
|
+
if (argv.length)
|
|
173
|
+
fail("usage: hd plan");
|
|
174
|
+
console.log("Open the HDX TUI and use /architect (or /plan).");
|
|
165
175
|
}
|
|
166
176
|
async function cmdLogs(argv) {
|
|
167
177
|
const { rest, bools } = flags(argv);
|
package/dist/out.js
CHANGED
|
@@ -65,8 +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 | new | queue | cancel")} ticket operations`,
|
|
68
|
-
` ${c.blue("hd epic new PATH | list")}
|
|
69
|
-
` ${c.blue("hd plan
|
|
68
|
+
` ${c.blue("hd epic new PATH | list | approve | rm")} epic operations`,
|
|
69
|
+
` ${c.blue("hd plan")} use /architect in the TUI`,
|
|
70
70
|
` ${c.blue("hd workspace ls | new | set | rotate-key")} workspace operations`,
|
|
71
71
|
` ${c.blue("hd agents [add | rm | set]")} manage agents`,
|
|
72
72
|
` ${c.blue("hd caps [set PROVIDER N]")} provider concurrency`,
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
export async function promptOnStdin(question, resumeStdin = false) {
|
|
3
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
4
|
+
try {
|
|
5
|
+
return await prompt.question(question);
|
|
6
|
+
}
|
|
7
|
+
finally {
|
|
8
|
+
prompt.close();
|
|
9
|
+
// Ink restores raw mode and listeners, but it cannot read a paused stream.
|
|
10
|
+
if (resumeStdin)
|
|
11
|
+
process.stdin.resume();
|
|
12
|
+
}
|
|
13
|
+
}
|
package/dist/tui/App.js
CHANGED
|
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
3
3
|
import { Box, Static, Text, useApp, useInput, useStdout } from "ink";
|
|
4
4
|
import { loadConfig } from "../config.js";
|
|
5
5
|
import { epicProgressRows } from "../epics.js";
|
|
6
|
-
import {
|
|
6
|
+
import { promptOnStdin } from "../prompt.js";
|
|
7
7
|
import { workspaceNew, workspaceRotateKey, workspaceSet } from "../workspace-commands.js";
|
|
8
8
|
import { Banner } from "./Banner.js";
|
|
9
9
|
import { Bubble } from "./Bubble.js";
|
|
@@ -18,13 +18,14 @@ import { alertOnce } from "./alert.js";
|
|
|
18
18
|
import { bubbleRows } from "./height.js";
|
|
19
19
|
import { planLayout, splitPanels } from "./layout.js";
|
|
20
20
|
import { parseLine } from "./parse.js";
|
|
21
|
-
import { configuredSlugs, cancelTicket, createAgent, createEpicFromFile, decisionOptions, deleteAgent, loadLiveEvents, loadTicketDetail, pollSnapshot,
|
|
21
|
+
import { configuredSlugs, approveEpic, cancelTicket, createAgent, createEpicFromFile, decisionOptions, deleteAgent, loadLiveEvents, loadTicketDetail, pollSnapshot, postAgentMessage, queueTicket, resolveDecision, setWorkspacePaused, switchWorkspace, updateAgent, updateProviderCap, updateWorkspace, } from "./data.js";
|
|
22
22
|
import { editFor, editableKeys, nextValue, seedFor, settingsRows } from "./settings-model.js";
|
|
23
23
|
import { appendLines, runLabels, toStreamLines } from "./stream.js";
|
|
24
24
|
import { UI } from "./theme.js";
|
|
25
25
|
import { WorkspaceLoads } from "./workspace-load.js";
|
|
26
26
|
let messageSeq = 0;
|
|
27
27
|
const nextId = () => `m${messageSeq++}`;
|
|
28
|
+
const tuiPrompt = (question) => promptOnStdin(question, true);
|
|
28
29
|
export function App({ initial }) {
|
|
29
30
|
const { exit, suspendTerminal } = useApp();
|
|
30
31
|
const { stdout } = useStdout();
|
|
@@ -191,18 +192,18 @@ export function App({ initial }) {
|
|
|
191
192
|
setBusy(false);
|
|
192
193
|
}
|
|
193
194
|
}, [config, say]);
|
|
194
|
-
const
|
|
195
|
+
const askAgent = useCallback(async (role, text) => {
|
|
195
196
|
setBusy(true);
|
|
196
197
|
const id = nextId();
|
|
197
|
-
setMessages((prior) => [...prior, { id, speaker:
|
|
198
|
+
setMessages((prior) => [...prior, { id, speaker: role, body: "", pending: true }]);
|
|
198
199
|
const since = new Date().toISOString();
|
|
199
200
|
try {
|
|
200
|
-
await
|
|
201
|
+
await postAgentMessage(role, text, config);
|
|
201
202
|
setMessages((prior) => prior.map((message) => message.id === id
|
|
202
203
|
? { ...message, steps: ["· queued, it answers on the next tick"] }
|
|
203
204
|
: message));
|
|
204
205
|
const { waitForReply } = await import("./data.js");
|
|
205
|
-
const reply = await waitForReply(config, since, 180_000);
|
|
206
|
+
const reply = await waitForReply(config, role, since, 180_000);
|
|
206
207
|
setMessages((prior) => prior.map((message) => message.id === id
|
|
207
208
|
? { ...message, body: reply ?? "No reply yet. It will land in /inbox.", pending: false, steps: [] }
|
|
208
209
|
: message));
|
|
@@ -256,18 +257,20 @@ export function App({ initial }) {
|
|
|
256
257
|
const action = parseLine(text);
|
|
257
258
|
if (action.kind === "say") {
|
|
258
259
|
say("you", text);
|
|
259
|
-
if (mode
|
|
260
|
-
await
|
|
260
|
+
if (mode !== "browse")
|
|
261
|
+
await askAgent(mode, text);
|
|
261
262
|
else
|
|
262
|
-
setNotice("Use /orchestrator before sending a message.");
|
|
263
|
+
setNotice("Use /architect or /orchestrator before sending a message.");
|
|
263
264
|
return;
|
|
264
265
|
}
|
|
265
266
|
switch (action.kind) {
|
|
266
267
|
case "mode":
|
|
267
|
-
setMode(
|
|
268
|
+
setMode(action.mode);
|
|
268
269
|
setCursor(null);
|
|
269
270
|
selectedRef.current = null;
|
|
270
|
-
say("system",
|
|
271
|
+
say("system", action.mode === "architect"
|
|
272
|
+
? "Talking to the architect. Ask questions, refine the epic, then explicitly request a draft."
|
|
273
|
+
: "Talking to the orchestrator. It moves work already in flight.");
|
|
271
274
|
return;
|
|
272
275
|
case "view":
|
|
273
276
|
setView(action.view);
|
|
@@ -305,7 +308,7 @@ export function App({ initial }) {
|
|
|
305
308
|
try {
|
|
306
309
|
if (action.command === "new") {
|
|
307
310
|
let created;
|
|
308
|
-
await suspendTerminal(async () => { created = await workspaceNew(action.args); });
|
|
311
|
+
await suspendTerminal(async () => { created = await workspaceNew(action.args, { prompt: tuiPrompt }); });
|
|
309
312
|
if (!created)
|
|
310
313
|
throw new Error("Workspace wizard did not finish.");
|
|
311
314
|
await changeWorkspace(created.slug, loadConfig());
|
|
@@ -361,6 +364,20 @@ export function App({ initial }) {
|
|
|
361
364
|
setBusy(false);
|
|
362
365
|
}
|
|
363
366
|
return;
|
|
367
|
+
case "epic-approve":
|
|
368
|
+
setBusy(true);
|
|
369
|
+
try {
|
|
370
|
+
const { epic } = await approveEpic(config, action.id);
|
|
371
|
+
say("system", `Approved epic ${epic.id}: ${epic.title}`);
|
|
372
|
+
await refresh();
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
setBusy(false);
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
364
381
|
case "epics": {
|
|
365
382
|
const rows = epicProgressRows(board.epics, board.tickets);
|
|
366
383
|
say("system", rows.length
|
|
@@ -368,6 +385,20 @@ export function App({ initial }) {
|
|
|
368
385
|
: "No epics.");
|
|
369
386
|
return;
|
|
370
387
|
}
|
|
388
|
+
case "paused":
|
|
389
|
+
setBusy(true);
|
|
390
|
+
try {
|
|
391
|
+
await setWorkspacePaused(config, action.paused);
|
|
392
|
+
say("system", `${workspace.slug} is now ${action.paused ? "off" : "on"}.`);
|
|
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;
|
|
371
402
|
case "queue":
|
|
372
403
|
setBusy(true);
|
|
373
404
|
try {
|
|
@@ -431,20 +462,6 @@ export function App({ initial }) {
|
|
|
431
462
|
}
|
|
432
463
|
return;
|
|
433
464
|
}
|
|
434
|
-
case "plan":
|
|
435
|
-
setBusy(true);
|
|
436
|
-
try {
|
|
437
|
-
await suspendTerminal(() => launchArchitect({ workspaceRepo: workspace.repo }));
|
|
438
|
-
say("system", "Architect session ended. Review the spec, then run /epic new PATH.");
|
|
439
|
-
await refresh();
|
|
440
|
-
}
|
|
441
|
-
catch (error) {
|
|
442
|
-
setNotice(error instanceof Error ? error.message : String(error));
|
|
443
|
-
}
|
|
444
|
-
finally {
|
|
445
|
-
setBusy(false);
|
|
446
|
-
}
|
|
447
|
-
return;
|
|
448
465
|
case "decide": {
|
|
449
466
|
const decision = board.decisions[0];
|
|
450
467
|
if (!decision) {
|
|
@@ -491,8 +508,8 @@ export function App({ initial }) {
|
|
|
491
508
|
default:
|
|
492
509
|
return;
|
|
493
510
|
}
|
|
494
|
-
}, [view, settings, applyEdit, board, browsing, mode, say,
|
|
495
|
-
config,
|
|
511
|
+
}, [view, settings, applyEdit, board, browsing, mode, say, askAgent, order, settingsOrder, changeWorkspace,
|
|
512
|
+
config, refresh, suspendTerminal, exit]);
|
|
496
513
|
useInput((input, key) => {
|
|
497
514
|
if (key.ctrl && input === "c")
|
|
498
515
|
exit();
|
|
@@ -534,7 +551,7 @@ export function App({ initial }) {
|
|
|
534
551
|
setDraft(next);
|
|
535
552
|
if (editingRef.current)
|
|
536
553
|
setEditing({ key: editingRef.current.key, draft: next });
|
|
537
|
-
}, onSubmit: (value) => void run(value), isActive: !busy, placeholder: busy ? "working…" : "message, or /help", prompt: _jsx(Text, { color: mode === "browse" ? UI.dim : UI.cream, children: mode === "
|
|
554
|
+
}, onSubmit: (value) => void run(value), isActive: !busy, placeholder: busy ? "working…" : "message, or /help", prompt: _jsx(Text, { color: mode === "browse" ? UI.dim : UI.cream, children: mode === "browse" ? "> " : `${mode}> ` }), color: UI.text, onCancel: () => {
|
|
538
555
|
if (editing) {
|
|
539
556
|
setEditing(null);
|
|
540
557
|
setDraft("");
|
package/dist/tui/Help.js
CHANGED
|
@@ -10,13 +10,16 @@ export const COMMANDS = [
|
|
|
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
12
|
{ name: "/cancel", args: "HD-12", help: "cancel a ticket" },
|
|
13
|
-
{ name: "/epic", args: "new PATH", help: "create
|
|
13
|
+
{ name: "/epic", args: "new PATH | approve ID", help: "create or approve a draft epic" },
|
|
14
14
|
{ name: "/epics", help: "list epics and ticket progress" },
|
|
15
|
-
{ name: "/
|
|
15
|
+
{ name: "/architect", help: "talk to the agent that shapes draft epics" },
|
|
16
|
+
{ name: "/plan", help: "alias for /architect" },
|
|
16
17
|
{ name: "/decide", args: "2 | text", help: "answer the decision on screen" },
|
|
17
18
|
{ name: "/agents", args: "[add ... | rm ROLE|ID]", help: "view or manage agents" },
|
|
18
19
|
{ name: "/settings", help: "change provider caps and agent settings" },
|
|
19
20
|
{ name: "/workspace", args: "[slug | new | set | rotate-key]", help: "list, switch, create, or configure" },
|
|
21
|
+
{ name: "/on", help: "turn on the current workspace" },
|
|
22
|
+
{ name: "/off", help: "turn off the current workspace" },
|
|
20
23
|
{ name: "/feed", help: "what just happened" },
|
|
21
24
|
{ name: "/orchestrator", help: "talk to the agent that gets work in flight finished" },
|
|
22
25
|
{ name: "/refresh", help: "reload the board now" },
|
package/dist/tui/data.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
1
|
+
import { approveEpic as approveEpicNow, 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, 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
|
export const POLL_MS = 5_000;
|
|
@@ -106,8 +106,8 @@ export async function switchWorkspace(slug, config = loadConfig()) {
|
|
|
106
106
|
selectWorkspace(slug);
|
|
107
107
|
return snapshot;
|
|
108
108
|
}
|
|
109
|
-
export async function
|
|
110
|
-
await sendMessage({ body_md: body, to_role:
|
|
109
|
+
export async function postAgentMessage(role, body, config) {
|
|
110
|
+
await sendMessage({ body_md: body, to_role: role, delivery: "queue" }, config);
|
|
111
111
|
}
|
|
112
112
|
export async function loadTicketDetail(config, key) {
|
|
113
113
|
return showTicket(key, config);
|
|
@@ -115,23 +115,29 @@ export async function loadTicketDetail(config, key) {
|
|
|
115
115
|
export async function createEpicFromFile(config, path) {
|
|
116
116
|
return postEpic(await readEpicSpec(path), config);
|
|
117
117
|
}
|
|
118
|
+
export async function approveEpic(config, id) {
|
|
119
|
+
return approveEpicNow(id, config);
|
|
120
|
+
}
|
|
118
121
|
export async function queueTicket(config, key) {
|
|
119
122
|
return queueTicketNow(key, config);
|
|
120
123
|
}
|
|
121
124
|
export async function cancelTicket(config, key) {
|
|
122
125
|
return cancelTicketNow(key, config);
|
|
123
126
|
}
|
|
127
|
+
export async function setWorkspacePaused(config, paused) {
|
|
128
|
+
return setPausedNow(paused, config);
|
|
129
|
+
}
|
|
124
130
|
export async function createAgent(config, fields) {
|
|
125
131
|
return postAgent(fields, config);
|
|
126
132
|
}
|
|
127
133
|
export async function deleteAgent(config, id) {
|
|
128
134
|
return removeAgent(id, config);
|
|
129
135
|
}
|
|
130
|
-
export async function waitForReply(config, since, timeoutMs) {
|
|
136
|
+
export async function waitForReply(config, role, since, timeoutMs) {
|
|
131
137
|
const deadline = Date.now() + timeoutMs;
|
|
132
138
|
while (Date.now() <= deadline) {
|
|
133
139
|
const { messages } = await listMessages({ since, limit: 50 }, config);
|
|
134
|
-
const reply = messages.find((message) =>
|
|
140
|
+
const reply = messages.find((message) => message.from_role === role &&
|
|
135
141
|
["human", "operator", "all"].includes(message.to_role));
|
|
136
142
|
if (reply)
|
|
137
143
|
return reply.body_md;
|
package/dist/tui/parse.js
CHANGED
|
@@ -13,6 +13,10 @@ export function parseLine(raw) {
|
|
|
13
13
|
switch (word.toLowerCase()) {
|
|
14
14
|
case "orchestrator":
|
|
15
15
|
return { kind: "mode", mode: "orchestrator" };
|
|
16
|
+
case "architect":
|
|
17
|
+
case "plan":
|
|
18
|
+
return rest.length ? { kind: "unknown", command: `${word.toLowerCase()} takes no arguments` }
|
|
19
|
+
: { kind: "mode", mode: "architect" };
|
|
16
20
|
case "board":
|
|
17
21
|
case "feed":
|
|
18
22
|
case "inbox":
|
|
@@ -48,11 +52,18 @@ export function parseLine(raw) {
|
|
|
48
52
|
? { kind: "ticket", key: argument.toUpperCase() }
|
|
49
53
|
: { kind: "unknown", command: "ticket needs a key" };
|
|
50
54
|
case "epic":
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
if (rest[0]?.toLowerCase() === "new" && rest.length > 1) {
|
|
56
|
+
return { kind: "epic-new", path: rest.slice(1).join(" ") };
|
|
57
|
+
}
|
|
58
|
+
return rest[0]?.toLowerCase() === "approve" && rest.length === 2
|
|
59
|
+
? { kind: "epic-approve", id: rest[1] }
|
|
60
|
+
: { kind: "unknown", command: "epic needs new PATH or approve ID" };
|
|
54
61
|
case "epics":
|
|
55
62
|
return { kind: "epics" };
|
|
63
|
+
case "on":
|
|
64
|
+
case "off":
|
|
65
|
+
return rest.length ? { kind: "unknown", command: `${word.toLowerCase()} takes no arguments` }
|
|
66
|
+
: { kind: "paused", paused: word.toLowerCase() === "off" };
|
|
56
67
|
case "queue":
|
|
57
68
|
return argument
|
|
58
69
|
? { kind: "queue", key: argument.toUpperCase() }
|
|
@@ -60,8 +71,6 @@ export function parseLine(raw) {
|
|
|
60
71
|
case "cancel":
|
|
61
72
|
return argument ? { kind: "cancel", key: argument.toUpperCase() }
|
|
62
73
|
: { kind: "unknown", command: "cancel needs a key" };
|
|
63
|
-
case "plan":
|
|
64
|
-
return argument ? { kind: "unknown", command: "plan takes no arguments" } : { kind: "plan" };
|
|
65
74
|
case "decide": {
|
|
66
75
|
const dismiss = /(^|\s)--(skip|dismiss)(\s|$)/.test(argument);
|
|
67
76
|
return {
|
package/dist/tui/theme.js
CHANGED
|
@@ -40,11 +40,12 @@ export function speakerStyle(speaker) {
|
|
|
40
40
|
case "you":
|
|
41
41
|
return { borderStyle: DOTTED, borderColor: UI.cream, label: "you" };
|
|
42
42
|
case "orchestrator":
|
|
43
|
+
case "architect":
|
|
43
44
|
return {
|
|
44
45
|
borderStyle: THIN,
|
|
45
46
|
borderColor: UI.orchestratorBorder,
|
|
46
47
|
backgroundColor: UI.orchestratorBg,
|
|
47
|
-
label:
|
|
48
|
+
label: speaker,
|
|
48
49
|
};
|
|
49
50
|
default:
|
|
50
51
|
return { borderStyle: DOTTED, borderColor: UI.dim, label: "" };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createInterface } from "node:readline/promises";
|
|
2
1
|
import { createWorkspace, rotateWorkspaceApiKey, updateWorkspace } from "./api.js";
|
|
3
2
|
import { loadConfig, writeHdConfig } from "./config.js";
|
|
3
|
+
import { promptOnStdin } from "./prompt.js";
|
|
4
4
|
import { defaultGh, HDX_RUNNER_GH_USER, preflightWorkspace } from "./workspace-preflight.js";
|
|
5
5
|
export const WORKSPACE_USAGE = "usage: hd workspace ls | new --name NAME --repo OWNER/NAME [--create|--no-create] [flags] | set [flags] | rotate-key";
|
|
6
6
|
function flags(argv) {
|
|
@@ -44,12 +44,7 @@ async function repoState(repo, gh) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
function lazyPrompt(deps) {
|
|
47
|
-
|
|
48
|
-
const ask = deps.prompt ?? (async (question) => {
|
|
49
|
-
readline ??= createInterface({ input: process.stdin, output: process.stdout });
|
|
50
|
-
return readline.question(question);
|
|
51
|
-
});
|
|
52
|
-
return { ask, close: () => readline?.close() };
|
|
47
|
+
return deps.prompt ?? promptOnStdin;
|
|
53
48
|
}
|
|
54
49
|
async function askDefault(ask, label, fallback = "") {
|
|
55
50
|
const answer = (await ask(`${label}${fallback ? ` [${fallback}]` : ""}: `)).trim();
|
|
@@ -65,7 +60,7 @@ export async function workspaceNew(argv, deps = {}) {
|
|
|
65
60
|
throw new Error(WORKSPACE_USAGE);
|
|
66
61
|
const gh = deps.gh ?? defaultGh;
|
|
67
62
|
const prompt = lazyPrompt(deps);
|
|
68
|
-
|
|
63
|
+
{
|
|
69
64
|
let name = opts.name ?? "";
|
|
70
65
|
let repo = opts.repo ?? "";
|
|
71
66
|
let branch = opts.branch;
|
|
@@ -79,7 +74,7 @@ export async function workspaceNew(argv, deps = {}) {
|
|
|
79
74
|
}
|
|
80
75
|
}
|
|
81
76
|
if (guided) {
|
|
82
|
-
name = await askDefault(prompt
|
|
77
|
+
name = await askDefault(prompt, "Name", name);
|
|
83
78
|
if (!name)
|
|
84
79
|
throw new Error("Name is required.");
|
|
85
80
|
let defaultRepo = repo;
|
|
@@ -87,7 +82,7 @@ export async function workspaceNew(argv, deps = {}) {
|
|
|
87
82
|
const owner = (await gh(["api", "user", "--jq", ".login"])).trim();
|
|
88
83
|
defaultRepo = `${owner}/${workspaceSlug(name)}`;
|
|
89
84
|
}
|
|
90
|
-
repo = await askDefault(prompt
|
|
85
|
+
repo = await askDefault(prompt, "Repo", defaultRepo);
|
|
91
86
|
if (!repo)
|
|
92
87
|
throw new Error("Repo is required.");
|
|
93
88
|
}
|
|
@@ -97,17 +92,17 @@ export async function workspaceNew(argv, deps = {}) {
|
|
|
97
92
|
if (state && !state.found) {
|
|
98
93
|
if (bools.has("no-create"))
|
|
99
94
|
throw new Error(`GitHub repository ${repo} does not exist and --no-create was set.`);
|
|
100
|
-
const create = bools.has("create") || (interactive && yes(await prompt
|
|
95
|
+
const create = bools.has("create") || (interactive && yes(await prompt(`Create ${repo} as a private GitHub repo? [Y/n] `)));
|
|
101
96
|
if (!create)
|
|
102
97
|
throw new Error(`GitHub repository ${repo} was not created.`);
|
|
103
98
|
await gh(["repo", "create", repo, "--private"]);
|
|
104
99
|
state = { found: true, branch: "main" };
|
|
105
100
|
}
|
|
106
101
|
if (guided) {
|
|
107
|
-
branch = await askDefault(prompt
|
|
108
|
-
host = await askDefault(prompt
|
|
102
|
+
branch = await askDefault(prompt, "Branch", branch ?? state?.branch ?? "main");
|
|
103
|
+
host = await askDefault(prompt, "Host", host);
|
|
109
104
|
const summary = `${name} | ${repo} | ${branch} | host ${host}`;
|
|
110
|
-
if (!yes(await prompt
|
|
105
|
+
if (!yes(await prompt(`Create ${summary}. Proceed? [Y/n] `)))
|
|
111
106
|
throw new Error("Workspace creation cancelled.");
|
|
112
107
|
}
|
|
113
108
|
const preflight = await (deps.preflightWorkspace ?? preflightWorkspace)({
|
|
@@ -122,9 +117,6 @@ export async function workspaceNew(argv, deps = {}) {
|
|
|
122
117
|
runnerUser: opts["runner-user"] ?? HDX_RUNNER_GH_USER,
|
|
123
118
|
initCommand: `hd init --url ${result.url} --api-key ${result.api_key} --slug ${result.workspace.slug}` };
|
|
124
119
|
}
|
|
125
|
-
finally {
|
|
126
|
-
prompt.close();
|
|
127
|
-
}
|
|
128
120
|
}
|
|
129
121
|
export async function workspaceSet(argv) {
|
|
130
122
|
const { opts, bools, rest } = flags(argv);
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@higherdev/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hd": "dist/index.js"
|
|
7
7
|
},
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"files": [
|
|
10
|
-
"dist"
|
|
11
|
-
"prompts"
|
|
10
|
+
"dist"
|
|
12
11
|
],
|
|
13
12
|
"scripts": {
|
|
14
13
|
"build": "tsc",
|
|
15
14
|
"test": "pnpm build && node --experimental-strip-types --test test/*.test.ts",
|
|
15
|
+
"test:pty": "pnpm build && expect test/tui-prompt.expect",
|
|
16
16
|
"prepublishOnly": "npm run build"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
package/dist/plan.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
5
|
-
import { createInterface } from "node:readline/promises";
|
|
6
|
-
const ARCHITECT_PROMPT = new URL("../prompts/architect.md", import.meta.url);
|
|
7
|
-
function gitOutput(cwd, args) {
|
|
8
|
-
const result = spawnSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
9
|
-
return result.status === 0 ? result.stdout.trim() : null;
|
|
10
|
-
}
|
|
11
|
-
export function remoteRepo(remote) {
|
|
12
|
-
const trimmed = remote.trim().replace(/\/$/, "").replace(/\.git$/, "");
|
|
13
|
-
const scp = trimmed.match(/^[^@]+@[^:]+:(.+)$/);
|
|
14
|
-
if (scp)
|
|
15
|
-
return scp[1] ?? null;
|
|
16
|
-
try {
|
|
17
|
-
const url = new URL(trimmed);
|
|
18
|
-
return url.pathname.replace(/^\//, "") || null;
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return trimmed.includes("/") ? trimmed : null;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function expanded(path) {
|
|
25
|
-
const value = path.trim();
|
|
26
|
-
if (value === "~")
|
|
27
|
-
return homedir();
|
|
28
|
-
if (value.startsWith("~/"))
|
|
29
|
-
return join(homedir(), value.slice(2));
|
|
30
|
-
return isAbsolute(value) ? value : resolve(value);
|
|
31
|
-
}
|
|
32
|
-
export function matchingWorkspaceRepo(directory, workspaceRepo) {
|
|
33
|
-
const root = gitOutput(expanded(directory), ["rev-parse", "--show-toplevel"]);
|
|
34
|
-
if (!root)
|
|
35
|
-
return null;
|
|
36
|
-
const origin = gitOutput(root, ["remote", "get-url", "origin"]);
|
|
37
|
-
return origin && remoteRepo(origin)?.toLowerCase() === workspaceRepo.toLowerCase() ? root : null;
|
|
38
|
-
}
|
|
39
|
-
async function askForRepo(workspaceRepo) {
|
|
40
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
41
|
-
throw new Error(`Current directory is not ${workspaceRepo}. Pass --repo DIR.`);
|
|
42
|
-
}
|
|
43
|
-
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
44
|
-
try {
|
|
45
|
-
return (await prompt.question(`Repository directory for ${workspaceRepo}: `)).trim();
|
|
46
|
-
}
|
|
47
|
-
finally {
|
|
48
|
-
prompt.close();
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
export async function resolveWorkspaceRepo(workspaceRepo, requested) {
|
|
52
|
-
if (requested) {
|
|
53
|
-
const match = matchingWorkspaceRepo(requested, workspaceRepo);
|
|
54
|
-
if (!match)
|
|
55
|
-
throw new Error(`${requested} is not a checkout of ${workspaceRepo}.`);
|
|
56
|
-
return match;
|
|
57
|
-
}
|
|
58
|
-
const current = matchingWorkspaceRepo(process.cwd(), workspaceRepo);
|
|
59
|
-
if (current)
|
|
60
|
-
return current;
|
|
61
|
-
const answer = await askForRepo(workspaceRepo);
|
|
62
|
-
if (!answer)
|
|
63
|
-
throw new Error("A repository directory is required.");
|
|
64
|
-
const match = matchingWorkspaceRepo(answer, workspaceRepo);
|
|
65
|
-
if (!match)
|
|
66
|
-
throw new Error(`${answer} is not a checkout of ${workspaceRepo}.`);
|
|
67
|
-
return match;
|
|
68
|
-
}
|
|
69
|
-
export function verifyCodex(checkVersion = () => spawnSync("codex", ["--version"], { stdio: "ignore" })) {
|
|
70
|
-
const check = checkVersion();
|
|
71
|
-
if (check.error || check.status !== 0) {
|
|
72
|
-
throw new Error("Codex CLI is unavailable. Install it if needed, then run `codex login`.");
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function runCodex(cwd, prompt) {
|
|
76
|
-
return new Promise((resolveRun, rejectRun) => {
|
|
77
|
-
const child = spawn("codex", [prompt], { cwd, stdio: "inherit" });
|
|
78
|
-
child.once("error", rejectRun);
|
|
79
|
-
child.once("exit", (code, signal) => {
|
|
80
|
-
if (code === 0)
|
|
81
|
-
resolveRun();
|
|
82
|
-
else
|
|
83
|
-
rejectRun(new Error(`codex exited ${signal ? `with signal ${signal}` : `with status ${code ?? "unknown"}`}.`));
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
export async function launchArchitect(options, runtime = {}) {
|
|
88
|
-
(runtime.verifyCodex ?? verifyCodex)();
|
|
89
|
-
const [cwd, prompt] = await Promise.all([
|
|
90
|
-
resolveWorkspaceRepo(options.workspaceRepo, options.repo),
|
|
91
|
-
readFile(ARCHITECT_PROMPT, "utf8"),
|
|
92
|
-
]);
|
|
93
|
-
await (runtime.invokeCodex ?? runCodex)(cwd, prompt);
|
|
94
|
-
}
|
package/prompts/architect.md
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
You are helping the operator author an HDX epic for this repository.
|
|
2
|
-
|
|
3
|
-
Start by interviewing the operator about the goal. Ask focused questions until the intended outcome, boundaries, and important tradeoffs are clear. Read the repository as needed so the plan reflects the code that actually exists.
|
|
4
|
-
|
|
5
|
-
Draft the epic in this exact structure:
|
|
6
|
-
|
|
7
|
-
# <title>
|
|
8
|
-
|
|
9
|
-
## Goal
|
|
10
|
-
|
|
11
|
-
## Scope
|
|
12
|
-
|
|
13
|
-
## Out of scope
|
|
14
|
-
|
|
15
|
-
## Acceptance
|
|
16
|
-
|
|
17
|
-
## Constraints
|
|
18
|
-
|
|
19
|
-
The operator is the author. Let them review and revise the draft before saving it. Do not submit the epic to HDX yourself.
|
|
20
|
-
|
|
21
|
-
Once the operator accepts the spec, write it to `docs/epics/<slug>.md`, where `<slug>` is a concise kebab-case name for the epic. End by printing the exact next command, with the real path substituted:
|
|
22
|
-
|
|
23
|
-
`hd epic new docs/epics/<slug>.md`
|