@higherdev/cli 0.29.0 → 0.30.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 CHANGED
@@ -30,7 +30,7 @@ workspace-map config shapes are migrated automatically when they are read.
30
30
  | `hd ticket new --title TITLE [--acceptance TEXT] [options]` | Create a ticket non-interactively |
31
31
  | `hd ticket queue KEY` | Queue a complete ticket now |
32
32
  | `hd ticket cancel KEY` | Cancel a ticket |
33
- | `hd epic new PATH [--title TITLE]` | Create an epic from a Markdown spec |
33
+ | `hd epic new PATH [--title TITLE] [--draft]` | Create an approved epic, or keep it as a draft |
34
34
  | `hd epic list` | List epics and ticket progress |
35
35
  | `hd epic approve ID` | Open a draft epic for orchestrator decomposition |
36
36
  | `hd epic rm ID` | Remove a draft epic |
package/dist/index.js CHANGED
@@ -186,18 +186,19 @@ async function cmdEpic(argv) {
186
186
  return;
187
187
  }
188
188
  console.log(table(["KEY", "STATUS", "PROGRESS", "TITLE"], rows.map((epic) => [
189
- epic.id, statusChip(epic.status), `${epic.merged}/${epic.total}`, epic.title,
189
+ epic.id, epic.status === "draft" ? c.yellow(`needs your approval; hd epic approve ${epic.id}`)
190
+ : statusChip(epic.status), `${epic.merged}/${epic.total}`, epic.title,
190
191
  ])));
191
192
  return;
192
193
  }
193
194
  if (action === "new") {
194
- const { rest: paths, opts } = flags(rest);
195
+ const { rest: paths, opts, bools } = flags(rest);
195
196
  const path = paths.join(" ");
196
- if (!path)
197
- fail("usage: hd epic new PATH [--title TITLE]");
197
+ if (!path || [...bools].some((name) => name !== "draft"))
198
+ fail("usage: hd epic new PATH [--title TITLE] [--draft]");
198
199
  const input = await readEpicSpec(path, opts.title);
199
- const { epic } = await createEpic(input);
200
- console.log(`${c.bold(epic.id)} ${statusChip(epic.status)} ${epic.title}`);
200
+ const { epic, message } = await createEpic({ ...input, draft: bools.has("draft") });
201
+ console.log(`${c.bold(epic.id)} ${message}`);
201
202
  return;
202
203
  }
203
204
  if (action === "approve" || action === "rm") {
@@ -233,7 +234,7 @@ async function cmdEpic(argv) {
233
234
  console.log(`${c.bold(epic.id)} position ${epic.position}`);
234
235
  return;
235
236
  }
236
- fail("usage: hd epic new PATH [--title TITLE] | list | approve ID | rm ID | set ID --position N");
237
+ fail("usage: hd epic new PATH [--title TITLE] [--draft] | list | approve ID | rm ID | set ID --position N");
237
238
  }
