@higherdev/cli 0.6.0 → 0.8.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 +9 -4
- package/dist/api.js +11 -1
- package/dist/epics.js +25 -0
- package/dist/index.js +55 -3
- package/dist/out.js +3 -1
- package/dist/plan.js +94 -0
- package/dist/tui/App.js +55 -3
- package/dist/tui/Help.js +4 -0
- package/dist/tui/data.js +8 -1
- package/dist/tui/parse.js +12 -0
- package/package.json +3 -2
- package/prompts/architect.md +23 -0
package/README.md
CHANGED
|
@@ -30,7 +30,11 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
30
30
|
| `hd status` | Show workspace, ticket, run, and decision status |
|
|
31
31
|
| `hd ticket list` | List tickets |
|
|
32
32
|
| `hd ticket show KEY` | Show one ticket |
|
|
33
|
-
| `hd ticket new --title TITLE [options]` | Create a ticket |
|
|
33
|
+
| `hd ticket new --title TITLE [--acceptance TEXT] [options]` | Create a ticket |
|
|
34
|
+
| `hd ticket queue KEY` | Queue a complete ticket now |
|
|
35
|
+
| `hd epic new PATH [--title TITLE]` | Create an epic from a Markdown spec |
|
|
36
|
+
| `hd epic list` | List epics and ticket progress |
|
|
37
|
+
| `hd plan [--repo DIR]` | Hand the terminal to Codex to author an epic spec |
|
|
34
38
|
| `hd workspace ls` | List every workspace available to the configured key |
|
|
35
39
|
| `hd workspace new --name NAME --repo OWNER/NAME [options]` | Create a paused workspace |
|
|
36
40
|
| `hd agents` | List agents |
|
|
@@ -44,6 +48,7 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
44
48
|
| `hd init [options]` | Configure this host and runner service |
|
|
45
49
|
| `hd upgrade [options]` | Refresh this host configuration |
|
|
46
50
|
|
|
47
|
-
Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/
|
|
48
|
-
`/
|
|
49
|
-
`/exit`. The display refreshes from
|
|
51
|
+
Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/queue`, `/epic new`,
|
|
52
|
+
`/epics`, `/plan`, `/decide`, `/agents`, `/settings`, `/workspace`, `/feed`,
|
|
53
|
+
`/orchestrator`, `/refresh`, `/help`, or `/exit`. The display refreshes from
|
|
54
|
+
the HDX API every five seconds.
|
package/dist/api.js
CHANGED
|
@@ -14,7 +14,7 @@ export function apiErrorMessage(status, parsed, fallback) {
|
|
|
14
14
|
: trimmed;
|
|
15
15
|
return `hd: ${status} ${body}`;
|
|
16
16
|
}
|
|
17
|
-
async function request(config, method, path, body) {
|
|
17
|
+
async function request(config, method, path, body, verbatimError = false) {
|
|
18
18
|
const response = await fetch(`${config.url}${path}`, {
|
|
19
19
|
method,
|
|
20
20
|
headers: {
|
|
@@ -32,6 +32,10 @@ async function request(config, method, path, body) {
|
|
|
32
32
|
throw new Error(apiErrorMessage(response.status, null, text || response.statusText));
|
|
33
33
|
}
|
|
34
34
|
if (!response.ok) {
|
|
35
|
+
if (verbatimError && parsed && typeof parsed === "object" && "error" in parsed
|
|
36
|
+
&& typeof parsed.error === "string") {
|
|
37
|
+
throw new Error(parsed.error);
|
|
38
|
+
}
|
|
35
39
|
throw new Error(apiErrorMessage(response.status, parsed, text || response.statusText));
|
|
36
40
|
}
|
|
37
41
|
return parsed;
|
|
@@ -48,6 +52,9 @@ export async function showTicket(key, config = loadConfig()) {
|
|
|
48
52
|
export async function createTicket(fields, config = loadConfig()) {
|
|
49
53
|
return request(config, "POST", `/api/w/${config.slug}/tickets`, fields);
|
|
50
54
|
}
|
|
55
|
+
export async function queueTicket(key, config = loadConfig()) {
|
|
56
|
+
return request(config, "POST", `/api/w/${config.slug}/tickets/${encodeURIComponent(key)}/queue`, undefined, true);
|
|
57
|
+
}
|
|
51
58
|
export async function listTicketEvents(key, afterAt, afterId, config = loadConfig()) {
|
|
52
59
|
const query = new URLSearchParams();
|
|
53
60
|
if (afterAt)
|
|
@@ -83,6 +90,9 @@ export async function updateAgent(id, fields, config = loadConfig()) {
|
|
|
83
90
|
export async function listEpics(config = loadConfig()) {
|
|
84
91
|
return request(config, "GET", `/api/w/${config.slug}/epics`);
|
|
85
92
|
}
|
|
93
|
+
export async function createEpic(fields, config = loadConfig()) {
|
|
94
|
+
return request(config, "POST", `/api/w/${config.slug}/epics`, fields);
|
|
95
|
+
}
|
|
86
96
|
export async function listMessages(options = {}, config = loadConfig()) {
|
|
87
97
|
const query = new URLSearchParams();
|
|
88
98
|
if (options.ticketId)
|
package/dist/epics.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
export function headingTitle(spec) {
|
|
3
|
+
const match = spec.match(/^#[ \t]+(.+?)\s*$/m);
|
|
4
|
+
const title = match?.[1]?.replace(/[ \t]+#+[ \t]*$/, "").trim();
|
|
5
|
+
return title || null;
|
|
6
|
+
}
|
|
7
|
+
export async function readEpicSpec(path, title) {
|
|
8
|
+
const spec_md = await readFile(path, "utf8");
|
|
9
|
+
const resolved = title?.trim() || headingTitle(spec_md);
|
|
10
|
+
if (!resolved)
|
|
11
|
+
throw new Error(`No # heading in ${path}. Pass --title TITLE.`);
|
|
12
|
+
return { title: resolved, spec_md };
|
|
13
|
+
}
|
|
14
|
+
export function epicProgressRows(epics, tickets) {
|
|
15
|
+
return epics.map((epic) => {
|
|
16
|
+
const linked = tickets.filter((ticket) => ticket.epic_id === epic.id);
|
|
17
|
+
return {
|
|
18
|
+
id: epic.id,
|
|
19
|
+
title: epic.title,
|
|
20
|
+
status: epic.status,
|
|
21
|
+
merged: linked.filter((ticket) => ticket.status === "merged").length,
|
|
22
|
+
total: linked.length,
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { answerDecision, createTicket, createWorkspace, getStatus, listAgents, listTicketEvents, listTickets, listWorkspaces, postMessage, setPaused, showTicket, updateAgent, } from "./api.js";
|
|
4
|
+
import { answerDecision, createEpic, createTicket, createWorkspace, getStatus, listAgents, listEpics, listTicketEvents, listTickets, listWorkspaces, postMessage, queueTicket, setPaused, showTicket, updateAgent, } from "./api.js";
|
|
5
5
|
import { initHost, parseHostFlags } from "./host.js";
|
|
6
6
|
import { loadConfig, writeHdConfig } from "./config.js";
|
|
7
|
+
import { epicProgressRows, readEpicSpec } from "./epics.js";
|
|
7
8
|
import { banner, c, statusChip, table, truncate, usage } from "./out.js";
|
|
9
|
+
import { launchArchitect } from "./plan.js";
|
|
8
10
|
function fail(message) {
|
|
9
11
|
console.error(message);
|
|
10
12
|
process.exit(1);
|
|
@@ -96,7 +98,7 @@ async function cmdTicket(argv) {
|
|
|
96
98
|
if (action === "new") {
|
|
97
99
|
const { opts } = flags(rest);
|
|
98
100
|
if (!opts.title)
|
|
99
|
-
fail("usage: hd ticket new --title TITLE [--body TEXT] [--area TAG] [--provider NAME] [--epic ID]");
|
|
101
|
+
fail("usage: hd ticket new --title TITLE [--body TEXT] [--acceptance TEXT] [--area TAG] [--provider NAME] [--epic ID]");
|
|
100
102
|
const { ticket } = await createTicket({
|
|
101
103
|
title: opts.title,
|
|
102
104
|
body_md: opts.body,
|
|
@@ -108,7 +110,49 @@ async function cmdTicket(argv) {
|
|
|
108
110
|
console.log(`${c.bold(ticket.key)} ${statusChip(ticket.status)} ${ticket.title}`);
|
|
109
111
|
return;
|
|
110
112
|
}
|
|
111
|
-
|
|
113
|
+
if (action === "queue") {
|
|
114
|
+
const key = rest[0];
|
|
115
|
+
if (!key)
|
|
116
|
+
fail("usage: hd ticket queue KEY");
|
|
117
|
+
const { ticket } = await queueTicket(key.toUpperCase());
|
|
118
|
+
console.log(`${c.bold(ticket.key)} ${statusChip(ticket.status)}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
fail("usage: hd ticket list | show KEY | new --title TITLE | queue KEY");
|
|
122
|
+
}
|
|
123
|
+
async function cmdEpic(argv) {
|
|
124
|
+
const [action, ...rest] = argv;
|
|
125
|
+
if (action === "list") {
|
|
126
|
+
const [{ epics }, { tickets }] = await Promise.all([listEpics(), listTickets()]);
|
|
127
|
+
const rows = epicProgressRows(epics, tickets);
|
|
128
|
+
if (!rows.length) {
|
|
129
|
+
console.log(c.dim("No epics."));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
console.log(table(["KEY", "STATUS", "PROGRESS", "TITLE"], rows.map((epic) => [
|
|
133
|
+
epic.id, statusChip(epic.status), `${epic.merged}/${epic.total}`, epic.title,
|
|
134
|
+
])));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (action === "new") {
|
|
138
|
+
const { rest: paths, opts } = flags(rest);
|
|
139
|
+
const path = paths.join(" ");
|
|
140
|
+
if (!path)
|
|
141
|
+
fail("usage: hd epic new PATH [--title TITLE]");
|
|
142
|
+
const input = await readEpicSpec(path, opts.title);
|
|
143
|
+
const { epic } = await createEpic(input);
|
|
144
|
+
console.log(`${c.bold(epic.id)} ${statusChip(epic.status)} ${epic.title}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
fail("usage: hd epic new PATH [--title TITLE] | list");
|
|
148
|
+
}
|
|
149
|
+
async function cmdPlan(argv) {
|
|
150
|
+
const { rest, opts, bools } = flags(argv);
|
|
151
|
+
if (rest.length || bools.size || Object.keys(opts).some((key) => key !== "repo")) {
|
|
152
|
+
fail("usage: hd plan [--repo DIR]");
|
|
153
|
+
}
|
|
154
|
+
const { workspace } = await getStatus();
|
|
155
|
+
await launchArchitect({ workspaceRepo: workspace.repo, repo: opts.repo });
|
|
112
156
|
}
|
|
113
157
|
async function cmdLogs(argv) {
|
|
114
158
|
const { rest, bools } = flags(argv);
|
|
@@ -264,6 +308,14 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
264
308
|
await cmdTicket(rest);
|
|
265
309
|
return;
|
|
266
310
|
}
|
|
311
|
+
if (cmd === "epic") {
|
|
312
|
+
await cmdEpic(rest);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (cmd === "plan") {
|
|
316
|
+
await cmdPlan(rest);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
267
319
|
if (cmd === "logs") {
|
|
268
320
|
await cmdLogs(rest);
|
|
269
321
|
return;
|
package/dist/out.js
CHANGED
|
@@ -64,7 +64,9 @@ 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")}
|
|
67
|
+
` ${c.blue("hd ticket list | show | new | queue")} ticket operations`,
|
|
68
|
+
` ${c.blue("hd epic new PATH | list")} epic operations`,
|
|
69
|
+
` ${c.blue("hd plan [--repo DIR]")} author an epic with Codex`,
|
|
68
70
|
` ${c.blue("hd workspace ls | new")} list or create workspaces`,
|
|
69
71
|
` ${c.blue("hd agents [set]")} inspect or update agents`,
|
|
70
72
|
` ${c.blue("hd logs KEY [-f]")} run events`,
|
package/dist/plan.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
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/dist/tui/App.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { Box, Static, Text, useApp, useInput, useStdout } from "ink";
|
|
4
|
+
import { epicProgressRows } from "../epics.js";
|
|
5
|
+
import { launchArchitect } from "../plan.js";
|
|
4
6
|
import { Banner } from "./Banner.js";
|
|
5
7
|
import { Bubble } from "./Bubble.js";
|
|
6
8
|
import { Cockpit, StreamPanel, boardTicketIds, nextCursor } from "./Dashboard.js";
|
|
@@ -14,7 +16,7 @@ import { alertOnce } from "./alert.js";
|
|
|
14
16
|
import { bubbleRows } from "./height.js";
|
|
15
17
|
import { planLayout, splitPanels } from "./layout.js";
|
|
16
18
|
import { parseLine } from "./parse.js";
|
|
17
|
-
import { configuredSlugs, decisionOptions, loadLiveEvents, loadTicketDetail, pollSnapshot, postOrchestrator, resolveDecision, switchWorkspace, updateAgent, updateProviderCap, } from "./data.js";
|
|
19
|
+
import { configuredSlugs, createEpicFromFile, decisionOptions, loadLiveEvents, loadTicketDetail, pollSnapshot, postOrchestrator, queueTicket, resolveDecision, switchWorkspace, updateAgent, updateProviderCap, } from "./data.js";
|
|
18
20
|
import { editFor, editableKeys, nextValue, seedFor, settingsRows } from "./settings-model.js";
|
|
19
21
|
import { appendLines, runLabels, toStreamLines } from "./stream.js";
|
|
20
22
|
import { UI } from "./theme.js";
|
|
@@ -22,7 +24,7 @@ import { WorkspaceLoads } from "./workspace-load.js";
|
|
|
22
24
|
let messageSeq = 0;
|
|
23
25
|
const nextId = () => `m${messageSeq++}`;
|
|
24
26
|
export function App({ initial }) {
|
|
25
|
-
const { exit } = useApp();
|
|
27
|
+
const { exit, suspendTerminal } = useApp();
|
|
26
28
|
const { stdout } = useStdout();
|
|
27
29
|
const columns = stdout?.columns && stdout.columns > 0 ? stdout.columns : 80;
|
|
28
30
|
const rows = stdout?.rows && stdout.rows > 0 ? stdout.rows : 24;
|
|
@@ -311,6 +313,55 @@ export function App({ initial }) {
|
|
|
311
313
|
setBusy(false);
|
|
312
314
|
}
|
|
313
315
|
return;
|
|
316
|
+
case "epic-new":
|
|
317
|
+
setBusy(true);
|
|
318
|
+
try {
|
|
319
|
+
const { epic } = await createEpicFromFile(config, action.path);
|
|
320
|
+
say("system", `Created epic ${epic.id}: ${epic.title}`);
|
|
321
|
+
await refresh();
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
setBusy(false);
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
case "epics": {
|
|
331
|
+
const rows = epicProgressRows(board.epics, board.tickets);
|
|
332
|
+
say("system", rows.length
|
|
333
|
+
? rows.map((epic) => `${epic.id} ${epic.status} ${epic.merged}/${epic.total} ${epic.title}`).join("\n")
|
|
334
|
+
: "No epics.");
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
case "queue":
|
|
338
|
+
setBusy(true);
|
|
339
|
+
try {
|
|
340
|
+
const { ticket: queued } = await queueTicket(config, action.key);
|
|
341
|
+
say("system", `${queued.key} queued.`);
|
|
342
|
+
await refresh();
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
setBusy(false);
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
case "plan":
|
|
352
|
+
setBusy(true);
|
|
353
|
+
try {
|
|
354
|
+
await suspendTerminal(() => launchArchitect({ workspaceRepo: workspace.repo }));
|
|
355
|
+
say("system", "Architect session ended. Review the spec, then run /epic new PATH.");
|
|
356
|
+
await refresh();
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
setBusy(false);
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
314
365
|
case "decide": {
|
|
315
366
|
const decision = board.decisions[0];
|
|
316
367
|
if (!decision) {
|
|
@@ -357,7 +408,8 @@ export function App({ initial }) {
|
|
|
357
408
|
default:
|
|
358
409
|
return;
|
|
359
410
|
}
|
|
360
|
-
}, [view, settings, applyEdit, board, browsing, mode, say, askOrchestrator, order, settingsOrder, changeWorkspace,
|
|
411
|
+
}, [view, settings, applyEdit, board, browsing, mode, say, askOrchestrator, order, settingsOrder, changeWorkspace,
|
|
412
|
+
config, workspace.repo, refresh, suspendTerminal, exit]);
|
|
361
413
|
useInput((input, key) => {
|
|
362
414
|
if (key.ctrl && input === "c")
|
|
363
415
|
exit();
|
package/dist/tui/Help.js
CHANGED
|
@@ -8,6 +8,10 @@ export const COMMANDS = [
|
|
|
8
8
|
{ name: "/board", help: "the kanban board" },
|
|
9
9
|
{ name: "/inbox", help: "decisions and messages waiting on you" },
|
|
10
10
|
{ name: "/ticket", args: "HD-12", help: "open one ticket" },
|
|
11
|
+
{ name: "/queue", args: "HD-12", help: "queue a complete ticket now" },
|
|
12
|
+
{ name: "/epic", args: "new PATH", help: "create an epic from a Markdown spec" },
|
|
13
|
+
{ name: "/epics", help: "list epics and ticket progress" },
|
|
14
|
+
{ name: "/plan", help: "author an epic with your Codex CLI" },
|
|
11
15
|
{ name: "/decide", args: "2 | text", help: "answer the decision on screen" },
|
|
12
16
|
{ name: "/agents", help: "every agent in full, and the live run stream" },
|
|
13
17
|
{ name: "/settings", help: "change provider caps and agent settings" },
|
package/dist/tui/data.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { answerDecision, getStatus, listAgents, listEpics, listFeed, listMessages, listTicketEvents, listTickets, listWorkspaces, postMessage as sendMessage, showTicket, updateAgent as patchAgent, updateCaps, } from "../api.js";
|
|
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";
|
|
2
2
|
import { loadConfig, switchWorkspace as selectWorkspace, } from "../config.js";
|
|
3
|
+
import { readEpicSpec } from "../epics.js";
|
|
3
4
|
export const POLL_MS = 5_000;
|
|
4
5
|
export const providers = ["claude", "codex", "gemini", "grok"];
|
|
5
6
|
export const efforts = ["low", "medium", "high"];
|
|
@@ -98,6 +99,12 @@ export async function postOrchestrator(body, config) {
|
|
|
98
99
|
export async function loadTicketDetail(config, key) {
|
|
99
100
|
return showTicket(key, config);
|
|
100
101
|
}
|
|
102
|
+
export async function createEpicFromFile(config, path) {
|
|
103
|
+
return postEpic(await readEpicSpec(path), config);
|
|
104
|
+
}
|
|
105
|
+
export async function queueTicket(config, key) {
|
|
106
|
+
return queueTicketNow(key, config);
|
|
107
|
+
}
|
|
101
108
|
export async function waitForReply(config, since, timeoutMs) {
|
|
102
109
|
const deadline = Date.now() + timeoutMs;
|
|
103
110
|
while (Date.now() <= deadline) {
|
package/dist/tui/parse.js
CHANGED
|
@@ -29,6 +29,18 @@ export function parseLine(raw) {
|
|
|
29
29
|
return argument
|
|
30
30
|
? { kind: "ticket", key: argument.toUpperCase() }
|
|
31
31
|
: { kind: "unknown", command: "ticket needs a key" };
|
|
32
|
+
case "epic":
|
|
33
|
+
return rest[0]?.toLowerCase() === "new" && rest.length > 1
|
|
34
|
+
? { kind: "epic-new", path: rest.slice(1).join(" ") }
|
|
35
|
+
: { kind: "unknown", command: "epic needs new PATH" };
|
|
36
|
+
case "epics":
|
|
37
|
+
return { kind: "epics" };
|
|
38
|
+
case "queue":
|
|
39
|
+
return argument
|
|
40
|
+
? { kind: "queue", key: argument.toUpperCase() }
|
|
41
|
+
: { kind: "unknown", command: "queue needs a key" };
|
|
42
|
+
case "plan":
|
|
43
|
+
return argument ? { kind: "unknown", command: "plan takes no arguments" } : { kind: "plan" };
|
|
32
44
|
case "decide": {
|
|
33
45
|
const dismiss = /(^|\s)--(skip|dismiss)(\s|$)/.test(argument);
|
|
34
46
|
return {
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@higherdev/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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"
|
|
10
|
+
"dist",
|
|
11
|
+
"prompts"
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|
|
13
14
|
"build": "tsc",
|
|
@@ -0,0 +1,23 @@
|
|
|
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`
|