@higherdev/cli 0.11.1 → 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 +5 -7
- package/dist/api.js +7 -1
- package/dist/index.js +19 -12
- package/dist/out.js +2 -2
- package/dist/tui/App.js +43 -40
- package/dist/tui/Help.js +5 -2
- package/dist/tui/data.js +11 -5
- package/dist/tui/parse.js +14 -17
- package/dist/tui/theme.js +2 -1
- package/package.json +2 -3
- package/dist/plan.js +0 -118
- package/prompts/architect.md +0 -23
package/README.md
CHANGED
|
@@ -36,7 +36,9 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
36
36
|
| `hd ticket cancel KEY` | Cancel a ticket |
|
|
37
37
|
| `hd epic new PATH [--title TITLE]` | Create an epic from a Markdown spec |
|
|
38
38
|
| `hd epic list` | List epics and ticket progress |
|
|
39
|
-
| `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 |
|
|
40
42
|
| `hd workspace ls` | List every workspace available to the configured key |
|
|
41
43
|
| `hd workspace new --name NAME --repo OWNER/NAME [options]` | Preflight GitHub, wire the runner, and create a paused workspace |
|
|
42
44
|
| `hd workspace set [options]` | Update settings; max turns uses `--max-turns KIND=N[,KIND=N...]` |
|
|
@@ -61,12 +63,8 @@ branch, bootstraps an empty repository unless `--no-bootstrap` is set, and invit
|
|
|
61
63
|
the repository is missing, approve private creation interactively, use `--create` to force it, or
|
|
62
64
|
`--no-create` to fail. Fully flagged calls remain non-interactive for scripts and Mel.
|
|
63
65
|
|
|
64
|
-
`hd plan` remembers a matching checkout per workspace. If none exists, it offers to clone into
|
|
65
|
-
`~/HigherDEV/<slug>`; change the parent with `--clone-root DIR`. An explicit `--repo DIR` is saved
|
|
66
|
-
after its Git origin is verified.
|
|
67
|
-
|
|
68
66
|
Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/queue`, `/cancel`, `/epic new`,
|
|
69
|
-
`/epics`, `/
|
|
67
|
+
`/epic approve`, `/epics`, `/architect` (or `/plan`), `/decide`, `/agents add`, `/agents rm`, `/settings`, `/workspace`,
|
|
70
68
|
`/workspace new`, `/workspace set`, `/workspace rotate-key`, `/feed`,
|
|
71
|
-
`/orchestrator`, `/refresh`, `/help`, or `/exit`. The display refreshes from
|
|
69
|
+
`/on`, `/off`, `/orchestrator`, `/refresh`, `/help`, or `/exit`. The display refreshes from
|
|
72
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/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,18 +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
|
-
const result = await launchArchitect({ workspaceRepo: workspace.repo, workspaceSlug: workspace.slug,
|
|
165
|
-
repo: opts.repo, cloneRoot: opts["clone-root"] });
|
|
166
|
-
if (result.cloneCommand)
|
|
167
|
-
console.log(`Clone it with: ${result.cloneCommand}`);
|
|
172
|
+
if (argv.length)
|
|
173
|
+
fail("usage: hd plan");
|
|
174
|
+
console.log("Open the HDX TUI and use /architect (or /plan).");
|
|
168
175
|
}
|
|
169
176
|
async function cmdLogs(argv) {
|
|
170
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/tui/App.js
CHANGED
|
@@ -3,7 +3,6 @@ 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 { launchArchitect } from "../plan.js";
|
|
7
6
|
import { promptOnStdin } from "../prompt.js";
|
|
8
7
|
import { workspaceNew, workspaceRotateKey, workspaceSet } from "../workspace-commands.js";
|
|
9
8
|
import { Banner } from "./Banner.js";
|
|
@@ -19,7 +18,7 @@ import { alertOnce } from "./alert.js";
|
|
|
19
18
|
import { bubbleRows } from "./height.js";
|
|
20
19
|
import { planLayout, splitPanels } from "./layout.js";
|
|
21
20
|
import { parseLine } from "./parse.js";
|
|
22
|
-
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";
|
|
23
22
|
import { editFor, editableKeys, nextValue, seedFor, settingsRows } from "./settings-model.js";
|
|
24
23
|
import { appendLines, runLabels, toStreamLines } from "./stream.js";
|
|
25
24
|
import { UI } from "./theme.js";
|
|
@@ -193,18 +192,18 @@ export function App({ initial }) {
|
|
|
193
192
|
setBusy(false);
|
|
194
193
|
}
|
|
195
194
|
}, [config, say]);
|
|
196
|
-
const
|
|
195
|
+
const askAgent = useCallback(async (role, text) => {
|
|
197
196
|
setBusy(true);
|
|
198
197
|
const id = nextId();
|
|
199
|
-
setMessages((prior) => [...prior, { id, speaker:
|
|
198
|
+
setMessages((prior) => [...prior, { id, speaker: role, body: "", pending: true }]);
|
|
200
199
|
const since = new Date().toISOString();
|
|
201
200
|
try {
|
|
202
|
-
await
|
|
201
|
+
await postAgentMessage(role, text, config);
|
|
203
202
|
setMessages((prior) => prior.map((message) => message.id === id
|
|
204
203
|
? { ...message, steps: ["· queued, it answers on the next tick"] }
|
|
205
204
|
: message));
|
|
206
205
|
const { waitForReply } = await import("./data.js");
|
|
207
|
-
const reply = await waitForReply(config, since, 180_000);
|
|
206
|
+
const reply = await waitForReply(config, role, since, 180_000);
|
|
208
207
|
setMessages((prior) => prior.map((message) => message.id === id
|
|
209
208
|
? { ...message, body: reply ?? "No reply yet. It will land in /inbox.", pending: false, steps: [] }
|
|
210
209
|
: message));
|
|
@@ -258,18 +257,20 @@ export function App({ initial }) {
|
|
|
258
257
|
const action = parseLine(text);
|
|
259
258
|
if (action.kind === "say") {
|
|
260
259
|
say("you", text);
|
|
261
|
-
if (mode
|
|
262
|
-
await
|
|
260
|
+
if (mode !== "browse")
|
|
261
|
+
await askAgent(mode, text);
|
|
263
262
|
else
|
|
264
|
-
setNotice("Use /orchestrator before sending a message.");
|
|
263
|
+
setNotice("Use /architect or /orchestrator before sending a message.");
|
|
265
264
|
return;
|
|
266
265
|
}
|
|
267
266
|
switch (action.kind) {
|
|
268
267
|
case "mode":
|
|
269
|
-
setMode(
|
|
268
|
+
setMode(action.mode);
|
|
270
269
|
setCursor(null);
|
|
271
270
|
selectedRef.current = null;
|
|
272
|
-
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.");
|
|
273
274
|
return;
|
|
274
275
|
case "view":
|
|
275
276
|
setView(action.view);
|
|
@@ -363,6 +364,20 @@ export function App({ initial }) {
|
|
|
363
364
|
setBusy(false);
|
|
364
365
|
}
|
|
365
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;
|
|
366
381
|
case "epics": {
|
|
367
382
|
const rows = epicProgressRows(board.epics, board.tickets);
|
|
368
383
|
say("system", rows.length
|
|
@@ -370,6 +385,20 @@ export function App({ initial }) {
|
|
|
370
385
|
: "No epics.");
|
|
371
386
|
return;
|
|
372
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;
|
|
373
402
|
case "queue":
|
|
374
403
|
setBusy(true);
|
|
375
404
|
try {
|
|
@@ -433,32 +462,6 @@ export function App({ initial }) {
|
|
|
433
462
|
}
|
|
434
463
|
return;
|
|
435
464
|
}
|
|
436
|
-
case "plan":
|
|
437
|
-
setBusy(true);
|
|
438
|
-
try {
|
|
439
|
-
let result;
|
|
440
|
-
await suspendTerminal(async () => {
|
|
441
|
-
result = await launchArchitect({
|
|
442
|
-
workspaceRepo: workspace.repo, workspaceSlug: workspace.slug,
|
|
443
|
-
repo: action.repo, cloneRoot: action.cloneRoot,
|
|
444
|
-
}, { prompt: tuiPrompt });
|
|
445
|
-
});
|
|
446
|
-
if (!result)
|
|
447
|
-
throw new Error("Architect did not finish.");
|
|
448
|
-
if (result.cloneCommand)
|
|
449
|
-
say("system", `Clone it with: ${result.cloneCommand}`);
|
|
450
|
-
else {
|
|
451
|
-
say("system", "Architect session ended. Review the spec, then run /epic new PATH.");
|
|
452
|
-
await refresh();
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
catch (error) {
|
|
456
|
-
setNotice(error instanceof Error ? error.message : String(error));
|
|
457
|
-
}
|
|
458
|
-
finally {
|
|
459
|
-
setBusy(false);
|
|
460
|
-
}
|
|
461
|
-
return;
|
|
462
465
|
case "decide": {
|
|
463
466
|
const decision = board.decisions[0];
|
|
464
467
|
if (!decision) {
|
|
@@ -505,8 +508,8 @@ export function App({ initial }) {
|
|
|
505
508
|
default:
|
|
506
509
|
return;
|
|
507
510
|
}
|
|
508
|
-
}, [view, settings, applyEdit, board, browsing, mode, say,
|
|
509
|
-
config,
|
|
511
|
+
}, [view, settings, applyEdit, board, browsing, mode, say, askAgent, order, settingsOrder, changeWorkspace,
|
|
512
|
+
config, refresh, suspendTerminal, exit]);
|
|
510
513
|
useInput((input, key) => {
|
|
511
514
|
if (key.ctrl && input === "c")
|
|
512
515
|
exit();
|
|
@@ -548,7 +551,7 @@ export function App({ initial }) {
|
|
|
548
551
|
setDraft(next);
|
|
549
552
|
if (editingRef.current)
|
|
550
553
|
setEditing({ key: editingRef.current.key, draft: next });
|
|
551
|
-
}, 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: () => {
|
|
552
555
|
if (editing) {
|
|
553
556
|
setEditing(null);
|
|
554
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,20 +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
|
-
const opts = {};
|
|
65
|
-
for (let i = 0; i < rest.length; i += 2) {
|
|
66
|
-
if (!rest[i]?.startsWith("--") || !rest[i + 1]) {
|
|
67
|
-
return { kind: "unknown", command: "plan accepts --repo DIR and --clone-root DIR" };
|
|
68
|
-
}
|
|
69
|
-
opts[rest[i].slice(2)] = rest[i + 1];
|
|
70
|
-
}
|
|
71
|
-
if (Object.keys(opts).some((key) => !["repo", "clone-root"].includes(key))) {
|
|
72
|
-
return { kind: "unknown", command: "plan accepts --repo DIR and --clone-root DIR" };
|
|
73
|
-
}
|
|
74
|
-
return { kind: "plan", ...(opts.repo ? { repo: opts.repo } : {}),
|
|
75
|
-
...(opts["clone-root"] ? { cloneRoot: opts["clone-root"] } : {}) };
|
|
76
|
-
}
|
|
77
74
|
case "decide": {
|
|
78
75
|
const dismiss = /(^|\s)--(skip|dismiss)(\s|$)/.test(argument);
|
|
79
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: "" };
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
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",
|
package/dist/plan.js
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import { mkdirSync } from "node:fs";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
6
|
-
import { loadStoredConfig, rememberWorkspaceRepo } from "./config.js";
|
|
7
|
-
import { promptOnStdin } from "./prompt.js";
|
|
8
|
-
import { defaultGh } from "./workspace-preflight.js";
|
|
9
|
-
const ARCHITECT_PROMPT = new URL("../prompts/architect.md", import.meta.url);
|
|
10
|
-
function gitOutput(cwd, args) {
|
|
11
|
-
const result = spawnSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
12
|
-
return result.status === 0 ? result.stdout.trim() : null;
|
|
13
|
-
}
|
|
14
|
-
export function remoteRepo(remote) {
|
|
15
|
-
const trimmed = remote.trim().replace(/\/$/, "").replace(/\.git$/, "");
|
|
16
|
-
const scp = trimmed.match(/^[^@]+@[^:]+:(.+)$/);
|
|
17
|
-
if (scp)
|
|
18
|
-
return scp[1] ?? null;
|
|
19
|
-
try {
|
|
20
|
-
const url = new URL(trimmed);
|
|
21
|
-
return url.pathname.replace(/^\//, "") || null;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return trimmed.includes("/") ? trimmed : null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function expanded(path, home = homedir(), cwd = process.cwd()) {
|
|
28
|
-
const value = path.trim();
|
|
29
|
-
if (value === "~")
|
|
30
|
-
return home;
|
|
31
|
-
if (value.startsWith("~/"))
|
|
32
|
-
return join(home, value.slice(2));
|
|
33
|
-
return isAbsolute(value) ? value : resolve(cwd, value);
|
|
34
|
-
}
|
|
35
|
-
export function matchingWorkspaceRepo(directory, workspaceRepo) {
|
|
36
|
-
const root = gitOutput(expanded(directory), ["rev-parse", "--show-toplevel"]);
|
|
37
|
-
if (!root)
|
|
38
|
-
return null;
|
|
39
|
-
const origin = gitOutput(root, ["remote", "get-url", "origin"]);
|
|
40
|
-
return origin && remoteRepo(origin)?.toLowerCase() === workspaceRepo.toLowerCase() ? root : null;
|
|
41
|
-
}
|
|
42
|
-
export function verifyCodex(checkVersion = () => spawnSync("codex", ["--version"], { stdio: "ignore" })) {
|
|
43
|
-
const check = checkVersion();
|
|
44
|
-
if (check.error || check.status !== 0) {
|
|
45
|
-
throw new Error("Codex CLI is unavailable. Install it if needed, then run `codex login`.");
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function runCodex(cwd, prompt) {
|
|
49
|
-
return new Promise((resolveRun, rejectRun) => {
|
|
50
|
-
const child = spawn("codex", [prompt], { cwd, stdio: "inherit" });
|
|
51
|
-
child.once("error", rejectRun);
|
|
52
|
-
child.once("exit", (code, signal) => {
|
|
53
|
-
if (code === 0)
|
|
54
|
-
resolveRun();
|
|
55
|
-
else
|
|
56
|
-
rejectRun(new Error(`codex exited ${signal ? `with signal ${signal}` : `with status ${code ?? "unknown"}`}.`));
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
function shellPath(path) {
|
|
61
|
-
return /^[A-Za-z0-9_./~-]+$/.test(path) ? path : `'${path.replaceAll("'", "'\\''")}'`;
|
|
62
|
-
}
|
|
63
|
-
export async function resolveWorkspaceRepo(options, runtime = {}) {
|
|
64
|
-
const matchRepo = runtime.matchRepo ?? matchingWorkspaceRepo;
|
|
65
|
-
const cwd = runtime.cwd?.() ?? process.cwd();
|
|
66
|
-
const home = runtime.home?.() ?? homedir();
|
|
67
|
-
const saveRepo = runtime.saveRepo ?? rememberWorkspaceRepo;
|
|
68
|
-
const accept = (directory) => {
|
|
69
|
-
saveRepo(options.workspaceSlug, directory);
|
|
70
|
-
return { directory };
|
|
71
|
-
};
|
|
72
|
-
if (options.repo) {
|
|
73
|
-
const match = matchRepo(options.repo, options.workspaceRepo);
|
|
74
|
-
if (!match)
|
|
75
|
-
throw new Error(`${options.repo} is not a checkout of ${options.workspaceRepo}.`);
|
|
76
|
-
return accept(match);
|
|
77
|
-
}
|
|
78
|
-
const saved = (runtime.savedRepo ?? ((slug) => loadStoredConfig().repos[slug]))(options.workspaceSlug);
|
|
79
|
-
if (saved) {
|
|
80
|
-
const match = matchRepo(saved, options.workspaceRepo);
|
|
81
|
-
if (match)
|
|
82
|
-
return { directory: match };
|
|
83
|
-
}
|
|
84
|
-
const current = matchRepo(cwd, options.workspaceRepo);
|
|
85
|
-
if (current)
|
|
86
|
-
return accept(current);
|
|
87
|
-
const defaultRoot = "~/HigherDEV";
|
|
88
|
-
const rootInput = options.cloneRoot ?? defaultRoot;
|
|
89
|
-
const root = expanded(rootInput, home, cwd);
|
|
90
|
-
const destination = join(root, options.workspaceSlug);
|
|
91
|
-
const shownDestination = join(rootInput, options.workspaceSlug);
|
|
92
|
-
const cloneCommand = `gh repo clone ${options.workspaceRepo} ${shellPath(shownDestination)}`;
|
|
93
|
-
const interactive = runtime.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
94
|
-
if (!interactive || ["n", "no"].includes((await (runtime.prompt ?? promptOnStdin)(`Clone ${options.workspaceRepo} to ${shownDestination}? [Y/n] `)).trim().toLowerCase()))
|
|
95
|
-
return { cloneCommand };
|
|
96
|
-
const gh = runtime.gh ?? defaultGh;
|
|
97
|
-
try {
|
|
98
|
-
await gh(["--version"]);
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
throw new Error("GitHub CLI is unavailable. Install it if needed, then run `gh auth login`.");
|
|
102
|
-
}
|
|
103
|
-
(runtime.mkdir ?? ((path) => mkdirSync(path, { recursive: true })))(root);
|
|
104
|
-
await gh(["repo", "clone", options.workspaceRepo, destination]);
|
|
105
|
-
const cloned = matchRepo(destination, options.workspaceRepo);
|
|
106
|
-
if (!cloned)
|
|
107
|
-
throw new Error(`Clone completed but ${destination} is not a checkout of ${options.workspaceRepo}.`);
|
|
108
|
-
return accept(cloned);
|
|
109
|
-
}
|
|
110
|
-
export async function launchArchitect(options, runtime = {}) {
|
|
111
|
-
const resolved = await resolveWorkspaceRepo(options, runtime);
|
|
112
|
-
if ("cloneCommand" in resolved)
|
|
113
|
-
return { launched: false, cloneCommand: resolved.cloneCommand };
|
|
114
|
-
(runtime.verifyCodex ?? verifyCodex)();
|
|
115
|
-
const prompt = await readFile(ARCHITECT_PROMPT, "utf8");
|
|
116
|
-
await (runtime.invokeCodex ?? runCodex)(resolved.directory, prompt);
|
|
117
|
-
return { launched: true };
|
|
118
|
-
}
|
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`
|