238
239
  async function cmdRoadmap(argv) {
239
240
  const parsed = flags(argv);
package/dist/out.js CHANGED
@@ -66,7 +66,7 @@ export function usage() {
66
66
  ` ${c.blue("hd status")} workspace overview`,
67
67
  ` ${c.blue("hd ticket list | show KEY [--json] | new [PATH] | queue | cancel | merge")} ticket operations`,
68
68
  ` ${c.blue("hd runs | hd run cancel ID")} inspect or cancel live runs`,
69
- ` ${c.blue("hd epic new PATH | list | approve | rm")} epic operations`,
69
+ ` ${c.blue("hd epic new PATH [--draft] | list | approve | rm")} epic operations`,
70
70
  ` ${c.blue("hd roadmap [--json]")} ordered workspace roadmap`,
71
71
  ` ${c.blue("hd plan")} use /architect in the TUI`,
72
72
  ` ${c.blue("hd workspace ls | use | new | set | rotate-key | grant-runner-access")} workspace operations`,
package/dist/roadmap.js CHANGED
@@ -1,7 +1,12 @@
1
1
  export function currentRoadmapEpic(epics) {
2
2
  const statuses = new Map(epics.map((epic) => [epic.id, epic.status]));
3
- return [...epics].sort((a, b) => a.position - b.position).find((epic) => !["draft", "done"].includes(epic.status)
4
- && epic.depends_on.every((id) => statuses.get(id) === "done")) ?? null;
3
+ const next = [...epics].sort((a, b) => a.position - b.position).find((epic) => epic.status !== "done" && epic.depends_on.every((id) => statuses.get(id) === "done"));
4
+ return next?.status === "draft" ? null : next ?? null;
5
+ }
6
+ export function orchestratorIdleReason(epics) {
7
+ const statuses = new Map(epics.map((epic) => [epic.id, epic.status]));
8
+ const next = [...epics].sort((a, b) => a.position - b.position).find((epic) => epic.status !== "done" && epic.depends_on.every((id) => statuses.get(id) === "done"));
9
+ return next?.status === "draft" ? `idle because ${next.title} is a draft and needs your approval` : null;
5
10
  }
6
11
  export function progressBar(merged, total, width = 10) {
7
12
  const complete = total > 0 ? Math.round((Math.max(0, Math.min(merged, total)) / total) * width) : 0;
@@ -12,7 +17,7 @@ export function roadmapText(epics) {
12
17
  const current = currentRoadmapEpic(epics);
13
18
  return [...epics].sort((a, b) => a.position - b.position).flatMap((epic) => {
14
19
  const dependencies = epic.depends_on.map((id) => names.get(id) ?? id);
15
- const status = epic.status === "draft" ? "draft, awaiting approval" : epic.status;
20
+ const status = epic.status === "draft" ? `needs your approval · /epic approve ${epic.id}` : epic.status;
16
21
  return [
17
22
  `${epic.id === current?.id ? "▶" : " "} ${epic.position}. ${epic.title} [${status}]`,
18
23
  ` Outcome: ${epic.outcome_md || "Not described"}`,
package/dist/tui/App.js CHANGED
@@ -683,8 +683,8 @@ export function App({ initial, availableUpdate: initialUpdate = null, updateDeps
683
683
  case "epic-new":
684
684
  setBusy(true);
685
685
  try {
686
- const { epic } = await createEpicFromFile(config, action.path);
687
- say("system", `Created epic ${epic.id}: ${epic.title}`);
686
+ const { epic, message } = await createEpicFromFile(config, action.path, action.draft);
687
+ say("system", `Created epic ${epic.id}: ${message}`);
688
688
  await refresh();
689
689
  }
690
690
  catch (error) {
@@ -725,7 +725,8 @@ export function App({ initial, availableUpdate: initialUpdate = null, updateDeps
725
725
  case "epics": {
726
726
  const rows = epicProgressRows(board.epics, board.tickets);
727
727
  say("system", rows.length
728
- ? rows.map((epic) => `${epic.id} ${epic.status} ${epic.merged}/${epic.total} ${epic.title}`).join("\n")
728
+ ? rows.map((epic) => `${epic.id} ${epic.status === "draft"
729
+ ? `needs your approval · /epic approve ${epic.id}` : epic.status} ${epic.merged}/${epic.total} ${epic.title}`).join("\n")
729
730
  : "No epics.");
730
731
  return;
731
732
  }
@@ -122,7 +122,7 @@ export function AgentsColumn({ board, width, rows, }) {
122
122
  ? "warning" : "muted";
123
123
  return (_jsxs(Box, { flexWrap: "nowrap", children: [_jsxs(Text, { color: inkColor(tone), children: [DOT, " "] }), _jsxs(Text, { color: UI.text, wrap: "truncate", children: [row.name, row.state === "draining" ? null : (_jsx(Text, { color: UI.dim, children: row.run
124
124
  ? ` · ${row.ticket?.key ?? row.run.kind} ${elapsed(row.run.started_at ?? row.run.created_at)}`
125
- : ` · ${row.limitedUntil ? `limited until ${row.limitedUntil}` : row.state}` }))] })] }, row.key));
125
+ : ` · ${row.limitedUntil ? `limited until ${row.limitedUntil}` : row.detail ?? row.state}` }))] })] }, row.key));
126
126
  }),
127
127
  _jsx(More, { count: displayRows.length - shown.length }, "more"),
128
128
  ] }));
