@higherdev/cli 0.2.1 → 0.3.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.
Files changed (2) hide show
  1. package/dist/index.js +477 -156
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -619,6 +619,13 @@ var init_defaults = __esm({
619
619
  }
620
620
  });
621
621
 
622
+ // ../../packages/db/src/desirability.ts
623
+ var init_desirability = __esm({
624
+ "../../packages/db/src/desirability.ts"() {
625
+ "use strict";
626
+ }
627
+ });
628
+
622
629
  // ../../packages/db/src/effective-config.ts
623
630
  var init_effective_config = __esm({
624
631
  "../../packages/db/src/effective-config.ts"() {
@@ -1009,53 +1016,6 @@ var init_schemas = __esm({
1009
1016
  }
1010
1017
  });
1011
1018
 
1012
- // ../../packages/db/src/stuck.ts
1013
- function stuckReason(opts) {
1014
- const { ticket } = opts;
1015
- if (QUIET.has(ticket.status)) return null;
1016
- if (ticket.human_owner) return null;
1017
- if (ticket.status === "needs_decision") return "needs decision";
1018
- if (ticket.status === "blocked") {
1019
- const keys = (opts.blockerKeys ?? []).filter(Boolean);
1020
- return keys.length > 0 ? `blocked by ${keys.join(", ")}` : "blocked";
1021
- }
1022
- if (opts.agent && !opts.agent.enabled) {
1023
- return `${opts.agent.display_name} is disabled`;
1024
- }
1025
- if (opts.hostOnline === false) return "host offline";
1026
- if (ticket.next_review_at) {
1027
- const until = Date.parse(ticket.next_review_at);
1028
- const now = opts.now ?? Date.now();
1029
- if (!Number.isNaN(until) && until > now) {
1030
- const clock2 = new Date(until).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
1031
- return `review backoff until ${clock2}`;
1032
- }
1033
- }
1034
- if (ticket.status === "queued" || ticket.status === "ready") {
1035
- if (!ticket.agent_id && !ticket.provider) return "no agent assigned";
1036
- const row = opts.providerStatus;
1037
- if (row && !row.available) {
1038
- const who = opts.agent?.display_name ?? ticket.provider ?? "agent";
1039
- return `${who}: ${availabilityLabel(row)}`;
1040
- }
1041
- if (ticket.status === "queued") {
1042
- const who = opts.agent?.display_name ?? ticket.provider;
1043
- return who ? `waiting for a slot (${who})` : "waiting for a slot";
1044
- }
1045
- }
1046
- if (ticket.status === "failed" && ticket.last_error) return ticket.last_error;
1047
- if (ticket.last_error) return ticket.last_error;
1048
- return null;
1049
- }
1050
- var QUIET;
1051
- var init_stuck = __esm({
1052
- "../../packages/db/src/stuck.ts"() {
1053
- "use strict";
1054
- init_availability();
1055
- QUIET = /* @__PURE__ */ new Set(["running", "merged", "cancelled"]);
1056
- }
1057
- });
1058
-
1059
1019
  // ../../packages/db/src/templates.ts
1060
1020
  function parseTicketTemplatesStrict(value) {
1061
1021
  let raw = value;
@@ -1067,25 +1027,25 @@ function parseTicketTemplatesStrict(value) {
1067
1027
  }
1068
1028
  }
1069
1029
  if (!Array.isArray(raw)) return { ok: false, error: "Templates must be a list." };
1070
- const templates = [];
1030
+ const templates2 = [];
1071
1031
  const ids = /* @__PURE__ */ new Set();
1072
1032
  for (const row of raw) {
1073
1033
  const parsed = ticketTemplateSchema.safeParse(row);
1074
1034
  if (!parsed.success) return { ok: false, error: "Each template needs an id, name, body, and acceptance." };
1075
1035
  if (ids.has(parsed.data.id)) return { ok: false, error: "Template ids must be unique." };
1076
1036
  ids.add(parsed.data.id);
1077
- templates.push(parsed.data);
1037
+ templates2.push(parsed.data);
1078
1038
  }
1079
- return { ok: true, templates };
1039
+ return { ok: true, templates: templates2 };
1080
1040
  }
1081
1041
  function parseTicketTemplates(value) {
1082
1042
  const parsed = parseTicketTemplatesStrict(value);
1083
1043
  if (parsed.ok) return parsed.templates;
1084
1044
  return DEFAULT_TICKET_TEMPLATES.map((row) => ({ ...row }));
1085
1045
  }
1086
- function applyTicketTemplate(templates, id) {
1046
+ function applyTicketTemplate(templates2, id) {
1087
1047
  if (!id) return null;
1088
- const template = templates.find((row) => row.id === id);
1048
+ const template = templates2.find((row) => row.id === id);
1089
1049
  if (!template) return null;
1090
1050
  return { body_md: template.body_md, acceptance_md: template.acceptance_md };
1091
1051
  }
@@ -1123,6 +1083,136 @@ var init_templates = __esm({
1123
1083
  }
1124
1084
  });
1125
1085
 
1086
+ // ../../packages/db/src/recipes.ts
1087
+ import { z as z2 } from "zod";
1088
+ var itemSchema, workflowRecipeSchema, templates, WORKFLOW_RECIPES;
1089
+ var init_recipes = __esm({
1090
+ "../../packages/db/src/recipes.ts"() {
1091
+ "use strict";
1092
+ init_templates();
1093
+ itemSchema = z2.object({
1094
+ id: z2.string().regex(/^[a-z][a-z0-9-]{0,39}$/),
1095
+ title: z2.string().min(1).max(200),
1096
+ body_md: z2.string().max(2e4),
1097
+ acceptance_md: z2.string().max(1e4),
1098
+ area: z2.string().max(100).nullable().default(null),
1099
+ priority: z2.number().int().min(-100).max(100).default(0),
1100
+ blocked_by: z2.array(z2.string()).max(20).default([])
1101
+ });
1102
+ workflowRecipeSchema = z2.object({
1103
+ id: z2.string().regex(/^[a-z][a-z0-9-]{0,39}$/),
1104
+ name: z2.string().min(1).max(100),
1105
+ description: z2.string().min(1).max(300),
1106
+ conventions_md: z2.string().max(2e4),
1107
+ ticket_templates: z2.array(z2.object({ id: z2.string(), name: z2.string(), body_md: z2.string(), acceptance_md: z2.string() })),
1108
+ epic: z2.object({ title: z2.string().min(1).max(200), spec_md: z2.string().max(5e4), tickets: z2.array(itemSchema).max(50) }).nullable()
1109
+ }).superRefine((recipe, context) => {
1110
+ const ids = new Set(recipe.epic?.tickets.map((ticket) => ticket.id) ?? []);
1111
+ if (ids.size !== (recipe.epic?.tickets.length ?? 0)) context.addIssue({ code: "custom", message: "Recipe ticket ids must be unique." });
1112
+ for (const ticket of recipe.epic?.tickets ?? []) {
1113
+ if (new Set(ticket.blocked_by).size !== ticket.blocked_by.length) context.addIssue({ code: "custom", message: `Ticket ${ticket.id} repeats a blocker.` });
1114
+ for (const blocker of ticket.blocked_by) if (!ids.has(blocker) || blocker === ticket.id) context.addIssue({ code: "custom", message: `Invalid blocker ${blocker}.` });
1115
+ }
1116
+ const visiting = /* @__PURE__ */ new Set();
1117
+ const visited = /* @__PURE__ */ new Set();
1118
+ const byId = new Map((recipe.epic?.tickets ?? []).map((ticket) => [ticket.id, ticket]));
1119
+ const visit = (id) => {
1120
+ if (visiting.has(id)) return false;
1121
+ if (visited.has(id)) return true;
1122
+ visiting.add(id);
1123
+ for (const blocker of byId.get(id)?.blocked_by ?? []) if (!visit(blocker)) return false;
1124
+ visiting.delete(id);
1125
+ visited.add(id);
1126
+ return true;
1127
+ };
1128
+ for (const id of ids) if (!visit(id)) {
1129
+ context.addIssue({ code: "custom", message: "Recipe dependencies must be acyclic." });
1130
+ break;
1131
+ }
1132
+ });
1133
+ templates = () => DEFAULT_TICKET_TEMPLATES.map((row) => ({ ...row }));
1134
+ WORKFLOW_RECIPES = [
1135
+ { id: "blank", name: "Blank workspace", description: "Agents and ticket templates only.", conventions_md: "", ticket_templates: templates(), epic: null },
1136
+ {
1137
+ id: "feature-delivery",
1138
+ name: "Feature delivery",
1139
+ description: "Discover, implement, verify, and release a product feature.",
1140
+ conventions_md: "Keep changes reviewable, preserve owner intent, and record verification evidence before release.",
1141
+ ticket_templates: templates(),
1142
+ epic: { title: "First feature delivery", spec_md: "Replace this starter specification with the product outcome and constraints.", tickets: [
1143
+ { id: "scope", title: "Confirm outcome and acceptance contract", body_md: "Document the user outcome, constraints, and non-goals.", acceptance_md: "- Acceptance criteria are testable\n- Owner decisions are explicit", area: "product", priority: 30, blocked_by: [] },
1144
+ { id: "build", title: "Implement the scoped feature", body_md: "Build the smallest complete vertical slice that satisfies the accepted contract.", acceptance_md: "- The accepted behavior works\n- Failure paths are covered", area: "product", priority: 20, blocked_by: ["scope"] },
1145
+ { id: "release", title: "Verify and release the feature", body_md: "Run complete gates, review release evidence, and execute the approved release plan.", acceptance_md: "- Required gates pass\n- Rollback and live verification are recorded", area: "release", priority: 10, blocked_by: ["build"] }
1146
+ ] }
1147
+ }
1148
+ ];
1149
+ }
1150
+ });
1151
+
1152
+ // ../../packages/db/src/slo.ts
1153
+ var SLO_THRESHOLDS;
1154
+ var init_slo = __esm({
1155
+ "../../packages/db/src/slo.ts"() {
1156
+ "use strict";
1157
+ init_budgets();
1158
+ SLO_THRESHOLDS = {
1159
+ queueLatencySeconds: 60,
1160
+ stuckAgeSeconds: 15 * 60,
1161
+ successRatePercent: 90,
1162
+ retryRatePercent: 10,
1163
+ expiredLeases: 0,
1164
+ webhookDeliveryPercent: 99
1165
+ };
1166
+ }
1167
+ });
1168
+
1169
+ // ../../packages/db/src/stuck.ts
1170
+ function stuckReason(opts) {
1171
+ const { ticket } = opts;
1172
+ if (QUIET.has(ticket.status)) return null;
1173
+ if (ticket.human_owner) return null;
1174
+ if (ticket.status === "needs_decision") return "needs decision";
1175
+ if (ticket.status === "blocked") {
1176
+ const keys = (opts.blockerKeys ?? []).filter(Boolean);
1177
+ return keys.length > 0 ? `blocked by ${keys.join(", ")}` : "blocked";
1178
+ }
1179
+ if (opts.agent && !opts.agent.enabled) {
1180
+ return `${opts.agent.display_name} is disabled`;
1181
+ }
1182
+ if (opts.hostOnline === false) return "host offline";
1183
+ if (ticket.next_review_at) {
1184
+ const until = Date.parse(ticket.next_review_at);
1185
+ const now = opts.now ?? Date.now();
1186
+ if (!Number.isNaN(until) && until > now) {
1187
+ const clock2 = new Date(until).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
1188
+ return `review backoff until ${clock2}`;
1189
+ }
1190
+ }
1191
+ if (ticket.status === "queued" || ticket.status === "ready") {
1192
+ if (!ticket.agent_id && !ticket.provider) return "no agent assigned";
1193
+ const row = opts.providerStatus;
1194
+ if (row && !row.available) {
1195
+ const who = opts.agent?.display_name ?? ticket.provider ?? "agent";
1196
+ return `${who}: ${availabilityLabel(row)}`;
1197
+ }
1198
+ if (ticket.status === "queued") {
1199
+ const who = opts.agent?.display_name ?? ticket.provider;
1200
+ return who ? `waiting for a slot (${who})` : "waiting for a slot";
1201
+ }
1202
+ }
1203
+ if (ticket.status === "failed" && ticket.last_error) return ticket.last_error;
1204
+ if (ticket.last_error) return ticket.last_error;
1205
+ return null;
1206
+ }
1207
+ var QUIET;
1208
+ var init_stuck = __esm({
1209
+ "../../packages/db/src/stuck.ts"() {
1210
+ "use strict";
1211
+ init_availability();
1212
+ QUIET = /* @__PURE__ */ new Set(["running", "merged", "cancelled"]);
1213
+ }
1214
+ });
1215
+
1126
1216
  // ../../packages/db/src/tickets.ts
1127
1217
  function uncertainCreateError(error) {
1128
1218
  return Boolean(error && (!error.code || /^PGRST00[0-3]$/.test(error.code)));
@@ -1570,6 +1660,7 @@ var init_src = __esm({
1570
1660
  init_commands();
1571
1661
  init_cockpit();
1572
1662
  init_defaults();
1663
+ init_desirability();
1573
1664
  init_effective_config();
1574
1665
  init_enums();
1575
1666
  init_epic_progress();
@@ -1577,7 +1668,9 @@ var init_src = __esm({
1577
1668
  init_host_health();
1578
1669
  init_models();
1579
1670
  init_quiet_hours();
1671
+ init_recipes();
1580
1672
  init_schemas();
1673
+ init_slo();
1581
1674
  init_status();
1582
1675
  init_stuck();
1583
1676
  init_templates();
@@ -3977,7 +4070,12 @@ async function createBuilder(ctx, name, input) {
3977
4070
  model,
3978
4071
  display_name: display,
3979
4072
  effort: asEffort(input.effort ?? DEFAULT_EFFORT),
3980
- routing_notes: input.notes ?? ""
4073
+ routing_notes: input.notes ?? "",
4074
+ prompt_addendum: input.prompt ?? "",
4075
+ // Absent means unlimited, which is the column default. Null is a cap
4076
+ // deliberately removed, and both arrive here as null.
4077
+ ...input.runsPerHour === void 0 ? {} : { runs_per_hour: input.runsPerHour },
4078
+ ...input.dailySpend === void 0 ? {} : { daily_spend_usd: input.dailySpend }
3981
4079
  }).select("*").single();
3982
4080
  if (error) return no(error.message);
3983
4081
  await ctx.audit(ctx.workspace.id, "create", { subject: `agent ${data.display_name}` });
@@ -4312,12 +4410,12 @@ function registerWorkspaceCommands(program) {
4312
4410
  });
4313
4411
  workspace.command("templates").description("ticket templates for this workspace").action(async function() {
4314
4412
  const ctx = await requireWorkspace(slugOf3(this));
4315
- const templates = parseTicketTemplates(ctx.workspace.ticket_templates);
4413
+ const templates2 = parseTicketTemplates(ctx.workspace.ticket_templates);
4316
4414
  emit(
4317
- templates,
4415
+ templates2,
4318
4416
  () => table(
4319
4417
  [{ header: "ID" }, { header: "NAME" }],
4320
- templates.map((row) => [c.bold(row.id), row.name])
4418
+ templates2.map((row) => [c.bold(row.id), row.name])
4321
4419
  )
4322
4420
  );
4323
4421
  });
@@ -4434,7 +4532,7 @@ var init_commands4 = __esm({
4434
4532
 
4435
4533
  // src/architect/tools.ts
4436
4534
  import { randomUUID as randomUUID3 } from "crypto";
4437
- import { z as z2 } from "zod";
4535
+ import { z as z3 } from "zod";
4438
4536
  async function ticketOf(ctx, wanted) {
4439
4537
  const board = await loadBoard(ctx);
4440
4538
  const ticket = board.tickets.find((row) => row.key === wanted.trim().toUpperCase());
@@ -4452,7 +4550,7 @@ function toolsFor(access2) {
4452
4550
  return access2 === "read" ? TOOLS.filter((tool) => tool.access === "read") : TOOLS;
4453
4551
  }
4454
4552
  function toolJsonSchema(tool) {
4455
- const schema = z2.toJSONSchema(tool.schema);
4553
+ const schema = z3.toJSONSchema(tool.schema);
4456
4554
  delete schema.$schema;
4457
4555
  if (!schema.type) schema.type = "object";
4458
4556
  if (!schema.properties) schema.properties = {};
@@ -4474,13 +4572,13 @@ var init_tools = __esm({
4474
4572
  init_epics();
4475
4573
  init_commands4();
4476
4574
  init_json();
4477
- key = z2.string().describe("Ticket key, for example HD-12.");
4575
+ key = z3.string().describe("Ticket key, for example HD-12.");
4478
4576
  TOOLS = [
4479
4577
  {
4480
4578
  name: "status",
4481
4579
  access: "read",
4482
4580
  description: "The whole board at a glance: ticket counts by state, what is running, what is stuck and why, open decisions, provider availability, and epic progress. Start here.",
4483
- schema: z2.object({}),
4581
+ schema: z3.object({}),
4484
4582
  async run(ctx) {
4485
4583
  const [board, pausedAll] = await Promise.all([loadBoard(ctx), loadGlobalPause(ctx)]);
4486
4584
  return {
@@ -4500,10 +4598,10 @@ var init_tools = __esm({
4500
4598
  name: "list_tickets",
4501
4599
  access: "read",
4502
4600
  description: "Every ticket, with its status, agent, area, dependencies, and stuck reason.",
4503
- schema: z2.object({
4504
- status: z2.string().optional().describe("Filter to one status."),
4505
- area: z2.string().optional(),
4506
- epic_id: z2.string().optional()
4601
+ schema: z3.object({
4602
+ status: z3.string().optional().describe("Filter to one status."),
4603
+ area: z3.string().optional(),
4604
+ epic_id: z3.string().optional()
4507
4605
  }),
4508
4606
  async run(ctx, args) {
4509
4607
  const board = await loadBoard(ctx);
@@ -4526,7 +4624,7 @@ var init_tools = __esm({
4526
4624
  name: "get_ticket",
4527
4625
  access: "read",
4528
4626
  description: "One ticket in full: body, acceptance criteria, runs, messages, and history.",
4529
- schema: z2.object({ key }),
4627
+ schema: z3.object({ key }),
4530
4628
  async run(ctx, args) {
4531
4629
  const ticket = await ticketOf(ctx, String(args.key));
4532
4630
  const [runs, messages, statusEvents] = await Promise.all([
@@ -4541,7 +4639,7 @@ var init_tools = __esm({
4541
4639
  name: "list_agents",
4542
4640
  access: "read",
4543
4641
  description: "The agent registry: role, provider, model, effort, routing notes, prompt addendum, caps, and whether each is enabled.",
4544
- schema: z2.object({}),
4642
+ schema: z3.object({}),
4545
4643
  async run(ctx) {
4546
4644
  const board = await loadBoard(ctx);
4547
4645
  return board.agents;
@@ -4551,7 +4649,7 @@ var init_tools = __esm({
4551
4649
  name: "list_epics",
4552
4650
  access: "read",
4553
4651
  description: "Epics with their specs and progress.",
4554
- schema: z2.object({ include_spec: z2.boolean().optional() }),
4652
+ schema: z3.object({ include_spec: z3.boolean().optional() }),
4555
4653
  async run(ctx, args) {
4556
4654
  const board = await loadBoard(ctx);
4557
4655
  return board.epics.map((epic) => {
@@ -4571,7 +4669,7 @@ var init_tools = __esm({
4571
4669
  name: "list_hosts",
4572
4670
  access: "read",
4573
4671
  description: "Registered runner hosts: online state, providers installed, queue depth, auth status.",
4574
- schema: z2.object({}),
4672
+ schema: z3.object({}),
4575
4673
  async run(ctx) {
4576
4674
  const board = await loadBoard(ctx);
4577
4675
  return board.hosts;
@@ -4581,7 +4679,7 @@ var init_tools = __esm({
4581
4679
  name: "get_cost",
4582
4680
  access: "read",
4583
4681
  description: "Spend by day and provider, so a plan can be weighed against the budget.",
4584
- schema: z2.object({}),
4682
+ schema: z3.object({}),
4585
4683
  async run(ctx) {
4586
4684
  const daily = await loadDailyCosts(ctx);
4587
4685
  const total = sumCost(daily.map((row) => ({ cost_usd: row.cost_usd })));
@@ -4592,7 +4690,7 @@ var init_tools = __esm({
4592
4690
  name: "get_conventions",
4593
4691
  access: "read",
4594
4692
  description: "The workspace conventions document injected into every builder and reviewer prompt.",
4595
- schema: z2.object({}),
4693
+ schema: z3.object({}),
4596
4694
  async run(ctx) {
4597
4695
  return { conventions_md: ctx.workspace.conventions_md ?? "" };
4598
4696
  }
@@ -4601,7 +4699,7 @@ var init_tools = __esm({
4601
4699
  name: "get_run_transcript",
4602
4700
  access: "read",
4603
4701
  description: "A readable transcript of one run: what the agent said, the tools it called, errors.",
4604
- schema: z2.object({ run_id: z2.string().describe("Run id or a unique prefix.") }),
4702
+ schema: z3.object({ run_id: z3.string().describe("Run id or a unique prefix.") }),
4605
4703
  async run(ctx, args) {
4606
4704
  const runs = await loadRuns(ctx, { limit: 200 });
4607
4705
  const run5 = runs.find((row) => row.id.startsWith(String(args.run_id)));
@@ -4614,15 +4712,15 @@ var init_tools = __esm({
4614
4712
  name: "create_ticket",
4615
4713
  access: "write",
4616
4714
  description: "File a ticket. One ticket is one PR, one area, under about 400 changed lines. Always give acceptance criteria and a test expectation. The orchestrator picks it up and dispatches it.",
4617
- schema: z2.object({
4618
- title: z2.string(),
4619
- body_md: z2.string().describe("What to build and why."),
4620
- acceptance_md: z2.string().describe("How anyone can tell it is done."),
4621
- area: z2.string().optional().describe("Surface tag, e.g. web/auth. Two live tickets must not share one."),
4622
- agent: z2.string().optional().describe("Builder display name, to pin it. Leave empty to let the orchestrator choose."),
4623
- priority: z2.number().optional(),
4624
- epic_id: z2.string().optional(),
4625
- blocked_by: z2.array(z2.string()).optional().describe("Ticket keys this must wait for.")
4715
+ schema: z3.object({
4716
+ title: z3.string(),
4717
+ body_md: z3.string().describe("What to build and why."),
4718
+ acceptance_md: z3.string().describe("How anyone can tell it is done."),
4719
+ area: z3.string().optional().describe("Surface tag, e.g. web/auth. Two live tickets must not share one."),
4720
+ agent: z3.string().optional().describe("Builder display name, to pin it. Leave empty to let the orchestrator choose."),
4721
+ priority: z3.number().optional(),
4722
+ epic_id: z3.string().optional(),
4723
+ blocked_by: z3.array(z3.string()).optional().describe("Ticket keys this must wait for.")
4626
4724
  }),
4627
4725
  async run(ctx, args) {
4628
4726
  const ticket = await createTicket(ctx, {
@@ -4642,17 +4740,17 @@ var init_tools = __esm({
4642
4740
  name: "update_ticket",
4643
4741
  access: "write",
4644
4742
  description: "Change a ticket's title, body, acceptance, area, agent, priority, status, or epic.",
4645
- schema: z2.object({
4743
+ schema: z3.object({
4646
4744
  key,
4647
- title: z2.string().optional(),
4648
- body_md: z2.string().optional(),
4649
- acceptance_md: z2.string().optional(),
4650
- area: z2.string().nullable().optional(),
4651
- agent: z2.string().nullable().optional(),
4652
- priority: z2.number().optional(),
4653
- status: z2.string().optional(),
4654
- epic_id: z2.string().nullable().optional(),
4655
- request_id: z2.string().optional()
4745
+ title: z3.string().optional(),
4746
+ body_md: z3.string().optional(),
4747
+ acceptance_md: z3.string().optional(),
4748
+ area: z3.string().nullable().optional(),
4749
+ agent: z3.string().nullable().optional(),
4750
+ priority: z3.number().optional(),
4751
+ status: z3.string().optional(),
4752
+ epic_id: z3.string().nullable().optional(),
4753
+ request_id: z3.string().optional()
4656
4754
  }),
4657
4755
  async run(ctx, args) {
4658
4756
  const ticket = await ticketOf(ctx, String(args.key));
@@ -4674,11 +4772,11 @@ var init_tools = __esm({
4674
4772
  name: "set_blocked_by",
4675
4773
  access: "write",
4676
4774
  description: "Set, add, or remove the tickets one ticket waits for. Cycles are refused.",
4677
- schema: z2.object({
4775
+ schema: z3.object({
4678
4776
  key,
4679
- blocked_by: z2.array(z2.string()).describe("Ticket keys."),
4680
- mode: z2.enum(["set", "add", "remove"]).optional(),
4681
- request_id: z2.string().optional()
4777
+ blocked_by: z3.array(z3.string()).describe("Ticket keys."),
4778
+ mode: z3.enum(["set", "add", "remove"]).optional(),
4779
+ request_id: z3.string().optional()
4682
4780
  }),
4683
4781
  async run(ctx, args) {
4684
4782
  const ticket = await ticketOf(ctx, String(args.key));
@@ -4697,7 +4795,7 @@ var init_tools = __esm({
4697
4795
  name: "create_epic",
4698
4796
  access: "write",
4699
4797
  description: "Create an epic from a spec. This is the handoff: the orchestrator is triggered by epic.created and decomposes the spec into tickets itself. Prefer one epic with a good spec over filing many tickets by hand.",
4700
- schema: z2.object({ title: z2.string(), spec_md: z2.string() }),
4798
+ schema: z3.object({ title: z3.string(), spec_md: z3.string() }),
4701
4799
  async run(ctx, args) {
4702
4800
  const epic = await createEpic(ctx, String(args.title), String(args.spec_md ?? ""));
4703
4801
  return { id: epic.id, title: epic.title, status: epic.status };
@@ -4707,11 +4805,11 @@ var init_tools = __esm({
4707
4805
  name: "update_epic",
4708
4806
  access: "write",
4709
4807
  description: "Change an epic's title, spec, or status.",
4710
- schema: z2.object({
4711
- epic_id: z2.string(),
4712
- title: z2.string().optional(),
4713
- spec_md: z2.string().optional(),
4714
- status: z2.string().optional()
4808
+ schema: z3.object({
4809
+ epic_id: z3.string(),
4810
+ title: z3.string().optional(),
4811
+ spec_md: z3.string().optional(),
4812
+ status: z3.string().optional()
4715
4813
  }),
4716
4814
  async run(ctx, args) {
4717
4815
  const epic = await epicOf(ctx, String(args.epic_id));
@@ -4727,7 +4825,7 @@ var init_tools = __esm({
4727
4825
  name: "replan_epic",
4728
4826
  access: "write",
4729
4827
  description: "Ask the orchestrator to re-decompose an epic's unmerged remainder after its spec changed.",
4730
- schema: z2.object({ epic_id: z2.string() }),
4828
+ schema: z3.object({ epic_id: z3.string() }),
4731
4829
  async run(ctx, args) {
4732
4830
  const epic = await epicOf(ctx, String(args.epic_id));
4733
4831
  await replanEpic(ctx, epic.id);
@@ -4738,11 +4836,11 @@ var init_tools = __esm({
4738
4836
  name: "post_message",
4739
4837
  access: "write",
4740
4838
  description: "Post on the board's message bus. Use to_role 'orchestrator' to hand over work or ask about progress, 'builder' with a ticket to steer a live session, 'human' to leave a note for the owner.",
4741
- schema: z2.object({
4742
- to_role: z2.enum(["orchestrator", "reviewer", "builder", "operator", "human", "all"]),
4743
- body_md: z2.string(),
4744
- ticket_key: z2.string().optional(),
4745
- delivery: z2.enum(["queue", "interrupt"]).optional()
4839
+ schema: z3.object({
4840
+ to_role: z3.enum(["orchestrator", "reviewer", "builder", "operator", "human", "all"]),
4841
+ body_md: z3.string(),
4842
+ ticket_key: z3.string().optional(),
4843
+ delivery: z3.enum(["queue", "interrupt"]).optional()
4746
4844
  }),
4747
4845
  async run(ctx, args) {
4748
4846
  const message = await postMessage(ctx, {
@@ -4759,10 +4857,10 @@ var init_tools = __esm({
4759
4857
  name: "answer_decision",
4760
4858
  access: "write",
4761
4859
  description: "Answer an open decision, or dismiss it.",
4762
- schema: z2.object({
4763
- decision_id: z2.string(),
4764
- answer_md: z2.string().optional(),
4765
- dismiss: z2.boolean().optional()
4860
+ schema: z3.object({
4861
+ decision_id: z3.string(),
4862
+ answer_md: z3.string().optional(),
4863
+ dismiss: z3.boolean().optional()
4766
4864
  }),
4767
4865
  async run(ctx, args) {
4768
4866
  const result = await answerDecision(ctx, String(args.decision_id), String(args.answer_md ?? ""), {
@@ -4775,7 +4873,7 @@ var init_tools = __esm({
4775
4873
  name: "take_over_ticket",
4776
4874
  access: "write",
4777
4875
  description: "Mark a ticket human-owned so the board stops dispatching it.",
4778
- schema: z2.object({ key, request_id: z2.string().optional() }),
4876
+ schema: z3.object({ key, request_id: z3.string().optional() }),
4779
4877
  async run(ctx, args) {
4780
4878
  const operationId = String(args.request_id ?? randomUUID3());
4781
4879
  const updated = await takeOver(ctx, await ticketOf(ctx, String(args.key)), operationId);
@@ -4786,7 +4884,7 @@ var init_tools = __esm({
4786
4884
  name: "hand_back_ticket",
4787
4885
  access: "write",
4788
4886
  description: "Give a human-owned ticket back to the board.",
4789
- schema: z2.object({ key, request_id: z2.string().optional() }),
4887
+ schema: z3.object({ key, request_id: z3.string().optional() }),
4790
4888
  async run(ctx, args) {
4791
4889
  const operationId = String(args.request_id ?? randomUUID3());
4792
4890
  const updated = await handBack(ctx, await ticketOf(ctx, String(args.key)), operationId);
@@ -4797,7 +4895,7 @@ var init_tools = __esm({
4797
4895
  name: "kill_run",
4798
4896
  access: "write",
4799
4897
  description: "Stop a live run.",
4800
- schema: z2.object({ run_id: z2.string() }),
4898
+ schema: z3.object({ run_id: z3.string() }),
4801
4899
  async run(ctx, args) {
4802
4900
  const runs = await loadRuns(ctx, { limit: 200 });
4803
4901
  const run5 = runs.find((row) => row.id.startsWith(String(args.run_id)));
@@ -4810,16 +4908,16 @@ var init_tools = __esm({
4810
4908
  name: "update_agent",
4811
4909
  access: "write",
4812
4910
  description: "Change an agent: model, effort, provider, routing notes the orchestrator reads when assigning, the prompt addendum prepended to its every run, whether it is enabled, and its budget caps. The orchestrator cannot do this; you can.",
4813
- schema: z2.object({
4814
- name: z2.string().describe("Agent display name."),
4815
- model: z2.string().optional(),
4816
- effort: z2.enum(["low", "medium", "high"]).optional(),
4817
- provider: z2.enum(["claude", "codex", "gemini", "grok"]).optional(),
4818
- routing_notes: z2.string().optional().describe("Free text, e.g. 'UI and isolated components only'."),
4819
- prompt_addendum: z2.string().optional().describe("Prepended to this agent's prompt on every run."),
4820
- enabled: z2.boolean().optional(),
4821
- runs_per_hour: z2.number().nullable().optional(),
4822
- daily_spend_usd: z2.number().nullable().optional()
4911
+ schema: z3.object({
4912
+ name: z3.string().describe("Agent display name."),
4913
+ model: z3.string().optional(),
4914
+ effort: z3.enum(["low", "medium", "high"]).optional(),
4915
+ provider: z3.enum(["claude", "codex", "gemini", "grok"]).optional(),
4916
+ routing_notes: z3.string().optional().describe("Free text, e.g. 'UI and isolated components only'."),
4917
+ prompt_addendum: z3.string().optional().describe("Prepended to this agent's prompt on every run."),
4918
+ enabled: z3.boolean().optional(),
4919
+ runs_per_hour: z3.number().nullable().optional(),
4920
+ daily_spend_usd: z3.number().nullable().optional()
4823
4921
  }),
4824
4922
  async run(ctx, args) {
4825
4923
  const { data, error } = await ctx.db.from("agents").select("*").eq("workspace_id", ctx.workspace.id);
@@ -4853,7 +4951,7 @@ var init_tools = __esm({
4853
4951
  name: "set_conventions",
4854
4952
  access: "write",
4855
4953
  description: "Replace the workspace conventions document. It is injected into every builder and reviewer prompt, so this is where stack rules, commands, style, and do-not-touch areas belong.",
4856
- schema: z2.object({ conventions_md: z2.string() }),
4954
+ schema: z3.object({ conventions_md: z3.string() }),
4857
4955
  async run(ctx, args) {
4858
4956
  const { error } = await ctx.db.from("workspaces").update({ conventions_md: String(args.conventions_md) }).eq("id", ctx.workspace.id);
4859
4957
  if (error) fail(error.message);
@@ -4865,10 +4963,10 @@ var init_tools = __esm({
4865
4963
  name: "set_paused",
4866
4964
  access: "write",
4867
4965
  description: "Pause or resume this workspace, everything, or one provider.",
4868
- schema: z2.object({
4869
- paused: z2.boolean(),
4870
- scope: z2.enum(["workspace", "all", "provider"]).optional(),
4871
- provider: z2.string().optional()
4966
+ schema: z3.object({
4967
+ paused: z3.boolean(),
4968
+ scope: z3.enum(["workspace", "all", "provider"]).optional(),
4969
+ provider: z3.string().optional()
4872
4970
  }),
4873
4971
  async run(ctx, args) {
4874
4972
  const scope = args.scope ?? "workspace";
@@ -4883,7 +4981,7 @@ var init_tools = __esm({
4883
4981
  name: "nudge_orchestrator",
4884
4982
  access: "write",
4885
4983
  description: "Wake the orchestrator with a note. Use after filing work so it grooms and dispatches without waiting for the heartbeat.",
4886
- schema: z2.object({ body_md: z2.string() }),
4984
+ schema: z3.object({ body_md: z3.string() }),
4887
4985
  async run(ctx, args) {
4888
4986
  await nudgeOrchestrator(ctx, String(args.body_md));
4889
4987
  return { ok: true };
@@ -5179,13 +5277,20 @@ function stormSchedule(opts = {}) {
5179
5277
  elapsed2 += frame.ms;
5180
5278
  };
5181
5279
  while (elapsed2 < durationMs) {
5182
- for (let strike = 0, strikes = between(2, 5); strike < strikes; strike += 1) {
5183
- push({ level: 3, ms: between(35, 70) });
5280
+ const strikes = between(2, 5);
5281
+ for (let strike = 0; strike < strikes; strike += 1) {
5282
+ const last2 = strike === strikes - 1;
5283
+ push({ level: 3, ms: last2 ? between(110, 170) : between(35, 70) });
5284
+ if (last2) {
5285
+ push({ level: 2, ms: between(120, 180) });
5286
+ push({ level: 1, ms: between(180, 260) });
5287
+ break;
5288
+ }
5184
5289
  push({ level: 1, ms: between(25, 55) });
5185
5290
  if (random() < 0.35) push({ level: 2, ms: between(30, 60) });
5186
5291
  push({ level: 0, ms: between(45, 160) });
5187
5292
  }
5188
- push({ level: 0, ms: between(2200, 13e3), quiet: true });
5293
+ push({ level: 0, ms: between(600, 3e3), quiet: true });
5189
5294
  }
5190
5295
  const last = frames[frames.length - 1];
5191
5296
  if (last && elapsed2 > durationMs) last.ms = Math.max(1, last.ms - (elapsed2 - durationMs));
@@ -5450,15 +5555,16 @@ var init_Help = __esm({
5450
5555
  init_height();
5451
5556
  HELP_FOOTER = "Anything not starting with / goes to whoever you are talking to. Ctrl-C leaves.";
5452
5557
  COMMANDS = [
5558
+ { name: "/home", help: "the board and the agents side by side, with what they are doing" },
5453
5559
  { name: "/architect", help: "talk to your own model, which can do anything in the platform" },
5454
5560
  { name: "/orchestrator", help: "talk to the agent that gets work in flight finished" },
5455
5561
  { name: "/browse", help: "put a cursor on the board; up and down move it, enter opens" },
5456
5562
  { name: "/workspace", args: "[slug]", help: "switch workspace, or list the ones you can reach" },
5457
5563
  { name: "/workspace new", args: "name owner/repo", help: "create a workspace, seeded with agents" },
5458
5564
  { name: "/settings", help: "change workspace and agent settings, with the arrow keys" },
5459
- { name: "/agent new", args: "name provider model", help: "add a builder" },
5565
+ { name: "/agent new", help: "add a builder, filling in a form" },
5460
5566
  { name: "/board", help: "the kanban board" },
5461
- { name: "/agents", help: "every agent and what it is doing" },
5567
+ { name: "/agents", help: "every agent in full, and the live run stream" },
5462
5568
  { name: "/feed", help: "what just happened" },
5463
5569
  { name: "/inbox", help: "decisions and messages waiting on you" },
5464
5570
  { name: "/decide", args: "2 | text", help: "answer the decision on screen, or --skip it" },
@@ -5519,6 +5625,11 @@ function planLayout(input) {
5519
5625
  const cockpit = Math.max(2, room - stream);
5520
5626
  return finish({ cockpit, stream, panels: input.home ? cockpit + stream + gap : room });
5521
5627
  }
5628
+ function splitPanels(budget) {
5629
+ const bottom = Math.min(14, Math.round((budget - PANEL_GAP) * 0.4));
5630
+ const top = budget - PANEL_GAP - bottom;
5631
+ return bottom >= 3 && top >= 3 ? { top, bottom } : { top: Math.max(0, budget), bottom: 0 };
5632
+ }
5522
5633
  var init_layout = __esm({
5523
5634
  "src/tui/layout.ts"() {
5524
5635
  "use strict";
@@ -5789,6 +5900,7 @@ function SettingsPanel({
5789
5900
  entries,
5790
5901
  width,
5791
5902
  rows,
5903
+ title = "Settings",
5792
5904
  cursor,
5793
5905
  editing
5794
5906
  }) {
@@ -5797,7 +5909,7 @@ function SettingsPanel({
5797
5909
  const { start, end } = scrollWindow(entries.length, inner, focus);
5798
5910
  const label = Math.max(10, Math.min(18, Math.round(width / 4)));
5799
5911
  return /* @__PURE__ */ jsx6(BoundedPanel, { width, rows, children: [
5800
- /* @__PURE__ */ jsx6(Heading, { text: "Settings", note: hidden(entries.length, start, end) }, "h"),
5912
+ /* @__PURE__ */ jsx6(Heading, { text: title, note: hidden(entries.length, start, end) }, "h"),
5801
5913
  ...entries.slice(start, end).map((row) => {
5802
5914
  if (row.kind === "heading") {
5803
5915
  return /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
@@ -5807,14 +5919,27 @@ function SettingsPanel({
5807
5919
  }
5808
5920
  const selected = row.key === cursor;
5809
5921
  const typing = editing?.key === row.key;
5922
+ if (row.kind === "action") {
5923
+ return /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
5924
+ /* @__PURE__ */ jsx6(Text5, { color: UI.accent, children: selected ? " \u203A " : " " }),
5925
+ /* @__PURE__ */ jsx6(Text5, { color: UI.cream, bold: true, inverse: selected, children: pad(truncate(row.label, label - 1), label) }),
5926
+ /* @__PURE__ */ jsx6(Text5, { color: UI.dim, children: truncate(row.value, Math.max(8, width - label - 6)) })
5927
+ ] }, row.key);
5928
+ }
5810
5929
  return /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
5811
5930
  /* @__PURE__ */ jsx6(Text5, { color: UI.accent, children: selected ? " \u203A " : " " }),
5812
5931
  /* @__PURE__ */ jsx6(Text5, { color: UI.dim, children: pad(truncate(row.label, label - 1), label) }),
5813
- /* @__PURE__ */ jsx6(Text5, { color: typing ? UI.cream : UI.text, inverse: selected && !typing, children: truncate(typing ? `${editing.draft}\u258F` : row.value || "-", Math.max(8, width - label - 6)) })
5932
+ /* @__PURE__ */ jsx6(Text5, { color: typing ? UI.cream : UI.text, inverse: selected && !typing, children: truncate(typing ? `${editing.draft}\u258F` : row.value || "-", valueWidth(width, label, row, selected)) }),
5933
+ selected && row.hint && !typing ? /* @__PURE__ */ jsx6(Text5, { color: UI.dim, children: ` ${row.hint}` }) : null
5814
5934
  ] }, row.key);
5815
5935
  })
5816
5936
  ] });
5817
5937
  }
5938
+ function valueWidth(width, label, row, selected) {
5939
+ const room = Math.max(8, width - label - 6);
5940
+ if (!selected || !row.hint) return room;
5941
+ return Math.max(8, room - row.hint.length - 2);
5942
+ }
5818
5943
  function hidden(count, start, end) {
5819
5944
  const above = start;
5820
5945
  const below = count - end;
@@ -5832,6 +5957,17 @@ var init_Settings = __esm({
5832
5957
  });
5833
5958
 
5834
5959
  // src/tui/settings-model.ts
5960
+ function modelExamples(provider) {
5961
+ const seen = /* @__PURE__ */ new Set();
5962
+ for (const agent of DEFAULT_AGENTS) {
5963
+ if (agent.provider === provider) seen.add(agent.model);
5964
+ }
5965
+ return [...seen];
5966
+ }
5967
+ function modelHint(provider) {
5968
+ const examples = modelExamples(provider);
5969
+ return examples.length ? `like ${examples.join(", ")}` : "the provider's model id";
5970
+ }
5835
5971
  function capLabel(value) {
5836
5972
  return value == null ? UNLIMITED : String(value);
5837
5973
  }
@@ -5885,7 +6021,8 @@ function settingsRows(workspace, agents) {
5885
6021
  kind: "text",
5886
6022
  label: "model",
5887
6023
  value: agent.model,
5888
- agent: agent.display_name
6024
+ agent: agent.display_name,
6025
+ hint: modelHint(agent.provider)
5889
6026
  });
5890
6027
  rows.push({
5891
6028
  key: `a:${agent.id}:effort`,
@@ -5916,7 +6053,16 @@ function settingsRows(workspace, agents) {
5916
6053
  kind: "text",
5917
6054
  label: "routing notes",
5918
6055
  value: agent.routing_notes,
5919
- agent: agent.display_name
6056
+ agent: agent.display_name,
6057
+ hint: "what the orchestrator reads when it picks who does the work"
6058
+ });
6059
+ rows.push({
6060
+ key: `a:${agent.id}:prompt_addendum`,
6061
+ kind: "text",
6062
+ label: "prompt",
6063
+ value: agent.prompt_addendum,
6064
+ agent: agent.display_name,
6065
+ hint: "prepended to every run this agent does"
5920
6066
  });
5921
6067
  }
5922
6068
  return rows;
@@ -5978,6 +6124,8 @@ function editFor(row, raw) {
5978
6124
  }
5979
6125
  case "routing_notes":
5980
6126
  return ok2({ target: "agent", name, input: { notes: raw } });
6127
+ case "prompt_addendum":
6128
+ return ok2({ target: "agent", name, input: { prompt: raw } });
5981
6129
  default:
5982
6130
  return no(`Nothing to change on ${row.label}.`);
5983
6131
  }
@@ -5985,6 +6133,95 @@ function editFor(row, raw) {
5985
6133
  function unlimitedAsNone(value) {
5986
6134
  return value.toLowerCase() === UNLIMITED || value === "" ? "none" : value;
5987
6135
  }
6136
+ function emptyDraft() {
6137
+ return {
6138
+ name: "",
6139
+ provider: providers[0],
6140
+ model: "",
6141
+ effort: DEFAULT_EFFORT,
6142
+ runsPerHour: UNLIMITED,
6143
+ dailySpend: UNLIMITED,
6144
+ notes: "",
6145
+ prompt: ""
6146
+ };
6147
+ }
6148
+ function draftRows(draft) {
6149
+ return [
6150
+ { key: "h:new", kind: "heading", label: "New builder", value: "enter fills a row, esc cancels" },
6151
+ { key: "n:name", kind: "text", label: "name", value: draft.name, hint: "what you will call it" },
6152
+ {
6153
+ key: "n:provider",
6154
+ kind: "choice",
6155
+ label: "provider",
6156
+ value: draft.provider,
6157
+ choices: providers
6158
+ },
6159
+ { key: "n:model", kind: "text", label: "model", value: draft.model, hint: modelHint(draft.provider) },
6160
+ { key: "n:effort", kind: "choice", label: "effort", value: draft.effort, choices: efforts },
6161
+ {
6162
+ key: "n:runsPerHour",
6163
+ kind: "number",
6164
+ label: "runs per hour",
6165
+ value: draft.runsPerHour,
6166
+ hint: `a number, or ${UNLIMITED}`
6167
+ },
6168
+ {
6169
+ key: "n:dailySpend",
6170
+ kind: "number",
6171
+ label: "usd per day",
6172
+ value: draft.dailySpend,
6173
+ hint: `a number, or ${UNLIMITED}`
6174
+ },
6175
+ {
6176
+ key: "n:notes",
6177
+ kind: "text",
6178
+ label: "routing notes",
6179
+ value: draft.notes,
6180
+ hint: "what the orchestrator reads when it picks who does the work"
6181
+ },
6182
+ {
6183
+ key: "n:prompt",
6184
+ kind: "text",
6185
+ label: "prompt",
6186
+ value: draft.prompt,
6187
+ hint: "prepended to every run this agent does"
6188
+ },
6189
+ { key: "n:save", kind: "action", label: "save", value: saveLabel(draft) }
6190
+ ];
6191
+ }
6192
+ function saveLabel(draft) {
6193
+ const ready = draftReady(draft);
6194
+ return ready.ok ? `create ${draft.name}` : ready.error;
6195
+ }
6196
+ function applyToDraft(draft, key2, value) {
6197
+ const field = key2.split(":")[1];
6198
+ if (!(field in draft)) return draft;
6199
+ const blankIsUnlimited = field === "runsPerHour" || field === "dailySpend";
6200
+ const next = blankIsUnlimited && value.trim() === "" ? UNLIMITED : value;
6201
+ return { ...draft, [field]: field === "prompt" || field === "notes" ? value : next.trim() };
6202
+ }
6203
+ function draftReady(draft) {
6204
+ if (!draft.name.trim()) return no("A name, first.");
6205
+ if (!draft.model.trim()) return no(`A model, ${modelHint(draft.provider)}.`);
6206
+ const provider = parseField(oneOf("provider", providers), draft.provider);
6207
+ if (!provider.ok) return no(provider.error);
6208
+ const effort = parseField(oneOf("effort", efforts), draft.effort);
6209
+ if (!effort.ok) return no(effort.error);
6210
+ const runs = parseField(optionalPositiveInteger("runs per hour"), unlimitedAsNone(draft.runsPerHour));
6211
+ if (!runs.ok) return no(runs.error);
6212
+ const spend = parseField(optionalPositiveNumber("usd per day"), unlimitedAsNone(draft.dailySpend));
6213
+ if (!spend.ok) return no(spend.error);
6214
+ return ok2({
6215
+ name: draft.name.trim(),
6216
+ provider: provider.value,
6217
+ model: draft.model.trim(),
6218
+ effort: effort.value,
6219
+ notes: draft.notes,
6220
+ prompt: draft.prompt,
6221
+ runsPerHour: runs.value,
6222
+ dailySpend: spend.value
6223
+ });
6224
+ }
5988
6225
  var UNLIMITED;
5989
6226
  var init_settings_model = __esm({
5990
6227
  "src/tui/settings-model.ts"() {
@@ -6573,6 +6810,9 @@ function parseLine(raw) {
6573
6810
  return { kind: "mode", mode: "orchestrator" };
6574
6811
  case "browse":
6575
6812
  return { kind: "mode", mode: "browse" };
6813
+ case "home":
6814
+ case "cockpit":
6815
+ return { kind: "view", view: "home" };
6576
6816
  case "board":
6577
6817
  case "agents":
6578
6818
  case "feed":
@@ -6584,10 +6824,14 @@ function parseLine(raw) {
6584
6824
  case "agent": {
6585
6825
  const [verb, name, provider, model] = argument.split(/\s+/).filter(Boolean);
6586
6826
  if (verb?.toLowerCase() !== "new") {
6587
- return { kind: "unknown", command: "agent, try /agent new <name> <provider> <model>" };
6827
+ return { kind: "unknown", command: "agent, try /agent new" };
6588
6828
  }
6589
- if (!name || !provider || !model) {
6590
- return { kind: "unknown", command: "agent new needs a name, a provider and a model" };
6829
+ if (!name) return { kind: "agent-form" };
6830
+ if (!provider || !model) {
6831
+ return {
6832
+ kind: "unknown",
6833
+ command: "agent new needs a provider and a model too, or /agent new on its own for the form"
6834
+ };
6591
6835
  }
6592
6836
  return { kind: "agent-new", name, provider, model };
6593
6837
  }
@@ -6710,9 +6954,10 @@ function App({
6710
6954
  );
6711
6955
  const liveRunIds = [...labels.current.keys()].sort().join(",");
6712
6956
  const order = useMemo2(() => board ? boardTicketIds(board) : [], [board]);
6957
+ const [agentDraft, setAgentDraft] = useState3(emptyDraft);
6713
6958
  const settings = useMemo2(
6714
- () => board ? settingsRows(current, board.agents) : [],
6715
- [current, board]
6959
+ () => view === "new-agent" ? draftRows(agentDraft) : board ? settingsRows(current, board.agents) : [],
6960
+ [view, agentDraft, current, board]
6716
6961
  );
6717
6962
  const settingsOrder = useMemo2(() => editableKeys(settings), [settings]);
6718
6963
  const [field, setField] = useState3(null);
@@ -6721,7 +6966,7 @@ function App({
6721
6966
  const editingRef = useRef3(null);
6722
6967
  editingRef.current = editing;
6723
6968
  const browsing = mode === "browse" && draft.length === 0 && cursor !== null;
6724
- const configuring = view === "settings" && !editing;
6969
+ const configuring = (view === "settings" || view === "new-agent") && !editing;
6725
6970
  const moveField = useCallback(
6726
6971
  (delta) => {
6727
6972
  const next = nextCursor(settingsOrder, fieldRef.current, delta);
@@ -6768,6 +7013,36 @@ function App({
6768
7013
  },
6769
7014
  [settings, workspaceCtx]
6770
7015
  );
7016
+ const saveDraft = useCallback(async () => {
7017
+ const ready2 = draftReady(agentDraft);
7018
+ if (!ready2.ok) {
7019
+ setNotice(ready2.error);
7020
+ return;
7021
+ }
7022
+ setBusy(true);
7023
+ try {
7024
+ const made = await createBuilder(workspaceCtx, ready2.value.name, {
7025
+ provider: ready2.value.provider,
7026
+ model: ready2.value.model,
7027
+ effort: ready2.value.effort,
7028
+ notes: ready2.value.notes,
7029
+ prompt: ready2.value.prompt,
7030
+ runsPerHour: ready2.value.runsPerHour,
7031
+ dailySpend: ready2.value.dailySpend
7032
+ });
7033
+ if (!made.ok) {
7034
+ setNotice(made.error);
7035
+ return;
7036
+ }
7037
+ setNotice(null);
7038
+ setAgentDraft(emptyDraft());
7039
+ setView("settings");
7040
+ say("system", `Added ${made.value.display_name}.`);
7041
+ await refreshRef.current?.();
7042
+ } finally {
7043
+ setBusy(false);
7044
+ }
7045
+ }, [agentDraft, workspaceCtx, say]);
6771
7046
  const refresh = useCallback(async () => {
6772
7047
  const loadToken = workspaceLoads.current.start(workspaceCtx.workspace.id);
6773
7048
  try {
@@ -6955,6 +7230,32 @@ function App({
6955
7230
  const run5 = useCallback(
6956
7231
  async (raw) => {
6957
7232
  const text = raw.trim();
7233
+ if (view === "new-agent") {
7234
+ const open = editingRef.current;
7235
+ if (open) {
7236
+ setEditing(null);
7237
+ setDraft("");
7238
+ setAgentDraft((prior) => applyToDraft(prior, open.key, raw));
7239
+ setNotice(null);
7240
+ return;
7241
+ }
7242
+ if (!text) {
7243
+ const key2 = fieldRef.current;
7244
+ const row = settings.find((entry) => entry.key === key2);
7245
+ if (!row || !key2) return;
7246
+ if (row.kind === "action") return saveDraft();
7247
+ const flipped = nextValue(row);
7248
+ if (flipped !== null) {
7249
+ setAgentDraft((prior) => applyToDraft(prior, key2, flipped));
7250
+ return;
7251
+ }
7252
+ const seed = seedFor(row);
7253
+ setEditing({ key: key2, draft: seed });
7254
+ setDraft(seed);
7255
+ setNotice(row.hint ?? null);
7256
+ return;
7257
+ }
7258
+ }
6958
7259
  if (view === "settings") {
6959
7260
  const open = editingRef.current;
6960
7261
  if (open) {
@@ -7036,6 +7337,16 @@ function App({
7036
7337
  setEditing(null);
7037
7338
  }
7038
7339
  return;
7340
+ case "agent-form": {
7341
+ setAgentDraft(emptyDraft());
7342
+ setView("new-agent");
7343
+ const first = editableKeys(draftRows(emptyDraft()))[0] ?? null;
7344
+ fieldRef.current = first;
7345
+ setField(first);
7346
+ setEditing(null);
7347
+ setNotice(null);
7348
+ return;
7349
+ }
7039
7350
  case "agent-new": {
7040
7351
  setBusy(true);
7041
7352
  try {
@@ -7137,6 +7448,7 @@ function App({
7137
7448
  view,
7138
7449
  settings,
7139
7450
  applyEdit,
7451
+ saveDraft,
7140
7452
  ctx,
7141
7453
  workspaceCtx,
7142
7454
  settingsOrder,
@@ -7177,6 +7489,7 @@ function App({
7177
7489
  const streamRows = plan.stream;
7178
7490
  const fits = plan.fits;
7179
7491
  const running = board?.runs.filter((run6) => run6.status === "running").length ?? 0;
7492
+ const agentsView2 = splitPanels(budget);
7180
7493
  const selected = cursor ? board?.tickets.find((row) => row.id === cursor) : null;
7181
7494
  return /* @__PURE__ */ jsxs10(Fragment3, { children: [
7182
7495
  /* @__PURE__ */ jsx11(Static, { items: scrollback, children: (item) => {
@@ -7198,14 +7511,26 @@ function App({
7198
7511
  ) : null,
7199
7512
  /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", width, children: [
7200
7513
  view === "board" && board && budget > 0 ? /* @__PURE__ */ jsx11(BoardPanel, { board, width, rows: budget, cursor }) : null,
7201
- view === "agents" && board && budget > 0 ? /* @__PURE__ */ jsx11(AgentsPanel, { board, width, rows: budget }) : null,
7514
+ view === "agents" && board && budget > 0 ? (
7515
+ // The registry and what it is doing, together. "What is that agent
7516
+ // up to" is the question this view is opened to answer, and the run
7517
+ // stream is the only thing that answers it.
7518
+ /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", children: [
7519
+ /* @__PURE__ */ jsx11(AgentsPanel, { board, width, rows: agentsView2.top }),
7520
+ agentsView2.bottom > 0 ? /* @__PURE__ */ jsxs10(Fragment3, { children: [
7521
+ /* @__PURE__ */ jsx11(Box8, { height: 1 }),
7522
+ /* @__PURE__ */ jsx11(StreamPanel, { lines: stream, width, rows: agentsView2.bottom, live: running > 0 })
7523
+ ] }) : null
7524
+ ] })
7525
+ ) : null,
7202
7526
  view === "feed" && budget > 0 ? /* @__PURE__ */ jsx11(FeedPanel, { entries: feed, width, rows: budget }) : null,
7203
- view === "settings" && budget > 0 ? /* @__PURE__ */ jsx11(
7527
+ (view === "settings" || view === "new-agent") && budget > 0 ? /* @__PURE__ */ jsx11(
7204
7528
  SettingsPanel,
7205
7529
  {
7206
7530
  entries: settings,
7207
7531
  width,
7208
7532
  rows: budget,
7533
+ title: view === "new-agent" ? "New agent" : "Settings",
7209
7534
  cursor: field,
7210
7535
  editing
7211
7536
  }
@@ -7271,11 +7596,7 @@ function App({
7271
7596
  setNotice(null);
7272
7597
  return;
7273
7598
  }
7274
- if (view === "settings") {
7275
- setView("home");
7276
- return;
7277
- }
7278
- if (view === "ticket") {
7599
+ if (view !== "home") {
7279
7600
  setView("home");
7280
7601
  setTicketKey(null);
7281
7602
  return;
@@ -8233,8 +8554,8 @@ function registerWriteCommands(program) {
8233
8554
  let body = opts.body ?? "";
8234
8555
  let acceptance = opts.acceptance ?? "";
8235
8556
  if (opts.template) {
8236
- const templates = parseTicketTemplates(ctx.workspace.ticket_templates);
8237
- const applied = applyTicketTemplate(templates, opts.template);
8557
+ const templates2 = parseTicketTemplates(ctx.workspace.ticket_templates);
8558
+ const applied = applyTicketTemplate(templates2, opts.template);
8238
8559
  if (!applied) fail(`No template ${opts.template}. Run \`hd workspace templates\`.`);
8239
8560
  body = body || applied.body_md;
8240
8561
  acceptance = acceptance || applied.acceptance_md;
@@ -8423,10 +8744,10 @@ import { createInterface as createInterface3 } from "readline/promises";
8423
8744
  // src/architect/mcp.ts
8424
8745
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8425
8746
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8426
- import { z as z3 } from "zod";
8747
+ import { z as z4 } from "zod";
8427
8748
 
8428
8749
  // src/version.ts
8429
- var VERSION = "0.2.1";
8750
+ var VERSION = "0.3.0";
8430
8751
 
8431
8752
  // src/architect/mcp.ts
8432
8753
  init_tools();
@@ -8451,7 +8772,7 @@ async function serveMcp(opts) {
8451
8772
  return work;
8452
8773
  };
8453
8774
  for (const tool of toolsFor(opts.readOnly ? "read" : "all")) {
8454
- const shape = tool.schema instanceof z3.ZodObject ? tool.schema.shape : {};
8775
+ const shape = tool.schema instanceof z4.ZodObject ? tool.schema.shape : {};
8455
8776
  server.registerTool(
8456
8777
  tool.name,
8457
8778
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@higherdev/cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "HigherDEV in your terminal. Full control of the board, live.",
6
6
  "bin": {