@higherdev/cli 0.1.4 → 0.2.1

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 +2412 -682
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -342,18 +342,6 @@ var init_blocked = __esm({
342
342
  });
343
343
 
344
344
  // ../../packages/db/src/budgets.ts
345
- function parseOptionalCap(raw, opts = {}) {
346
- const text = String(raw ?? "").trim();
347
- if (text === "") return { ok: true, value: null };
348
- const n = Number(text);
349
- if (!Number.isFinite(n) || n <= 0) {
350
- return { ok: false, error: "Budget caps must be a positive number, or empty for unlimited." };
351
- }
352
- if (opts.integer && !Number.isInteger(n)) {
353
- return { ok: false, error: "Runs per hour must be a whole number, or empty for unlimited." };
354
- }
355
- return { ok: true, value: n };
356
- }
357
345
  var init_budgets = __esm({
358
346
  "../../packages/db/src/budgets.ts"() {
359
347
  "use strict";
@@ -522,6 +510,88 @@ var init_commands = __esm({
522
510
  }
523
511
  });
524
512
 
513
+ // ../../packages/db/src/status.ts
514
+ function statusLabel(status) {
515
+ return status.replaceAll("_", " ");
516
+ }
517
+ function statusTone(status) {
518
+ switch (status) {
519
+ case "queued":
520
+ case "running":
521
+ case "in_review":
522
+ return "blue";
523
+ case "approved":
524
+ case "merged":
525
+ case "ready":
526
+ return "success";
527
+ case "blocked":
528
+ case "needs_decision":
529
+ case "changes_requested":
530
+ return "warning";
531
+ case "failed":
532
+ case "cancelled":
533
+ return "danger";
534
+ default:
535
+ return "muted";
536
+ }
537
+ }
538
+ function resolveTicketStatus(requested, blockerStatuses2) {
539
+ if (requested === "cancelled") return "cancelled";
540
+ const open = blockerStatuses2.filter((status) => status !== "merged" && status !== "cancelled");
541
+ if (open.length > 0) return "blocked";
542
+ if (requested === "blocked") return "backlog";
543
+ return requested;
544
+ }
545
+ function initialHumanStatus(opts) {
546
+ const groomed = Boolean(opts.acceptanceMd.trim() && opts.area && opts.assigned);
547
+ return resolveTicketStatus(groomed ? "ready" : "backlog", opts.blockerStatuses);
548
+ }
549
+ function statusAfterAnswer(opts) {
550
+ return opts.assigned && Boolean(opts.acceptanceMd.trim()) ? "queued" : "ready";
551
+ }
552
+ function handBackStatus(opts) {
553
+ if (opts.acceptanceMd.trim() && opts.assigned) {
554
+ return resolveTicketStatus(statusAfterAnswer(opts), opts.blockerStatuses);
555
+ }
556
+ return initialHumanStatus(opts);
557
+ }
558
+ function cancelledSuffix(cancelled) {
559
+ return cancelled > 0 ? ` +${cancelled} cancelled` : "";
560
+ }
561
+ function epicProgressCaption(merged, total, cancelled = 0) {
562
+ if (total === 0 && cancelled === 0) return "No tickets";
563
+ return `${merged}/${total} merged${cancelledSuffix(cancelled)}`;
564
+ }
565
+ var BOARD_COLUMNS;
566
+ var init_status = __esm({
567
+ "../../packages/db/src/status.ts"() {
568
+ "use strict";
569
+ init_enums();
570
+ BOARD_COLUMNS = [
571
+ "backlog",
572
+ "ready",
573
+ "blocked",
574
+ "queued",
575
+ "running",
576
+ "failed",
577
+ "needs_decision",
578
+ "in_review",
579
+ "changes_requested",
580
+ "approved",
581
+ "merged",
582
+ "cancelled"
583
+ ];
584
+ }
585
+ });
586
+
587
+ // ../../packages/db/src/cockpit.ts
588
+ var init_cockpit = __esm({
589
+ "../../packages/db/src/cockpit.ts"() {
590
+ "use strict";
591
+ init_status();
592
+ }
593
+ });
594
+
525
595
  // ../../packages/db/src/defaults.ts
526
596
  function slugify(name) {
527
597
  const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -549,6 +619,14 @@ var init_defaults = __esm({
549
619
  }
550
620
  });
551
621
 
622
+ // ../../packages/db/src/effective-config.ts
623
+ var init_effective_config = __esm({
624
+ "../../packages/db/src/effective-config.ts"() {
625
+ "use strict";
626
+ init_enums();
627
+ }
628
+ });
629
+
552
630
  // ../../packages/db/src/epic-progress.ts
553
631
  function round4(value) {
554
632
  return Math.round(value * 1e4) / 1e4;
@@ -722,6 +800,10 @@ var init_schemas = __esm({
722
800
  timeout_minutes: z.number().int().positive(),
723
801
  max_attempts: z.number().int().positive(),
724
802
  pr_size_target: z.number().int().positive(),
803
+ orchestrator_max_cycles: z.number().int().min(1).max(12).optional(),
804
+ orchestrator_max_total_turns: z.number().int().min(1).max(1e3).optional(),
805
+ orchestrator_sweep_interval_minutes: z.number().int().min(1).max(60).optional(),
806
+ orchestrator_stall_minutes: z.number().int().min(5).max(1440).optional(),
725
807
  webhook_url: z.union([z.url(), z.literal("")]).optional(),
726
808
  smoke_url: z.union([z.url(), z.literal("")]).optional(),
727
809
  quiet_hours: z.object({
@@ -927,80 +1009,6 @@ var init_schemas = __esm({
927
1009
  }
928
1010
  });
929
1011
 
930
- // ../../packages/db/src/status.ts
931
- function statusLabel(status) {
932
- return status.replaceAll("_", " ");
933
- }
934
- function statusTone(status) {
935
- switch (status) {
936
- case "queued":
937
- case "running":
938
- case "in_review":
939
- return "blue";
940
- case "approved":
941
- case "merged":
942
- case "ready":
943
- return "success";
944
- case "blocked":
945
- case "needs_decision":
946
- case "changes_requested":
947
- return "warning";
948
- case "failed":
949
- case "cancelled":
950
- return "danger";
951
- default:
952
- return "muted";
953
- }
954
- }
955
- function resolveTicketStatus(requested, blockerStatuses2) {
956
- if (requested === "cancelled") return "cancelled";
957
- const open = blockerStatuses2.filter((status) => status !== "merged" && status !== "cancelled");
958
- if (open.length > 0) return "blocked";
959
- if (requested === "blocked") return "backlog";
960
- return requested;
961
- }
962
- function initialHumanStatus(opts) {
963
- const groomed = Boolean(opts.acceptanceMd.trim() && opts.area && opts.assigned);
964
- return resolveTicketStatus(groomed ? "ready" : "backlog", opts.blockerStatuses);
965
- }
966
- function statusAfterAnswer(opts) {
967
- return opts.assigned && Boolean(opts.acceptanceMd.trim()) ? "queued" : "ready";
968
- }
969
- function handBackStatus(opts) {
970
- if (opts.acceptanceMd.trim() && opts.assigned) {
971
- return resolveTicketStatus(statusAfterAnswer(opts), opts.blockerStatuses);
972
- }
973
- return initialHumanStatus(opts);
974
- }
975
- function cancelledSuffix(cancelled) {
976
- return cancelled > 0 ? ` +${cancelled} cancelled` : "";
977
- }
978
- function epicProgressCaption(merged, total, cancelled = 0) {
979
- if (total === 0 && cancelled === 0) return "No tickets";
980
- return `${merged}/${total} merged${cancelledSuffix(cancelled)}`;
981
- }
982
- var BOARD_COLUMNS;
983
- var init_status = __esm({
984
- "../../packages/db/src/status.ts"() {
985
- "use strict";
986
- init_enums();
987
- BOARD_COLUMNS = [
988
- "backlog",
989
- "ready",
990
- "blocked",
991
- "queued",
992
- "running",
993
- "failed",
994
- "needs_decision",
995
- "in_review",
996
- "changes_requested",
997
- "approved",
998
- "merged",
999
- "cancelled"
1000
- ];
1001
- }
1002
- });
1003
-
1004
1012
  // ../../packages/db/src/stuck.ts
1005
1013
  function stuckReason(opts) {
1006
1014
  const { ticket } = opts;
@@ -1173,125 +1181,6 @@ var init_ticket_writes = __esm({
1173
1181
  }
1174
1182
  });
1175
1183
 
1176
- // ../../packages/db/src/timeline.ts
1177
- function formatDuration(ms) {
1178
- const totalSeconds = Math.max(0, Math.round(ms / 1e3));
1179
- const hours = Math.floor(totalSeconds / 3600);
1180
- const minutes = Math.floor(totalSeconds % 3600 / 60);
1181
- const seconds = totalSeconds % 60;
1182
- if (hours > 0) return minutes ? `${hours}h ${minutes}m` : `${hours}h`;
1183
- if (minutes > 0) return seconds ? `${minutes}m ${seconds}s` : `${minutes}m`;
1184
- return `${seconds}s`;
1185
- }
1186
- function msBetween(from, to) {
1187
- const start = Date.parse(from);
1188
- const end = Date.parse(to);
1189
- if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null;
1190
- return end - start;
1191
- }
1192
- function clip(text, max = 160) {
1193
- const one2 = text.trim().replace(/\s+/g, " ");
1194
- if (one2.length <= max) return one2;
1195
- return `${one2.slice(0, max - 3)}...`;
1196
- }
1197
- function clock(iso) {
1198
- if (!iso) return "";
1199
- const date = new Date(iso);
1200
- if (Number.isNaN(date.getTime())) return "";
1201
- return date.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
1202
- }
1203
- function runSpan(run5) {
1204
- const start = clock(run5.started_at ?? run5.created_at);
1205
- const end = clock(run5.ended_at);
1206
- if (start && end) return `${start}-${end}`;
1207
- if (start && !run5.ended_at) return `${start}-`;
1208
- return start;
1209
- }
1210
- function runDurationMs(run5, nowIso) {
1211
- const start = run5.started_at ?? run5.created_at;
1212
- if (run5.ended_at) return msBetween(start, run5.ended_at);
1213
- if (run5.status === "running" || run5.status === "queued") return msBetween(start, nowIso);
1214
- return null;
1215
- }
1216
- function buildTimeline(input) {
1217
- const nowIso = typeof input.now === "string" ? input.now : new Date(input.now ?? Date.now()).toISOString();
1218
- const items = [];
1219
- const statuses = [...input.statusEvents].sort((a, b) => a.at.localeCompare(b.at) || a.id.localeCompare(b.id));
1220
- for (let i = 0; i < statuses.length; i++) {
1221
- const event = statuses[i];
1222
- const next = statuses[i + 1];
1223
- const from = event.from_status ? statusLabel(event.from_status) : "new";
1224
- const to = statusLabel(event.to_status);
1225
- items.push({
1226
- id: `status:${event.id}`,
1227
- at: event.at,
1228
- kind: "status",
1229
- title: `${from} to ${to}`,
1230
- durationMs: msBetween(event.at, next?.at ?? nowIso),
1231
- endedAt: next?.at ?? null
1232
- });
1233
- }
1234
- for (const run5 of input.runs) {
1235
- const at = run5.started_at ?? run5.created_at;
1236
- const span = runSpan(run5);
1237
- const kind = run5.kind === "review" ? "verdict" : "run";
1238
- items.push({
1239
- id: `run:${run5.id}`,
1240
- at,
1241
- kind,
1242
- title: [
1243
- run5.kind,
1244
- run5.provider,
1245
- run5.status,
1246
- run5.reviewed_sha ? `@ ${run5.reviewed_sha.slice(0, 12)}` : "",
1247
- span
1248
- ].filter(Boolean).join(" "),
1249
- detail: run5.summary ? clip(run5.summary) : void 0,
1250
- durationMs: runDurationMs(run5, nowIso),
1251
- endedAt: run5.ended_at
1252
- });
1253
- }
1254
- for (const message of input.messages) {
1255
- const kind = message.from_role === "reviewer" ? "verdict" : "message";
1256
- items.push({
1257
- id: `message:${message.id}`,
1258
- at: message.created_at,
1259
- kind,
1260
- title: `${message.from_name} to ${message.to_role}`,
1261
- detail: clip(message.body_md),
1262
- durationMs: null
1263
- });
1264
- }
1265
- for (const decision of input.decisions) {
1266
- const answered = Boolean(decision.answered_at);
1267
- items.push({
1268
- id: `decision:${decision.id}`,
1269
- at: decision.created_at,
1270
- kind: "decision",
1271
- title: answered ? `answered (${decision.asked_by_role})` : `asked by ${decision.asked_by_role}`,
1272
- detail: clip(answered ? decision.answer_md || decision.question_md : decision.question_md),
1273
- durationMs: msBetween(decision.created_at, decision.answered_at ?? nowIso),
1274
- endedAt: decision.answered_at
1275
- });
1276
- }
1277
- items.sort((a, b) => a.at.localeCompare(b.at) || KIND_RANK[a.kind] - KIND_RANK[b.kind] || a.id.localeCompare(b.id));
1278
- return items;
1279
- }
1280
- var KIND_RANK;
1281
- var init_timeline = __esm({
1282
- "../../packages/db/src/timeline.ts"() {
1283
- "use strict";
1284
- init_status();
1285
- KIND_RANK = {
1286
- status: 0,
1287
- run: 1,
1288
- verdict: 2,
1289
- message: 3,
1290
- decision: 4
1291
- };
1292
- }
1293
- });
1294
-
1295
1184
  // ../../packages/db/src/transcript.ts
1296
1185
  function asRecord(value) {
1297
1186
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
@@ -1355,12 +1244,12 @@ function codexItemLine(event, item) {
1355
1244
  return {
1356
1245
  id: event.id,
1357
1246
  kind: failed ? "error" : "tool",
1358
- title: `${name}${clip2(args, 120)}${status && status !== "completed" ? ` (${status})` : ""}`,
1359
- body: failed ? clip2(str(asRecord(item.error).message ?? item.error), 400) || void 0 : void 0
1247
+ title: `${name}${clip(args, 120)}${status && status !== "completed" ? ` (${status})` : ""}`,
1248
+ body: failed ? clip(str(asRecord(item.error).message ?? item.error), 400) || void 0 : void 0
1360
1249
  };
1361
1250
  }
1362
1251
  if (kind === "command_execution") {
1363
- const command = clip2(str(item.command ?? item.cmd), 120);
1252
+ const command = clip(str(item.command ?? item.cmd), 120);
1364
1253
  const code = item.exit_code ?? item.exitCode;
1365
1254
  return {
1366
1255
  id: event.id,
@@ -1375,7 +1264,7 @@ function codexItemLine(event, item) {
1375
1264
  if (kind === "reasoning" || kind === "todo_list") return SKIP;
1376
1265
  return { id: event.id, kind: "status", title: kind.replaceAll("_", " ") };
1377
1266
  }
1378
- function clip2(text, max) {
1267
+ function clip(text, max) {
1379
1268
  const flat = String(text ?? "").trim();
1380
1269
  return flat.length <= max ? flat : `${flat.slice(0, Math.max(0, max - 3))}...`;
1381
1270
  }
@@ -1448,6 +1337,185 @@ var init_transcript = __esm({
1448
1337
  }
1449
1338
  });
1450
1339
 
1340
+ // ../../packages/db/src/timeline.ts
1341
+ function formatDuration(ms) {
1342
+ const totalSeconds = Math.max(0, Math.round(ms / 1e3));
1343
+ const hours = Math.floor(totalSeconds / 3600);
1344
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
1345
+ const seconds = totalSeconds % 60;
1346
+ if (hours > 0) return minutes ? `${hours}h ${minutes}m` : `${hours}h`;
1347
+ if (minutes > 0) return seconds ? `${minutes}m ${seconds}s` : `${minutes}m`;
1348
+ return `${seconds}s`;
1349
+ }
1350
+ function msBetween(from, to) {
1351
+ const start = Date.parse(from);
1352
+ const end = Date.parse(to);
1353
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null;
1354
+ return end - start;
1355
+ }
1356
+ function clip2(text, max = 160) {
1357
+ const one2 = text.trim().replace(/\s+/g, " ");
1358
+ if (one2.length <= max) return one2;
1359
+ return `${one2.slice(0, max - 3)}...`;
1360
+ }
1361
+ function clock(iso) {
1362
+ if (!iso) return "";
1363
+ const date = new Date(iso);
1364
+ if (Number.isNaN(date.getTime())) return "";
1365
+ return date.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
1366
+ }
1367
+ function runSpan(run5) {
1368
+ const start = clock(run5.started_at ?? run5.created_at);
1369
+ const end = clock(run5.ended_at);
1370
+ if (start && end) return `${start}-${end}`;
1371
+ if (start && !run5.ended_at) return `${start}-`;
1372
+ return start;
1373
+ }
1374
+ function runDurationMs(run5, nowIso) {
1375
+ const start = run5.started_at ?? run5.created_at;
1376
+ if (run5.ended_at) return msBetween(start, run5.ended_at);
1377
+ if (run5.status === "running" || run5.status === "queued") return msBetween(start, nowIso);
1378
+ return null;
1379
+ }
1380
+ function record(value) {
1381
+ return value != null && typeof value === "object" && !Array.isArray(value) ? value : null;
1382
+ }
1383
+ function auditBelongsToTicket(event, ticket) {
1384
+ const payload = record(event.payload);
1385
+ if (!payload) return false;
1386
+ const ticketId = typeof payload.ticket_id === "string" ? payload.ticket_id : null;
1387
+ const ticketKey = typeof payload.ticket_key === "string" ? payload.ticket_key : null;
1388
+ if (ticketId != null && ticketId !== ticket.id) return false;
1389
+ if (ticketKey != null && ticketKey !== ticket.key) return false;
1390
+ return ticketId === ticket.id || ticketKey === ticket.key;
1391
+ }
1392
+ function buildTimeline(input) {
1393
+ const nowIso = typeof input.now === "string" ? input.now : new Date(input.now ?? Date.now()).toISOString();
1394
+ const items = [];
1395
+ const statuses = [...input.statusEvents].sort((a, b) => a.at.localeCompare(b.at) || a.id.localeCompare(b.id));
1396
+ for (let i = 0; i < statuses.length; i++) {
1397
+ const event = statuses[i];
1398
+ const next = statuses[i + 1];
1399
+ const from = event.from_status ? statusLabel(event.from_status) : "new";
1400
+ const to = statusLabel(event.to_status);
1401
+ items.push({
1402
+ id: `status:${event.id}`,
1403
+ at: event.at,
1404
+ kind: "status",
1405
+ title: `${from} to ${to}`,
1406
+ durationMs: msBetween(event.at, next?.at ?? nowIso),
1407
+ endedAt: next?.at ?? null
1408
+ });
1409
+ }
1410
+ for (const run5 of input.runs) {
1411
+ const at = run5.started_at ?? run5.created_at;
1412
+ const span = runSpan(run5);
1413
+ const kind = run5.kind === "review" ? "verdict" : "run";
1414
+ items.push({
1415
+ id: `run:${run5.id}`,
1416
+ at,
1417
+ kind,
1418
+ title: [
1419
+ run5.kind,
1420
+ run5.provider,
1421
+ run5.status,
1422
+ run5.reviewed_sha ? `@ ${run5.reviewed_sha.slice(0, 12)}` : "",
1423
+ run5.cost_usd != null && Number.isFinite(Number(run5.cost_usd)) ? formatCostUsd(Number(run5.cost_usd)) : "",
1424
+ span
1425
+ ].filter(Boolean).join(" "),
1426
+ detail: run5.summary ? clip2(run5.summary) : void 0,
1427
+ durationMs: runDurationMs(run5, nowIso),
1428
+ endedAt: run5.ended_at
1429
+ });
1430
+ }
1431
+ const runsById = new Map(input.runs.map((run5) => [run5.id, run5]));
1432
+ const readableEvents = [...input.runEvents ?? []].filter((event) => runsById.has(event.run_id)).sort((left, right) => left.at.localeCompare(right.at) || left.seq - right.seq || left.id.localeCompare(right.id));
1433
+ for (const event of readableEvents) {
1434
+ const run5 = runsById.get(event.run_id);
1435
+ for (const line of transcriptLines([event])) {
1436
+ items.push({
1437
+ id: `transcript:${event.id}:${line.id}`,
1438
+ at: event.at,
1439
+ kind: "transcript",
1440
+ title: `${run5?.kind ?? "run"} ${line.kind}: ${clip2(line.title)}`,
1441
+ detail: line.body ? clip2(line.body) : void 0,
1442
+ durationMs: null
1443
+ });
1444
+ }
1445
+ }
1446
+ for (const message of input.messages) {
1447
+ const kind = message.from_role === "reviewer" ? "verdict" : "message";
1448
+ items.push({
1449
+ id: `message:${message.id}`,
1450
+ at: message.created_at,
1451
+ kind,
1452
+ title: `${message.from_name} to ${message.to_role}`,
1453
+ detail: clip2(message.body_md),
1454
+ durationMs: null
1455
+ });
1456
+ }
1457
+ for (const decision of input.decisions) {
1458
+ const answered = Boolean(decision.answered_at);
1459
+ items.push({
1460
+ id: `decision:${decision.id}`,
1461
+ at: decision.created_at,
1462
+ kind: "decision",
1463
+ title: answered ? `answered (${decision.asked_by_role})` : `asked by ${decision.asked_by_role}`,
1464
+ detail: clip2(answered ? decision.answer_md || decision.question_md : decision.question_md),
1465
+ durationMs: msBetween(decision.created_at, decision.answered_at ?? nowIso),
1466
+ endedAt: decision.answered_at
1467
+ });
1468
+ }
1469
+ if (input.ticket) {
1470
+ for (const event of input.auditEvents ?? []) {
1471
+ if (!auditBelongsToTicket(event, input.ticket)) continue;
1472
+ const payload = record(event.payload);
1473
+ const summary = typeof payload?.summary === "string" ? payload.summary : null;
1474
+ items.push({
1475
+ id: `audit:${event.id}`,
1476
+ at: event.at,
1477
+ kind: "audit",
1478
+ title: clip2(summary ?? `${event.actor} ${event.action.replaceAll("_", " ")}`),
1479
+ durationMs: null
1480
+ });
1481
+ }
1482
+ }
1483
+ if (input.pr && Number.isFinite(Date.parse(input.pr.updatedAt))) {
1484
+ const review = input.pr.review ? input.pr.review.replaceAll("_", " ") : "no review";
1485
+ items.push({
1486
+ id: `delivery:pr:${input.pr.number}`,
1487
+ at: input.pr.updatedAt,
1488
+ kind: "delivery",
1489
+ title: `current PR #${input.pr.number} ${input.pr.headSha ? `@ ${input.pr.headSha.slice(0, 12)} ` : ""}checks ${input.pr.checks}; ${review}`,
1490
+ detail: `${input.pr.changedFiles} files +${input.pr.additions}/-${input.pr.deletions}; mergeable ${input.pr.mergeable == null ? "unknown" : input.pr.mergeable ? "yes" : "no"}`,
1491
+ durationMs: null,
1492
+ href: input.pr.url,
1493
+ previewHref: input.pr.previewUrl ?? void 0
1494
+ });
1495
+ }
1496
+ items.sort((a, b) => a.at.localeCompare(b.at) || KIND_RANK[a.kind] - KIND_RANK[b.kind] || a.id.localeCompare(b.id));
1497
+ return items;
1498
+ }
1499
+ var KIND_RANK;
1500
+ var init_timeline = __esm({
1501
+ "../../packages/db/src/timeline.ts"() {
1502
+ "use strict";
1503
+ init_status();
1504
+ init_transcript();
1505
+ init_cost();
1506
+ KIND_RANK = {
1507
+ status: 0,
1508
+ run: 1,
1509
+ verdict: 2,
1510
+ transcript: 3,
1511
+ message: 4,
1512
+ decision: 5,
1513
+ audit: 6,
1514
+ delivery: 7
1515
+ };
1516
+ }
1517
+ });
1518
+
1451
1519
  // ../../packages/db/src/waves.ts
1452
1520
  function ticketWaves(tickets) {
1453
1521
  const ids = new Set(tickets.map((ticket) => ticket.id));
@@ -1500,7 +1568,9 @@ var init_src = __esm({
1500
1568
  init_budgets();
1501
1569
  init_cost();
1502
1570
  init_commands();
1571
+ init_cockpit();
1503
1572
  init_defaults();
1573
+ init_effective_config();
1504
1574
  init_enums();
1505
1575
  init_epic_progress();
1506
1576
  init_filter();
@@ -2157,6 +2227,12 @@ async function loadRunEvents(ctx, runId, afterSeq = -1) {
2157
2227
  if (error) fail(error.message);
2158
2228
  return data ?? [];
2159
2229
  }
2230
+ async function loadLiveEvents(ctx, runIds, limit = 80) {
2231
+ if (runIds.length === 0) return [];
2232
+ const { data, error } = await ctx.db.from("run_events").select("*").in("run_id", runIds).order("at", { ascending: false }).limit(limit);
2233
+ if (error) fail(error.message);
2234
+ return (data ?? []).slice().reverse();
2235
+ }
2160
2236
  async function loadMessages(ctx, opts = {}) {
2161
2237
  let query = ctx.db.from("messages").select("*").eq("workspace_id", ctx.workspace.id).order("created_at", { ascending: true }).limit(opts.limit ?? 200);
2162
2238
  if (opts.ticketId) query = query.eq("ticket_id", opts.ticketId);
@@ -2517,9 +2593,9 @@ async function runAgent(opts) {
2517
2593
  opts.onEvent?.({ kind: "tool", name: call.name, args });
2518
2594
  const tool = byName.get(call.name);
2519
2595
  let output;
2520
- let ok2 = true;
2596
+ let ok3 = true;
2521
2597
  if (!tool) {
2522
- ok2 = false;
2598
+ ok3 = false;
2523
2599
  output = `Error: no tool named ${call.name}.`;
2524
2600
  } else {
2525
2601
  try {
@@ -2527,11 +2603,11 @@ async function runAgent(opts) {
2527
2603
  output = typeof result === "string" ? result : JSON.stringify(result ?? null);
2528
2604
  if (!output) output = "(no result)";
2529
2605
  } catch (error) {
2530
- ok2 = false;
2606
+ ok3 = false;
2531
2607
  output = `Error: ${error instanceof Error ? error.message : String(error)}`;
2532
2608
  }
2533
2609
  }
2534
- opts.onEvent?.({ kind: "tool_result", name: call.name, ok: ok2, detail: output.slice(0, 200) });
2610
+ opts.onEvent?.({ kind: "tool_result", name: call.name, ok: ok3, detail: output.slice(0, 200) });
2535
2611
  input.push({
2536
2612
  type: "function_call",
2537
2613
  name: call.name,
@@ -2734,6 +2810,95 @@ var init_group = __esm({
2734
2810
  }
2735
2811
  });
2736
2812
 
2813
+ // src/argv-parsers.ts
2814
+ import { InvalidArgumentError } from "commander";
2815
+ function oneOf(label, values) {
2816
+ return (value) => {
2817
+ const normalized = value.trim().toLowerCase();
2818
+ if (!values.includes(normalized)) {
2819
+ throw new InvalidArgumentError(`${label} must be one of: ${values.join(", ")}.`);
2820
+ }
2821
+ return normalized;
2822
+ };
2823
+ }
2824
+ function integer(label) {
2825
+ return (value) => {
2826
+ if (!/^-?(?:0|[1-9]\d*)$/.test(value)) {
2827
+ throw new InvalidArgumentError(`${label} must be an integer.`);
2828
+ }
2829
+ const parsed = Number(value);
2830
+ if (!Number.isSafeInteger(parsed)) throw new InvalidArgumentError(`${label} must be an integer.`);
2831
+ return parsed;
2832
+ };
2833
+ }
2834
+ function positiveNumber(label) {
2835
+ return (value) => {
2836
+ if (!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) {
2837
+ throw new InvalidArgumentError(`${label} must be greater than zero.`);
2838
+ }
2839
+ const parsed = Number(value);
2840
+ if (!Number.isFinite(parsed) || parsed <= 0) {
2841
+ throw new InvalidArgumentError(`${label} must be greater than zero.`);
2842
+ }
2843
+ return parsed;
2844
+ };
2845
+ }
2846
+ function positiveInteger(label) {
2847
+ return (value) => {
2848
+ if (!/^(?:0|[1-9]\d*)$/.test(value)) {
2849
+ throw new InvalidArgumentError(`${label} must be a positive integer.`);
2850
+ }
2851
+ const parsed = Number(value);
2852
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
2853
+ throw new InvalidArgumentError(`${label} must be a positive integer.`);
2854
+ }
2855
+ return parsed;
2856
+ };
2857
+ }
2858
+ function rangedInteger(label, minimum, maximum) {
2859
+ const parse = positiveInteger(label);
2860
+ return (value) => {
2861
+ const parsed = parse(value);
2862
+ if (parsed < minimum || parsed > maximum) {
2863
+ throw new InvalidArgumentError(`${label} must be between ${minimum} and ${maximum}.`);
2864
+ }
2865
+ return parsed;
2866
+ };
2867
+ }
2868
+ function optionalPositiveInteger(label) {
2869
+ const parse = positiveInteger(label);
2870
+ return (value) => value === "none" ? null : parse(value);
2871
+ }
2872
+ function optionalPositiveNumber(label) {
2873
+ const parse = positiveNumber(label);
2874
+ return (value) => value === "none" ? null : parse(value);
2875
+ }
2876
+ function providerCaps(values) {
2877
+ return (value, previous = []) => {
2878
+ const match = value.match(/^([^=]+)=((?:0|[1-9]\d*))$/);
2879
+ if (!match || !values.includes(match[1])) {
2880
+ throw new InvalidArgumentError(`cap must be provider=whole-number for: ${values.join(", ")}.`);
2881
+ }
2882
+ const cap = Number(match[2]);
2883
+ if (!Number.isSafeInteger(cap)) {
2884
+ throw new InvalidArgumentError(`cap must be provider=whole-number for: ${values.join(", ")}.`);
2885
+ }
2886
+ return [...previous, { provider: match[1], cap }];
2887
+ };
2888
+ }
2889
+ function uuid(value) {
2890
+ const normalized = value.trim().toLowerCase();
2891
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(normalized)) {
2892
+ throw new InvalidArgumentError("request id must be a UUID.");
2893
+ }
2894
+ return normalized;
2895
+ }
2896
+ var init_argv_parsers = __esm({
2897
+ "src/argv-parsers.ts"() {
2898
+ "use strict";
2899
+ }
2900
+ });
2901
+
2737
2902
  // src/out/format.ts
2738
2903
  function truncate(text, max) {
2739
2904
  const flat = String(text ?? "").replace(/\s+/g, " ").trim();
@@ -3200,7 +3365,7 @@ function registerReadCommands(program) {
3200
3365
  ${statusView(board, pausedAll)}`
3201
3366
  );
3202
3367
  });
3203
- program.command("ls").description("list tickets").option("-s, --status <status>", "filter by status").option("-a, --agent <name>", "filter by agent display name").option("-p, --provider <provider>", "filter by provider").option("--area <area>", "filter by area").option("-q, --query <text>", "search titles, bodies, and messages").option("--stuck", "only tickets with a reason they are not moving").action(async function() {
3368
+ program.command("ls").description("list tickets").option("-s, --status <status>", "filter by status", oneOf("status", ticketStatuses)).option("-a, --agent <name>", "filter by agent display name").option("-p, --provider <provider>", "filter by provider", oneOf("provider", providers)).option("--area <area>", "filter by area").option("-q, --query <text>", "search titles, bodies, and messages").option("--stuck", "only tickets with a reason they are not moving").action(async function() {
3204
3369
  const opts = this.opts();
3205
3370
  const ctx = await requireWorkspace(slugOf(this));
3206
3371
  const board = await loadBoard(ctx);
@@ -3249,7 +3414,7 @@ ${statusView(board, pausedAll)}`
3249
3414
  const board = await loadBoard(ctx);
3250
3415
  emit(board.hosts, () => hostsView(board.hosts));
3251
3416
  });
3252
- program.command("cost").description("spend by day, provider, ticket, and epic").option("-d, --days <n>", "window in days", "14").action(async function() {
3417
+ program.command("cost").description("spend by day, provider, ticket, and epic").option("-d, --days <n>", "window in days", positiveInteger("days"), 14).action(async function() {
3253
3418
  const ctx = await requireWorkspace(slugOf(this));
3254
3419
  const days = Number(this.opts().days) || 14;
3255
3420
  const [daily, tickets, epics] = await Promise.all([
@@ -3303,6 +3468,7 @@ var init_commands2 = __esm({
3303
3468
  init_theme();
3304
3469
  init_queries();
3305
3470
  init_group();
3471
+ init_argv_parsers();
3306
3472
  init_views();
3307
3473
  }
3308
3474
  });
@@ -3343,31 +3509,46 @@ async function postMessage(ctx, input) {
3343
3509
  await ctx.audit(ctx.workspace.id, "message", { to, ticket_key: input.ticketKey });
3344
3510
  return data;
3345
3511
  }
3346
- async function answerDecision(ctx, decisionId, answer, opts = {}) {
3512
+ function decisionOptions(decision) {
3513
+ return Array.isArray(decision.options) ? decision.options.map(String) : [];
3514
+ }
3515
+ function resolveAnswer(decision, answer) {
3516
+ const options = decisionOptions(decision);
3517
+ const index = Number(answer.trim());
3518
+ return options.length && Number.isInteger(index) && index >= 1 && index <= options.length ? options[index - 1] : answer;
3519
+ }
3520
+ async function resolveDecision(ctx, decisionId, answer, opts = {}) {
3347
3521
  const { data: rows, error: loadError } = await ctx.db.from("decisions").select("*").eq("workspace_id", ctx.workspace.id).is("answered_at", null).is("dismissed_at", null);
3348
- if (loadError) fail(loadError.message);
3522
+ if (loadError) return { ok: false, error: loadError.message };
3349
3523
  const matches = (rows ?? []).filter((row) => row.id.startsWith(decisionId));
3350
- if (matches.length === 0) fail(`No open decision matching ${decisionId}.`);
3351
- if (matches.length > 1) fail(`${decisionId} matches ${matches.length} decisions. Use more of the id.`);
3524
+ if (matches.length === 0) return { ok: false, error: `No open decision matching ${decisionId}.` };
3525
+ if (matches.length > 1) {
3526
+ return { ok: false, error: `${decisionId} matches ${matches.length} decisions. Use more of the id.` };
3527
+ }
3352
3528
  const decision = matches[0];
3353
3529
  if (opts.dismiss) {
3354
3530
  const { error: error2 } = await ctx.db.from("decisions").update({ dismissed_at: (/* @__PURE__ */ new Date()).toISOString(), answered_by: ctx.email }).eq("id", decision.id);
3355
- if (error2) fail(error2.message);
3531
+ if (error2) return { ok: false, error: error2.message };
3356
3532
  await ctx.audit(ctx.workspace.id, "dismiss", { decision_id: decision.id });
3357
- return { decision, dismissed: true };
3533
+ return { ok: true, decision, answer: "", dismissed: true };
3534
+ }
3535
+ const resolved = resolveAnswer(decision, answer);
3536
+ if (!resolved.trim()) {
3537
+ return { ok: false, error: "An answer needs text, or the number of an option." };
3358
3538
  }
3359
- const options = Array.isArray(decision.options) ? decision.options : [];
3360
- const index = Number(answer);
3361
- const resolved = options.length && Number.isInteger(index) && index >= 1 && index <= options.length ? options[index - 1] : answer;
3362
- if (!resolved.trim()) fail("An answer needs text, or the number of an option.");
3363
3539
  const { error } = await ctx.db.from("decisions").update({
3364
3540
  answer_md: resolved,
3365
3541
  answered_by: ctx.email,
3366
3542
  answered_at: (/* @__PURE__ */ new Date()).toISOString()
3367
3543
  }).eq("id", decision.id);
3368
- if (error) fail(error.message);
3544
+ if (error) return { ok: false, error: error.message };
3369
3545
  await ctx.audit(ctx.workspace.id, "answer", { decision_id: decision.id });
3370
- return { decision, answer: resolved, dismissed: false };
3546
+ return { ok: true, decision, answer: resolved, dismissed: false };
3547
+ }
3548
+ async function answerDecision(ctx, decisionId, answer, opts = {}) {
3549
+ const result = await resolveDecision(ctx, decisionId, answer, opts);
3550
+ if (!result.ok) fail(result.error);
3551
+ return { decision: result.decision, answer: result.answer, dismissed: result.dismissed };
3371
3552
  }
3372
3553
  async function awaitReply(ctx, since, timeoutMs) {
3373
3554
  return new Promise((resolve) => {
@@ -3396,7 +3577,7 @@ async function awaitReply(ctx, since, timeoutMs) {
3396
3577
  });
3397
3578
  }
3398
3579
  function registerMessagingCommands(program) {
3399
- program.command("msg").argument("[key]", "ticket key, when the message is about one").argument("<body...>", "what to say").description("post a message on the board").option("-t, --to <role>", "orchestrator, reviewer, builder, operator, human, or all", "orchestrator").option("-i, --interrupt", "kill the running session and deliver now").action(async function(key2, body) {
3580
+ program.command("msg").argument("[key]", "ticket key, when the message is about one").argument("<body...>", "what to say").description("post a message on the board").option("-t, --to <role>", "orchestrator, reviewer, builder, operator, human, or all", oneOf("target", messageToRoles), "orchestrator").option("-i, --interrupt", "kill the running session and deliver now").action(async function(key2, body) {
3400
3581
  const opts = this.opts();
3401
3582
  const ctx = await requireWorkspace(slugOf2(this));
3402
3583
  const looksLikeKey = key2 && /^HD-\d+$/i.test(key2);
@@ -3409,7 +3590,7 @@ function registerMessagingCommands(program) {
3409
3590
  });
3410
3591
  ok(`Sent to ${c.bold(opts.to)}${looksLikeKey ? ` on ${key2}` : ""}.`, { message });
3411
3592
  });
3412
- program.command("ask").argument("<question...>", "what to ask").description("ask the orchestrator about the work, and wait for its reply").option("-k, --ticket <key>", "ask about one ticket").option("-t, --to <role>", "ask a different role", "orchestrator").option("--no-wait", "post the question and return immediately").option("--timeout <seconds>", "how long to wait for the reply", "180").action(async function(question) {
3593
+ program.command("ask").argument("<question...>", "what to ask").description("ask the orchestrator about the work, and wait for its reply").option("-k, --ticket <key>", "ask about one ticket").option("-t, --to <role>", "ask a different role", oneOf("target", messageToRoles), "orchestrator").option("--no-wait", "post the question and return immediately").option("--timeout <seconds>", "how long to wait for the reply", positiveNumber("timeout"), 180).action(async function(question) {
3413
3594
  const opts = this.opts();
3414
3595
  const ctx = await requireWorkspace(slugOf2(this));
3415
3596
  const since = (/* @__PURE__ */ new Date()).toISOString();
@@ -3465,6 +3646,7 @@ var init_commands3 = __esm({
3465
3646
  init_queries();
3466
3647
  init_views();
3467
3648
  init_subscribe();
3649
+ init_argv_parsers();
3468
3650
  }
3469
3651
  });
3470
3652
 
@@ -3719,6 +3901,160 @@ var init_epics = __esm({
3719
3901
  }
3720
3902
  });
3721
3903
 
3904
+ // src/control/config.ts
3905
+ function parseField(parse, value) {
3906
+ try {
3907
+ return ok2(parse(value));
3908
+ } catch (error) {
3909
+ return no(error instanceof Error ? error.message : String(error));
3910
+ }
3911
+ }
3912
+ function checkProvider(provider) {
3913
+ return providers.includes(provider) ? ok2(provider) : no(`Unknown provider ${provider}. One of: ${providers.join(", ")}.`);
3914
+ }
3915
+ function agentChanges(input) {
3916
+ const fields = {};
3917
+ if (input.name !== void 0) {
3918
+ const name = input.name.trim();
3919
+ if (!name) return no("An agent needs a name.");
3920
+ fields.display_name = name;
3921
+ }
3922
+ if (input.model !== void 0) {
3923
+ const model = input.model.trim();
3924
+ if (!model) return no("An agent needs a model.");
3925
+ fields.model = model;
3926
+ }
3927
+ if (input.provider !== void 0) {
3928
+ const provider = checkProvider(input.provider.trim());
3929
+ if (!provider.ok) return no(provider.error);
3930
+ fields.provider = provider.value;
3931
+ }
3932
+ if (input.effort !== void 0) fields.effort = asEffort(input.effort);
3933
+ if (input.notes !== void 0) fields.routing_notes = input.notes;
3934
+ if (input.prompt !== void 0) fields.prompt_addendum = input.prompt;
3935
+ if (input.enabled !== void 0) fields.enabled = input.enabled;
3936
+ if (input.runsPerHour !== void 0) fields.runs_per_hour = input.runsPerHour;
3937
+ if (input.dailySpend !== void 0) fields.daily_spend_usd = input.dailySpend;
3938
+ if (Object.keys(fields).length === 0) return no("Nothing to change.");
3939
+ return ok2(fields);
3940
+ }
3941
+ async function findAgent(ctx, name) {
3942
+ const { data, error } = await ctx.db.from("agents").select("*").eq("workspace_id", ctx.workspace.id);
3943
+ if (error) return no(error.message);
3944
+ const wanted = name.trim().toLowerCase();
3945
+ const matches = (data ?? []).filter(
3946
+ (row) => row.id === name || row.display_name.toLowerCase() === wanted || row.role.toLowerCase() === wanted
3947
+ );
3948
+ if (matches.length === 0) return no(`No agent named ${name}.`);
3949
+ if (matches.length > 1) return no(`${name} matches ${matches.length} agents. Use a display name.`);
3950
+ return ok2(matches[0]);
3951
+ }
3952
+ async function updateAgent(ctx, name, input) {
3953
+ const found = await findAgent(ctx, name);
3954
+ if (!found.ok) return found;
3955
+ const changes = agentChanges(input);
3956
+ if (!changes.ok) return changes;
3957
+ if (changes.value.enabled === true && (found.value.role === "orchestrator" || found.value.role === "reviewer")) {
3958
+ const { data: other } = await ctx.db.from("agents").select("id, display_name").eq("workspace_id", ctx.workspace.id).eq("role", found.value.role).eq("enabled", true).neq("id", found.value.id).maybeSingle();
3959
+ if (other) return no(`${other.display_name} is already the enabled ${found.value.role}. Disable it first.`);
3960
+ }
3961
+ const { data, error } = await ctx.db.from("agents").update(changes.value).eq("id", found.value.id).eq("workspace_id", ctx.workspace.id).select("*").single();
3962
+ if (error) return no(error.message);
3963
+ await ctx.audit(ctx.workspace.id, "configure", { subject: `agent ${data.display_name}` });
3964
+ return ok2(data);
3965
+ }
3966
+ async function createBuilder(ctx, name, input) {
3967
+ const display = name.trim();
3968
+ if (!display) return no("An agent needs a name.");
3969
+ const provider = checkProvider(input.provider.trim());
3970
+ if (!provider.ok) return no(provider.error);
3971
+ const model = input.model.trim();
3972
+ if (!model) return no("An agent needs a model.");
3973
+ const { data, error } = await ctx.db.from("agents").insert({
3974
+ workspace_id: ctx.workspace.id,
3975
+ role: "builder",
3976
+ provider: provider.value,
3977
+ model,
3978
+ display_name: display,
3979
+ effort: asEffort(input.effort ?? DEFAULT_EFFORT),
3980
+ routing_notes: input.notes ?? ""
3981
+ }).select("*").single();
3982
+ if (error) return no(error.message);
3983
+ await ctx.audit(ctx.workspace.id, "create", { subject: `agent ${data.display_name}` });
3984
+ return ok2(data);
3985
+ }
3986
+ function mergeCaps(existing, pairs) {
3987
+ const caps = {};
3988
+ for (const [key2, value] of Object.entries(existing ?? {})) {
3989
+ if (typeof value === "number") caps[key2] = value;
3990
+ }
3991
+ for (const { provider, cap } of pairs) caps[provider] = cap;
3992
+ return caps;
3993
+ }
3994
+ function workspaceChanges(workspace, input) {
3995
+ const fields = {};
3996
+ if (input.branch !== void 0) {
3997
+ const branch = input.branch.trim();
3998
+ if (!branch) return no("A workspace needs a default branch.");
3999
+ fields.default_branch = branch;
4000
+ }
4001
+ if (input.host !== void 0) {
4002
+ const host = input.host.trim();
4003
+ if (!host) return no("A workspace needs a default host.");
4004
+ fields.default_host = host;
4005
+ }
4006
+ if (input.autoMerge !== void 0) fields.auto_merge = input.autoMerge;
4007
+ if (input.caps?.length) {
4008
+ fields.provider_caps = mergeCaps(
4009
+ workspace.provider_caps ?? {},
4010
+ input.caps
4011
+ );
4012
+ }
4013
+ if (Object.keys(fields).length === 0) return no("Nothing to change.");
4014
+ return ok2(fields);
4015
+ }
4016
+ async function updateWorkspace(ctx, input) {
4017
+ const changes = workspaceChanges(ctx.workspace, input);
4018
+ if (!changes.ok) return changes;
4019
+ const { data, error } = await ctx.db.from("workspaces").update(changes.value).eq("id", ctx.workspace.id).select("*").single();
4020
+ if (error) return no(error.message);
4021
+ await ctx.audit(ctx.workspace.id, "configure", { subject: "workspace settings" });
4022
+ return ok2(data);
4023
+ }
4024
+ function checkRepo(repo) {
4025
+ const trimmed = repo.trim();
4026
+ return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? ok2(trimmed) : no("Repo looks like owner/name.");
4027
+ }
4028
+ async function createWorkspace(ctx, name, repo, input = {}) {
4029
+ const display = name.trim();
4030
+ if (!display) return no("A workspace needs a name.");
4031
+ const checked = checkRepo(repo);
4032
+ if (!checked.ok) return no(checked.error);
4033
+ const { data, error } = await ctx.db.from("workspaces").insert({
4034
+ name: display,
4035
+ slug: slugify(display),
4036
+ repo: checked.value,
4037
+ default_branch: input.branch ?? "main",
4038
+ default_host: input.host ?? "box"
4039
+ }).select("*").single();
4040
+ if (error) return no(error.message);
4041
+ const { error: agentsError } = await ctx.db.from("agents").insert(DEFAULT_AGENTS.map((agent) => ({ ...agent, workspace_id: data.id })));
4042
+ if (agentsError) {
4043
+ return no(`Workspace created, but seeding its agents failed: ${agentsError.message}`);
4044
+ }
4045
+ await ctx.audit(data.id, "create", { subject: `workspace ${data.slug}` });
4046
+ return ok2({ workspace: data, agents: DEFAULT_AGENTS.length });
4047
+ }
4048
+ var ok2, no;
4049
+ var init_config2 = __esm({
4050
+ "src/control/config.ts"() {
4051
+ "use strict";
4052
+ init_src();
4053
+ ok2 = (value) => ({ ok: true, value });
4054
+ no = (error) => ({ ok: false, error });
4055
+ }
4056
+ });
4057
+
3722
4058
  // src/write/editor.ts
3723
4059
  import { spawn as spawn2 } from "child_process";
3724
4060
  import { mkdtemp, readFile as readFile4, rm as rm2, writeFile as writeFile2 } from "fs/promises";
@@ -3756,6 +4092,10 @@ import { randomUUID as randomUUID2 } from "crypto";
3756
4092
  function slugOf3(command) {
3757
4093
  return command.optsWithGlobals().workspace;
3758
4094
  }
4095
+ function must(result) {
4096
+ if (!result.ok) fail(result.error);
4097
+ return result.value;
4098
+ }
3759
4099
  async function setWorkspacePaused(ctx, paused) {
3760
4100
  const { error } = await ctx.db.from("workspaces").update({ paused }).eq("id", ctx.workspace.id);
3761
4101
  if (error) fail(error.message);
@@ -3780,7 +4120,7 @@ async function setProviderPaused(ctx, provider, paused) {
3780
4120
  await ctx.audit(ctx.workspace.id, paused ? "pause" : "resume", { subject: provider });
3781
4121
  }
3782
4122
  function registerControlCommands(program) {
3783
- program.command("pause").description("stop dispatching new work").option("--all", "pause every workspace").option("-p, --provider <provider>", "pause one provider by setting its cap to zero").action(async function() {
4123
+ program.command("pause").description("stop dispatching new work").option("--all", "pause every workspace").option("-p, --provider <provider>", "pause one provider by setting its cap to zero", oneOf("provider", providers)).action(async function() {
3784
4124
  const opts = this.opts();
3785
4125
  if (opts.all) {
3786
4126
  const ctx2 = await requireCtx();
@@ -3797,7 +4137,7 @@ function registerControlCommands(program) {
3797
4137
  await setWorkspacePaused(ctx, true);
3798
4138
  ok(c.yellow(`${ctx.workspace.slug} is paused.`));
3799
4139
  });
3800
- program.command("resume").description("start dispatching again").option("--all", "resume every workspace").option("-p, --provider <provider>", "resume one provider").action(async function() {
4140
+ program.command("resume").description("start dispatching again").option("--all", "resume every workspace").option("-p, --provider <provider>", "resume one provider", oneOf("provider", providers)).action(async function() {
3801
4141
  const opts = this.opts();
3802
4142
  if (opts.all) {
3803
4143
  const ctx2 = await requireCtx();
@@ -3850,7 +4190,7 @@ function registerAgentCommands(program) {
3850
4190
  });
3851
4191
  agent.command("show").argument("<name>", "agent display name").description("one agent in full").action(async function(name) {
3852
4192
  const ctx = await requireWorkspace(slugOf3(this));
3853
- const found = await findAgent(ctx, name);
4193
+ const found = must(await findAgent(ctx, name));
3854
4194
  emit(
3855
4195
  found,
3856
4196
  () => [
@@ -3867,80 +4207,43 @@ ${found.prompt_addendum}` : ""
3867
4207
  ].filter(Boolean).join("\n")
3868
4208
  );
3869
4209
  });
3870
- agent.command("set").argument("<name>", "agent display name").description("change an agent's model, effort, routing, prompt, or caps").option("--model <model>", "model id").option("--effort <level>", "low, medium, or high").option("--provider <provider>", "claude, codex, gemini, or grok").option("--name <text>", "new display name").option("--notes <text>", "routing notes the orchestrator reads").option("--prompt <text>", "prompt addendum prepended on every run").option("--edit-prompt", "edit the prompt addendum in $EDITOR").option("--enable", "enable the agent").option("--disable", "disable the agent").option("--runs-per-hour <n>", "cap runs per hour, or 'none'").option("--daily-spend <usd>", "cap spend per day, or 'none'").action(async function(name) {
4210
+ agent.command("set").argument("<name>", "agent display name").description("change an agent's model, effort, routing, prompt, or caps").option("--model <model>", "model id").option("--effort <level>", "low, medium, or high", oneOf("effort", efforts)).option("--provider <provider>", "claude, codex, gemini, or grok", oneOf("provider", providers)).option("--name <text>", "new display name").option("--notes <text>", "routing notes the orchestrator reads").option("--prompt <text>", "prompt addendum prepended on every run").option("--edit-prompt", "edit the prompt addendum in $EDITOR").option("--enable", "enable the agent").option("--disable", "disable the agent").option("--runs-per-hour <n>", "cap runs per hour, or 'none'", optionalPositiveInteger("runs per hour")).option("--daily-spend <usd>", "cap spend per day, or 'none'", optionalPositiveNumber("daily spend")).action(async function(name) {
3871
4211
  const opts = this.opts();
3872
4212
  const ctx = await requireWorkspace(slugOf3(this));
3873
- const found = await findAgent(ctx, name);
3874
- const fields = {};
3875
- if (opts.name) fields.display_name = String(opts.name).trim();
3876
- if (opts.model) fields.model = String(opts.model).trim();
3877
- if (opts.effort) fields.effort = asEffort(opts.effort);
3878
- if (opts.provider) {
3879
- if (!providers.includes(opts.provider)) {
3880
- fail(`Unknown provider ${opts.provider}. One of: ${providers.join(", ")}.`);
3881
- }
3882
- fields.provider = opts.provider;
3883
- }
3884
- if (opts.notes !== void 0) fields.routing_notes = opts.notes;
3885
- if (opts.prompt !== void 0) fields.prompt_addendum = opts.prompt;
4213
+ let prompt2 = opts.prompt;
3886
4214
  if (opts.editPrompt) {
3887
- fields.prompt_addendum = (await editInEditor(found.prompt_addendum ?? "", "hd-prompt.md")).trim();
4215
+ const found = must(await findAgent(ctx, name));
4216
+ prompt2 = (await editInEditor(found.prompt_addendum ?? "", "hd-prompt.md")).trim();
3888
4217
  }
3889
- if (opts.enable) fields.enabled = true;
3890
- if (opts.disable) fields.enabled = false;
3891
- if (opts.runsPerHour !== void 0) {
3892
- const cap = parseOptionalCap(opts.runsPerHour === "none" ? "" : opts.runsPerHour, {
3893
- integer: true
3894
- });
3895
- if (!cap.ok) fail(cap.error);
3896
- fields.runs_per_hour = cap.value;
3897
- }
3898
- if (opts.dailySpend !== void 0) {
3899
- const cap = parseOptionalCap(opts.dailySpend === "none" ? "" : opts.dailySpend);
3900
- if (!cap.ok) fail(cap.error);
3901
- fields.daily_spend_usd = cap.value;
3902
- }
3903
- if (Object.keys(fields).length === 0) fail("Nothing to change.");
3904
- if (fields.enabled === true && (found.role === "orchestrator" || found.role === "reviewer")) {
3905
- const { data: other } = await ctx.db.from("agents").select("id, display_name").eq("workspace_id", ctx.workspace.id).eq("role", found.role).eq("enabled", true).neq("id", found.id).maybeSingle();
3906
- if (other) fail(`${other.display_name} is already the enabled ${found.role}. Disable it first.`);
3907
- }
3908
- const { data, error } = await ctx.db.from("agents").update(fields).eq("id", found.id).eq("workspace_id", ctx.workspace.id).select("*").single();
3909
- if (error) fail(error.message);
3910
- await ctx.audit(ctx.workspace.id, "configure", { subject: `agent ${data.display_name}` });
4218
+ const data = must(
4219
+ await updateAgent(ctx, name, {
4220
+ name: opts.name,
4221
+ model: opts.model,
4222
+ provider: opts.provider,
4223
+ effort: opts.effort,
4224
+ notes: opts.notes,
4225
+ prompt: prompt2,
4226
+ enabled: opts.enable ? true : opts.disable ? false : void 0,
4227
+ runsPerHour: opts.runsPerHour,
4228
+ dailySpend: opts.dailySpend
4229
+ })
4230
+ );
3911
4231
  ok(`${c.bold(data.display_name)} updated.`, { agent: data });
3912
4232
  });
3913
- agent.command("new").argument("<name>", "display name").description("add a builder").requiredOption("--provider <provider>", "claude, codex, gemini, or grok").requiredOption("--model <model>", "model id").option("--effort <level>", "low, medium, or high", DEFAULT_EFFORT).option("--notes <text>", "routing notes").action(async function(name) {
4233
+ agent.command("new").argument("<name>", "display name").description("add a builder").requiredOption("--provider <provider>", "claude, codex, gemini, or grok", oneOf("provider", providers)).requiredOption("--model <model>", "model id").option("--effort <level>", "low, medium, or high", oneOf("effort", efforts), DEFAULT_EFFORT).option("--notes <text>", "routing notes").action(async function(name) {
3914
4234
  const opts = this.opts();
3915
4235
  const ctx = await requireWorkspace(slugOf3(this));
3916
- if (!providers.includes(opts.provider)) {
3917
- fail(`Unknown provider ${opts.provider}. One of: ${providers.join(", ")}.`);
3918
- }
3919
- const { data, error } = await ctx.db.from("agents").insert({
3920
- workspace_id: ctx.workspace.id,
3921
- role: "builder",
3922
- provider: opts.provider,
3923
- model: opts.model,
3924
- display_name: name.trim(),
3925
- effort: asEffort(opts.effort),
3926
- routing_notes: opts.notes ?? ""
3927
- }).select("*").single();
3928
- if (error) fail(error.message);
3929
- await ctx.audit(ctx.workspace.id, "create", { subject: `agent ${data.display_name}` });
4236
+ const data = must(
4237
+ await createBuilder(ctx, name, {
4238
+ provider: opts.provider,
4239
+ model: opts.model,
4240
+ effort: opts.effort,
4241
+ notes: opts.notes
4242
+ })
4243
+ );
3930
4244
  ok(`${c.bold(data.display_name)} added.`, { agent: data });
3931
4245
  });
3932
4246
  }
3933
- async function findAgent(ctx, name) {
3934
- const { data, error } = await ctx.db.from("agents").select("*").eq("workspace_id", ctx.workspace.id);
3935
- if (error) fail(error.message);
3936
- const wanted = name.trim().toLowerCase();
3937
- const matches = (data ?? []).filter(
3938
- (row) => row.id === name || row.display_name.toLowerCase() === wanted || row.role.toLowerCase() === wanted
3939
- );
3940
- if (matches.length === 0) fail(`No agent named ${name}. Run \`hd agent ls\`.`);
3941
- if (matches.length > 1) fail(`${name} matches ${matches.length} agents. Use a display name.`);
3942
- return matches[0];
3943
- }
3944
4247
  function registerWorkspaceCommands(program) {
3945
4248
  const workspace = program.command("workspace").description("workspaces and their settings");
3946
4249
  workspace.command("ls", { isDefault: true }).description("every workspace").action(async function() {
@@ -3986,45 +4289,25 @@ function registerWorkspaceCommands(program) {
3986
4289
  workspace.command("new").argument("<name>", "workspace name").argument("<repo>", "owner/name on GitHub").description("create a workspace").option("--branch <name>", "default branch", "main").option("--host <id>", "default host", "box").action(async function(name, repo) {
3987
4290
  const opts = this.opts();
3988
4291
  const ctx = await requireCtx();
3989
- if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) fail("Repo looks like owner/name.");
3990
- const { data, error } = await ctx.db.from("workspaces").insert({
3991
- name: name.trim(),
3992
- slug: slugify(name),
3993
- repo,
3994
- default_branch: opts.branch,
3995
- default_host: opts.host
3996
- }).select("*").single();
3997
- if (error) fail(error.message);
3998
- const { error: agentsError } = await ctx.db.from("agents").insert(DEFAULT_AGENTS.map((agent) => ({ ...agent, workspace_id: data.id })));
3999
- if (agentsError) fail(`Workspace created, but seeding its agents failed: ${agentsError.message}`);
4000
- await ctx.audit(data.id, "create", { subject: `workspace ${data.slug}` });
4292
+ const made = must(
4293
+ await createWorkspace(ctx, name, repo, { branch: opts.branch, host: opts.host })
4294
+ );
4001
4295
  ok(
4002
- `${c.bold(data.slug)} created with ${DEFAULT_AGENTS.length} agents. Run \`hd use ${data.slug}\`.`,
4003
- { workspace: data }
4296
+ `${c.bold(made.workspace.slug)} created with ${made.agents} agents. Run \`hd use ${made.workspace.slug}\`.`,
4297
+ { workspace: made.workspace }
4004
4298
  );
4005
4299
  });
4006
- workspace.command("set").description("change workspace settings").option("--branch <name>", "default branch").option("--host <id>", "default host").option("--auto-merge", "merge approved PRs automatically").option("--no-auto-merge", "wait for a human to merge").option("--cap <provider=n...>", "set a provider cap, e.g. --cap claude=2").action(async function() {
4300
+ workspace.command("set").description("change workspace settings").option("--branch <name>", "default branch").option("--host <id>", "default host").option("--auto-merge", "merge approved PRs automatically").option("--no-auto-merge", "wait for a human to merge").option("--cap <provider=n...>", "set a provider cap, e.g. --cap claude=2", providerCaps(providers)).action(async function() {
4007
4301
  const opts = this.opts();
4008
4302
  const ctx = await requireWorkspace(slugOf3(this));
4009
- const fields = {};
4010
- if (opts.branch) fields.default_branch = opts.branch;
4011
- if (opts.host) fields.default_host = opts.host;
4012
- if (opts.autoMerge !== void 0) fields.auto_merge = Boolean(opts.autoMerge);
4013
- if (opts.cap) {
4014
- const caps = { ...ctx.workspace.provider_caps ?? {} };
4015
- for (const pair of [].concat(opts.cap)) {
4016
- const [provider, value] = String(pair).split("=");
4017
- if (!providers.includes(provider)) fail(`Unknown provider ${provider}.`);
4018
- const n = Number(value);
4019
- if (!Number.isInteger(n) || n < 0) fail(`Cap for ${provider} must be a whole number.`);
4020
- caps[provider] = n;
4021
- }
4022
- fields.provider_caps = caps;
4023
- }
4024
- if (Object.keys(fields).length === 0) fail("Nothing to change.");
4025
- const { data, error } = await ctx.db.from("workspaces").update(fields).eq("id", ctx.workspace.id).select("*").single();
4026
- if (error) fail(error.message);
4027
- await ctx.audit(ctx.workspace.id, "configure", { subject: "workspace settings" });
4303
+ const data = must(
4304
+ await updateWorkspace(ctx, {
4305
+ branch: opts.branch,
4306
+ host: opts.host,
4307
+ autoMerge: opts.autoMerge === void 0 ? void 0 : Boolean(opts.autoMerge),
4308
+ caps: opts.cap
4309
+ })
4310
+ );
4028
4311
  ok(`${c.bold(data.slug)} updated.`, { workspace: data });
4029
4312
  });
4030
4313
  workspace.command("templates").description("ticket templates for this workspace").action(async function() {
@@ -4114,7 +4397,7 @@ function registerAttachCommands(program) {
4114
4397
  paths: uploaded
4115
4398
  });
4116
4399
  });
4117
- attach.command("ls").argument("<key>", "ticket key").description("signed URLs for a ticket's images").option("--ttl <seconds>", "how long the URLs stay valid").action(async function(key2) {
4400
+ attach.command("ls").argument("<key>", "ticket key").description("signed URLs for a ticket's images").option("--ttl <seconds>", "how long the URLs stay valid", rangedInteger("ttl", 30, 3600)).action(async function(key2) {
4118
4401
  const ctx = await requireWorkspace(slugOf3(this));
4119
4402
  const { data: ticket } = await ctx.db.from("tickets").select("id, key").eq("workspace_id", ctx.workspace.id).eq("key", key2.trim().toUpperCase()).maybeSingle();
4120
4403
  if (!ticket) fail(`No ticket ${key2} in ${ctx.workspace.slug}.`);
@@ -4137,12 +4420,14 @@ var init_commands4 = __esm({
4137
4420
  "use strict";
4138
4421
  init_src();
4139
4422
  init_context();
4423
+ init_config2();
4140
4424
  init_json();
4141
4425
  init_theme();
4142
4426
  init_format();
4143
4427
  init_queries();
4144
4428
  init_views();
4145
4429
  init_editor();
4430
+ init_argv_parsers();
4146
4431
  DEFAULT_CAPS = { claude: 2, codex: 3, gemini: 1, grok: 2 };
4147
4432
  }
4148
4433
  });
@@ -4717,7 +5002,7 @@ async function architectTurn(opts) {
4717
5002
  if (runError) fail(runError.message);
4718
5003
  let seq = 0;
4719
5004
  const events = [];
4720
- const record = (type, payload) => events.push({ run_id: runRow.id, seq: seq++, type, payload });
5005
+ const record2 = (type, payload) => events.push({ run_id: runRow.id, seq: seq++, type, payload });
4721
5006
  const flush = async () => {
4722
5007
  if (events.length) await ctx.db.from("run_events").insert(events);
4723
5008
  };
@@ -4731,14 +5016,14 @@ async function architectTurn(opts) {
4731
5016
  sessionId: session.sessionId,
4732
5017
  signal: opts.signal,
4733
5018
  onEvent: (event) => {
4734
- if (event.kind === "tool") record("tool_use", { name: event.name, input: event.args });
5019
+ if (event.kind === "tool") record2("tool_use", { name: event.name, input: event.args });
4735
5020
  else if (event.kind === "tool_result") {
4736
- record("tool_result", { name: event.name, ok: event.ok, content: event.detail });
5021
+ record2("tool_result", { name: event.name, ok: event.ok, content: event.detail });
4737
5022
  }
4738
5023
  opts.onEvent(event);
4739
5024
  }
4740
5025
  });
4741
- if (result.text) record("text", { text: result.text });
5026
+ if (result.text) record2("text", { text: result.text });
4742
5027
  session.history.push({ role: "user", content: opts.request });
4743
5028
  session.history.push({ role: "assistant", content: result.text });
4744
5029
  await flush();
@@ -4753,7 +5038,7 @@ async function architectTurn(opts) {
4753
5038
  return { text: result.text, runId: runRow.id, toolCalls: result.toolCalls };
4754
5039
  } catch (error) {
4755
5040
  const message = error instanceof Error ? error.message : String(error);
4756
- record("error", { error: message });
5041
+ record2("error", { error: message });
4757
5042
  await flush();
4758
5043
  await ctx.db.from("runs").update({
4759
5044
  status: "failed",
@@ -4832,297 +5117,1335 @@ var init_theme2 = __esm({
4832
5117
  });
4833
5118
 
4834
5119
  // src/tui/Banner.tsx
4835
- import { useEffect, useState } from "react";
4836
- import { Box, Text } from "ink";
5120
+ import { useEffect, useMemo, useRef, useState } from "react";
5121
+ import { Box, Text, useStdout } from "ink";
4837
5122
  import { jsx } from "react/jsx-runtime";
4838
- function bannerRows(word = WORD, gap = 1) {
4839
- const rows = ["", "", "", "", ""];
4840
- for (const letter of word) {
5123
+ function bannerSize(columns, rows) {
5124
+ if (columns >= BANNER_WIDTH.large + 2 && rows >= BANNER_HEIGHT.large + 26) return "large";
5125
+ if (columns >= BANNER_WIDTH.small + 2 && rows >= BANNER_HEIGHT.small + 8) return "small";
5126
+ return "text";
5127
+ }
5128
+ function pixelRows(word = WORD, gap = 1) {
5129
+ const rows = Array.from({ length: PIXEL_ROWS }, () => "");
5130
+ const letters = [...word].filter((letter) => GLYPHS[letter]);
5131
+ letters.forEach((letter, index) => {
4841
5132
  const glyph = GLYPHS[letter];
4842
- if (!glyph) continue;
4843
5133
  const width = Math.max(...glyph.map((row) => row.length));
4844
- for (let i = 0; i < rows.length; i += 1) {
4845
- rows[i] += (glyph[i] ?? "").padEnd(width) + " ".repeat(gap);
5134
+ const spacer = index === letters.length - 1 ? 0 : gap;
5135
+ for (let i = 0; i < PIXEL_ROWS; i += 1) {
5136
+ rows[i] += (glyph[i] ?? "").padEnd(width, ".") + ".".repeat(spacer);
5137
+ }
5138
+ });
5139
+ return rows;
5140
+ }
5141
+ function bannerRows(size = "small", word = WORD) {
5142
+ if (size === "text") return [word];
5143
+ if (size === "large") {
5144
+ return pixelRows(word, 1).map(
5145
+ (row) => [...row].map((pixel) => pixel === "#" ? "\u2588\u2588" : " ").join("")
5146
+ );
5147
+ }
5148
+ const pixels = pixelRows(word, 1);
5149
+ const rows = [];
5150
+ for (let i = 0; i < pixels.length; i += 2) {
5151
+ const top = pixels[i] ?? "";
5152
+ const bottom = pixels[i + 1] ?? "";
5153
+ let row = "";
5154
+ for (let x = 0; x < top.length; x += 1) {
5155
+ const key2 = `${top[x] === "#" ? "1" : "0"}${bottom[x] === "#" ? "1" : "0"}`;
5156
+ row += HALF[key2];
4846
5157
  }
5158
+ rows.push(row);
4847
5159
  }
4848
- return rows.map((row) => row.replace(/\s+$/, ""));
5160
+ return rows;
4849
5161
  }
4850
- function Banner({ animate = true, onDone }) {
4851
- const [frame, setFrame] = useState(animate ? 0 : FLASH.length - 1);
4852
- useEffect(() => {
4853
- if (!animate) {
4854
- onDone?.();
4855
- return;
5162
+ function mulberry32(seed) {
5163
+ let a = seed >>> 0;
5164
+ return () => {
5165
+ a = a + 1831565813 | 0;
5166
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
5167
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
5168
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
5169
+ };
5170
+ }
5171
+ function stormSchedule(opts = {}) {
5172
+ const durationMs = opts.durationMs ?? STORM_MS;
5173
+ const random = mulberry32(opts.seed ?? 1);
5174
+ const between = (min, max) => min + Math.floor(random() * (max - min + 1));
5175
+ const frames = [];
5176
+ let elapsed2 = 0;
5177
+ const push = (frame) => {
5178
+ frames.push(frame);
5179
+ elapsed2 += frame.ms;
5180
+ };
5181
+ while (elapsed2 < durationMs) {
5182
+ for (let strike = 0, strikes = between(2, 5); strike < strikes; strike += 1) {
5183
+ push({ level: 3, ms: between(35, 70) });
5184
+ push({ level: 1, ms: between(25, 55) });
5185
+ if (random() < 0.35) push({ level: 2, ms: between(30, 60) });
5186
+ push({ level: 0, ms: between(45, 160) });
4856
5187
  }
4857
- if (frame >= FLASH.length - 1) {
5188
+ push({ level: 0, ms: between(2200, 13e3), quiet: true });
5189
+ }
5190
+ const last = frames[frames.length - 1];
5191
+ if (last && elapsed2 > durationMs) last.ms = Math.max(1, last.ms - (elapsed2 - durationMs));
5192
+ return frames;
5193
+ }
5194
+ function Banner({
5195
+ animate = true,
5196
+ onDone,
5197
+ columns,
5198
+ rows,
5199
+ seed,
5200
+ durationMs
5201
+ }) {
5202
+ const { stdout } = useStdout();
5203
+ const wide = columns ?? (stdout?.columns && stdout.columns > 0 ? stdout.columns : 80);
5204
+ const tall = rows ?? (stdout?.rows && stdout.rows > 0 ? stdout.rows : 24);
5205
+ const size = bannerSize(wide, tall);
5206
+ const storm = useMemo(
5207
+ () => stormSchedule({ seed: seed ?? Math.floor(Math.random() * 2 ** 31), durationMs }),
5208
+ [seed, durationMs]
5209
+ );
5210
+ const [frame, setFrame] = useState(0);
5211
+ const announced = useRef(false);
5212
+ useEffect(() => {
5213
+ if (announced.current) return;
5214
+ if (!animate || storm.length === 0 || storm[frame]?.quiet) {
5215
+ announced.current = true;
4858
5216
  onDone?.();
4859
- return;
4860
5217
  }
4861
- const timer = setTimeout(() => setFrame((current) => current + 1), FRAME_MS);
5218
+ }, [animate, storm, frame, onDone]);
5219
+ useEffect(() => {
5220
+ if (!animate || frame >= storm.length) return;
5221
+ const timer = setTimeout(() => setFrame((current) => current + 1), storm[frame].ms);
4862
5222
  return () => clearTimeout(timer);
4863
- }, [frame, animate, onDone]);
4864
- const style = FLASH[Math.min(frame, FLASH.length - 1)];
4865
- const rows = bannerRows();
4866
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginBottom: 1, children: rows.map((row, index) => /* @__PURE__ */ jsx(Text, { color: style.color, dimColor: style.dim, bold: style.bold, children: row }, index)) });
5223
+ }, [animate, storm, frame]);
5224
+ const over = !animate || frame >= storm.length;
5225
+ const style = over ? SETTLED : LEVELS[storm[frame].level];
5226
+ const lines = bannerRows(size);
5227
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginBottom: 1, children: lines.map((line, index) => /* @__PURE__ */ jsx(Text, { color: style.color, dimColor: style.dimColor, bold: style.bold, children: line }, index)) });
4867
5228
  }
4868
- var GLYPHS, WORD, FLASH, FRAME_MS;
5229
+ var WORD, GLYPHS, PIXEL_ROWS, BANNER_WIDTH, BANNER_HEIGHT, HALF, LEVELS, SETTLED, STORM_MS;
4869
5230
  var init_Banner = __esm({
4870
5231
  "src/tui/Banner.tsx"() {
4871
5232
  "use strict";
4872
5233
  init_theme2();
5234
+ WORD = "HigherDEV";
4873
5235
  GLYPHS = {
4874
- H: ["\u2588 \u2588", "\u2588 \u2588", "\u2588\u2588\u2588\u2588", "\u2588 \u2588", "\u2588 \u2588"],
4875
- i: ["\u2588", " ", "\u2588", "\u2588", "\u2588"],
4876
- " ": [" ", " ", " ", " ", " "],
4877
- g: [" ", " \u2588\u2588\u2588\u2588", "\u2588 \u2588", " \u2588\u2588\u2588\u2588", " \u2588"],
4878
- h: ["\u2588 ", "\u2588 ", "\u2588\u2588\u2588 ", "\u2588 \u2588", "\u2588 \u2588"],
4879
- e: [" ", " \u2588\u2588\u2588 ", "\u2588\u2588\u2588\u2588\u2588", "\u2588 ", " \u2588\u2588\u2588\u2588"],
4880
- r: [" ", " ", "\u2588 \u2588\u2588", "\u2588\u2588 ", "\u2588 "],
4881
- D: ["\u2588\u2588\u2588 ", "\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588", "\u2588\u2588\u2588 "],
4882
- E: ["\u2588\u2588\u2588\u2588", "\u2588 ", "\u2588\u2588\u2588 ", "\u2588 ", "\u2588\u2588\u2588\u2588"],
4883
- V: ["\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588", " \u2588 \u2588 ", " \u2588 "]
5236
+ H: [
5237
+ "##...##",
5238
+ "##...##",
5239
+ "##...##",
5240
+ "##...##",
5241
+ "#######",
5242
+ "#######",
5243
+ "##...##",
5244
+ "##...##",
5245
+ "##...##",
5246
+ "##...##",
5247
+ ".......",
5248
+ "......."
5249
+ ],
5250
+ i: ["##", "##", "..", "##", "##", "##", "##", "##", "##", "##", "..", ".."],
5251
+ g: [
5252
+ ".......",
5253
+ ".......",
5254
+ ".......",
5255
+ ".######",
5256
+ "##...##",
5257
+ "##...##",
5258
+ "##...##",
5259
+ "##...##",
5260
+ "##...##",
5261
+ ".######",
5262
+ ".....##",
5263
+ ".#####."
5264
+ ],
5265
+ h: [
5266
+ "##.....",
5267
+ "##.....",
5268
+ "##.....",
5269
+ "##.###.",
5270
+ "###..##",
5271
+ "##...##",
5272
+ "##...##",
5273
+ "##...##",
5274
+ "##...##",
5275
+ "##...##",
5276
+ ".......",
5277
+ "......."
5278
+ ],
5279
+ e: [
5280
+ ".......",
5281
+ ".......",
5282
+ ".......",
5283
+ ".#####.",
5284
+ "##...##",
5285
+ "##...##",
5286
+ "#######",
5287
+ "##.....",
5288
+ "##...##",
5289
+ ".#####.",
5290
+ ".......",
5291
+ "......."
5292
+ ],
5293
+ r: [
5294
+ "......",
5295
+ "......",
5296
+ "......",
5297
+ "######",
5298
+ "#####.",
5299
+ "###...",
5300
+ "##....",
5301
+ "##....",
5302
+ "##....",
5303
+ "##....",
5304
+ "......",
5305
+ "......"
5306
+ ],
5307
+ D: [
5308
+ "#####..",
5309
+ "##..##.",
5310
+ "##...##",
5311
+ "##...##",
5312
+ "##...##",
5313
+ "##...##",
5314
+ "##...##",
5315
+ "##...##",
5316
+ "##..##.",
5317
+ "#####..",
5318
+ ".......",
5319
+ "......."
5320
+ ],
5321
+ E: [
5322
+ "######",
5323
+ "######",
5324
+ "##....",
5325
+ "##....",
5326
+ "#####.",
5327
+ "#####.",
5328
+ "##....",
5329
+ "##....",
5330
+ "######",
5331
+ "######",
5332
+ "......",
5333
+ "......"
5334
+ ],
5335
+ V: [
5336
+ "##...##",
5337
+ "##...##",
5338
+ "##...##",
5339
+ "##...##",
5340
+ "##...##",
5341
+ ".##.##.",
5342
+ ".##.##.",
5343
+ ".##.##.",
5344
+ "..###..",
5345
+ "..###..",
5346
+ ".......",
5347
+ "......."
5348
+ ],
5349
+ " ": ["..", "..", "..", "..", "..", "..", "..", "..", "..", "..", "..", ".."]
4884
5350
  };
4885
- WORD = "HigherDEV";
4886
- FLASH = [
4887
- { color: UI.dim, dim: true, bold: false },
4888
- { color: UI.text, dim: false, bold: true },
4889
- { color: UI.dim, dim: true, bold: false },
4890
- { color: UI.text, dim: false, bold: true },
4891
- { color: UI.text, dim: false, bold: true },
4892
- { color: UI.dim, dim: true, bold: false },
4893
- { color: UI.text, dim: false, bold: true },
4894
- { color: UI.text, dim: false, bold: false }
5351
+ PIXEL_ROWS = 12;
5352
+ BANNER_WIDTH = {
5353
+ // 56 pixels of letter and 8 of gap, doubled, with the gaps left single.
5354
+ large: 128,
5355
+ small: 64,
5356
+ text: WORD.length
5357
+ };
5358
+ BANNER_HEIGHT = {
5359
+ large: PIXEL_ROWS,
5360
+ small: PIXEL_ROWS / 2,
5361
+ text: 1
5362
+ };
5363
+ HALF = { "00": " ", "10": "\u2580", "01": "\u2584", "11": "\u2588" };
5364
+ LEVELS = [
5365
+ { color: UI.dim, dimColor: true, bold: false },
5366
+ { color: UI.accent, dimColor: false, bold: false },
5367
+ { color: UI.text, dimColor: false, bold: false },
5368
+ { color: UI.text, dimColor: false, bold: true }
4895
5369
  ];
4896
- FRAME_MS = 70;
5370
+ SETTLED = { color: UI.text, dimColor: false, bold: false };
5371
+ STORM_MS = 24e4;
4897
5372
  }
4898
5373
  });
4899
5374
 
4900
- // src/tui/Bubble.tsx
5375
+ // src/tui/height.ts
5376
+ function wrappedRows(text, width) {
5377
+ if (width <= 0) return 1;
5378
+ return text.split("\n").reduce((total, line) => total + Math.max(1, Math.ceil(line.length / width)), 0);
5379
+ }
5380
+ function bubbleRows(message, width) {
5381
+ const inner = Math.max(1, width - 4);
5382
+ const body = message.body.trim() ? wrappedRows(message.body.trim(), inner) : message.pending ? 1 : 0;
5383
+ return 2 + 1 + (message.steps?.length ?? 0) + body + 1;
5384
+ }
5385
+ function helpNameColumn(commands) {
5386
+ return commands.reduce(
5387
+ (widest, command) => Math.max(widest, `${command.name}${command.args ? ` ${command.args}` : ""}`.length + 1),
5388
+ 12
5389
+ );
5390
+ }
5391
+ function helpRows(commands, width) {
5392
+ const inner = Math.max(1, width - 6);
5393
+ const column = helpNameColumn(commands);
5394
+ const lines = commands.reduce(
5395
+ (total, command) => total + Math.max(
5396
+ wrappedRows(command.help, Math.max(1, inner - column)),
5397
+ // The name itself can be wider than the room left for it.
5398
+ Math.ceil(column / Math.max(1, inner))
5399
+ ),
5400
+ 0
5401
+ );
5402
+ return 2 + 1 + lines + wrappedRows(HELP_FOOTER, inner);
5403
+ }
5404
+ var FRAME_CHROME, PANEL_GAP, HELP_HINT_ROWS;
5405
+ var init_height = __esm({
5406
+ "src/tui/height.ts"() {
5407
+ "use strict";
5408
+ init_Help();
5409
+ FRAME_CHROME = 3;
5410
+ PANEL_GAP = 1;
5411
+ HELP_HINT_ROWS = 2;
5412
+ }
5413
+ });
5414
+
5415
+ // src/tui/Help.tsx
4901
5416
  import "react";
4902
5417
  import { Box as Box2, Text as Text2 } from "ink";
4903
5418
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
4904
- function Bubble({ message, width }) {
4905
- const style = speakerStyle(message.speaker);
4906
- const body = message.body.replace(/\s+$/, "");
4907
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginBottom: 1, width, children: /* @__PURE__ */ jsxs(
5419
+ function HelpHint({ width }) {
5420
+ return /* @__PURE__ */ jsx2(Box2, { width, marginBottom: 1, flexWrap: "nowrap", children: /* @__PURE__ */ jsx2(Text2, { color: UI.dim, wrap: "truncate", children: `${COMMANDS.length} commands. Type /help for the list, or just say what you want.` }) });
5421
+ }
5422
+ function Help({ width }) {
5423
+ return /* @__PURE__ */ jsxs(
4908
5424
  Box2,
4909
5425
  {
4910
- borderStyle: style.borderStyle,
4911
- borderColor: style.borderColor,
4912
- ...style.backgroundColor ? { backgroundColor: style.backgroundColor } : {},
5426
+ borderStyle: "single",
5427
+ borderColor: UI.text,
4913
5428
  flexDirection: "column",
4914
- paddingX: 1,
5429
+ paddingX: 2,
5430
+ width,
4915
5431
  children: [
4916
- style.label ? /* @__PURE__ */ jsx2(Text2, { color: UI.text, bold: true, children: style.label }) : null,
4917
- (message.steps ?? []).map((step, index) => /* @__PURE__ */ jsx2(Text2, { color: UI.dim, children: step }, index)),
4918
- body ? /* @__PURE__ */ jsx2(Text2, { color: UI.text, wrap: "wrap", children: body }) : message.pending ? /* @__PURE__ */ jsx2(Text2, { color: UI.dim, children: "thinking\u2026" }) : null
5432
+ /* @__PURE__ */ jsx2(Text2, { color: UI.text, bold: true, children: "Commands" }),
5433
+ COMMANDS.map((command) => /* @__PURE__ */ jsxs(Box2, { flexWrap: "nowrap", children: [
5434
+ /* @__PURE__ */ jsx2(Box2, { width: helpNameColumn(COMMANDS), flexShrink: 0, children: /* @__PURE__ */ jsxs(Text2, { color: UI.text, wrap: "truncate", children: [
5435
+ command.name,
5436
+ command.args ? ` ${command.args}` : ""
5437
+ ] }) }),
5438
+ /* @__PURE__ */ jsx2(Text2, { color: UI.dim, wrap: "truncate", children: command.help })
5439
+ ] }, command.name)),
5440
+ /* @__PURE__ */ jsx2(Text2, { color: UI.dim, children: HELP_FOOTER })
4919
5441
  ]
4920
5442
  }
4921
- ) });
5443
+ );
5444
+ }
5445
+ var HELP_FOOTER, COMMANDS;
5446
+ var init_Help = __esm({
5447
+ "src/tui/Help.tsx"() {
5448
+ "use strict";
5449
+ init_theme2();
5450
+ init_height();
5451
+ HELP_FOOTER = "Anything not starting with / goes to whoever you are talking to. Ctrl-C leaves.";
5452
+ COMMANDS = [
5453
+ { name: "/architect", help: "talk to your own model, which can do anything in the platform" },
5454
+ { name: "/orchestrator", help: "talk to the agent that gets work in flight finished" },
5455
+ { name: "/browse", help: "put a cursor on the board; up and down move it, enter opens" },
5456
+ { name: "/workspace", args: "[slug]", help: "switch workspace, or list the ones you can reach" },
5457
+ { name: "/workspace new", args: "name owner/repo", help: "create a workspace, seeded with agents" },
5458
+ { name: "/settings", help: "change workspace and agent settings, with the arrow keys" },
5459
+ { name: "/agent new", args: "name provider model", help: "add a builder" },
5460
+ { name: "/board", help: "the kanban board" },
5461
+ { name: "/agents", help: "every agent and what it is doing" },
5462
+ { name: "/feed", help: "what just happened" },
5463
+ { name: "/inbox", help: "decisions and messages waiting on you" },
5464
+ { name: "/decide", args: "2 | text", help: "answer the decision on screen, or --skip it" },
5465
+ { name: "/ticket", args: "HD-12", help: "open one ticket" },
5466
+ { name: "/refresh", help: "reload the board now" },
5467
+ { name: "/help", help: "this list" },
5468
+ { name: "/exit", help: "leave" }
5469
+ ];
5470
+ }
5471
+ });
5472
+
5473
+ // src/tui/Splash.tsx
5474
+ import "react";
5475
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
5476
+ function Splash({
5477
+ columns,
5478
+ rows,
5479
+ width,
5480
+ ready,
5481
+ helpFull,
5482
+ animate,
5483
+ onDone
5484
+ }) {
5485
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
5486
+ /* @__PURE__ */ jsx3(Banner, { animate, onDone, columns, rows }),
5487
+ ready ? helpFull ? /* @__PURE__ */ jsx3(Help, { width }) : /* @__PURE__ */ jsx3(HelpHint, { width }) : null
5488
+ ] });
5489
+ }
5490
+ var init_Splash = __esm({
5491
+ "src/tui/Splash.tsx"() {
5492
+ "use strict";
5493
+ init_Banner();
5494
+ init_Help();
5495
+ }
5496
+ });
5497
+
5498
+ // src/tui/layout.ts
5499
+ function planLayout(input) {
5500
+ const banner = input.splash ? BANNER_HEIGHT[bannerSize(input.columns, input.rows)] + 1 : 0;
5501
+ const notice = input.notice ? 2 : 0;
5502
+ const other = input.inFlight + notice + FRAME_CHROME + 1;
5503
+ const wantHelp = input.splash && input.ready ? helpRows(COMMANDS, input.width) : 0;
5504
+ const hint = input.splash && input.ready ? HELP_HINT_ROWS : 0;
5505
+ const helpFull = wantHelp > 0 && banner + wantHelp + input.decision + other <= input.rows;
5506
+ const help = wantHelp === 0 ? 0 : helpFull ? wantHelp : hint;
5507
+ const decision = input.decision === 0 ? 0 : Math.max(1, Math.min(input.decision, input.rows - banner - help - other));
5508
+ const fixed = banner + help + decision + input.inFlight + notice;
5509
+ const empty = { cockpit: 0, stream: 0, panels: 0 };
5510
+ const finish = (parts) => {
5511
+ const total = fixed + parts.panels + FRAME_CHROME;
5512
+ return { banner, help, helpFull, decision, ...parts, total, fits: total < input.rows };
5513
+ };
5514
+ if (input.splash) return finish(empty);
5515
+ const gap = input.home ? PANEL_GAP : 0;
5516
+ const room = input.rows - fixed - FRAME_CHROME - gap - 1;
5517
+ if (room < 4) return finish(empty);
5518
+ const stream = Math.max(2, Math.min(14, Math.round(room * 0.4)));
5519
+ const cockpit = Math.max(2, room - stream);
5520
+ return finish({ cockpit, stream, panels: input.home ? cockpit + stream + gap : room });
5521
+ }
5522
+ var init_layout = __esm({
5523
+ "src/tui/layout.ts"() {
5524
+ "use strict";
5525
+ init_Banner();
5526
+ init_Help();
5527
+ init_height();
5528
+ }
5529
+ });
5530
+
5531
+ // src/tui/bounded.tsx
5532
+ import "react";
5533
+ import { Box as Box3, Text as Text3 } from "ink";
5534
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
5535
+ function BoundedPanel({
5536
+ width,
5537
+ rows,
5538
+ children
5539
+ }) {
5540
+ return /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", width, children: children.filter(Boolean).slice(0, Math.max(0, rows)) });
5541
+ }
5542
+ function contentRows(rows, items) {
5543
+ const forContent = Math.max(0, rows - 1);
5544
+ return items <= forContent ? forContent : Math.max(0, forContent - 1);
5545
+ }
5546
+ function More({ count }) {
5547
+ if (count <= 0) return null;
5548
+ return /* @__PURE__ */ jsx4(Text3, { color: UI.dim, children: ` +${count} more` });
5549
+ }
5550
+ function Heading({ text, note }) {
5551
+ return /* @__PURE__ */ jsxs3(Box3, { flexWrap: "nowrap", children: [
5552
+ /* @__PURE__ */ jsx4(Text3, { color: UI.text, bold: true, wrap: "truncate", children: text }),
5553
+ note ? /* @__PURE__ */ jsxs3(Text3, { color: UI.dim, wrap: "truncate", children: [
5554
+ " ",
5555
+ note
5556
+ ] }) : null
5557
+ ] });
5558
+ }
5559
+ var init_bounded = __esm({
5560
+ "src/tui/bounded.tsx"() {
5561
+ "use strict";
5562
+ init_theme2();
5563
+ }
5564
+ });
5565
+
5566
+ // src/tui/Dashboard.tsx
5567
+ import "react";
5568
+ import { Box as Box4, Text as Text4 } from "ink";
5569
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
5570
+ function splitWidths(width) {
5571
+ if (width < SPLIT_AT) return null;
5572
+ const agents = Math.max(30, Math.min(42, Math.round(width * 0.4)));
5573
+ return { board: width - agents - 1, agents };
5574
+ }
5575
+ function boardEntries(board) {
5576
+ const entries = [];
5577
+ for (const status of BOARD_COLUMNS) {
5578
+ const tickets = board.tickets.filter((ticket) => ticket.status === status);
5579
+ if (tickets.length === 0) continue;
5580
+ entries.push({ key: `h:${status}`, kind: "heading", status, count: tickets.length });
5581
+ for (const ticket of tickets) entries.push({ key: ticket.id, kind: "ticket", ticket });
5582
+ }
5583
+ return entries;
5584
+ }
5585
+ function boardTicketIds(board) {
5586
+ return boardEntries(board).filter((entry) => entry.kind === "ticket").map((entry) => entry.key);
5587
+ }
5588
+ function nextCursor(order, current, delta) {
5589
+ if (order.length === 0) return null;
5590
+ const at = current ? order.indexOf(current) : -1;
5591
+ if (at < 0) return delta > 0 ? order[0] : order[order.length - 1];
5592
+ return order[Math.min(order.length - 1, Math.max(0, at + delta))];
5593
+ }
5594
+ function scrollWindow(count, rows, focus) {
5595
+ if (rows >= count) return { start: 0, end: count };
5596
+ if (rows <= 0) return { start: 0, end: 0 };
5597
+ if (focus < 0) return { start: 0, end: rows };
5598
+ const start = Math.max(0, Math.min(focus - Math.floor(rows / 2), count - rows));
5599
+ return { start, end: start + rows };
5600
+ }
5601
+ function hiddenTickets(entries, start, end) {
5602
+ const tickets = (from, to) => entries.slice(from, to).filter((entry) => entry.kind === "ticket").length;
5603
+ return { above: tickets(0, start), below: tickets(end, entries.length) };
5604
+ }
5605
+ function hiddenNote(total, hidden2) {
5606
+ if (hidden2.above === 0 && hidden2.below === 0) return `${total}`;
5607
+ return `${total} ${hidden2.above > 0 ? `${hidden2.above}\u2191 ` : ""}${hidden2.below > 0 ? `${hidden2.below}\u2193` : ""}`.trimEnd();
5608
+ }
5609
+ function BoardColumn({
5610
+ board,
5611
+ width,
5612
+ rows,
5613
+ cursor
5614
+ }) {
5615
+ const entries = boardEntries(board);
5616
+ const inner = Math.max(0, rows - 1);
5617
+ const focus = cursor ? entries.findIndex((entry) => entry.key === cursor) : -1;
5618
+ const { start, end } = scrollWindow(entries.length, inner, focus);
5619
+ const title = Math.max(8, width - 10);
5620
+ return /* @__PURE__ */ jsx5(BoundedPanel, { width, rows, children: [
5621
+ /* @__PURE__ */ jsx5(
5622
+ Heading,
5623
+ {
5624
+ text: "Board",
5625
+ note: hiddenNote(board.tickets.length, hiddenTickets(entries, start, end))
5626
+ },
5627
+ "h"
5628
+ ),
5629
+ ...entries.length === 0 ? [
5630
+ /* @__PURE__ */ jsx5(Text4, { color: UI.dim, children: "No tickets yet." }, "empty")
5631
+ ] : entries.slice(start, end).map(
5632
+ (entry) => entry.kind === "heading" ? /* @__PURE__ */ jsxs4(Text4, { color: inkColor(statusTone(entry.status)), wrap: "truncate", children: [
5633
+ statusLabel(entry.status),
5634
+ " ",
5635
+ /* @__PURE__ */ jsxs4(Text4, { color: UI.dim, children: [
5636
+ "(",
5637
+ entry.count,
5638
+ ")"
5639
+ ] })
5640
+ ] }, entry.key) : /* @__PURE__ */ jsxs4(Box4, { flexWrap: "nowrap", children: [
5641
+ /* @__PURE__ */ jsx5(Box4, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text4, { color: UI.accent, children: entry.key === cursor ? "\u203A" : " " }) }),
5642
+ /* @__PURE__ */ jsx5(Box4, { width: 8, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text4, { color: UI.text, bold: true, wrap: "truncate", children: entry.ticket.key }) }),
5643
+ /* @__PURE__ */ jsx5(
5644
+ Text4,
5645
+ {
5646
+ color: entry.ticket.stuck ? UI.warn : UI.text,
5647
+ inverse: entry.key === cursor,
5648
+ wrap: "truncate",
5649
+ children: truncate(entry.ticket.title, title - 2)
5650
+ }
5651
+ )
5652
+ ] }, entry.key)
5653
+ )
5654
+ ] });
5655
+ }
5656
+ function AgentsColumn({
5657
+ board,
5658
+ width,
5659
+ rows
5660
+ }) {
5661
+ const ordered = [...board.agents].sort((left, right) => {
5662
+ const busy = (id) => board.runs.some((run5) => run5.agent_id === id && run5.status === "running") ? 0 : 1;
5663
+ return busy(left.id) - busy(right.id) || left.display_name.localeCompare(right.display_name);
5664
+ });
5665
+ const shown = ordered.slice(0, contentRows(rows, ordered.length));
5666
+ return /* @__PURE__ */ jsx5(BoundedPanel, { width, rows, children: [
5667
+ /* @__PURE__ */ jsx5(Heading, { text: "Agents", note: `${board.agents.filter((a) => a.enabled).length} on` }, "h"),
5668
+ ...board.agents.length === 0 ? [
5669
+ /* @__PURE__ */ jsx5(Text4, { color: UI.dim, children: "No agents yet." }, "empty")
5670
+ ] : [],
5671
+ ...shown.map((agent) => {
5672
+ const run5 = board.runs.find((row) => row.agent_id === agent.id && row.status === "running");
5673
+ const ticket = run5?.ticket_id ? board.tickets.find((item) => item.id === run5.ticket_id) : void 0;
5674
+ const availability = board.availability.find((row) => row.provider === agent.provider);
5675
+ const blocked = availability && !availability.available ? availabilityLabel(availability) : "";
5676
+ const tone2 = !agent.enabled ? "muted" : run5 ? "blue" : blocked ? "warning" : "muted";
5677
+ const name = Math.max(8, Math.min(22, width - 14));
5678
+ return /* @__PURE__ */ jsxs4(Box4, { flexWrap: "nowrap", children: [
5679
+ /* @__PURE__ */ jsxs4(Text4, { color: inkColor(tone2), children: [
5680
+ DOT2,
5681
+ " "
5682
+ ] }),
5683
+ /* @__PURE__ */ jsx5(Box4, { width: name, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text4, { color: UI.text, wrap: "truncate", children: truncate(agent.display_name, name - 1) }) }),
5684
+ /* @__PURE__ */ jsx5(Text4, { color: UI.dim, wrap: "truncate", children: run5 ? `${ticket ? `${ticket.key} ` : ""}${elapsed(run5.started_at ?? run5.created_at)}` : blocked || (agent.enabled ? "idle" : "off") })
5685
+ ] }, agent.id);
5686
+ }),
5687
+ /* @__PURE__ */ jsx5(More, { count: ordered.length - shown.length }, "more")
5688
+ ] });
5689
+ }
5690
+ function StreamPanel({
5691
+ lines,
5692
+ width,
5693
+ rows,
5694
+ live
5695
+ }) {
5696
+ const name = Math.max(8, Math.min(18, Math.round(width / 5)));
5697
+ const budget = Math.max(0, rows - 1);
5698
+ return /* @__PURE__ */ jsx5(BoundedPanel, { width, rows, children: [
5699
+ /* @__PURE__ */ jsx5(Heading, { text: "Activity", note: live ? "" : "idle" }, "h"),
5700
+ ...lines.length === 0 ? [
5701
+ /* @__PURE__ */ jsx5(Text4, { color: UI.dim, children: live ? "Waiting for the first step." : "Nothing running." }, "empty")
5702
+ ] : [],
5703
+ ...lines.slice(-budget).map((line) => /* @__PURE__ */ jsxs4(Box4, { flexWrap: "nowrap", children: [
5704
+ /* @__PURE__ */ jsx5(Box4, { width: name, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text4, { color: UI.dim, wrap: "truncate", children: truncate(line.agent, name - 1) }) }),
5705
+ /* @__PURE__ */ jsxs4(Text4, { color: KIND_COLOR[line.kind], wrap: "truncate", children: [
5706
+ line.kind === "tool" ? "\xB7 " : "",
5707
+ truncate(line.title, Math.max(12, width - name - 3))
5708
+ ] })
5709
+ ] }, line.id))
5710
+ ] });
5711
+ }
5712
+ function epicsRows(board, cap = 3) {
5713
+ const open = epicProgress(board).filter((row) => row.epic.status !== "done");
5714
+ return open.length === 0 ? 0 : Math.min(open.length, cap) + 1;
5715
+ }
5716
+ function EpicsStrip({ board, width, rows }) {
5717
+ const open = epicProgress(board).filter((row) => row.epic.status !== "done");
5718
+ if (open.length === 0 || rows < 2) return null;
5719
+ const shown = open.slice(0, contentRows(rows, open.length));
5720
+ const title = Math.max(12, Math.min(52, width - 24));
5721
+ return /* @__PURE__ */ jsx5(BoundedPanel, { width, rows, children: [
5722
+ /* @__PURE__ */ jsx5(Heading, { text: "Epics", note: `${open.length}` }, "h"),
5723
+ ...shown.map((row) => /* @__PURE__ */ jsxs4(Box4, { flexWrap: "nowrap", children: [
5724
+ /* @__PURE__ */ jsx5(Box4, { width: title, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text4, { color: UI.text, wrap: "truncate", children: truncate(row.epic.title, title - 1) }) }),
5725
+ /* @__PURE__ */ jsx5(Text4, { color: UI.dim, wrap: "truncate", children: epicProgressCaption(row.merged, row.total, row.cancelled) })
5726
+ ] }, row.epic.id)),
5727
+ /* @__PURE__ */ jsx5(More, { count: open.length - shown.length }, "more")
5728
+ ] });
5729
+ }
5730
+ function Cockpit({
5731
+ board,
5732
+ width,
5733
+ rows,
5734
+ cursor
5735
+ }) {
5736
+ const epics = Math.min(epicsRows(board), Math.max(0, rows - 4));
5737
+ const rest = rows - (epics > 0 ? epics + 1 : 0);
5738
+ const columns = renderColumns(board, width, rest, cursor);
5739
+ if (epics === 0) return columns;
5740
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
5741
+ /* @__PURE__ */ jsx5(EpicsStrip, { board, width, rows: epics }),
5742
+ /* @__PURE__ */ jsx5(Box4, { height: 1 }),
5743
+ columns
5744
+ ] });
5745
+ }
5746
+ function renderColumns(board, width, rows, cursor) {
5747
+ const split = splitWidths(width);
5748
+ if (!split) {
5749
+ const agents = Math.max(1, Math.min(board.agents.length + 1, Math.floor(rows / 3)));
5750
+ const boardRows = Math.max(0, rows - agents - 1);
5751
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
5752
+ boardRows > 0 ? /* @__PURE__ */ jsx5(BoardColumn, { board, width, rows: boardRows, cursor }) : null,
5753
+ boardRows > 0 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
5754
+ /* @__PURE__ */ jsx5(AgentsColumn, { board, width, rows: agents })
5755
+ ] });
5756
+ }
5757
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", flexWrap: "nowrap", children: [
5758
+ /* @__PURE__ */ jsx5(BoardColumn, { board, width: split.board, rows, cursor }),
5759
+ /* @__PURE__ */ jsx5(Box4, { width: 1, flexShrink: 0 }),
5760
+ /* @__PURE__ */ jsx5(AgentsColumn, { board, width: split.agents, rows })
5761
+ ] });
5762
+ }
5763
+ var DOT2, SPLIT_AT, KIND_COLOR;
5764
+ var init_Dashboard = __esm({
5765
+ "src/tui/Dashboard.tsx"() {
5766
+ "use strict";
5767
+ init_src();
5768
+ init_format();
5769
+ init_theme();
5770
+ init_queries();
5771
+ init_theme2();
5772
+ init_bounded();
5773
+ DOT2 = "\u25CF";
5774
+ SPLIT_AT = 96;
5775
+ KIND_COLOR = {
5776
+ error: UI.danger,
5777
+ tool: UI.dim,
5778
+ status: UI.dim,
5779
+ text: UI.text
5780
+ };
5781
+ }
5782
+ });
5783
+
5784
+ // src/tui/Settings.tsx
5785
+ import "react";
5786
+ import { Text as Text5 } from "ink";
5787
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
5788
+ function SettingsPanel({
5789
+ entries,
5790
+ width,
5791
+ rows,
5792
+ cursor,
5793
+ editing
5794
+ }) {
5795
+ const inner = Math.max(0, rows - 1);
5796
+ const focus = cursor ? entries.findIndex((row) => row.key === cursor) : -1;
5797
+ const { start, end } = scrollWindow(entries.length, inner, focus);
5798
+ const label = Math.max(10, Math.min(18, Math.round(width / 4)));
5799
+ return /* @__PURE__ */ jsx6(BoundedPanel, { width, rows, children: [
5800
+ /* @__PURE__ */ jsx6(Heading, { text: "Settings", note: hidden(entries.length, start, end) }, "h"),
5801
+ ...entries.slice(start, end).map((row) => {
5802
+ if (row.kind === "heading") {
5803
+ return /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
5804
+ /* @__PURE__ */ jsx6(Text5, { color: UI.text, bold: true, children: row.label }),
5805
+ /* @__PURE__ */ jsx6(Text5, { color: UI.dim, children: ` ${row.value}` })
5806
+ ] }, row.key);
5807
+ }
5808
+ const selected = row.key === cursor;
5809
+ const typing = editing?.key === row.key;
5810
+ return /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
5811
+ /* @__PURE__ */ jsx6(Text5, { color: UI.accent, children: selected ? " \u203A " : " " }),
5812
+ /* @__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)) })
5814
+ ] }, row.key);
5815
+ })
5816
+ ] });
5817
+ }
5818
+ function hidden(count, start, end) {
5819
+ const above = start;
5820
+ const below = count - end;
5821
+ if (above === 0 && below === 0) return `${count}`;
5822
+ return `${count} ${above > 0 ? `${above}\u2191 ` : ""}${below > 0 ? `${below}\u2193` : ""}`.trimEnd();
5823
+ }
5824
+ var init_Settings = __esm({
5825
+ "src/tui/Settings.tsx"() {
5826
+ "use strict";
5827
+ init_format();
5828
+ init_bounded();
5829
+ init_Dashboard();
5830
+ init_theme2();
5831
+ }
5832
+ });
5833
+
5834
+ // src/tui/settings-model.ts
5835
+ function capLabel(value) {
5836
+ return value == null ? UNLIMITED : String(value);
5837
+ }
5838
+ function settingsRows(workspace, agents) {
5839
+ const caps = workspace.provider_caps ?? {};
5840
+ const rows = [
5841
+ { key: "h:workspace", kind: "heading", label: workspace.slug, value: workspace.repo },
5842
+ { key: "w:branch", kind: "text", label: "branch", value: workspace.default_branch },
5843
+ { key: "w:host", kind: "text", label: "host", value: workspace.default_host },
5844
+ {
5845
+ key: "w:auto_merge",
5846
+ kind: "toggle",
5847
+ label: "auto merge",
5848
+ value: workspace.auto_merge ? "yes" : "no"
5849
+ }
5850
+ ];
5851
+ for (const provider of providers) {
5852
+ const cap = caps[provider];
5853
+ rows.push({
5854
+ key: `w:cap:${provider}`,
5855
+ kind: "number",
5856
+ label: `cap ${provider}`,
5857
+ value: typeof cap === "number" ? String(cap) : "0",
5858
+ hint: "a whole number, 0 to stop using it"
5859
+ });
5860
+ }
5861
+ for (const agent of agents) {
5862
+ rows.push({
5863
+ key: `h:${agent.id}`,
5864
+ kind: "heading",
5865
+ label: agent.display_name,
5866
+ value: agent.role
5867
+ });
5868
+ rows.push({
5869
+ key: `a:${agent.id}:enabled`,
5870
+ kind: "toggle",
5871
+ label: "enabled",
5872
+ value: agent.enabled ? "yes" : "no",
5873
+ agent: agent.display_name
5874
+ });
5875
+ rows.push({
5876
+ key: `a:${agent.id}:provider`,
5877
+ kind: "choice",
5878
+ label: "provider",
5879
+ value: agent.provider,
5880
+ choices: providers,
5881
+ agent: agent.display_name
5882
+ });
5883
+ rows.push({
5884
+ key: `a:${agent.id}:model`,
5885
+ kind: "text",
5886
+ label: "model",
5887
+ value: agent.model,
5888
+ agent: agent.display_name
5889
+ });
5890
+ rows.push({
5891
+ key: `a:${agent.id}:effort`,
5892
+ kind: "choice",
5893
+ label: "effort",
5894
+ value: asEffort(agent.effort),
5895
+ choices: efforts,
5896
+ agent: agent.display_name
5897
+ });
5898
+ rows.push({
5899
+ key: `a:${agent.id}:runs_per_hour`,
5900
+ kind: "number",
5901
+ label: "runs per hour",
5902
+ value: capLabel(agent.runs_per_hour),
5903
+ agent: agent.display_name,
5904
+ hint: `a number, or ${UNLIMITED}`
5905
+ });
5906
+ rows.push({
5907
+ key: `a:${agent.id}:daily_spend_usd`,
5908
+ kind: "number",
5909
+ label: "usd per day",
5910
+ value: capLabel(agent.daily_spend_usd),
5911
+ agent: agent.display_name,
5912
+ hint: `a number, or ${UNLIMITED}`
5913
+ });
5914
+ rows.push({
5915
+ key: `a:${agent.id}:routing_notes`,
5916
+ kind: "text",
5917
+ label: "routing notes",
5918
+ value: agent.routing_notes,
5919
+ agent: agent.display_name
5920
+ });
5921
+ }
5922
+ return rows;
5923
+ }
5924
+ function editableKeys(rows) {
5925
+ return rows.filter((row) => row.kind !== "heading" && row.kind !== "readonly").map((row) => row.key);
5926
+ }
5927
+ function nextValue(row) {
5928
+ if (row.kind === "toggle") return row.value === "yes" ? "no" : "yes";
5929
+ if (row.kind === "choice" && row.choices?.length) {
5930
+ const at = row.choices.indexOf(row.value);
5931
+ return row.choices[(at + 1) % row.choices.length];
5932
+ }
5933
+ return null;
5934
+ }
5935
+ function seedFor(row) {
5936
+ if (row.value === UNLIMITED || row.value === "-" || row.value === "") return "";
5937
+ return row.value;
5938
+ }
5939
+ function editFor(row, raw) {
5940
+ const value = raw.trim();
5941
+ const [scope, id, field] = row.key.split(":");
5942
+ if (scope === "w") {
5943
+ if (id === "branch") return ok2({ target: "workspace", input: { branch: value } });
5944
+ if (id === "host") return ok2({ target: "workspace", input: { host: value } });
5945
+ if (id === "auto_merge") return ok2({ target: "workspace", input: { autoMerge: value === "yes" } });
5946
+ if (id === "cap") {
5947
+ const cap = parseField(optionalPositiveInteger(`cap ${field}`), value === "0" ? "none" : value);
5948
+ if (!cap.ok) return no(cap.error);
5949
+ return ok2({
5950
+ target: "workspace",
5951
+ input: { caps: [{ provider: field, cap: cap.value ?? 0 }] }
5952
+ });
5953
+ }
5954
+ return no(`Nothing to change on ${row.label}.`);
5955
+ }
5956
+ const name = row.agent;
5957
+ if (scope !== "a" || !name) return no(`Nothing to change on ${row.label}.`);
5958
+ switch (field) {
5959
+ case "enabled":
5960
+ return ok2({ target: "agent", name, input: { enabled: value === "yes" } });
5961
+ case "provider": {
5962
+ const provider = parseField(oneOf("provider", providers), value);
5963
+ return provider.ok ? ok2({ target: "agent", name, input: { provider: provider.value } }) : no(provider.error);
5964
+ }
5965
+ case "model":
5966
+ return ok2({ target: "agent", name, input: { model: value } });
5967
+ case "effort": {
5968
+ const effort = parseField(oneOf("effort", efforts), value);
5969
+ return effort.ok ? ok2({ target: "agent", name, input: { effort: effort.value } }) : no(effort.error);
5970
+ }
5971
+ case "runs_per_hour": {
5972
+ const cap = parseField(optionalPositiveInteger("runs per hour"), unlimitedAsNone(value));
5973
+ return cap.ok ? ok2({ target: "agent", name, input: { runsPerHour: cap.value } }) : no(cap.error);
5974
+ }
5975
+ case "daily_spend_usd": {
5976
+ const cap = parseField(optionalPositiveNumber("usd per day"), unlimitedAsNone(value));
5977
+ return cap.ok ? ok2({ target: "agent", name, input: { dailySpend: cap.value } }) : no(cap.error);
5978
+ }
5979
+ case "routing_notes":
5980
+ return ok2({ target: "agent", name, input: { notes: raw } });
5981
+ default:
5982
+ return no(`Nothing to change on ${row.label}.`);
5983
+ }
5984
+ }
5985
+ function unlimitedAsNone(value) {
5986
+ return value.toLowerCase() === UNLIMITED || value === "" ? "none" : value;
5987
+ }
5988
+ var UNLIMITED;
5989
+ var init_settings_model = __esm({
5990
+ "src/tui/settings-model.ts"() {
5991
+ "use strict";
5992
+ init_src();
5993
+ init_argv_parsers();
5994
+ init_config2();
5995
+ UNLIMITED = "unlimited";
5996
+ }
5997
+ });
5998
+
5999
+ // src/tui/Panels.tsx
6000
+ import "react";
6001
+ import { Box as Box5, Text as Text6 } from "ink";
6002
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
6003
+ function AgentsPanel({
6004
+ board,
6005
+ width = 80,
6006
+ rows = 12
6007
+ }) {
6008
+ const shown = board.agents.slice(0, contentRows(rows, board.agents.length));
6009
+ return /* @__PURE__ */ jsx7(BoundedPanel, { width, rows, children: [
6010
+ /* @__PURE__ */ jsx7(Heading, { text: "Agents", note: `${board.agents.length}` }, "h"),
6011
+ ...board.agents.length === 0 ? [
6012
+ /* @__PURE__ */ jsx7(Text6, { color: UI.dim, children: "No agents yet." }, "empty")
6013
+ ] : [],
6014
+ ...shown.map((agent) => {
6015
+ const run5 = board.runs.find((row) => row.agent_id === agent.id && row.status === "running");
6016
+ const ticket = run5?.ticket_id ? board.tickets.find((item) => item.id === run5.ticket_id) : void 0;
6017
+ const availability = board.availability.find((row) => row.provider === agent.provider);
6018
+ const blocked = availability && !availability.available ? availabilityLabel(availability) : "";
6019
+ const tone2 = !agent.enabled ? "muted" : run5 ? "blue" : blocked ? "warning" : "muted";
6020
+ return (
6021
+ // One truncating line, with the columns padded inside it. A row of
6022
+ // fixed width boxes wraps once they add up to more than the
6023
+ // terminal, however the text inside them is set to wrap.
6024
+ /* @__PURE__ */ jsxs6(Text6, { wrap: "truncate", children: [
6025
+ /* @__PURE__ */ jsxs6(Text6, { color: inkColor(tone2), children: [
6026
+ DOT3,
6027
+ " "
6028
+ ] }),
6029
+ /* @__PURE__ */ jsx7(Text6, { color: UI.text, children: pad(truncate(agent.display_name, 13), 14) }),
6030
+ /* @__PURE__ */ jsx7(Text6, { color: UI.dim, children: pad(agent.role, 13) }),
6031
+ /* @__PURE__ */ jsx7(Text6, { color: UI.dim, children: pad(truncate(agent.model, 23), 24) }),
6032
+ /* @__PURE__ */ jsx7(Text6, { color: UI.text, children: pad(run5 ? "running" : blocked ? "blocked" : "idle", 9) }),
6033
+ /* @__PURE__ */ jsxs6(Text6, { color: UI.dim, children: [
6034
+ ticket ? `${ticket.key} ` : "",
6035
+ run5 ? elapsed(run5.started_at ?? run5.created_at) : blocked
6036
+ ] })
6037
+ ] }, agent.id)
6038
+ );
6039
+ }),
6040
+ /* @__PURE__ */ jsx7(More, { count: board.agents.length - shown.length }, "more")
6041
+ ] });
6042
+ }
6043
+ function BoardPanel({
6044
+ board,
6045
+ width = 80,
6046
+ rows = 12,
6047
+ cursor
6048
+ }) {
6049
+ const active = BOARD_COLUMNS.filter(
6050
+ (status) => board.tickets.some((ticket) => ticket.status === status)
6051
+ );
6052
+ const budget = contentRows(rows, board.tickets.length + active.length);
6053
+ const lines = [];
6054
+ let dropped = 0;
6055
+ for (const status of active) {
6056
+ const tickets = board.tickets.filter((ticket) => ticket.status === status);
6057
+ if (lines.length + 1 >= budget) {
6058
+ dropped += tickets.length;
6059
+ continue;
6060
+ }
6061
+ lines.push(
6062
+ /* @__PURE__ */ jsxs6(Text6, { color: inkColor(statusTone(status)), wrap: "truncate", children: [
6063
+ statusLabel(status),
6064
+ " ",
6065
+ /* @__PURE__ */ jsxs6(Text6, { color: UI.dim, children: [
6066
+ "(",
6067
+ tickets.length,
6068
+ ")"
6069
+ ] })
6070
+ ] }, status)
6071
+ );
6072
+ for (const ticket of tickets) {
6073
+ if (lines.length >= budget) {
6074
+ dropped += 1;
6075
+ continue;
6076
+ }
6077
+ lines.push(
6078
+ /* @__PURE__ */ jsx7(TicketLine, { ticket, width, selected: ticket.id === cursor }, ticket.id)
6079
+ );
6080
+ }
6081
+ }
6082
+ return /* @__PURE__ */ jsx7(BoundedPanel, { width, rows, children: [
6083
+ /* @__PURE__ */ jsx7(Heading, { text: "Board", note: `${board.tickets.length}` }, "h"),
6084
+ ...board.tickets.length === 0 ? [
6085
+ /* @__PURE__ */ jsx7(Text6, { color: UI.dim, children: "No tickets yet." }, "empty")
6086
+ ] : lines,
6087
+ /* @__PURE__ */ jsx7(More, { count: dropped }, "more")
6088
+ ] });
6089
+ }
6090
+ function TicketLine({
6091
+ ticket,
6092
+ width,
6093
+ selected
6094
+ }) {
6095
+ return /* @__PURE__ */ jsxs6(Text6, { wrap: "truncate", children: [
6096
+ /* @__PURE__ */ jsx7(Text6, { color: selected ? UI.accent : UI.dim, children: selected ? " > " : " " }),
6097
+ /* @__PURE__ */ jsx7(Text6, { color: UI.text, bold: true, children: pad(ticket.key, 8) }),
6098
+ /* @__PURE__ */ jsx7(Text6, { color: UI.text, children: pad(truncate(ticket.title, 43), 44) }),
6099
+ /* @__PURE__ */ jsx7(Text6, { color: UI.dim, children: pad(ticket.agent_name ?? "", 14) }),
6100
+ ticket.stuck ? /* @__PURE__ */ jsx7(Text6, { color: UI.warn, children: truncate(ticket.stuck, Math.max(8, width - 70)) }) : null
6101
+ ] });
6102
+ }
6103
+ function FeedPanel({
6104
+ entries,
6105
+ width = 80,
6106
+ rows = 12
6107
+ }) {
6108
+ const shown = entries.slice(0, contentRows(rows, entries.length));
6109
+ return /* @__PURE__ */ jsx7(BoundedPanel, { width, rows, children: [
6110
+ /* @__PURE__ */ jsx7(Heading, { text: "Feed", note: `${entries.length}` }, "h"),
6111
+ ...entries.length === 0 ? [
6112
+ /* @__PURE__ */ jsx7(Text6, { color: UI.dim, children: "Nothing yet." }, "empty")
6113
+ ] : [],
6114
+ ...shown.map((row, index) => /* @__PURE__ */ jsxs6(Text6, { wrap: "truncate", children: [
6115
+ /* @__PURE__ */ jsx7(Text6, { color: UI.dim, children: pad(relativeTime(row.at), 10) }),
6116
+ /* @__PURE__ */ jsx7(Text6, { color: UI.dim, children: pad(row.kind ?? "", 18) }),
6117
+ /* @__PURE__ */ jsx7(Text6, { color: UI.text, children: truncate(row.summary ?? "", Math.max(8, width - 30)) })
6118
+ ] }, row.id ?? index)),
6119
+ /* @__PURE__ */ jsx7(More, { count: entries.length - shown.length }, "more")
6120
+ ] });
6121
+ }
6122
+ function InboxPanel({
6123
+ board,
6124
+ width = 80,
6125
+ rows = 12
6126
+ }) {
6127
+ const shown = board.decisions.slice(0, contentRows(rows, board.decisions.length));
6128
+ return /* @__PURE__ */ jsx7(BoundedPanel, { width, rows, children: [
6129
+ /* @__PURE__ */ jsx7(Heading, { text: "Decisions", note: `${board.decisions.length}` }, "h"),
6130
+ ...board.decisions.length === 0 ? [
6131
+ /* @__PURE__ */ jsx7(Text6, { color: UI.dim, children: "Nothing waiting on you." }, "empty")
6132
+ ] : [],
6133
+ ...shown.map((decision, index) => {
6134
+ const ticket = board.tickets.find((row) => row.id === decision.ticket_id);
6135
+ return /* @__PURE__ */ jsxs6(Text6, { wrap: "truncate", children: [
6136
+ /* @__PURE__ */ jsx7(Text6, { color: index === 0 ? UI.warn : UI.dim, children: pad(`${index + 1})`, 3) }),
6137
+ /* @__PURE__ */ jsx7(Text6, { color: UI.dim, children: pad(ticket?.key ?? decision.id.slice(0, 8), 9) }),
6138
+ /* @__PURE__ */ jsx7(Text6, { color: UI.text, children: truncate(decision.question_md, Math.max(12, width - 14)) })
6139
+ ] }, decision.id);
6140
+ }),
6141
+ /* @__PURE__ */ jsx7(More, { count: board.decisions.length - shown.length }, "more")
6142
+ ] });
6143
+ }
6144
+ function TicketPanel({
6145
+ ticket,
6146
+ width = 80,
6147
+ rows = 24
6148
+ }) {
6149
+ const lines = [
6150
+ /* @__PURE__ */ jsxs6(Text6, { color: UI.text, bold: true, wrap: "truncate", children: [
6151
+ ticket.key,
6152
+ " ",
6153
+ ticket.title
6154
+ ] }, "title"),
6155
+ /* @__PURE__ */ jsxs6(Box5, { flexWrap: "nowrap", children: [
6156
+ /* @__PURE__ */ jsx7(Text6, { color: inkColor(statusTone(ticket.status)), wrap: "truncate", children: statusLabel(ticket.status) }),
6157
+ /* @__PURE__ */ jsxs6(Text6, { color: UI.dim, wrap: "truncate", children: [
6158
+ ticket.area ? ` ${ticket.area}` : "",
6159
+ ticket.agent_name ? ` ${ticket.agent_name}` : " unassigned",
6160
+ ticket.attempts ? ` attempt ${ticket.attempts}` : ""
6161
+ ] })
6162
+ ] }, "status")
6163
+ ];
6164
+ if (ticket.stuck) {
6165
+ lines.push(
6166
+ /* @__PURE__ */ jsxs6(Text6, { color: UI.warn, wrap: "truncate", children: [
6167
+ "why: ",
6168
+ ticket.stuck
6169
+ ] }, "stuck")
6170
+ );
6171
+ }
6172
+ if (ticket.blocker_keys.length) {
6173
+ lines.push(
6174
+ /* @__PURE__ */ jsxs6(Text6, { color: UI.dim, wrap: "truncate", children: [
6175
+ "blocked by ",
6176
+ ticket.blocker_keys.join(", ")
6177
+ ] }, "blocked")
6178
+ );
6179
+ }
6180
+ if (ticket.pr_url) {
6181
+ lines.push(
6182
+ /* @__PURE__ */ jsx7(Text6, { color: UI.accent, wrap: "truncate", children: ticket.pr_url }, "pr")
6183
+ );
6184
+ }
6185
+ const drawable = Math.max(0, rows);
6186
+ const body = clipToRows(
6187
+ ticket.body_md.trim(),
6188
+ Math.max(20, width),
6189
+ Math.max(0, drawable - lines.length - 1)
6190
+ );
6191
+ if (body) {
6192
+ lines.push(/* @__PURE__ */ jsx7(Box5, { height: 1 }, "gap"));
6193
+ lines.push(
6194
+ /* @__PURE__ */ jsx7(Text6, { color: UI.text, wrap: "wrap", children: body }, "body")
6195
+ );
6196
+ }
6197
+ return /* @__PURE__ */ jsx7(BoundedPanel, { width, rows: drawable, children: lines });
6198
+ }
6199
+ function clipToRows(text, width, rows) {
6200
+ if (rows <= 0 || !text || width <= 0) return "";
6201
+ const out2 = [];
6202
+ let dropped = false;
6203
+ for (const paragraph of text.split("\n")) {
6204
+ if (out2.length >= rows) {
6205
+ dropped = true;
6206
+ break;
6207
+ }
6208
+ let line = "";
6209
+ for (const word of paragraph.split(/\s+/).filter(Boolean)) {
6210
+ const candidate = line ? `${line} ${word}` : word;
6211
+ if (candidate.length <= width) {
6212
+ line = candidate;
6213
+ continue;
6214
+ }
6215
+ if (line) {
6216
+ out2.push(line);
6217
+ line = "";
6218
+ if (out2.length >= rows) break;
6219
+ }
6220
+ let rest = word;
6221
+ while (rest.length > width) {
6222
+ if (out2.length >= rows) break;
6223
+ out2.push(rest.slice(0, width));
6224
+ rest = rest.slice(width);
6225
+ }
6226
+ if (out2.length >= rows) break;
6227
+ line = rest;
6228
+ }
6229
+ if (out2.length >= rows) {
6230
+ dropped = true;
6231
+ break;
6232
+ }
6233
+ out2.push(line);
6234
+ }
6235
+ if (out2.length > rows) {
6236
+ out2.length = rows;
6237
+ dropped = true;
6238
+ }
6239
+ if (dropped && out2.length > 0) {
6240
+ const last = out2[out2.length - 1];
6241
+ out2[out2.length - 1] = last.length >= width ? `${last.slice(0, width - 1)}\u2026` : `${last}\u2026`;
6242
+ }
6243
+ return out2.join("\n");
4922
6244
  }
4923
- var init_Bubble = __esm({
4924
- "src/tui/Bubble.tsx"() {
6245
+ var DOT3;
6246
+ var init_Panels = __esm({
6247
+ "src/tui/Panels.tsx"() {
4925
6248
  "use strict";
6249
+ init_src();
6250
+ init_format();
6251
+ init_theme();
4926
6252
  init_theme2();
6253
+ init_bounded();
6254
+ DOT3 = "\u25CF";
4927
6255
  }
4928
6256
  });
4929
6257
 
4930
- // src/tui/Help.tsx
6258
+ // src/tui/Decision.tsx
4931
6259
  import "react";
4932
- import { Box as Box3, Text as Text3 } from "ink";
4933
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
4934
- function Help({ width }) {
4935
- return /* @__PURE__ */ jsxs2(
4936
- Box3,
6260
+ import { Box as Box6, Text as Text7 } from "ink";
6261
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
6262
+ function decisionRows(decisions, cap = 9) {
6263
+ if (decisions.length === 0) return 0;
6264
+ return Math.min(cap, DECISION_CHROME + 2 + decisionOptions(decisions[0]).length);
6265
+ }
6266
+ function DecisionPanel({
6267
+ decisions,
6268
+ board,
6269
+ width,
6270
+ rows
6271
+ }) {
6272
+ if (decisions.length === 0) return null;
6273
+ const decision = decisions[0];
6274
+ const ticket = board?.tickets.find((row) => row.id === decision.ticket_id);
6275
+ const options = decisionOptions(decision);
6276
+ const inner = Math.max(8, width - 4);
6277
+ const forBody = rows - DECISION_CHROME;
6278
+ if (forBody < 1) {
6279
+ return /* @__PURE__ */ jsxs7(Box6, { width, flexWrap: "nowrap", children: [
6280
+ /* @__PURE__ */ jsx8(Text7, { color: UI.warn, bold: true, wrap: "truncate", children: decisions.length > 1 ? `${decisions.length} decisions waiting` : "Decision waiting" }),
6281
+ /* @__PURE__ */ jsx8(Text7, { color: UI.dim, wrap: "truncate", children: ` ${truncate(decision.question_md, Math.max(8, width - 26))} /decide` })
6282
+ ] });
6283
+ }
6284
+ const optionRows = Math.min(options.length, Math.max(0, forBody - 1));
6285
+ const question = clipToRows(decision.question_md.trim(), inner, forBody - optionRows);
6286
+ return /* @__PURE__ */ jsxs7(
6287
+ Box6,
4937
6288
  {
4938
6289
  borderStyle: "single",
4939
- borderColor: UI.text,
6290
+ borderColor: UI.warn,
4940
6291
  flexDirection: "column",
4941
- paddingX: 2,
4942
- paddingY: 1,
6292
+ paddingX: 1,
4943
6293
  width,
6294
+ marginBottom: 1,
4944
6295
  children: [
4945
- /* @__PURE__ */ jsx3(Text3, { color: UI.text, bold: true, children: "Commands" }),
4946
- /* @__PURE__ */ jsx3(Box3, { height: 1 }),
4947
- COMMANDS.map((command) => /* @__PURE__ */ jsxs2(Box3, { children: [
4948
- /* @__PURE__ */ jsx3(Box3, { width: 18, children: /* @__PURE__ */ jsxs2(Text3, { color: UI.text, children: [
4949
- command.name,
4950
- command.args ? ` ${command.args}` : ""
6296
+ /* @__PURE__ */ jsxs7(Box6, { flexWrap: "nowrap", children: [
6297
+ /* @__PURE__ */ jsx8(Text7, { color: UI.warn, bold: true, wrap: "truncate", children: decisions.length > 1 ? `Decision 1 of ${decisions.length}` : "Decision" }),
6298
+ /* @__PURE__ */ jsxs7(Text7, { color: UI.dim, wrap: "truncate", children: [
6299
+ ticket ? ` ${ticket.key}` : "",
6300
+ ` asked by ${decision.asked_by_role}`,
6301
+ ` ${relativeTime(decision.created_at)}`
6302
+ ] })
6303
+ ] }),
6304
+ /* @__PURE__ */ jsx8(Text7, { color: UI.text, wrap: "wrap", children: question }),
6305
+ options.slice(0, optionRows).map((option, index) => /* @__PURE__ */ jsxs7(Box6, { flexWrap: "nowrap", children: [
6306
+ /* @__PURE__ */ jsx8(Box6, { width: 3, flexShrink: 0, children: /* @__PURE__ */ jsxs7(Text7, { color: UI.accent, children: [
6307
+ index + 1,
6308
+ ")"
4951
6309
  ] }) }),
4952
- /* @__PURE__ */ jsx3(Text3, { color: UI.dim, children: command.help })
4953
- ] }, command.name)),
4954
- /* @__PURE__ */ jsx3(Box3, { height: 1 }),
4955
- /* @__PURE__ */ jsx3(Text3, { color: UI.dim, children: "Anything not starting with / goes to whoever you are talking to. Ctrl-C leaves." })
6310
+ /* @__PURE__ */ jsx8(Text7, { color: UI.text, wrap: "truncate", children: truncate(option, Math.max(8, inner - 4)) })
6311
+ ] }, index)),
6312
+ /* @__PURE__ */ jsx8(Text7, { color: UI.dim, wrap: "truncate", children: options.length ? `/decide 1 to ${options.length}, or /decide <your answer>, or /decide --skip` : "/decide <your answer>, or /decide --skip" })
4956
6313
  ]
4957
6314
  }
4958
6315
  );
4959
6316
  }
4960
- var COMMANDS;
4961
- var init_Help = __esm({
4962
- "src/tui/Help.tsx"() {
6317
+ var DECISION_CHROME;
6318
+ var init_Decision = __esm({
6319
+ "src/tui/Decision.tsx"() {
4963
6320
  "use strict";
6321
+ init_format();
6322
+ init_Panels();
6323
+ init_commands3();
4964
6324
  init_theme2();
4965
- COMMANDS = [
4966
- { name: "/architect", help: "talk to your own model, which can do anything in the platform" },
4967
- { name: "/orchestrator", help: "talk to the agent that gets work in flight finished" },
4968
- { name: "/browse", help: "stop talking, look around" },
4969
- { name: "/workspace", args: "[slug]", help: "switch workspace, or list the ones you can reach" },
4970
- { name: "/board", help: "the kanban board" },
4971
- { name: "/agents", help: "every agent and what it is doing" },
4972
- { name: "/feed", help: "what just happened" },
4973
- { name: "/inbox", help: "decisions and messages waiting on you" },
4974
- { name: "/ticket", args: "HD-12", help: "open one ticket" },
4975
- { name: "/refresh", help: "reload the board now" },
4976
- { name: "/help", help: "this list" },
4977
- { name: "/exit", help: "leave" }
4978
- ];
6325
+ DECISION_CHROME = 5;
4979
6326
  }
4980
6327
  });
4981
6328
 
4982
- // src/tui/Panels.tsx
4983
- import "react";
4984
- import { Box as Box4, Text as Text4 } from "ink";
4985
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
4986
- function Panel({ title, children }) {
4987
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, children: [
4988
- /* @__PURE__ */ jsx4(Text4, { color: UI.text, bold: true, children: title }),
4989
- children
4990
- ] });
4991
- }
4992
- function AgentsPanel({ board }) {
4993
- return /* @__PURE__ */ jsx4(Panel, { title: "Agents", children: board.agents.map((agent) => {
4994
- const run5 = board.runs.find((row) => row.agent_id === agent.id && row.status === "running");
4995
- const ticket = run5?.ticket_id ? board.tickets.find((item) => item.id === run5.ticket_id) : void 0;
4996
- const availability = board.availability.find((row) => row.provider === agent.provider);
4997
- const blocked = availability && !availability.available ? availabilityLabel(availability) : "";
4998
- const tone2 = !agent.enabled ? "muted" : run5 ? "blue" : blocked ? "warning" : "muted";
4999
- return /* @__PURE__ */ jsxs3(Box4, { children: [
5000
- /* @__PURE__ */ jsxs3(Text4, { color: inkColor(tone2), children: [
5001
- DOT2,
5002
- " "
5003
- ] }),
5004
- /* @__PURE__ */ jsx4(Box4, { width: 14, children: /* @__PURE__ */ jsx4(Text4, { color: UI.text, children: truncate(agent.display_name, 13) }) }),
5005
- /* @__PURE__ */ jsx4(Box4, { width: 13, children: /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: agent.role }) }),
5006
- /* @__PURE__ */ jsx4(Box4, { width: 24, children: /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: truncate(agent.model, 23) }) }),
5007
- /* @__PURE__ */ jsx4(Box4, { width: 9, children: /* @__PURE__ */ jsx4(Text4, { color: UI.text, children: run5 ? "running" : blocked ? "blocked" : "idle" }) }),
5008
- /* @__PURE__ */ jsxs3(Text4, { color: UI.dim, children: [
5009
- ticket ? `${ticket.key} ` : "",
5010
- run5 ? elapsed(run5.started_at ?? run5.created_at) : blocked
5011
- ] })
5012
- ] }, agent.id);
5013
- }) });
6329
+ // src/tui/alert.ts
6330
+ function alertOnce(stream = process.stdout, env = process.env) {
6331
+ if (env.HIGHERDEV_NO_BELL) return false;
6332
+ if (!stream.isTTY) return false;
6333
+ stream.write(BELL);
6334
+ return true;
6335
+ }
6336
+ var BELL;
6337
+ var init_alert = __esm({
6338
+ "src/tui/alert.ts"() {
6339
+ "use strict";
6340
+ BELL = String.fromCharCode(7);
6341
+ }
6342
+ });
6343
+
6344
+ // src/tui/stream.ts
6345
+ function toStreamLines(events, runs) {
6346
+ const lines = [];
6347
+ for (const event of events) {
6348
+ const run5 = runs.get(event.run_id);
6349
+ if (!run5) continue;
6350
+ const produced = transcriptLines([{ id: event.id, type: event.type, payload: event.payload }]);
6351
+ produced.forEach((line, index) => {
6352
+ lines.push({
6353
+ id: `${event.id}:${index}`,
6354
+ runId: event.run_id,
6355
+ agent: run5.agent,
6356
+ at: String(event.at),
6357
+ seq: event.seq,
6358
+ kind: line.kind,
6359
+ title: line.title
6360
+ });
6361
+ });
6362
+ }
6363
+ return lines;
5014
6364
  }
5015
- function BoardPanel({ board, cursor }) {
5016
- const active = BOARD_COLUMNS.filter(
5017
- (status) => board.tickets.some((ticket) => ticket.status === status)
6365
+ function appendLines(prior, incoming, limit = STREAM_LIMIT) {
6366
+ if (incoming.length === 0) return prior;
6367
+ const seen = new Set(prior.map((line) => line.id));
6368
+ const fresh = incoming.filter((line) => !seen.has(line.id));
6369
+ if (fresh.length === 0) return prior;
6370
+ const next = [...prior, ...fresh].sort(
6371
+ (left, right) => left.at.localeCompare(right.at) || left.seq - right.seq
5018
6372
  );
5019
- if (active.length === 0) {
5020
- return /* @__PURE__ */ jsx4(Panel, { title: "Board", children: /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: "No tickets yet." }) });
5021
- }
5022
- return /* @__PURE__ */ jsx4(Panel, { title: "Board", children: active.map((status) => {
5023
- const rows = board.tickets.filter((ticket) => ticket.status === status);
5024
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, children: [
5025
- /* @__PURE__ */ jsxs3(Text4, { color: inkColor(statusTone(status)), children: [
5026
- statusLabel(status),
5027
- " ",
5028
- /* @__PURE__ */ jsxs3(Text4, { color: UI.dim, children: [
5029
- "(",
5030
- rows.length,
5031
- ")"
5032
- ] })
5033
- ] }),
5034
- rows.map((ticket) => /* @__PURE__ */ jsx4(TicketLine, { ticket, selected: ticket.id === cursor }, ticket.id))
5035
- ] }, status);
5036
- }) });
5037
- }
5038
- function TicketLine({ ticket, selected }) {
5039
- return /* @__PURE__ */ jsxs3(Box4, { children: [
5040
- /* @__PURE__ */ jsx4(Text4, { color: selected ? UI.accent : UI.dim, children: selected ? " > " : " " }),
5041
- /* @__PURE__ */ jsx4(Box4, { width: 8, children: /* @__PURE__ */ jsx4(Text4, { color: UI.text, bold: true, children: ticket.key }) }),
5042
- /* @__PURE__ */ jsx4(Box4, { width: 44, children: /* @__PURE__ */ jsx4(Text4, { color: UI.text, children: truncate(ticket.title, 43) }) }),
5043
- /* @__PURE__ */ jsx4(Box4, { width: 14, children: /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: ticket.agent_name ?? "" }) }),
5044
- ticket.stuck ? /* @__PURE__ */ jsx4(Text4, { color: UI.warn, children: truncate(ticket.stuck, 30) }) : null
5045
- ] });
6373
+ return next.length > limit ? next.slice(next.length - limit) : next;
5046
6374
  }
5047
- function FeedPanel({
5048
- rows
5049
- }) {
5050
- return /* @__PURE__ */ jsxs3(Panel, { title: "Feed", children: [
5051
- rows.length === 0 ? /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: "Nothing yet." }) : null,
5052
- rows.slice(0, 12).map((row, index) => /* @__PURE__ */ jsxs3(Box4, { children: [
5053
- /* @__PURE__ */ jsx4(Box4, { width: 10, children: /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: relativeTime(row.at) }) }),
5054
- /* @__PURE__ */ jsx4(Box4, { width: 18, children: /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: row.kind ?? "" }) }),
5055
- /* @__PURE__ */ jsx4(Text4, { color: UI.text, children: truncate(row.summary ?? "", 60) })
5056
- ] }, row.id ?? index))
5057
- ] });
6375
+ async function backfill(opts) {
6376
+ if (opts.runIds.length === 0) return;
6377
+ const runs = new Map(opts.runs);
6378
+ let events;
6379
+ try {
6380
+ events = await opts.load(opts.runIds);
6381
+ } catch {
6382
+ return;
6383
+ }
6384
+ if (!opts.isCurrent()) return;
6385
+ const lines = toStreamLines(events, runs);
6386
+ if (lines.length === 0) return;
6387
+ opts.apply((prior) => appendLines(prior, lines));
5058
6388
  }
5059
- function InboxPanel({ board }) {
5060
- if (board.decisions.length === 0) {
5061
- return /* @__PURE__ */ jsx4(Panel, { title: "Inbox", children: /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: "Nothing waiting on you." }) });
6389
+ function runLabels(board) {
6390
+ const map = /* @__PURE__ */ new Map();
6391
+ for (const run5 of board.runs) {
6392
+ if (run5.status !== "running") continue;
6393
+ const agent = board.agents.find((row) => row.id === run5.agent_id);
6394
+ const ticket = board.tickets.find((row) => row.id === run5.ticket_id);
6395
+ map.set(run5.id, {
6396
+ runId: run5.id,
6397
+ agent: agent?.display_name ?? run5.kind,
6398
+ ticket: ticket?.key ?? null
6399
+ });
5062
6400
  }
5063
- return /* @__PURE__ */ jsx4(Panel, { title: `Decisions (${board.decisions.length})`, children: board.decisions.map((decision) => {
5064
- const ticket = board.tickets.find((row) => row.id === decision.ticket_id);
5065
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, children: [
5066
- /* @__PURE__ */ jsxs3(Text4, { color: UI.warn, children: [
5067
- decision.id.slice(0, 8),
5068
- ticket ? ` ${ticket.key}` : ""
5069
- ] }),
5070
- /* @__PURE__ */ jsx4(Text4, { color: UI.text, children: truncate(decision.question_md, 100) })
5071
- ] }, decision.id);
5072
- }) });
5073
- }
5074
- function EpicsPanel({ board }) {
5075
- const rows = epicProgress(board).filter((row) => row.epic.status !== "done");
5076
- if (rows.length === 0) return null;
5077
- return /* @__PURE__ */ jsx4(Panel, { title: "Epics", children: rows.map((row) => /* @__PURE__ */ jsxs3(Box4, { children: [
5078
- /* @__PURE__ */ jsx4(Box4, { width: 40, children: /* @__PURE__ */ jsx4(Text4, { color: UI.text, children: truncate(row.epic.title, 39) }) }),
5079
- /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: epicProgressCaption(row.merged, row.total, row.cancelled) })
5080
- ] }, row.epic.id)) });
5081
- }
5082
- function TicketPanel({ ticket }) {
5083
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, children: [
5084
- /* @__PURE__ */ jsxs3(Text4, { color: UI.text, bold: true, children: [
5085
- ticket.key,
5086
- " ",
5087
- ticket.title
5088
- ] }),
5089
- /* @__PURE__ */ jsxs3(Box4, { children: [
5090
- /* @__PURE__ */ jsx4(Text4, { color: inkColor(statusTone(ticket.status)), children: statusLabel(ticket.status) }),
5091
- /* @__PURE__ */ jsxs3(Text4, { color: UI.dim, children: [
5092
- ticket.area ? ` ${ticket.area}` : "",
5093
- ticket.agent_name ? ` ${ticket.agent_name}` : " unassigned",
5094
- ticket.attempts ? ` attempt ${ticket.attempts}` : ""
5095
- ] })
5096
- ] }),
5097
- ticket.stuck ? /* @__PURE__ */ jsxs3(Text4, { color: UI.warn, children: [
5098
- "why: ",
5099
- ticket.stuck
5100
- ] }) : null,
5101
- ticket.blocker_keys.length ? /* @__PURE__ */ jsxs3(Text4, { color: UI.dim, children: [
5102
- "blocked by ",
5103
- ticket.blocker_keys.join(", ")
5104
- ] }) : null,
5105
- ticket.pr_url ? /* @__PURE__ */ jsx4(Text4, { color: UI.accent, children: ticket.pr_url }) : null,
5106
- ticket.body_md.trim() ? /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: UI.text, wrap: "wrap", children: ticket.body_md.trim().slice(0, 1200) }) }) : null
5107
- ] });
6401
+ return map;
5108
6402
  }
5109
- var DOT2;
5110
- var init_Panels = __esm({
5111
- "src/tui/Panels.tsx"() {
6403
+ var STREAM_LIMIT;
6404
+ var init_stream = __esm({
6405
+ "src/tui/stream.ts"() {
5112
6406
  "use strict";
5113
6407
  init_src();
5114
- init_format();
5115
- init_theme();
5116
- init_queries();
6408
+ STREAM_LIMIT = 200;
6409
+ }
6410
+ });
6411
+
6412
+ // src/tui/Bubble.tsx
6413
+ import "react";
6414
+ import { Box as Box7, Text as Text8 } from "ink";
6415
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
6416
+ function Bubble({ message, width }) {
6417
+ const style = speakerStyle(message.speaker);
6418
+ const body = message.body.replace(/\s+$/, "");
6419
+ return /* @__PURE__ */ jsx9(Box7, { flexDirection: "column", marginBottom: 1, width, children: /* @__PURE__ */ jsxs8(
6420
+ Box7,
6421
+ {
6422
+ borderStyle: style.borderStyle,
6423
+ borderColor: style.borderColor,
6424
+ ...style.backgroundColor ? { backgroundColor: style.backgroundColor } : {},
6425
+ flexDirection: "column",
6426
+ paddingX: 1,
6427
+ children: [
6428
+ style.label ? /* @__PURE__ */ jsx9(Text8, { color: UI.text, bold: true, children: style.label }) : null,
6429
+ (message.steps ?? []).map((step, index) => /* @__PURE__ */ jsx9(Text8, { color: UI.dim, children: step }, index)),
6430
+ body ? /* @__PURE__ */ jsx9(Text8, { color: UI.text, wrap: "wrap", children: body }) : message.pending ? /* @__PURE__ */ jsx9(Text8, { color: UI.dim, children: "thinking\u2026" }) : null
6431
+ ]
6432
+ }
6433
+ ) });
6434
+ }
6435
+ var init_Bubble = __esm({
6436
+ "src/tui/Bubble.tsx"() {
6437
+ "use strict";
5117
6438
  init_theme2();
5118
- DOT2 = "\u25CF";
5119
6439
  }
5120
6440
  });
5121
6441
 
5122
6442
  // src/tui/TextInput.tsx
5123
- import { useEffect as useEffect2, useState as useState2 } from "react";
5124
- import { Text as Text5, useInput, usePaste } from "ink";
5125
- import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
6443
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
6444
+ import { Text as Text9, useInput, usePaste } from "ink";
6445
+ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
6446
+ function printableOf(text) {
6447
+ return text.replace(/[\x00-\x1f\x7f]/g, "");
6448
+ }
5126
6449
  function TextInput({
5127
6450
  value,
5128
6451
  onChange,
@@ -5136,12 +6459,19 @@ function TextInput({
5136
6459
  color
5137
6460
  }) {
5138
6461
  const [cursor, setCursor] = useState2(value.length);
6462
+ const ours = useRef2(value);
5139
6463
  useEffect2(() => {
5140
- setCursor((c2) => Math.min(c2, value.length));
6464
+ if (value === ours.current) return;
6465
+ ours.current = value;
6466
+ setCursor(value.length);
5141
6467
  }, [value]);
6468
+ const change = (next) => {
6469
+ ours.current = next;
6470
+ onChange(next);
6471
+ };
5142
6472
  const insert = (text) => {
5143
6473
  const next = value.slice(0, cursor) + text + value.slice(cursor);
5144
- onChange(next);
6474
+ change(next);
5145
6475
  setCursor(cursor + text.length);
5146
6476
  };
5147
6477
  usePaste((text) => {
@@ -5153,18 +6483,31 @@ function TextInput({
5153
6483
  if (key2.escape) return onCancel?.();
5154
6484
  if (key2.upArrow) return onUp?.();
5155
6485
  if (key2.downArrow) return onDown?.();
6486
+ if (input.length > 1 && /[\r\n]/.test(input)) {
6487
+ const parts = input.split(/\r\n|\r|\n/);
6488
+ const submits = parts.length === 2 && parts[1] === "";
6489
+ if (submits) {
6490
+ const next = value.slice(0, cursor) + printableOf(parts[0]) + value.slice(cursor);
6491
+ change(next);
6492
+ setCursor(next.length);
6493
+ return onSubmit?.(next);
6494
+ }
6495
+ const flat = printableOf(input.replace(/[\r\n]+/g, " "));
6496
+ change(value.slice(0, cursor) + flat + value.slice(cursor));
6497
+ return setCursor(cursor + flat.length);
6498
+ }
5156
6499
  if (key2.leftArrow) return setCursor((c2) => Math.max(0, c2 - 1));
5157
6500
  if (key2.rightArrow) return setCursor((c2) => Math.min(value.length, c2 + 1));
5158
6501
  if (key2.home) return setCursor(0);
5159
6502
  if (key2.end) return setCursor(value.length);
5160
6503
  if (key2.backspace) {
5161
6504
  if (cursor === 0) return;
5162
- onChange(value.slice(0, cursor - 1) + value.slice(cursor));
6505
+ change(value.slice(0, cursor - 1) + value.slice(cursor));
5163
6506
  return setCursor(cursor - 1);
5164
6507
  }
5165
6508
  if (key2.delete) {
5166
6509
  if (cursor >= value.length) return;
5167
- return onChange(value.slice(0, cursor) + value.slice(cursor + 1));
6510
+ return change(value.slice(0, cursor) + value.slice(cursor + 1));
5168
6511
  }
5169
6512
  if (key2.ctrl) {
5170
6513
  switch (input) {
@@ -5173,14 +6516,14 @@ function TextInput({
5173
6516
  case "e":
5174
6517
  return setCursor(value.length);
5175
6518
  case "u":
5176
- onChange(value.slice(cursor));
6519
+ change(value.slice(cursor));
5177
6520
  return setCursor(0);
5178
6521
  case "k":
5179
- return onChange(value.slice(0, cursor));
6522
+ return change(value.slice(0, cursor));
5180
6523
  case "w": {
5181
6524
  const head = value.slice(0, cursor);
5182
6525
  const trimmed = head.replace(/\s*\S*$/, "");
5183
- onChange(trimmed + value.slice(cursor));
6526
+ change(trimmed + value.slice(cursor));
5184
6527
  return setCursor(trimmed.length);
5185
6528
  }
5186
6529
  default:
@@ -5188,7 +6531,7 @@ function TextInput({
5188
6531
  }
5189
6532
  }
5190
6533
  if (key2.meta || !input) return;
5191
- const printable = input.replace(/[\x00-\x1f\x7f]/g, "");
6534
+ const printable = printableOf(input);
5192
6535
  if (printable) insert(printable);
5193
6536
  },
5194
6537
  { isActive }
@@ -5197,15 +6540,15 @@ function TextInput({
5197
6540
  const before = value.slice(0, cursor);
5198
6541
  const at = value[cursor] ?? " ";
5199
6542
  const after = value.slice(cursor + 1);
5200
- return /* @__PURE__ */ jsxs4(Text5, { children: [
6543
+ return /* @__PURE__ */ jsxs9(Text9, { children: [
5201
6544
  prompt2,
5202
- showPlaceholder ? /* @__PURE__ */ jsxs4(Fragment, { children: [
5203
- isActive ? /* @__PURE__ */ jsx5(Text5, { inverse: true, children: placeholder[0] }) : /* @__PURE__ */ jsx5(Text5, { children: placeholder[0] }),
5204
- /* @__PURE__ */ jsx5(Text5, { color: UI.dim, children: placeholder.slice(1) })
5205
- ] }) : /* @__PURE__ */ jsxs4(Fragment, { children: [
5206
- /* @__PURE__ */ jsx5(Text5, { color, children: before }),
5207
- isActive ? /* @__PURE__ */ jsx5(Text5, { inverse: true, color, children: at }) : /* @__PURE__ */ jsx5(Text5, { color, children: at === " " ? "" : at }),
5208
- /* @__PURE__ */ jsx5(Text5, { color, children: after })
6545
+ showPlaceholder ? /* @__PURE__ */ jsxs9(Fragment2, { children: [
6546
+ isActive ? /* @__PURE__ */ jsx10(Text9, { inverse: true, children: placeholder[0] }) : /* @__PURE__ */ jsx10(Text9, { children: placeholder[0] }),
6547
+ /* @__PURE__ */ jsx10(Text9, { color: UI.dim, children: placeholder.slice(1) })
6548
+ ] }) : /* @__PURE__ */ jsxs9(Fragment2, { children: [
6549
+ /* @__PURE__ */ jsx10(Text9, { color, children: before }),
6550
+ isActive ? /* @__PURE__ */ jsx10(Text9, { inverse: true, color, children: at }) : /* @__PURE__ */ jsx10(Text9, { color, children: at === " " ? "" : at }),
6551
+ /* @__PURE__ */ jsx10(Text9, { color, children: after })
5209
6552
  ] })
5210
6553
  ] });
5211
6554
  }
@@ -5234,13 +6577,42 @@ function parseLine(raw) {
5234
6577
  case "agents":
5235
6578
  case "feed":
5236
6579
  case "inbox":
5237
- case "help":
6580
+ case "settings":
5238
6581
  return { kind: "view", view: word.toLowerCase() };
6582
+ case "help":
6583
+ return { kind: "help" };
6584
+ case "agent": {
6585
+ const [verb, name, provider, model] = argument.split(/\s+/).filter(Boolean);
6586
+ if (verb?.toLowerCase() !== "new") {
6587
+ return { kind: "unknown", command: "agent, try /agent new <name> <provider> <model>" };
6588
+ }
6589
+ if (!name || !provider || !model) {
6590
+ return { kind: "unknown", command: "agent new needs a name, a provider and a model" };
6591
+ }
6592
+ return { kind: "agent-new", name, provider, model };
6593
+ }
5239
6594
  case "workspace":
5240
- case "ws":
6595
+ case "ws": {
6596
+ const parts = argument.split(/\s+/).filter(Boolean);
6597
+ if (parts[0]?.toLowerCase() === "new") {
6598
+ const [, name, repo] = parts;
6599
+ if (!name || !repo) {
6600
+ return { kind: "unknown", command: "workspace new needs a name and owner/repo" };
6601
+ }
6602
+ return { kind: "workspace-new", name, repo };
6603
+ }
5241
6604
  return { kind: "workspace", slug: argument || null };
6605
+ }
5242
6606
  case "ticket":
5243
6607
  return argument ? { kind: "ticket", key: argument.toUpperCase() } : { kind: "unknown", command: "ticket needs a key" };
6608
+ case "decide": {
6609
+ const dismiss = /(^|\s)--(skip|dismiss)(\s|$)/.test(argument);
6610
+ return {
6611
+ kind: "decide",
6612
+ answer: argument.replace(/(^|\s)--(skip|dismiss)(\s|$)/g, " ").trim(),
6613
+ dismiss
6614
+ };
6615
+ }
5244
6616
  case "refresh":
5245
6617
  return { kind: "refresh" };
5246
6618
  case "exit":
@@ -5287,9 +6659,9 @@ __export(App_exports, {
5287
6659
  App: () => App,
5288
6660
  COMMANDS: () => COMMANDS
5289
6661
  });
5290
- import { useCallback, useEffect as useEffect3, useMemo, useRef, useState as useState3 } from "react";
5291
- import { Box as Box5, Text as Text6, useApp, useInput as useInput2, useStdout } from "ink";
5292
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
6662
+ import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState3 } from "react";
6663
+ import { Box as Box8, Static, Text as Text10, useApp, useInput as useInput2, useStdout as useStdout2 } from "ink";
6664
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
5293
6665
  function App({
5294
6666
  ctx,
5295
6667
  workspace,
@@ -5297,8 +6669,9 @@ function App({
5297
6669
  brainLabel
5298
6670
  }) {
5299
6671
  const { exit } = useApp();
5300
- const { stdout } = useStdout();
6672
+ const { stdout } = useStdout2();
5301
6673
  const columns = stdout?.columns && stdout.columns > 0 ? stdout.columns : 80;
6674
+ const rows = stdout?.rows && stdout.rows > 0 ? stdout.rows : 24;
5302
6675
  const width = Math.max(48, Math.min(columns - 1, 120));
5303
6676
  const [current, setCurrent] = useState3(workspace);
5304
6677
  const [available, setAvailable] = useState3(workspaces);
@@ -5314,14 +6687,87 @@ function App({
5314
6687
  const [notice, setNotice] = useState3(null);
5315
6688
  const [ticketKey, setTicketKey] = useState3(null);
5316
6689
  const [ready, setReady] = useState3(false);
5317
- const workspaceCtx = useMemo(() => ({ ...ctx, workspace: current }), [ctx, current]);
5318
- const session = useRef(null);
5319
- const workspaceLoads = useRef(new WorkspaceLoads(current.id));
5320
- const history = useRef([]);
5321
- const historyAt = useRef(-1);
6690
+ const [stream, setStream] = useState3([]);
6691
+ const [cursor, setCursor] = useState3(null);
6692
+ const selectedRef = useRef3(null);
6693
+ const [started, setStarted] = useState3(false);
6694
+ useEffect3(() => {
6695
+ if (messages.length > 0 || view !== "home") setStarted(true);
6696
+ }, [messages.length, view]);
6697
+ const workspaceCtx = useMemo2(() => ({ ...ctx, workspace: current }), [ctx, current]);
6698
+ const session = useRef3(null);
6699
+ const workspaceLoads = useRef3(new WorkspaceLoads(current.id));
6700
+ const refreshRef = useRef3(null);
6701
+ const history = useRef3([]);
6702
+ const historyAt = useRef3(-1);
5322
6703
  const say = useCallback((speaker, body, steps) => {
5323
- setMessages((prior) => [...prior, { id: nextId(), speaker, body, steps }]);
6704
+ setMessages((prior) => [...prior, { id: nextId(), speaker, body, steps, done: true }]);
5324
6705
  }, []);
6706
+ const labels = useRef3(/* @__PURE__ */ new Map());
6707
+ labels.current = useMemo2(
6708
+ () => board ? runLabels(board) : /* @__PURE__ */ new Map(),
6709
+ [board]
6710
+ );
6711
+ const liveRunIds = [...labels.current.keys()].sort().join(",");
6712
+ const order = useMemo2(() => board ? boardTicketIds(board) : [], [board]);
6713
+ const settings = useMemo2(
6714
+ () => board ? settingsRows(current, board.agents) : [],
6715
+ [current, board]
6716
+ );
6717
+ const settingsOrder = useMemo2(() => editableKeys(settings), [settings]);
6718
+ const [field, setField] = useState3(null);
6719
+ const [editing, setEditing] = useState3(null);
6720
+ const fieldRef = useRef3(null);
6721
+ const editingRef = useRef3(null);
6722
+ editingRef.current = editing;
6723
+ const browsing = mode === "browse" && draft.length === 0 && cursor !== null;
6724
+ const configuring = view === "settings" && !editing;
6725
+ const moveField = useCallback(
6726
+ (delta) => {
6727
+ const next = nextCursor(settingsOrder, fieldRef.current, delta);
6728
+ if (!next) return false;
6729
+ fieldRef.current = next;
6730
+ setField(next);
6731
+ return true;
6732
+ },
6733
+ [settingsOrder]
6734
+ );
6735
+ const moveCursor = useCallback(
6736
+ (delta) => {
6737
+ if (order.length === 0) return false;
6738
+ const next = nextCursor(order, selectedRef.current, delta);
6739
+ if (!next) return false;
6740
+ selectedRef.current = next;
6741
+ setCursor(next);
6742
+ return true;
6743
+ },
6744
+ [order]
6745
+ );
6746
+ const applyEdit = useCallback(
6747
+ async (key2, raw) => {
6748
+ const row = settings.find((entry) => entry.key === key2);
6749
+ if (!row) return;
6750
+ const edit = editFor(row, raw);
6751
+ if (!edit.ok) {
6752
+ setNotice(edit.error);
6753
+ return;
6754
+ }
6755
+ setBusy(true);
6756
+ try {
6757
+ const result = edit.value.target === "workspace" ? await updateWorkspace(workspaceCtx, edit.value.input) : await updateAgent(workspaceCtx, edit.value.name, edit.value.input);
6758
+ if (!result.ok) {
6759
+ setNotice(result.error);
6760
+ return;
6761
+ }
6762
+ setNotice(null);
6763
+ if (edit.value.target === "workspace") setCurrent(result.value);
6764
+ await refreshRef.current?.();
6765
+ } finally {
6766
+ setBusy(false);
6767
+ }
6768
+ },
6769
+ [settings, workspaceCtx]
6770
+ );
5325
6771
  const refresh = useCallback(async () => {
5326
6772
  const loadToken = workspaceLoads.current.start(workspaceCtx.workspace.id);
5327
6773
  try {
@@ -5339,12 +6785,23 @@ function App({
5339
6785
  setNotice(error instanceof Error ? error.message : String(error));
5340
6786
  }
5341
6787
  }, [workspaceCtx]);
6788
+ refreshRef.current = refresh;
5342
6789
  useEffect3(() => {
5343
6790
  void refresh();
5344
6791
  const repaint = debounce(() => void refresh(), 250);
5345
6792
  const sub = subscribe({
5346
6793
  db: ctx.db,
5347
- tables: ["tickets", "runs", "messages", "decisions", "agents", "hosts", "provider_pauses", "runtime"],
6794
+ tables: [
6795
+ "tickets",
6796
+ "runs",
6797
+ "run_events",
6798
+ "messages",
6799
+ "decisions",
6800
+ "agents",
6801
+ "hosts",
6802
+ "provider_pauses",
6803
+ "runtime"
6804
+ ],
5348
6805
  filter: {
5349
6806
  tickets: `workspace_id=eq.${current.id}`,
5350
6807
  runs: `workspace_id=eq.${current.id}`,
@@ -5352,7 +6809,16 @@ function App({
5352
6809
  decisions: `workspace_id=eq.${current.id}`,
5353
6810
  agents: `workspace_id=eq.${current.id}`
5354
6811
  },
5355
- onChange: () => repaint.trigger(),
6812
+ onChange: (table2, payload) => {
6813
+ if (table2 === "run_events") {
6814
+ const row = payload.new;
6815
+ if (!row?.run_id) return;
6816
+ const lines = toStreamLines([row], labels.current);
6817
+ if (lines.length > 0) setStream((prior) => appendLines(prior, lines));
6818
+ return;
6819
+ }
6820
+ repaint.trigger();
6821
+ },
5356
6822
  onState: setLive
5357
6823
  });
5358
6824
  return () => {
@@ -5360,6 +6826,31 @@ function App({
5360
6826
  void sub.close();
5361
6827
  };
5362
6828
  }, [ctx, current.id, refresh]);
6829
+ useEffect3(() => {
6830
+ const ids = liveRunIds ? liveRunIds.split(",") : [];
6831
+ if (ids.length === 0) return;
6832
+ const token = workspaceLoads.current.start(workspaceCtx.workspace.id);
6833
+ void backfill({
6834
+ runIds: ids,
6835
+ runs: labels.current,
6836
+ load: (runIds) => loadLiveEvents(workspaceCtx, runIds),
6837
+ isCurrent: () => workspaceLoads.current.isCurrent(token),
6838
+ apply: setStream
6839
+ });
6840
+ }, [liveRunIds]);
6841
+ const decisions = board?.decisions ?? [];
6842
+ const waiting = decisions.length;
6843
+ const announced = useRef3(0);
6844
+ useEffect3(() => {
6845
+ if (waiting > announced.current) alertOnce();
6846
+ announced.current = waiting;
6847
+ }, [waiting]);
6848
+ useEffect3(() => {
6849
+ if (cursor && order.length > 0 && !order.includes(cursor)) {
6850
+ setCursor(null);
6851
+ selectedRef.current = null;
6852
+ }
6853
+ }, [cursor, order]);
5363
6854
  useEffect3(() => {
5364
6855
  session.current = null;
5365
6856
  }, [current.id]);
@@ -5375,7 +6866,8 @@ function App({
5375
6866
  setBoard(null);
5376
6867
  setFeed([]);
5377
6868
  setPausedAll2(false);
5378
- setMessages([]);
6869
+ setStream([]);
6870
+ setCursor(null);
5379
6871
  setNotice(null);
5380
6872
  setCurrent(found);
5381
6873
  setView("home");
@@ -5425,6 +6917,7 @@ function App({
5425
6917
  (prior) => prior.map((m) => m.id === id ? { ...m, body: message, pending: false } : m)
5426
6918
  );
5427
6919
  } finally {
6920
+ setMessages((prior) => prior.map((m) => m.id === id ? { ...m, done: true } : m));
5428
6921
  setBusy(false);
5429
6922
  }
5430
6923
  },
@@ -5453,6 +6946,7 @@ function App({
5453
6946
  const message = error instanceof Error ? error.message : String(error);
5454
6947
  setMessages((prior) => prior.map((m) => m.id === id ? { ...m, body: message, pending: false } : m));
5455
6948
  } finally {
6949
+ setMessages((prior) => prior.map((m) => m.id === id ? { ...m, done: true } : m));
5456
6950
  setBusy(false);
5457
6951
  }
5458
6952
  },
@@ -5461,7 +6955,41 @@ function App({
5461
6955
  const run5 = useCallback(
5462
6956
  async (raw) => {
5463
6957
  const text = raw.trim();
5464
- if (!text) return;
6958
+ if (view === "settings") {
6959
+ const open = editingRef.current;
6960
+ if (open) {
6961
+ setEditing(null);
6962
+ setDraft("");
6963
+ await applyEdit(open.key, raw);
6964
+ return;
6965
+ }
6966
+ if (!text) {
6967
+ const key2 = fieldRef.current;
6968
+ const row = settings.find((entry) => entry.key === key2);
6969
+ if (!row || !key2) return;
6970
+ const flipped = nextValue(row);
6971
+ if (flipped !== null) {
6972
+ await applyEdit(key2, flipped);
6973
+ return;
6974
+ }
6975
+ const seed = seedFor(row);
6976
+ setEditing({ key: key2, draft: seed });
6977
+ setDraft(seed);
6978
+ setNotice(row.hint ?? null);
6979
+ return;
6980
+ }
6981
+ }
6982
+ if (!text) {
6983
+ const at = selectedRef.current;
6984
+ if (browsing && at) {
6985
+ const selected2 = board?.tickets.find((row) => row.id === at);
6986
+ if (selected2) {
6987
+ setTicketKey(selected2.key);
6988
+ setView("ticket");
6989
+ }
6990
+ }
6991
+ return;
6992
+ }
5465
6993
  history.current.push(text);
5466
6994
  historyAt.current = -1;
5467
6995
  setDraft("");
@@ -5477,14 +7005,105 @@ function App({
5477
7005
  switch (action.kind) {
5478
7006
  case "mode":
5479
7007
  setMode(action.mode);
5480
- if (action.mode !== "browse") setView("home");
5481
- say(
5482
- "system",
5483
- action.mode === "architect" ? "Talking to the architect. It can do anything in the platform." : action.mode === "orchestrator" ? "Talking to the orchestrator. It moves work already in flight." : "Browsing."
5484
- );
7008
+ if (action.mode !== "browse") {
7009
+ setView("home");
7010
+ setCursor(null);
7011
+ say(
7012
+ "system",
7013
+ action.mode === "architect" ? "Talking to the architect. It can do anything in the platform." : "Talking to the orchestrator. It moves work already in flight."
7014
+ );
7015
+ return;
7016
+ }
7017
+ setView("home");
7018
+ setTicketKey(null);
7019
+ setStarted(true);
7020
+ if (order.length === 0) {
7021
+ setNotice("Nothing on the board to browse yet.");
7022
+ return;
7023
+ }
7024
+ setCursor((prior) => {
7025
+ const next = prior && order.includes(prior) ? prior : order[0];
7026
+ selectedRef.current = next;
7027
+ return next;
7028
+ });
5485
7029
  return;
5486
7030
  case "view":
5487
7031
  setView(action.view);
7032
+ if (action.view === "settings") {
7033
+ const first = settingsOrder[0] ?? null;
7034
+ fieldRef.current = first;
7035
+ setField(first);
7036
+ setEditing(null);
7037
+ }
7038
+ return;
7039
+ case "agent-new": {
7040
+ setBusy(true);
7041
+ try {
7042
+ const made = await createBuilder(workspaceCtx, action.name, {
7043
+ provider: action.provider,
7044
+ model: action.model
7045
+ });
7046
+ if (!made.ok) {
7047
+ setNotice(made.error);
7048
+ return;
7049
+ }
7050
+ say("system", `Added ${made.value.display_name}. Open /settings to configure it.`);
7051
+ await refresh();
7052
+ } finally {
7053
+ setBusy(false);
7054
+ }
7055
+ return;
7056
+ }
7057
+ case "workspace-new": {
7058
+ setBusy(true);
7059
+ try {
7060
+ const made = await createWorkspace(ctx, action.name, action.repo);
7061
+ if (!made.ok) {
7062
+ setNotice(made.error);
7063
+ return;
7064
+ }
7065
+ const { workspace: created, agents } = made.value;
7066
+ setAvailable((prior) => [...prior, created]);
7067
+ say("system", `Created ${created.slug} with ${agents} agents. /workspace ${created.slug} to open it.`);
7068
+ } finally {
7069
+ setBusy(false);
7070
+ }
7071
+ return;
7072
+ }
7073
+ case "decide": {
7074
+ const open = board?.decisions ?? [];
7075
+ if (open.length === 0) {
7076
+ setNotice("Nothing is waiting on a decision.");
7077
+ return;
7078
+ }
7079
+ if (!action.answer && !action.dismiss) {
7080
+ setNotice("Answer it with /decide 1, /decide <your answer>, or /decide --skip.");
7081
+ return;
7082
+ }
7083
+ setBusy(true);
7084
+ try {
7085
+ const result = await resolveDecision(workspaceCtx, open[0].id, action.answer, {
7086
+ dismiss: action.dismiss
7087
+ });
7088
+ if (!result.ok) {
7089
+ setNotice(result.error);
7090
+ return;
7091
+ }
7092
+ say(
7093
+ "system",
7094
+ result.dismissed ? `Skipped the decision on ${open[0].id.slice(0, 8)}.` : `Answered: ${result.answer}`
7095
+ );
7096
+ await refresh();
7097
+ } finally {
7098
+ setBusy(false);
7099
+ }
7100
+ return;
7101
+ }
7102
+ case "help":
7103
+ setMessages((prior) => [
7104
+ ...prior,
7105
+ { id: nextId(), speaker: "system", body: "", panel: "help", done: true }
7106
+ ]);
5488
7107
  return;
5489
7108
  case "workspace":
5490
7109
  if (!action.slug) {
@@ -5509,85 +7128,183 @@ function App({
5509
7128
  return;
5510
7129
  }
5511
7130
  },
5512
- [mode, available, askArchitect, askOrchestrator, switchWorkspace, refresh, say, exit]
7131
+ [
7132
+ mode,
7133
+ available,
7134
+ order,
7135
+ browsing,
7136
+ board,
7137
+ view,
7138
+ settings,
7139
+ applyEdit,
7140
+ ctx,
7141
+ workspaceCtx,
7142
+ settingsOrder,
7143
+ refresh,
7144
+ askArchitect,
7145
+ askOrchestrator,
7146
+ switchWorkspace,
7147
+ refresh,
7148
+ say,
7149
+ exit
7150
+ ]
5513
7151
  );
5514
7152
  useInput2((input, key2) => {
5515
7153
  if (key2.ctrl && input === "c") exit();
5516
7154
  });
5517
7155
  const ticket = ticketKey ? board?.tickets.find((row) => row.key === ticketKey) : null;
5518
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", width, children: [
5519
- /* @__PURE__ */ jsx6(Banner, { onDone: () => setReady(true) }),
5520
- ready && view === "home" && messages.length === 0 ? /* @__PURE__ */ jsx6(Help, { width }) : null,
5521
- /* @__PURE__ */ jsxs5(Box5, { children: [
5522
- /* @__PURE__ */ jsx6(Text6, { color: UI.text, bold: true, children: current.slug }),
5523
- /* @__PURE__ */ jsxs5(Text6, { color: UI.dim, children: [
5524
- " ",
5525
- current.repo,
5526
- " "
5527
- ] }),
5528
- /* @__PURE__ */ jsx6(Text6, { color: live === "live" ? UI.accent : UI.dim, children: "\u25CF " }),
5529
- /* @__PURE__ */ jsxs5(Text6, { color: UI.dim, children: [
5530
- live,
5531
- " "
5532
- ] }),
5533
- /* @__PURE__ */ jsx6(Text6, { color: UI.dim, children: board ? `${board.runs.filter((r) => r.status === "running").length} running ` : "" }),
5534
- board?.decisions.length ? /* @__PURE__ */ jsxs5(Text6, { color: UI.warn, children: [
5535
- board.decisions.length,
5536
- " decisions "
5537
- ] }) : null,
5538
- pausedAll || current.paused ? /* @__PURE__ */ jsx6(Text6, { color: UI.warn, children: "paused " }) : null,
5539
- /* @__PURE__ */ jsxs5(Text6, { color: UI.dim, children: [
5540
- "\xB7 ",
5541
- mode
5542
- ] })
5543
- ] }),
5544
- /* @__PURE__ */ jsx6(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { color: UI.dim, children: brainLabel }) }),
5545
- view === "help" ? /* @__PURE__ */ jsx6(Help, { width }) : null,
5546
- view === "board" && board ? /* @__PURE__ */ jsx6(BoardPanel, { board }) : null,
5547
- view === "agents" && board ? /* @__PURE__ */ jsx6(AgentsPanel, { board }) : null,
5548
- view === "feed" ? /* @__PURE__ */ jsx6(FeedPanel, { rows: feed }) : null,
5549
- view === "inbox" && board ? /* @__PURE__ */ jsx6(InboxPanel, { board }) : null,
5550
- view === "ticket" ? ticket ? /* @__PURE__ */ jsx6(TicketPanel, { ticket }) : /* @__PURE__ */ jsxs5(Text6, { color: UI.warn, children: [
5551
- "No ticket ",
5552
- ticketKey,
5553
- " here."
5554
- ] }) : null,
5555
- view === "home" && board ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
5556
- /* @__PURE__ */ jsx6(AgentsPanel, { board }),
5557
- /* @__PURE__ */ jsx6(EpicsPanel, { board })
5558
- ] }) : null,
5559
- messages.map((message) => /* @__PURE__ */ jsx6(Bubble, { message, width }, message.id)),
5560
- notice ? /* @__PURE__ */ jsx6(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { color: UI.warn, children: notice }) }) : null,
5561
- /* @__PURE__ */ jsx6(Box5, { children: /* @__PURE__ */ jsx6(
5562
- TextInput,
7156
+ const settled = messages.filter((message) => message.done);
7157
+ const inFlight = messages.filter((message) => !message.done);
7158
+ const splash = !started;
7159
+ const scrollback = splash ? [] : [
7160
+ { key: "banner" },
7161
+ { key: "help", message: { id: "help", speaker: "system", body: "", panel: "help" } },
7162
+ ...settled.map((message) => ({ key: message.id, message }))
7163
+ ];
7164
+ const plan = planLayout({
7165
+ rows,
7166
+ columns,
7167
+ width,
7168
+ splash,
7169
+ ready,
7170
+ decision: decisionRows(decisions),
7171
+ inFlight: inFlight.reduce((total, message) => total + bubbleRows(message, width), 0),
7172
+ notice: Boolean(notice),
7173
+ home: view === "home"
7174
+ });
7175
+ const budget = plan.panels;
7176
+ const cockpitRows = plan.cockpit;
7177
+ const streamRows = plan.stream;
7178
+ const fits = plan.fits;
7179
+ const running = board?.runs.filter((run6) => run6.status === "running").length ?? 0;
7180
+ const selected = cursor ? board?.tickets.find((row) => row.id === cursor) : null;
7181
+ return /* @__PURE__ */ jsxs10(Fragment3, { children: [
7182
+ /* @__PURE__ */ jsx11(Static, { items: scrollback, children: (item) => {
7183
+ if (!item.message) return /* @__PURE__ */ jsx11(Banner, { animate: false }, item.key);
7184
+ if (item.message.panel === "help") return /* @__PURE__ */ jsx11(Help, { width }, item.key);
7185
+ return /* @__PURE__ */ jsx11(Bubble, { message: item.message, width }, item.key);
7186
+ } }),
7187
+ splash ? /* @__PURE__ */ jsx11(
7188
+ Splash,
5563
7189
  {
5564
- value: draft,
5565
- onChange: setDraft,
5566
- onSubmit: (value) => void run5(value),
5567
- isActive: !busy,
5568
- placeholder: busy ? "working\u2026" : "message, or /help",
5569
- prompt: /* @__PURE__ */ jsxs5(Text6, { color: mode === "browse" ? UI.dim : UI.cream, children: [
5570
- promptFor(mode),
7190
+ columns,
7191
+ rows,
7192
+ width,
7193
+ ready,
7194
+ helpFull: plan.helpFull,
7195
+ animate: fits,
7196
+ onDone: () => setReady(true)
7197
+ }
7198
+ ) : null,
7199
+ /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", width, children: [
7200
+ 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,
7202
+ view === "feed" && budget > 0 ? /* @__PURE__ */ jsx11(FeedPanel, { entries: feed, width, rows: budget }) : null,
7203
+ view === "settings" && budget > 0 ? /* @__PURE__ */ jsx11(
7204
+ SettingsPanel,
7205
+ {
7206
+ entries: settings,
7207
+ width,
7208
+ rows: budget,
7209
+ cursor: field,
7210
+ editing
7211
+ }
7212
+ ) : null,
7213
+ view === "inbox" && board && budget > 0 ? /* @__PURE__ */ jsx11(InboxPanel, { board, width, rows: budget }) : null,
7214
+ view === "ticket" && budget > 0 ? ticket ? /* @__PURE__ */ jsx11(TicketPanel, { ticket, width, rows: budget }) : /* @__PURE__ */ jsxs10(Text10, { color: UI.warn, children: [
7215
+ "No ticket ",
7216
+ ticketKey,
7217
+ " here."
7218
+ ] }) : null,
7219
+ view === "home" && board && plan.cockpit > 0 ? /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", children: [
7220
+ /* @__PURE__ */ jsx11(Cockpit, { board, width, rows: cockpitRows, cursor }),
7221
+ /* @__PURE__ */ jsx11(Box8, { height: 1 }),
7222
+ /* @__PURE__ */ jsx11(StreamPanel, { lines: stream, width, rows: streamRows, live: running > 0 })
7223
+ ] }) : null,
7224
+ inFlight.map((message) => /* @__PURE__ */ jsx11(Bubble, { message, width }, message.id)),
7225
+ /* @__PURE__ */ jsx11(DecisionPanel, { decisions, board, width, rows: plan.decision }),
7226
+ notice ? /* @__PURE__ */ jsx11(Box8, { marginBottom: 1, children: /* @__PURE__ */ jsx11(Text10, { color: UI.warn, children: notice }) }) : null,
7227
+ /* @__PURE__ */ jsxs10(Box8, { marginTop: 1, children: [
7228
+ /* @__PURE__ */ jsx11(Text10, { color: UI.text, bold: true, children: current.slug }),
7229
+ /* @__PURE__ */ jsxs10(Text10, { color: UI.dim, children: [
7230
+ " ",
7231
+ current.repo,
5571
7232
  " "
5572
7233
  ] }),
5573
- color: UI.text,
5574
- onUp: () => {
5575
- if (history.current.length === 0) return;
5576
- historyAt.current = historyAt.current < 0 ? history.current.length - 1 : Math.max(0, historyAt.current - 1);
5577
- setDraft(history.current[historyAt.current] ?? "");
5578
- },
5579
- onDown: () => {
5580
- if (historyAt.current < 0) return;
5581
- historyAt.current += 1;
5582
- if (historyAt.current >= history.current.length) {
5583
- historyAt.current = -1;
5584
- setDraft("");
5585
- return;
7234
+ /* @__PURE__ */ jsx11(Text10, { color: live === "live" ? UI.accent : UI.dim, children: "\u25CF " }),
7235
+ /* @__PURE__ */ jsxs10(Text10, { color: UI.dim, children: [
7236
+ live,
7237
+ " "
7238
+ ] }),
7239
+ /* @__PURE__ */ jsx11(Text10, { color: UI.dim, children: running ? `${running} running ` : "" }),
7240
+ board?.decisions.length ? /* @__PURE__ */ jsxs10(Text10, { color: UI.warn, children: [
7241
+ board.decisions.length,
7242
+ " decisions "
7243
+ ] }) : null,
7244
+ pausedAll || current.paused ? /* @__PURE__ */ jsx11(Text10, { color: UI.warn, children: "paused " }) : null,
7245
+ /* @__PURE__ */ jsxs10(Text10, { color: UI.dim, wrap: "truncate", children: [
7246
+ "\xB7 ",
7247
+ mode,
7248
+ cursor && selected ? ` ${selected.key} \u2191\u2193 move \xB7 enter opens \xB7 esc leaves` : ` \xB7 ${brainLabel}`
7249
+ ] })
7250
+ ] }),
7251
+ /* @__PURE__ */ jsx11(Box8, { children: /* @__PURE__ */ jsx11(
7252
+ TextInput,
7253
+ {
7254
+ value: draft,
7255
+ onChange: (next) => {
7256
+ setDraft(next);
7257
+ if (editingRef.current) setEditing({ key: editingRef.current.key, draft: next });
7258
+ },
7259
+ onSubmit: (value) => void run5(value),
7260
+ isActive: !busy,
7261
+ placeholder: busy ? "working\u2026" : "message, or /help",
7262
+ prompt: /* @__PURE__ */ jsxs10(Text10, { color: mode === "browse" ? UI.dim : UI.cream, children: [
7263
+ promptFor(mode),
7264
+ " "
7265
+ ] }),
7266
+ color: UI.text,
7267
+ onCancel: () => {
7268
+ if (editing) {
7269
+ setEditing(null);
7270
+ setDraft("");
7271
+ setNotice(null);
7272
+ return;
7273
+ }
7274
+ if (view === "settings") {
7275
+ setView("home");
7276
+ return;
7277
+ }
7278
+ if (view === "ticket") {
7279
+ setView("home");
7280
+ setTicketKey(null);
7281
+ return;
7282
+ }
7283
+ setCursor(null);
7284
+ selectedRef.current = null;
7285
+ },
7286
+ onUp: () => {
7287
+ if (configuring && moveField(-1)) return;
7288
+ if (browsing && moveCursor(-1)) return;
7289
+ if (history.current.length === 0) return;
7290
+ historyAt.current = historyAt.current < 0 ? history.current.length - 1 : Math.max(0, historyAt.current - 1);
7291
+ setDraft(history.current[historyAt.current] ?? "");
7292
+ },
7293
+ onDown: () => {
7294
+ if (configuring && moveField(1)) return;
7295
+ if (browsing && moveCursor(1)) return;
7296
+ if (historyAt.current < 0) return;
7297
+ historyAt.current += 1;
7298
+ if (historyAt.current >= history.current.length) {
7299
+ historyAt.current = -1;
7300
+ setDraft("");
7301
+ return;
7302
+ }
7303
+ setDraft(history.current[historyAt.current] ?? "");
5586
7304
  }
5587
- setDraft(history.current[historyAt.current] ?? "");
5588
7305
  }
5589
- }
5590
- ) })
7306
+ ) })
7307
+ ] })
5591
7308
  ] });
5592
7309
  }
5593
7310
  function promptFor(mode) {
@@ -5615,6 +7332,16 @@ var init_App = __esm({
5615
7332
  init_plan();
5616
7333
  init_commands3();
5617
7334
  init_Banner();
7335
+ init_Splash();
7336
+ init_height();
7337
+ init_layout();
7338
+ init_Dashboard();
7339
+ init_Settings();
7340
+ init_settings_model();
7341
+ init_config2();
7342
+ init_Decision();
7343
+ init_alert();
7344
+ init_stream();
5618
7345
  init_Bubble();
5619
7346
  init_Help();
5620
7347
  init_Panels();
@@ -5635,7 +7362,7 @@ __export(launch_exports, {
5635
7362
  async function launchApp(slug) {
5636
7363
  if (!canLaunchApp()) return;
5637
7364
  const ctx = await requireWorkspace(slug);
5638
- const [{ render }, React7, { App: App2 }] = await Promise.all([
7365
+ const [{ render }, React12, { App: App2 }] = await Promise.all([
5639
7366
  import("ink"),
5640
7367
  import("react"),
5641
7368
  Promise.resolve().then(() => (init_App(), App_exports))
@@ -5643,7 +7370,7 @@ async function launchApp(slug) {
5643
7370
  const { data: workspaces } = await ctx.db.from("workspaces").select("*").order("name");
5644
7371
  const config = await loadConfig();
5645
7372
  const instance = render(
5646
- React7.createElement(App2, {
7373
+ React12.createElement(App2, {
5647
7374
  ctx,
5648
7375
  workspace: ctx.workspace,
5649
7376
  workspaces: workspaces ?? [],
@@ -5808,7 +7535,7 @@ function loadConfig2(env = process.env) {
5808
7535
  stopTimeoutMs: Number(env.RUNNER_STOP_TIMEOUT_MS ?? 9e4)
5809
7536
  };
5810
7537
  }
5811
- var init_config2 = __esm({
7538
+ var init_config3 = __esm({
5812
7539
  "../runner/src/config.ts"() {
5813
7540
  "use strict";
5814
7541
  }
@@ -6125,7 +7852,7 @@ var init_init = __esm({
6125
7852
  "../runner/src/init.ts"() {
6126
7853
  "use strict";
6127
7854
  init_auth_check();
6128
- init_config2();
7855
+ init_config3();
6129
7856
  init_db();
6130
7857
  init_heartbeat();
6131
7858
  init_providers();
@@ -6261,6 +7988,7 @@ init_commands2();
6261
7988
  init_context();
6262
7989
  init_json();
6263
7990
  init_theme();
7991
+ init_argv_parsers();
6264
7992
  init_format();
6265
7993
 
6266
7994
  // src/live/types.ts
@@ -6440,7 +8168,7 @@ function registerLiveCommands(program) {
6440
8168
  render: async () => agentsView(await loadBoard(ctx))
6441
8169
  });
6442
8170
  });
6443
- program.command("feed").description("live activity stream").option("-n, --limit <n>", "rows to show", "40").action(async function() {
8171
+ program.command("feed").description("live activity stream").option("-n, --limit <n>", "rows to show", positiveInteger("limit"), 40).action(async function() {
6444
8172
  const ctx = await requireWorkspace(slugOf4(this));
6445
8173
  const limit = Number(this.opts().limit) || 40;
6446
8174
  if (isJsonMode()) {
@@ -6479,6 +8207,7 @@ init_group();
6479
8207
  init_editor();
6480
8208
  init_epics();
6481
8209
  init_tickets2();
8210
+ init_argv_parsers();
6482
8211
  import { readFile as readFile6 } from "fs/promises";
6483
8212
  import { randomUUID as randomUUID4 } from "crypto";
6484
8213
  function slugOf5(command) {
@@ -6497,7 +8226,7 @@ function requestId(command) {
6497
8226
  return String(command.opts().requestId ?? randomUUID4());
6498
8227
  }
6499
8228
  function registerWriteCommands(program) {
6500
- program.command("new").argument("[title...]", "ticket title").description("file a ticket").option("-b, --body <text>", "body markdown").option("-A, --acceptance <text>", "acceptance criteria").option("--area <area>", "area tag, e.g. web/auth").option("-a, --agent <name>", "pin a builder by display name").option("-p, --priority <n>", "priority, higher runs first", "0").option("--host <id>", "run on a specific host").option("-e, --epic <id>", "attach to an epic").option("--block <keys>", "comma-separated keys this depends on").option("-t, --template <id>", "start from a workspace template").option("--edit", "open $EDITOR for the body and acceptance").action(async function(titleWords) {
8229
+ program.command("new").argument("[title...]", "ticket title").description("file a ticket").option("-b, --body <text>", "body markdown").option("-A, --acceptance <text>", "acceptance criteria").option("--area <area>", "area tag, e.g. web/auth").option("-a, --agent <name>", "pin a builder by display name").option("-p, --priority <n>", "priority, higher runs first", integer("priority"), 0).option("--host <id>", "run on a specific host").option("-e, --epic <id>", "attach to an epic").option("--block <keys>", "comma-separated keys this depends on").option("-t, --template <id>", "start from a workspace template").option("--edit", "open $EDITOR for the body and acceptance").action(async function(titleWords) {
6501
8230
  const opts = this.opts();
6502
8231
  const ctx = await requireWorkspace(slugOf5(this));
6503
8232
  let title = titleWords.join(" ").trim();
@@ -6541,7 +8270,7 @@ function registerWriteCommands(program) {
6541
8270
  });
6542
8271
  ok(`${c.bold(ticket.key)} ${ticket.title} ${c.dim(`(${ticket.status})`)}`, { ticket });
6543
8272
  });
6544
- program.command("set").argument("<key>", "ticket key").description("change a ticket").option("--title <text>", "new title").option("-b, --body <text>", "new body").option("-A, --acceptance <text>", "new acceptance criteria").option("--area <area>", "new area").option("-a, --agent <name>", "assign a builder, or 'none' to unassign").option("-p, --priority <n>", "new priority").option("--host <id>", "run on a specific host, or 'none'").option("-s, --status <status>", "move to a status").option("-e, --epic <id>", "attach to an epic, or 'none'").option("--edit", "open $EDITOR for the body and acceptance").option("--request-id <uuid>", "stable operation id for safe retry").action(async function(key2) {
8273
+ program.command("set").argument("<key>", "ticket key").description("change a ticket").option("--title <text>", "new title").option("-b, --body <text>", "new body").option("-A, --acceptance <text>", "new acceptance criteria").option("--area <area>", "new area").option("-a, --agent <name>", "assign a builder, or 'none' to unassign").option("-p, --priority <n>", "new priority", integer("priority")).option("--host <id>", "run on a specific host, or 'none'").option("-s, --status <status>", "move to a status", oneOf("status", ticketStatuses)).option("-e, --epic <id>", "attach to an epic, or 'none'").option("--edit", "open $EDITOR for the body and acceptance").option("--request-id <uuid>", "stable operation id for safe retry", uuid).action(async function(key2) {
6545
8274
  const opts = this.opts();
6546
8275
  const ctx = await requireWorkspace(slugOf5(this));
6547
8276
  const ticket = await findTicket(ctx, key2);
@@ -6571,21 +8300,21 @@ function registerWriteCommands(program) {
6571
8300
  const updated = await updateTicket(ctx, ticket, patch, operationId);
6572
8301
  ok(`${c.bold(updated.key)} updated ${c.dim(`(${updated.status})`)} ${c.dim(`request ${operationId}`)}`, { ticket: updated, request_id: operationId });
6573
8302
  });
6574
- program.command("block").argument("<key>", "ticket key").argument("<blockers...>", "keys it should wait for").description("make a ticket wait for others").option("--request-id <uuid>", "stable operation id for safe retry").action(async function(key2, blockers) {
8303
+ program.command("block").argument("<key>", "ticket key").argument("<blockers...>", "keys it should wait for").description("make a ticket wait for others").option("--request-id <uuid>", "stable operation id for safe retry", uuid).action(async function(key2, blockers) {
6575
8304
  const ctx = await requireWorkspace(slugOf5(this));
6576
8305
  const ticket = await findTicket(ctx, key2);
6577
8306
  const operationId = requestId(this);
6578
8307
  const updated = await setBlockedBy(ctx, ticket, blockers, "add", operationId);
6579
8308
  ok(`${c.bold(updated.key)} is ${updated.status} ${c.dim(`request ${operationId}`)}`, { ticket: updated, request_id: operationId });
6580
8309
  });
6581
- program.command("unblock").argument("<key>", "ticket key").argument("[blockers...]", "keys to drop; omit to clear them all").description("drop dependencies").option("--request-id <uuid>", "stable operation id for safe retry").action(async function(key2, blockers) {
8310
+ program.command("unblock").argument("<key>", "ticket key").argument("[blockers...]", "keys to drop; omit to clear them all").description("drop dependencies").option("--request-id <uuid>", "stable operation id for safe retry", uuid).action(async function(key2, blockers) {
6582
8311
  const ctx = await requireWorkspace(slugOf5(this));
6583
8312
  const ticket = await findTicket(ctx, key2);
6584
8313
  const operationId = requestId(this);
6585
8314
  const updated = blockers.length ? await setBlockedBy(ctx, ticket, blockers, "remove", operationId) : await setBlockedBy(ctx, ticket, [], "set", operationId);
6586
8315
  ok(`${c.bold(updated.key)} is ${updated.status} ${c.dim(`request ${operationId}`)}`, { ticket: updated, request_id: operationId });
6587
8316
  });
6588
- program.command("take").argument("<key>", "ticket key").description("take a ticket over so the board stops dispatching it").option("--request-id <uuid>", "stable operation id for safe retry").action(async function(key2) {
8317
+ program.command("take").argument("<key>", "ticket key").description("take a ticket over so the board stops dispatching it").option("--request-id <uuid>", "stable operation id for safe retry", uuid).action(async function(key2) {
6589
8318
  const ctx = await requireWorkspace(slugOf5(this));
6590
8319
  const operationId = requestId(this);
6591
8320
  const updated = await takeOver(ctx, await findTicket(ctx, key2), operationId);
@@ -6594,13 +8323,13 @@ function registerWriteCommands(program) {
6594
8323
  request_id: operationId
6595
8324
  });
6596
8325
  });
6597
- program.command("handback").argument("<key>", "ticket key").description("give a ticket back to the board").option("--request-id <uuid>", "stable operation id for safe retry").action(async function(key2) {
8326
+ program.command("handback").argument("<key>", "ticket key").description("give a ticket back to the board").option("--request-id <uuid>", "stable operation id for safe retry", uuid).action(async function(key2) {
6598
8327
  const ctx = await requireWorkspace(slugOf5(this));
6599
8328
  const operationId = requestId(this);
6600
8329
  const updated = await handBack(ctx, await findTicket(ctx, key2), operationId);
6601
8330
  ok(`${c.bold(updated.key)} handed back ${c.dim(`(${updated.status})`)} ${c.dim(`request ${operationId}`)}`, { ticket: updated, request_id: operationId });
6602
8331
  });
6603
- program.command("cancel").argument("<key>", "ticket key").description("cancel a ticket").option("--request-id <uuid>", "stable operation id for safe retry").action(async function(key2) {
8332
+ program.command("cancel").argument("<key>", "ticket key").description("cancel a ticket").option("--request-id <uuid>", "stable operation id for safe retry", uuid).action(async function(key2) {
6604
8333
  const ctx = await requireWorkspace(slugOf5(this));
6605
8334
  const ticket = await findTicket(ctx, key2);
6606
8335
  const operationId = requestId(this);
@@ -6634,7 +8363,7 @@ function registerWriteCommands(program) {
6634
8363
  { epic: created }
6635
8364
  );
6636
8365
  });
6637
- epic.command("set").argument("<id>", "epic id or prefix").description("change an epic").option("--title <text>", "new title").option("--spec <path>", "replace the spec from a file").option("-s, --status <status>", "draft, decomposing, active, or done").action(async function(id) {
8366
+ epic.command("set").argument("<id>", "epic id or prefix").description("change an epic").option("--title <text>", "new title").option("--spec <path>", "replace the spec from a file").option("-s, --status <status>", "draft, decomposing, active, or done", oneOf("status", epicStatuses)).action(async function(id) {
6638
8367
  const opts = this.opts();
6639
8368
  const ctx = await requireWorkspace(slugOf5(this));
6640
8369
  const found = resolveEpic(await loadBoard(ctx), id);
@@ -6697,7 +8426,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6697
8426
  import { z as z3 } from "zod";
6698
8427
 
6699
8428
  // src/version.ts
6700
- var VERSION = "0.1.4";
8429
+ var VERSION = "0.2.1";
6701
8430
 
6702
8431
  // src/architect/mcp.ts
6703
8432
  init_tools();
@@ -6772,6 +8501,7 @@ init_brain();
6772
8501
  init_config();
6773
8502
  init_connection();
6774
8503
  init_tools();
8504
+ init_argv_parsers();
6775
8505
  function slugOf6(command) {
6776
8506
  return command.optsWithGlobals().workspace;
6777
8507
  }
@@ -6870,7 +8600,7 @@ function registerArchitectCommands(program) {
6870
8600
  }
6871
8601
  await serveMcp({ readOnly, workspace: slugOf6(this) });
6872
8602
  });
6873
- program.command("brain").description("the model you talk to, and the subscription behind it").option("--model <model>", "which model to use").option("--effort <level>", "low, medium, or high").action(async function() {
8603
+ program.command("brain").description("the model you talk to, and the subscription behind it").option("--model <model>", "which model to use").option("--effort <level>", "low, medium, or high", oneOf("effort", ["low", "medium", "high"])).action(async function() {
6874
8604
  const opts = this.opts();
6875
8605
  if (opts.model || opts.effort) {
6876
8606
  const next = await setBrain({