package/dist/tui/Help.js CHANGED
@@ -15,7 +15,7 @@ export const COMMANDS = [
15
15
  { name: "/msg", args: "HD-12 TEXT", help: "message a ticket's builder" },
16
16
  { name: "/attach", args: "PATH [HD-12]", help: "attach a file to the open or named ticket" },
17
17
  { name: "/logs", args: "[raw] [HD-12]", help: "filter activity; raw reveals event JSON" },
18
- { name: "/epic", args: "new PATH | approve ID | rm ID", help: "create, approve, or remove a draft epic" },
18
+ { name: "/epic", args: "new PATH [--draft] | approve ID | rm ID", help: "create, approve, or remove an epic" },
19
19
  { name: "/epics", help: "list epics and ticket progress" },
20
20
  { name: "/architect", help: "open a chat with the architect" },
21
21
  { name: "/plan", help: "alias for /architect" },
@@ -93,14 +93,15 @@ export function InboxPanel({ board, width = 80, rows = 12, focus = 0, selectedId
93
93
  const entries = inboxEntries(board, width, answeringId);
94
94
  const unread = board.messages ?? [];
95
95
  const earlier = board.earlier ?? [];
96
+ const drafts = board.epics.filter((epic) => epic.status === "draft").length;
96
97
  const selected = selectedId ?? decisionIdAt(entries, focus);
97
98
  const inner = Math.max(0, rows - 1);
98
99
  const window = scrollWindow(entries.length, inner, focus);
99
100
  const hiddenAbove = window.start;
100
101
  const hiddenBelow = entries.length - window.end;
101
- const note = `${board.decisions.length}d · ${unread.length} unread`
102
+ const note = `${drafts ? `${drafts} epic${drafts === 1 ? "" : "s"} · ` : ""}${board.decisions.length}d · ${unread.length} unread`
102
103
  + `${hiddenAbove ? ` ${hiddenAbove}↑` : ""}${hiddenBelow ? ` ${hiddenBelow}↓` : ""}`;
103
- const empty = board.decisions.length === 0 && unread.length === 0 && earlier.length === 0;
104
+ const empty = drafts === 0 && board.decisions.length === 0 && unread.length === 0 && earlier.length === 0;
104
105
  return (_jsx(Panel, { width: width, rows: rows, children: [
105
106
  _jsx(Heading, { text: "Inbox", note: note }, "h"),
106
107
  ...(empty
@@ -122,6 +123,15 @@ export function inboxEntries(board, width, answeringId) {
122
123
  const bodyWidth = Math.max(12, width - 2);
123
124
  const unread = board.messages ?? [];
124
125
  const earlier = board.earlier ?? [];
126
+ const drafts = board.epics.filter((epic) => epic.status === "draft");
127
+ if (drafts.length) {
128
+ entries.push({ key: "drafts:section", kind: "section", text: "Draft epics" });
129
+ for (const epic of drafts) {
130
+ entries.push({ key: `draft:${epic.id}`, kind: "header", text: epic.title });
131
+ entries.push({ key: `draft:${epic.id}:action`, kind: "body",
132
+ text: `Needs your approval · /epic approve ${epic.id}` });
133
+ }
134
+ }
125
135
  board.decisions.forEach((decision, index) => {
126
136
  const ticket = board.tickets.find((row) => row.id === decision.ticket_id);
127
137
  entries.push({
@@ -30,7 +30,7 @@ export function RoadmapPanel({ board, width = 80, rows = 12, offset = 0 }) {
30
30
  _jsx(Heading, { text: "Roadmap", note: note }, "h"),
31
31
  ...shown.map((line, index) => {
32
32
  const current = line.startsWith("▶");
33
- const draft = line.includes("draft, awaiting approval");
33
+ const draft = line.includes("needs your approval");
34
34
  return _jsx(Text, { color: current ? UI.accent
35
35
  : draft ? inkColor("warning") : line === "Vision" ? UI.text : UI.dim, bold: current || line === "Vision", inverse: current, wrap: "truncate", children: line || " " }, `${start + index}:${line}`);
36
36
  }),
@@ -1,4 +1,5 @@
1
1
  import { formatDrainStatus } from "../host.js";
2
+ import { orchestratorIdleReason } from "../roadmap.js";
2
3
  const LIMITED_UNTIL = /^(?:Waiting on )?(\w+) (?:limited )?until (\d{1,2}:\d{2})\.?$/;
3
4
  export function limitedUntilByProvider(tickets) {
4
5
  const limited = new Map();
@@ -33,6 +34,7 @@ export function agentDisplayRows(board, now = Date.now()) {
33
34
  ticket: null,
34
35
  state: "draining",
35
36
  limitedUntil: null,
37
+ detail: null,
36
38
  }]
37
39
  : [];
38
40
  const activeAgents = new Set();
@@ -61,6 +63,7 @@ export function agentDisplayRows(board, now = Date.now()) {
61
63
  ticket,
62
64
  state: run.status,
63
65
  limitedUntil: null,
66
+ detail: null,
64
67
  };
65
68
  });
66
69
  const limited = limitedUntilByProvider(board.tickets);
@@ -81,6 +84,7 @@ export function agentDisplayRows(board, now = Date.now()) {
81
84
  ? "offline"
82
85
  : "idle",
83
86
  limitedUntil: until ?? null,
87
+ detail: agent.role === "orchestrator" ? orchestratorIdleReason(board.epics) : null,
84
88
  };
85
89
  });
86
90
  return [...drain, ...live, ...idle];
package/dist/tui/data.js CHANGED
@@ -177,8 +177,8 @@ export async function loadTicketDetail(config, key) {
177
177
  },
178
178
  };
179
179
  }
180
- export async function createEpicFromFile(config, path) {
181
- return postEpic(await readEpicSpec(path), config);
180
+ export async function createEpicFromFile(config, path, draft = false) {
181
+ return postEpic({ ...await readEpicSpec(path), draft }, config);
182
182
  }
183
183
  export async function approveEpic(config, id) {
184
184
  return approveEpicNow(id, config);
package/dist/tui/parse.js CHANGED
@@ -61,7 +61,10 @@ export function parseLine(raw) {
61
61
  : { kind: "unknown", command: "ticket needs a key" };
62
62
  case "epic":
63
63
  if (rest[0]?.toLowerCase() === "new" && rest.length > 1) {
64
- return { kind: "epic-new", path: rest.slice(1).join(" ") };
64
+ const draft = rest.includes("--draft");
65
+ const path = rest.slice(1).filter((part) => part !== "--draft").join(" ");
66
+ return path ? { kind: "epic-new", path, draft }
67
+ : { kind: "unknown", command: "epic needs new PATH, approve ID, or rm ID" };
65
68
  }
66
69
  return rest[0]?.toLowerCase() === "approve" && rest.length === 2
67
70
  ? { kind: "epic-approve", id: rest[1] }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@higherdev/cli",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",