@higherdev/cli 0.2.0 → 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 +2664 -801
  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";
@@ -482,11 +470,125 @@ var init_lifecycle = __esm({
482
470
  }
483
471
  });
484
472
 
473
+ // ../../packages/db/src/commands/execute.ts
474
+ function ownerTicketCommand(opts) {
475
+ return {
476
+ commandId: opts.operationId,
477
+ idempotencyKey: `owner-ticket:${opts.workspaceId}:${opts.ticketId}:${opts.operationId}`,
478
+ actor: { type: "human", id: opts.actorEmail.trim().toLowerCase() },
479
+ workspaceId: opts.workspaceId,
480
+ target: { type: "ticket", id: opts.ticketId },
481
+ expected: { status: opts.expectedStatus, version: opts.expectedVersion },
482
+ intent: opts.intent
483
+ };
484
+ }
485
+ function isLifecycleResult(value) {
486
+ return Boolean(
487
+ value && typeof value === "object" && !Array.isArray(value) && (value.outcome === "accepted" || value.outcome === "rejected")
488
+ );
489
+ }
490
+ async function applyOwnerTicketCommand(db, command) {
491
+ const { data, error } = await db.rpc("apply_owner_ticket_command", {
492
+ p_command: command
493
+ });
494
+ if (error) throw error;
495
+ if (!isLifecycleResult(data)) throw new Error("Owner ticket command returned an invalid receipt.");
496
+ return data;
497
+ }
498
+ var init_execute = __esm({
499
+ "../../packages/db/src/commands/execute.ts"() {
500
+ "use strict";
501
+ }
502
+ });
503
+
485
504
  // ../../packages/db/src/commands/index.ts
486
505
  var init_commands = __esm({
487
506
  "../../packages/db/src/commands/index.ts"() {
488
507
  "use strict";
489
508
  init_lifecycle();
509
+ init_execute();
510
+ }
511
+ });
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();
490
592
  }
491
593
  });
492
594
 
@@ -517,6 +619,14 @@ var init_defaults = __esm({
517
619
  }
518
620
  });
519
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
+
520
630
  // ../../packages/db/src/epic-progress.ts
521
631
  function round4(value) {
522
632
  return Math.round(value * 1e4) / 1e4;
@@ -690,6 +800,10 @@ var init_schemas = __esm({
690
800
  timeout_minutes: z.number().int().positive(),
691
801
  max_attempts: z.number().int().positive(),
692
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(),
693
807
  webhook_url: z.union([z.url(), z.literal("")]).optional(),
694
808
  smoke_url: z.union([z.url(), z.literal("")]).optional(),
695
809
  quiet_hours: z.object({
@@ -723,6 +837,7 @@ var init_schemas = __esm({
723
837
  api_key_hash: z.string().nullable(),
724
838
  paused: z.boolean(),
725
839
  webhook_secret: z.string().nullable().optional(),
840
+ webhook_secret_pending: z.string().nullable().optional(),
726
841
  webhook_last_status: z.string().nullable().optional(),
727
842
  webhook_last_at: isoDateSchema.nullable().optional(),
728
843
  webhook_attempts: z.number().int().nonnegative().optional(),
@@ -894,80 +1009,6 @@ var init_schemas = __esm({
894
1009
  }
895
1010
  });
896
1011
 
897
- // ../../packages/db/src/status.ts
898
- function statusLabel(status) {
899
- return status.replaceAll("_", " ");
900
- }
901
- function statusTone(status) {
902
- switch (status) {
903
- case "queued":
904
- case "running":
905
- case "in_review":
906
- return "blue";
907
- case "approved":
908
- case "merged":
909
- case "ready":
910
- return "success";
911
- case "blocked":
912
- case "needs_decision":
913
- case "changes_requested":
914
- return "warning";
915
- case "failed":
916
- case "cancelled":
917
- return "danger";
918
- default:
919
- return "muted";
920
- }
921
- }
922
- function resolveTicketStatus(requested, blockerStatuses2) {
923
- if (requested === "cancelled") return "cancelled";
924
- const open = blockerStatuses2.filter((status) => status !== "merged" && status !== "cancelled");
925
- if (open.length > 0) return "blocked";
926
- if (requested === "blocked") return "backlog";
927
- return requested;
928
- }
929
- function initialHumanStatus(opts) {
930
- const groomed = Boolean(opts.acceptanceMd.trim() && opts.area && opts.assigned);
931
- return resolveTicketStatus(groomed ? "ready" : "backlog", opts.blockerStatuses);
932
- }
933
- function statusAfterAnswer(opts) {
934
- return opts.assigned && Boolean(opts.acceptanceMd.trim()) ? "queued" : "ready";
935
- }
936
- function handBackStatus(opts) {
937
- if (opts.acceptanceMd.trim() && opts.assigned) {
938
- return resolveTicketStatus(statusAfterAnswer(opts), opts.blockerStatuses);
939
- }
940
- return initialHumanStatus(opts);
941
- }
942
- function cancelledSuffix(cancelled) {
943
- return cancelled > 0 ? ` +${cancelled} cancelled` : "";
944
- }
945
- function epicProgressCaption(merged, total, cancelled = 0) {
946
- if (total === 0 && cancelled === 0) return "No tickets";
947
- return `${merged}/${total} merged${cancelledSuffix(cancelled)}`;
948
- }
949
- var BOARD_COLUMNS;
950
- var init_status = __esm({
951
- "../../packages/db/src/status.ts"() {
952
- "use strict";
953
- init_enums();
954
- BOARD_COLUMNS = [
955
- "backlog",
956
- "ready",
957
- "blocked",
958
- "queued",
959
- "running",
960
- "failed",
961
- "needs_decision",
962
- "in_review",
963
- "changes_requested",
964
- "approved",
965
- "merged",
966
- "cancelled"
967
- ];
968
- }
969
- });
970
-
971
1012
  // ../../packages/db/src/stuck.ts
972
1013
  function stuckReason(opts) {
973
1014
  const { ticket } = opts;
@@ -1087,9 +1128,9 @@ function uncertainCreateError(error) {
1087
1128
  return Boolean(error && (!error.code || /^PGRST00[0-3]$/.test(error.code)));
1088
1129
  }
1089
1130
  async function createTicketRecord(db, workspaceId, fields, ticketId) {
1090
- const requestId = ticketId ?? globalThis.crypto.randomUUID();
1131
+ const requestId2 = ticketId ?? globalThis.crypto.randomUUID();
1091
1132
  const args = {
1092
- p_ticket_id: requestId,
1133
+ p_ticket_id: requestId2,
1093
1134
  p_workspace_id: workspaceId,
1094
1135
  p_fields: fields
1095
1136
  };
@@ -1140,173 +1181,54 @@ var init_ticket_writes = __esm({
1140
1181
  }
1141
1182
  });
1142
1183
 
1143
- // ../../packages/db/src/timeline.ts
1144
- function formatDuration(ms) {
1145
- const totalSeconds = Math.max(0, Math.round(ms / 1e3));
1146
- const hours = Math.floor(totalSeconds / 3600);
1147
- const minutes = Math.floor(totalSeconds % 3600 / 60);
1148
- const seconds = totalSeconds % 60;
1149
- if (hours > 0) return minutes ? `${hours}h ${minutes}m` : `${hours}h`;
1150
- if (minutes > 0) return seconds ? `${minutes}m ${seconds}s` : `${minutes}m`;
1151
- return `${seconds}s`;
1184
+ // ../../packages/db/src/transcript.ts
1185
+ function asRecord(value) {
1186
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
1152
1187
  }
1153
- function msBetween(from, to) {
1154
- const start = Date.parse(from);
1155
- const end = Date.parse(to);
1156
- if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null;
1157
- return end - start;
1188
+ function str(value) {
1189
+ return typeof value === "string" ? value : value == null ? "" : String(value);
1158
1190
  }
1159
- function clip(text, max = 160) {
1160
- const one2 = text.trim().replace(/\s+/g, " ");
1161
- if (one2.length <= max) return one2;
1162
- return `${one2.slice(0, max - 3)}...`;
1191
+ function assistantText(payload) {
1192
+ if (typeof payload.text === "string") return payload.text;
1193
+ const item = asRecord(payload.item);
1194
+ if (typeof item.text === "string") return item.text;
1195
+ if (Array.isArray(item.content)) {
1196
+ const joined = item.content.map((entry) => str(asRecord(entry).text)).filter(Boolean).join("\n");
1197
+ if (joined) return joined;
1198
+ }
1199
+ const message = asRecord(payload.message);
1200
+ const content = message.content;
1201
+ if (Array.isArray(content)) {
1202
+ return content.map((item2) => {
1203
+ const row = asRecord(item2);
1204
+ return str(row.text);
1205
+ }).filter(Boolean).join("\n");
1206
+ }
1207
+ if (typeof content === "string") return content;
1208
+ return "";
1163
1209
  }
1164
- function clock(iso) {
1165
- if (!iso) return "";
1166
- const date = new Date(iso);
1167
- if (Number.isNaN(date.getTime())) return "";
1168
- return date.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
1210
+ function toolName(payload) {
1211
+ return str(payload.name || payload.tool || payload.tool_name || asRecord(payload.input).command || "tool");
1169
1212
  }
1170
- function runSpan(run5) {
1171
- const start = clock(run5.started_at ?? run5.created_at);
1172
- const end = clock(run5.ended_at);
1173
- if (start && end) return `${start}-${end}`;
1174
- if (start && !run5.ended_at) return `${start}-`;
1175
- return start;
1213
+ function toolDetail(payload) {
1214
+ const input = asRecord(payload.input ?? payload.args ?? payload);
1215
+ const file = str(input.path || input.file_path || input.file || input.target);
1216
+ const command = str(input.command || input.cmd);
1217
+ const additions = Number(input.additions ?? input.added);
1218
+ const deletions = Number(input.deletions ?? input.removed);
1219
+ const stat2 = Number.isFinite(additions) || Number.isFinite(deletions) ? ` +${Number.isFinite(additions) ? additions : 0}/-${Number.isFinite(deletions) ? deletions : 0}` : "";
1220
+ if (file) return `${file}${stat2}`;
1221
+ if (command) return command.length > 80 ? `${command.slice(0, 77)}...` : command;
1222
+ return "";
1176
1223
  }
1177
- function runDurationMs(run5, nowIso) {
1178
- const start = run5.started_at ?? run5.created_at;
1179
- if (run5.ended_at) return msBetween(start, run5.ended_at);
1180
- if (run5.status === "running" || run5.status === "queued") return msBetween(start, nowIso);
1181
- return null;
1182
- }
1183
- function buildTimeline(input) {
1184
- const nowIso = typeof input.now === "string" ? input.now : new Date(input.now ?? Date.now()).toISOString();
1185
- const items = [];
1186
- const statuses = [...input.statusEvents].sort((a, b) => a.at.localeCompare(b.at) || a.id.localeCompare(b.id));
1187
- for (let i = 0; i < statuses.length; i++) {
1188
- const event = statuses[i];
1189
- const next = statuses[i + 1];
1190
- const from = event.from_status ? statusLabel(event.from_status) : "new";
1191
- const to = statusLabel(event.to_status);
1192
- items.push({
1193
- id: `status:${event.id}`,
1194
- at: event.at,
1195
- kind: "status",
1196
- title: `${from} to ${to}`,
1197
- durationMs: msBetween(event.at, next?.at ?? nowIso),
1198
- endedAt: next?.at ?? null
1199
- });
1200
- }
1201
- for (const run5 of input.runs) {
1202
- const at = run5.started_at ?? run5.created_at;
1203
- const span = runSpan(run5);
1204
- const kind = run5.kind === "review" ? "verdict" : "run";
1205
- items.push({
1206
- id: `run:${run5.id}`,
1207
- at,
1208
- kind,
1209
- title: [
1210
- run5.kind,
1211
- run5.provider,
1212
- run5.status,
1213
- run5.reviewed_sha ? `@ ${run5.reviewed_sha.slice(0, 12)}` : "",
1214
- span
1215
- ].filter(Boolean).join(" "),
1216
- detail: run5.summary ? clip(run5.summary) : void 0,
1217
- durationMs: runDurationMs(run5, nowIso),
1218
- endedAt: run5.ended_at
1219
- });
1220
- }
1221
- for (const message of input.messages) {
1222
- const kind = message.from_role === "reviewer" ? "verdict" : "message";
1223
- items.push({
1224
- id: `message:${message.id}`,
1225
- at: message.created_at,
1226
- kind,
1227
- title: `${message.from_name} to ${message.to_role}`,
1228
- detail: clip(message.body_md),
1229
- durationMs: null
1230
- });
1231
- }
1232
- for (const decision of input.decisions) {
1233
- const answered = Boolean(decision.answered_at);
1234
- items.push({
1235
- id: `decision:${decision.id}`,
1236
- at: decision.created_at,
1237
- kind: "decision",
1238
- title: answered ? `answered (${decision.asked_by_role})` : `asked by ${decision.asked_by_role}`,
1239
- detail: clip(answered ? decision.answer_md || decision.question_md : decision.question_md),
1240
- durationMs: msBetween(decision.created_at, decision.answered_at ?? nowIso),
1241
- endedAt: decision.answered_at
1242
- });
1243
- }
1244
- items.sort((a, b) => a.at.localeCompare(b.at) || KIND_RANK[a.kind] - KIND_RANK[b.kind] || a.id.localeCompare(b.id));
1245
- return items;
1246
- }
1247
- var KIND_RANK;
1248
- var init_timeline = __esm({
1249
- "../../packages/db/src/timeline.ts"() {
1250
- "use strict";
1251
- init_status();
1252
- KIND_RANK = {
1253
- status: 0,
1254
- run: 1,
1255
- verdict: 2,
1256
- message: 3,
1257
- decision: 4
1258
- };
1259
- }
1260
- });
1261
-
1262
- // ../../packages/db/src/transcript.ts
1263
- function asRecord(value) {
1264
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
1265
- }
1266
- function str(value) {
1267
- return typeof value === "string" ? value : value == null ? "" : String(value);
1268
- }
1269
- function assistantText(payload) {
1270
- if (typeof payload.text === "string") return payload.text;
1271
- const item = asRecord(payload.item);
1272
- if (typeof item.text === "string") return item.text;
1273
- if (Array.isArray(item.content)) {
1274
- const joined = item.content.map((entry) => str(asRecord(entry).text)).filter(Boolean).join("\n");
1275
- if (joined) return joined;
1276
- }
1277
- const message = asRecord(payload.message);
1278
- const content = message.content;
1279
- if (Array.isArray(content)) {
1280
- return content.map((item2) => {
1281
- const row = asRecord(item2);
1282
- return str(row.text);
1283
- }).filter(Boolean).join("\n");
1284
- }
1285
- if (typeof content === "string") return content;
1286
- return "";
1287
- }
1288
- function toolName(payload) {
1289
- return str(payload.name || payload.tool || payload.tool_name || asRecord(payload.input).command || "tool");
1290
- }
1291
- function toolDetail(payload) {
1292
- const input = asRecord(payload.input ?? payload.args ?? payload);
1293
- const file = str(input.path || input.file_path || input.file || input.target);
1294
- const command = str(input.command || input.cmd);
1295
- const additions = Number(input.additions ?? input.added);
1296
- const deletions = Number(input.deletions ?? input.removed);
1297
- const stat2 = Number.isFinite(additions) || Number.isFinite(deletions) ? ` +${Number.isFinite(additions) ? additions : 0}/-${Number.isFinite(deletions) ? deletions : 0}` : "";
1298
- if (file) return `${file}${stat2}`;
1299
- if (command) return command.length > 80 ? `${command.slice(0, 77)}...` : command;
1300
- return "";
1301
- }
1302
- function resultTail(payload) {
1303
- const content = str(payload.content ?? payload.output ?? payload.stdout ?? payload.result);
1304
- const code = payload.exit_code ?? payload.exitCode ?? asRecord(payload.tool_result).exit_code;
1305
- const tail = content.trim().split("\n").slice(-8).join("\n");
1306
- const prefix = code == null ? "" : `exit ${code}`;
1307
- if (prefix && tail) return `${prefix}
1308
- ${tail}`;
1309
- return prefix || tail;
1224
+ function resultTail(payload) {
1225
+ const content = str(payload.content ?? payload.output ?? payload.stdout ?? payload.result);
1226
+ const code = payload.exit_code ?? payload.exitCode ?? asRecord(payload.tool_result).exit_code;
1227
+ const tail = content.trim().split("\n").slice(-8).join("\n");
1228
+ const prefix = code == null ? "" : `exit ${code}`;
1229
+ if (prefix && tail) return `${prefix}
1230
+ ${tail}`;
1231
+ return prefix || tail;
1310
1232
  }
1311
1233
  function codexItemLine(event, item) {
1312
1234
  const kind = str(item.type);
@@ -1322,12 +1244,12 @@ function codexItemLine(event, item) {
1322
1244
  return {
1323
1245
  id: event.id,
1324
1246
  kind: failed ? "error" : "tool",
1325
- title: `${name}${clip2(args, 120)}${status && status !== "completed" ? ` (${status})` : ""}`,
1326
- 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
1327
1249
  };
1328
1250
  }
1329
1251
  if (kind === "command_execution") {
1330
- const command = clip2(str(item.command ?? item.cmd), 120);
1252
+ const command = clip(str(item.command ?? item.cmd), 120);
1331
1253
  const code = item.exit_code ?? item.exitCode;
1332
1254
  return {
1333
1255
  id: event.id,
@@ -1342,7 +1264,7 @@ function codexItemLine(event, item) {
1342
1264
  if (kind === "reasoning" || kind === "todo_list") return SKIP;
1343
1265
  return { id: event.id, kind: "status", title: kind.replaceAll("_", " ") };
1344
1266
  }
1345
- function clip2(text, max) {
1267
+ function clip(text, max) {
1346
1268
  const flat = String(text ?? "").trim();
1347
1269
  return flat.length <= max ? flat : `${flat.slice(0, Math.max(0, max - 3))}...`;
1348
1270
  }
@@ -1415,6 +1337,185 @@ var init_transcript = __esm({
1415
1337
  }
1416
1338
  });
1417
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
+
1418
1519
  // ../../packages/db/src/waves.ts
1419
1520
  function ticketWaves(tickets) {
1420
1521
  const ids = new Set(tickets.map((ticket) => ticket.id));
@@ -1467,7 +1568,9 @@ var init_src = __esm({
1467
1568
  init_budgets();
1468
1569
  init_cost();
1469
1570
  init_commands();
1571
+ init_cockpit();
1470
1572
  init_defaults();
1573
+ init_effective_config();
1471
1574
  init_enums();
1472
1575
  init_epic_progress();
1473
1576
  init_filter();
@@ -1931,6 +2034,17 @@ var init_connection = __esm({
1931
2034
  }
1932
2035
  });
1933
2036
 
2037
+ // src/tui/capability.ts
2038
+ function canLaunchApp(streams = { stdin: process.stdin, stdout: process.stdout }) {
2039
+ return !isJsonMode() && streams.stdin.isTTY === true && streams.stdout.isTTY === true;
2040
+ }
2041
+ var init_capability = __esm({
2042
+ "src/tui/capability.ts"() {
2043
+ "use strict";
2044
+ init_json();
2045
+ }
2046
+ });
2047
+
1934
2048
  // src/architect/brain.ts
1935
2049
  import { createInterface } from "readline/promises";
1936
2050
  function brainById(id) {
@@ -1952,36 +2066,40 @@ async function confirm(question) {
1952
2066
  async function requireBrain(opts = {}) {
1953
2067
  const config = await loadConfig();
1954
2068
  const chosen = config.brain ?? DEFAULT_BRAIN;
1955
- const interactive = opts.interactive ?? (process.stdin.isTTY && process.stdout.isTTY);
2069
+ const interactive = opts.interactive ?? (opts.fatal !== false && process.stdin.isTTY && process.stdout.isTTY);
1956
2070
  const connection = await codexConnection();
1957
2071
  if (connection.state === "connected") {
1958
2072
  if (!config.brain) await saveConfig({ brain: chosen });
1959
2073
  return chosen;
1960
2074
  }
1961
2075
  if (!interactive) {
1962
- fail(`${connectionSummary(connection)}
1963
- ${howToFix(connection) ?? ""}`.trim());
2076
+ brainFailure(`${connectionSummary(connection)}
2077
+ ${howToFix(connection) ?? ""}`.trim(), opts.fatal);
1964
2078
  }
1965
2079
  if (connection.state === "not_installed") {
1966
- fail(`${connectionSummary(connection)}
1967
- ${howToFix(connection) ?? ""}`.trim());
2080
+ brainFailure(`${connectionSummary(connection)}
2081
+ ${howToFix(connection) ?? ""}`.trim(), opts.fatal);
1968
2082
  }
1969
2083
  const brain = brainById(chosen.id);
1970
2084
  out("");
1971
2085
  out(`${c.bold("HigherDEV needs your ChatGPT subscription to answer.")}`);
1972
2086
  out(c.dim(`${brain?.billing ?? ""}. The sign-in happens in your browser; hd never sees the token.`));
1973
2087
  if (!await confirm(c.dim("Press enter to sign in, or n to cancel: "))) {
1974
- fail("Not connected. Run `hd brain` when you want to.");
2088
+ brainFailure("Not connected. Run `hd brain` when you want to.", opts.fatal);
1975
2089
  }
1976
2090
  const connected = await connectCodex();
1977
2091
  if (connected.state !== "connected") {
1978
- fail(`${connectionSummary(connected)}
1979
- ${howToFix(connected) ?? ""}`.trim());
2092
+ brainFailure(`${connectionSummary(connected)}
2093
+ ${howToFix(connected) ?? ""}`.trim(), opts.fatal);
1980
2094
  }
1981
2095
  out(c.green(connectionSummary(connected)));
1982
2096
  await saveConfig({ brain: chosen });
1983
2097
  return chosen;
1984
2098
  }
2099
+ function brainFailure(message, fatal = true) {
2100
+ if (fatal) fail(message);
2101
+ throw new Error(message);
2102
+ }
1985
2103
  async function setBrain(patch) {
1986
2104
  const config = await loadConfig();
1987
2105
  const next = { ...config.brain ?? DEFAULT_BRAIN, ...patch };
@@ -2109,6 +2227,12 @@ async function loadRunEvents(ctx, runId, afterSeq = -1) {
2109
2227
  if (error) fail(error.message);
2110
2228
  return data ?? [];
2111
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
+ }
2112
2236
  async function loadMessages(ctx, opts = {}) {
2113
2237
  let query = ctx.db.from("messages").select("*").eq("workspace_id", ctx.workspace.id).order("created_at", { ascending: true }).limit(opts.limit ?? 200);
2114
2238
  if (opts.ticketId) query = query.eq("ticket_id", opts.ticketId);
@@ -2469,9 +2593,9 @@ async function runAgent(opts) {
2469
2593
  opts.onEvent?.({ kind: "tool", name: call.name, args });
2470
2594
  const tool = byName.get(call.name);
2471
2595
  let output;
2472
- let ok2 = true;
2596
+ let ok3 = true;
2473
2597
  if (!tool) {
2474
- ok2 = false;
2598
+ ok3 = false;
2475
2599
  output = `Error: no tool named ${call.name}.`;
2476
2600
  } else {
2477
2601
  try {
@@ -2479,11 +2603,11 @@ async function runAgent(opts) {
2479
2603
  output = typeof result === "string" ? result : JSON.stringify(result ?? null);
2480
2604
  if (!output) output = "(no result)";
2481
2605
  } catch (error) {
2482
- ok2 = false;
2606
+ ok3 = false;
2483
2607
  output = `Error: ${error instanceof Error ? error.message : String(error)}`;
2484
2608
  }
2485
2609
  }
2486
- 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) });
2487
2611
  input.push({
2488
2612
  type: "function_call",
2489
2613
  name: call.name,
@@ -2686,7 +2810,96 @@ var init_group = __esm({
2686
2810
  }
2687
2811
  });
2688
2812
 
2689
- // src/out/format.ts
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
+
2902
+ // src/out/format.ts
2690
2903
  function truncate(text, max) {
2691
2904
  const flat = String(text ?? "").replace(/\s+/g, " ").trim();
2692
2905
  if (flat.length <= max) return flat;
@@ -3152,7 +3365,7 @@ function registerReadCommands(program) {
3152
3365
  ${statusView(board, pausedAll)}`
3153
3366
  );
3154
3367
  });
3155
- 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() {
3156
3369
  const opts = this.opts();
3157
3370
  const ctx = await requireWorkspace(slugOf(this));
3158
3371
  const board = await loadBoard(ctx);
@@ -3201,7 +3414,7 @@ ${statusView(board, pausedAll)}`
3201
3414
  const board = await loadBoard(ctx);
3202
3415
  emit(board.hosts, () => hostsView(board.hosts));
3203
3416
  });
3204
- 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() {
3205
3418
  const ctx = await requireWorkspace(slugOf(this));
3206
3419
  const days = Number(this.opts().days) || 14;
3207
3420
  const [daily, tickets, epics] = await Promise.all([
@@ -3255,6 +3468,7 @@ var init_commands2 = __esm({
3255
3468
  init_theme();
3256
3469
  init_queries();
3257
3470
  init_group();
3471
+ init_argv_parsers();
3258
3472
  init_views();
3259
3473
  }
3260
3474
  });
@@ -3295,31 +3509,46 @@ async function postMessage(ctx, input) {
3295
3509
  await ctx.audit(ctx.workspace.id, "message", { to, ticket_key: input.ticketKey });
3296
3510
  return data;
3297
3511
  }
3298
- 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 = {}) {
3299
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);
3300
- if (loadError) fail(loadError.message);
3522
+ if (loadError) return { ok: false, error: loadError.message };
3301
3523
  const matches = (rows ?? []).filter((row) => row.id.startsWith(decisionId));
3302
- if (matches.length === 0) fail(`No open decision matching ${decisionId}.`);
3303
- 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
+ }
3304
3528
  const decision = matches[0];
3305
3529
  if (opts.dismiss) {
3306
3530
  const { error: error2 } = await ctx.db.from("decisions").update({ dismissed_at: (/* @__PURE__ */ new Date()).toISOString(), answered_by: ctx.email }).eq("id", decision.id);
3307
- if (error2) fail(error2.message);
3531
+ if (error2) return { ok: false, error: error2.message };
3308
3532
  await ctx.audit(ctx.workspace.id, "dismiss", { decision_id: decision.id });
3309
- 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." };
3310
3538
  }
3311
- const options = Array.isArray(decision.options) ? decision.options : [];
3312
- const index = Number(answer);
3313
- const resolved = options.length && Number.isInteger(index) && index >= 1 && index <= options.length ? options[index - 1] : answer;
3314
- if (!resolved.trim()) fail("An answer needs text, or the number of an option.");
3315
3539
  const { error } = await ctx.db.from("decisions").update({
3316
3540
  answer_md: resolved,
3317
3541
  answered_by: ctx.email,
3318
3542
  answered_at: (/* @__PURE__ */ new Date()).toISOString()
3319
3543
  }).eq("id", decision.id);
3320
- if (error) fail(error.message);
3544
+ if (error) return { ok: false, error: error.message };
3321
3545
  await ctx.audit(ctx.workspace.id, "answer", { decision_id: decision.id });
3322
- 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 };
3323
3552
  }
3324
3553
  async function awaitReply(ctx, since, timeoutMs) {
3325
3554
  return new Promise((resolve) => {
@@ -3348,7 +3577,7 @@ async function awaitReply(ctx, since, timeoutMs) {
3348
3577
  });
3349
3578
  }
3350
3579
  function registerMessagingCommands(program) {
3351
- 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) {
3352
3581
  const opts = this.opts();
3353
3582
  const ctx = await requireWorkspace(slugOf2(this));
3354
3583
  const looksLikeKey = key2 && /^HD-\d+$/i.test(key2);
@@ -3361,7 +3590,7 @@ function registerMessagingCommands(program) {
3361
3590
  });
3362
3591
  ok(`Sent to ${c.bold(opts.to)}${looksLikeKey ? ` on ${key2}` : ""}.`, { message });
3363
3592
  });
3364
- 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) {
3365
3594
  const opts = this.opts();
3366
3595
  const ctx = await requireWorkspace(slugOf2(this));
3367
3596
  const since = (/* @__PURE__ */ new Date()).toISOString();
@@ -3417,6 +3646,7 @@ var init_commands3 = __esm({
3417
3646
  init_queries();
3418
3647
  init_views();
3419
3648
  init_subscribe();
3649
+ init_argv_parsers();
3420
3650
  }
3421
3651
  });
3422
3652
 
@@ -3481,14 +3711,31 @@ async function createTicket(ctx, input) {
3481
3711
  await ctx.audit(ctx.workspace.id, "create", { ticket_key: data.key, subject: "a ticket" });
3482
3712
  return data;
3483
3713
  }
3484
- async function updateTicket(ctx, ticket, patch) {
3714
+ async function applyOwnerCommand(ctx, ticket, operationId, intent) {
3715
+ const result = await applyOwnerTicketCommand(ctx.db, ownerTicketCommand({
3716
+ operationId,
3717
+ actorEmail: ctx.email,
3718
+ workspaceId: ctx.workspace.id,
3719
+ ticketId: ticket.id,
3720
+ expectedStatus: ticket.status,
3721
+ expectedVersion: ticket.lifecycle_version,
3722
+ intent
3723
+ }));
3724
+ if (result.outcome === "rejected") {
3725
+ fail(`Ticket command rejected: ${result.code}. Retry with a new --request-id after reloading the ticket.`);
3726
+ }
3727
+ const { data, error } = await ctx.db.from("tickets").select("*").eq("id", ticket.id).single();
3728
+ if (error) fail(error.message);
3729
+ return data;
3730
+ }
3731
+ async function updateTicket(ctx, ticket, patch, operationId) {
3485
3732
  const fields = {};
3486
3733
  if (patch.title !== void 0) fields.title = patch.title.trim();
3487
- if (patch.body !== void 0) fields.body_md = patch.body;
3488
- if (patch.acceptance !== void 0) fields.acceptance_md = patch.acceptance;
3734
+ if (patch.body !== void 0) fields.bodyMd = patch.body;
3735
+ if (patch.acceptance !== void 0) fields.acceptanceMd = patch.acceptance;
3489
3736
  if (patch.area !== void 0) fields.area = patch.area;
3490
3737
  if (patch.priority !== void 0) fields.priority = patch.priority;
3491
- if (patch.epic !== void 0) fields.epic_id = patch.epic;
3738
+ if (patch.epic !== void 0) fields.epicId = patch.epic;
3492
3739
  if (patch.host !== void 0) {
3493
3740
  const hostError = await assertHost(ctx.db, patch.host, ctx.workspace.repo);
3494
3741
  if (hostError) fail(hostError);
@@ -3498,9 +3745,7 @@ async function updateTicket(ctx, ticket, patch) {
3498
3745
  const agentId = await agentIdFor(ctx, patch.agent);
3499
3746
  const assigned = await resolveAssignedBuilder(ctx.db, ctx.workspace.id, agentId, null);
3500
3747
  if (!assigned.ok) fail(assigned.error);
3501
- fields.agent_id = assigned.agent?.id ?? null;
3502
- fields.provider = assigned.agent?.provider ?? null;
3503
- fields.provider_pinned = Boolean(assigned.agent);
3748
+ fields.agentId = assigned.agent?.id ?? null;
3504
3749
  }
3505
3750
  if (patch.status !== void 0) {
3506
3751
  const wanted = patch.status.trim();
@@ -3509,19 +3754,19 @@ async function updateTicket(ctx, ticket, patch) {
3509
3754
  }
3510
3755
  const blockers = await blockerStatuses(ctx.db, ctx.workspace.id, ticket.blocked_by ?? [], ticket.id);
3511
3756
  if (blockers.error) fail(blockers.error);
3512
- fields.status = resolveTicketStatus(wanted, blockers.statuses);
3513
3757
  }
3514
- if (Object.keys(fields).length === 0) fail("Nothing to change.");
3515
- const { data, error } = await ctx.db.from("tickets").update(fields).eq("id", ticket.id).select("*").single();
3516
- if (error) fail(error.message);
3517
- await ctx.audit(ctx.workspace.id, "update", {
3518
- ticket_key: ticket.key,
3519
- subject: "a ticket",
3520
- fields: Object.keys(fields).join(", ")
3758
+ if (Object.keys(fields).length === 0 && patch.status === void 0) fail("Nothing to change.");
3759
+ if (Object.keys(fields).length === 0) fields.title = ticket.title;
3760
+ const requested = patch.status === void 0 ? ticket.status : patch.status.trim();
3761
+ return applyOwnerCommand(ctx, ticket, operationId, {
3762
+ type: "edit_ticket",
3763
+ fields,
3764
+ blockedBy: ticket.blocked_by ?? [],
3765
+ toStatus: requested,
3766
+ reason: `CLI owner edited ${ticket.key}`
3521
3767
  });
3522
- return data;
3523
3768
  }
3524
- async function setBlockedBy(ctx, ticket, keys, mode) {
3769
+ async function setBlockedBy(ctx, ticket, keys, mode, operationId) {
3525
3770
  const ids = await keysToIds(ctx, keys);
3526
3771
  const current = ticket.blocked_by ?? [];
3527
3772
  const next = mode === "set" ? ids : mode === "add" ? [.../* @__PURE__ */ new Set([...current, ...ids])] : current.filter((id) => !ids.includes(id));
@@ -3532,23 +3777,22 @@ async function setBlockedBy(ctx, ticket, keys, mode) {
3532
3777
  }
3533
3778
  const blockers = await blockerStatuses(ctx.db, ctx.workspace.id, next, ticket.id);
3534
3779
  if (blockers.error) fail(blockers.error);
3535
- const status = resolveTicketStatus(ticket.status, blockers.statuses);
3536
- const { data, error } = await ctx.db.from("tickets").update({ blocked_by: blockers.ids, status }).eq("id", ticket.id).select("*").single();
3537
- if (error) fail(error.message);
3538
- await ctx.audit(ctx.workspace.id, "update", {
3539
- ticket_key: ticket.key,
3540
- subject: "dependencies"
3780
+ return applyOwnerCommand(ctx, ticket, operationId, {
3781
+ type: "set_dependencies",
3782
+ blockedBy: blockers.ids,
3783
+ toStatus: ticket.status,
3784
+ reason: `CLI owner changed dependencies for ${ticket.key}`
3541
3785
  });
3542
- return data;
3543
3786
  }
3544
- async function takeOver(ctx, ticket) {
3787
+ async function takeOver(ctx, ticket, operationId) {
3545
3788
  if (ticket.human_owner) fail(`${ticket.key} is already owned by ${ticket.human_owner}.`);
3546
- const { data, error } = await ctx.db.from("tickets").update({ human_owner: ctx.email }).eq("id", ticket.id).select("*").single();
3547
- if (error) fail(error.message);
3548
- await ctx.audit(ctx.workspace.id, "takeover", { ticket_key: ticket.key });
3549
- return data;
3789
+ return applyOwnerCommand(ctx, ticket, operationId, {
3790
+ type: "take_over",
3791
+ owner: ctx.email,
3792
+ reason: `CLI owner took over ${ticket.key}`
3793
+ });
3550
3794
  }
3551
- async function handBack(ctx, ticket) {
3795
+ async function handBack(ctx, ticket, operationId) {
3552
3796
  if (!ticket.human_owner) fail(`${ticket.key} is not human-owned.`);
3553
3797
  const blockers = await blockerStatuses(ctx.db, ctx.workspace.id, ticket.blocked_by ?? [], ticket.id);
3554
3798
  if (blockers.error) fail(blockers.error);
@@ -3558,10 +3802,11 @@ async function handBack(ctx, ticket) {
3558
3802
  assigned: Boolean(ticket.agent_id),
3559
3803
  blockerStatuses: blockers.statuses
3560
3804
  });
3561
- const { data, error } = await ctx.db.from("tickets").update({ human_owner: null, status }).eq("id", ticket.id).select("*").single();
3562
- if (error) fail(error.message);
3563
- await ctx.audit(ctx.workspace.id, "hand_back", { ticket_key: ticket.key });
3564
- return data;
3805
+ return applyOwnerCommand(ctx, ticket, operationId, {
3806
+ type: "hand_back",
3807
+ toStatus: status,
3808
+ reason: `CLI owner handed back ${ticket.key}`
3809
+ });
3565
3810
  }
3566
3811
  async function killRun(ctx, runId, ticketKey) {
3567
3812
  const { data, error } = await ctx.db.rpc("request_run_cancellation", {
@@ -3656,6 +3901,160 @@ var init_epics = __esm({
3656
3901
  }
3657
3902
  });
3658
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
+
3659
4058
  // src/write/editor.ts
3660
4059
  import { spawn as spawn2 } from "child_process";
3661
4060
  import { mkdtemp, readFile as readFile4, rm as rm2, writeFile as writeFile2 } from "fs/promises";
@@ -3693,6 +4092,10 @@ import { randomUUID as randomUUID2 } from "crypto";
3693
4092
  function slugOf3(command) {
3694
4093
  return command.optsWithGlobals().workspace;
3695
4094
  }
4095
+ function must(result) {
4096
+ if (!result.ok) fail(result.error);
4097
+ return result.value;
4098
+ }
3696
4099
  async function setWorkspacePaused(ctx, paused) {
3697
4100
  const { error } = await ctx.db.from("workspaces").update({ paused }).eq("id", ctx.workspace.id);
3698
4101
  if (error) fail(error.message);
@@ -3717,7 +4120,7 @@ async function setProviderPaused(ctx, provider, paused) {
3717
4120
  await ctx.audit(ctx.workspace.id, paused ? "pause" : "resume", { subject: provider });
3718
4121
  }
3719
4122
  function registerControlCommands(program) {
3720
- 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() {
3721
4124
  const opts = this.opts();
3722
4125
  if (opts.all) {
3723
4126
  const ctx2 = await requireCtx();
@@ -3734,7 +4137,7 @@ function registerControlCommands(program) {
3734
4137
  await setWorkspacePaused(ctx, true);
3735
4138
  ok(c.yellow(`${ctx.workspace.slug} is paused.`));
3736
4139
  });
3737
- 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() {
3738
4141
  const opts = this.opts();
3739
4142
  if (opts.all) {
3740
4143
  const ctx2 = await requireCtx();
@@ -3787,7 +4190,7 @@ function registerAgentCommands(program) {
3787
4190
  });
3788
4191
  agent.command("show").argument("<name>", "agent display name").description("one agent in full").action(async function(name) {
3789
4192
  const ctx = await requireWorkspace(slugOf3(this));
3790
- const found = await findAgent(ctx, name);
4193
+ const found = must(await findAgent(ctx, name));
3791
4194
  emit(
3792
4195
  found,
3793
4196
  () => [
@@ -3804,80 +4207,43 @@ ${found.prompt_addendum}` : ""
3804
4207
  ].filter(Boolean).join("\n")
3805
4208
  );
3806
4209
  });
3807
- 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) {
3808
4211
  const opts = this.opts();
3809
4212
  const ctx = await requireWorkspace(slugOf3(this));
3810
- const found = await findAgent(ctx, name);
3811
- const fields = {};
3812
- if (opts.name) fields.display_name = String(opts.name).trim();
3813
- if (opts.model) fields.model = String(opts.model).trim();
3814
- if (opts.effort) fields.effort = asEffort(opts.effort);
3815
- if (opts.provider) {
3816
- if (!providers.includes(opts.provider)) {
3817
- fail(`Unknown provider ${opts.provider}. One of: ${providers.join(", ")}.`);
3818
- }
3819
- fields.provider = opts.provider;
3820
- }
3821
- if (opts.notes !== void 0) fields.routing_notes = opts.notes;
3822
- if (opts.prompt !== void 0) fields.prompt_addendum = opts.prompt;
4213
+ let prompt2 = opts.prompt;
3823
4214
  if (opts.editPrompt) {
3824
- fields.prompt_addendum = (await editInEditor(found.prompt_addendum ?? "", "hd-prompt.md")).trim();
3825
- }
3826
- if (opts.enable) fields.enabled = true;
3827
- if (opts.disable) fields.enabled = false;
3828
- if (opts.runsPerHour !== void 0) {
3829
- const cap = parseOptionalCap(opts.runsPerHour === "none" ? "" : opts.runsPerHour, {
3830
- integer: true
3831
- });
3832
- if (!cap.ok) fail(cap.error);
3833
- fields.runs_per_hour = cap.value;
4215
+ const found = must(await findAgent(ctx, name));
4216
+ prompt2 = (await editInEditor(found.prompt_addendum ?? "", "hd-prompt.md")).trim();
3834
4217
  }
3835
- if (opts.dailySpend !== void 0) {
3836
- const cap = parseOptionalCap(opts.dailySpend === "none" ? "" : opts.dailySpend);
3837
- if (!cap.ok) fail(cap.error);
3838
- fields.daily_spend_usd = cap.value;
3839
- }
3840
- if (Object.keys(fields).length === 0) fail("Nothing to change.");
3841
- if (fields.enabled === true && (found.role === "orchestrator" || found.role === "reviewer")) {
3842
- 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();
3843
- if (other) fail(`${other.display_name} is already the enabled ${found.role}. Disable it first.`);
3844
- }
3845
- const { data, error } = await ctx.db.from("agents").update(fields).eq("id", found.id).eq("workspace_id", ctx.workspace.id).select("*").single();
3846
- if (error) fail(error.message);
3847
- 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
+ );
3848
4231
  ok(`${c.bold(data.display_name)} updated.`, { agent: data });
3849
4232
  });
3850
- 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) {
3851
4234
  const opts = this.opts();
3852
4235
  const ctx = await requireWorkspace(slugOf3(this));
3853
- if (!providers.includes(opts.provider)) {
3854
- fail(`Unknown provider ${opts.provider}. One of: ${providers.join(", ")}.`);
3855
- }
3856
- const { data, error } = await ctx.db.from("agents").insert({
3857
- workspace_id: ctx.workspace.id,
3858
- role: "builder",
3859
- provider: opts.provider,
3860
- model: opts.model,
3861
- display_name: name.trim(),
3862
- effort: asEffort(opts.effort),
3863
- routing_notes: opts.notes ?? ""
3864
- }).select("*").single();
3865
- if (error) fail(error.message);
3866
- 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
+ );
3867
4244
  ok(`${c.bold(data.display_name)} added.`, { agent: data });
3868
4245
  });
3869
4246
  }
3870
- async function findAgent(ctx, name) {
3871
- const { data, error } = await ctx.db.from("agents").select("*").eq("workspace_id", ctx.workspace.id);
3872
- if (error) fail(error.message);
3873
- const wanted = name.trim().toLowerCase();
3874
- const matches = (data ?? []).filter(
3875
- (row) => row.id === name || row.display_name.toLowerCase() === wanted || row.role.toLowerCase() === wanted
3876
- );
3877
- if (matches.length === 0) fail(`No agent named ${name}. Run \`hd agent ls\`.`);
3878
- if (matches.length > 1) fail(`${name} matches ${matches.length} agents. Use a display name.`);
3879
- return matches[0];
3880
- }
3881
4247
  function registerWorkspaceCommands(program) {
3882
4248
  const workspace = program.command("workspace").description("workspaces and their settings");
3883
4249
  workspace.command("ls", { isDefault: true }).description("every workspace").action(async function() {
@@ -3923,45 +4289,25 @@ function registerWorkspaceCommands(program) {
3923
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) {
3924
4290
  const opts = this.opts();
3925
4291
  const ctx = await requireCtx();
3926
- if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) fail("Repo looks like owner/name.");
3927
- const { data, error } = await ctx.db.from("workspaces").insert({
3928
- name: name.trim(),
3929
- slug: slugify(name),
3930
- repo,
3931
- default_branch: opts.branch,
3932
- default_host: opts.host
3933
- }).select("*").single();
3934
- if (error) fail(error.message);
3935
- const { error: agentsError } = await ctx.db.from("agents").insert(DEFAULT_AGENTS.map((agent) => ({ ...agent, workspace_id: data.id })));
3936
- if (agentsError) fail(`Workspace created, but seeding its agents failed: ${agentsError.message}`);
3937
- 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
+ );
3938
4295
  ok(
3939
- `${c.bold(data.slug)} created with ${DEFAULT_AGENTS.length} agents. Run \`hd use ${data.slug}\`.`,
3940
- { workspace: data }
4296
+ `${c.bold(made.workspace.slug)} created with ${made.agents} agents. Run \`hd use ${made.workspace.slug}\`.`,
4297
+ { workspace: made.workspace }
3941
4298
  );
3942
4299
  });
3943
- 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() {
3944
4301
  const opts = this.opts();
3945
4302
  const ctx = await requireWorkspace(slugOf3(this));
3946
- const fields = {};
3947
- if (opts.branch) fields.default_branch = opts.branch;
3948
- if (opts.host) fields.default_host = opts.host;
3949
- if (opts.autoMerge !== void 0) fields.auto_merge = Boolean(opts.autoMerge);
3950
- if (opts.cap) {
3951
- const caps = { ...ctx.workspace.provider_caps ?? {} };
3952
- for (const pair of [].concat(opts.cap)) {
3953
- const [provider, value] = String(pair).split("=");
3954
- if (!providers.includes(provider)) fail(`Unknown provider ${provider}.`);
3955
- const n = Number(value);
3956
- if (!Number.isInteger(n) || n < 0) fail(`Cap for ${provider} must be a whole number.`);
3957
- caps[provider] = n;
3958
- }
3959
- fields.provider_caps = caps;
3960
- }
3961
- if (Object.keys(fields).length === 0) fail("Nothing to change.");
3962
- const { data, error } = await ctx.db.from("workspaces").update(fields).eq("id", ctx.workspace.id).select("*").single();
3963
- if (error) fail(error.message);
3964
- 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
+ );
3965
4311
  ok(`${c.bold(data.slug)} updated.`, { workspace: data });
3966
4312
  });
3967
4313
  workspace.command("templates").description("ticket templates for this workspace").action(async function() {
@@ -4051,7 +4397,7 @@ function registerAttachCommands(program) {
4051
4397
  paths: uploaded
4052
4398
  });
4053
4399
  });
4054
- 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) {
4055
4401
  const ctx = await requireWorkspace(slugOf3(this));
4056
4402
  const { data: ticket } = await ctx.db.from("tickets").select("id, key").eq("workspace_id", ctx.workspace.id).eq("key", key2.trim().toUpperCase()).maybeSingle();
4057
4403
  if (!ticket) fail(`No ticket ${key2} in ${ctx.workspace.slug}.`);
@@ -4074,17 +4420,20 @@ var init_commands4 = __esm({
4074
4420
  "use strict";
4075
4421
  init_src();
4076
4422
  init_context();
4423
+ init_config2();
4077
4424
  init_json();
4078
4425
  init_theme();
4079
4426
  init_format();
4080
4427
  init_queries();
4081
4428
  init_views();
4082
4429
  init_editor();
4430
+ init_argv_parsers();
4083
4431
  DEFAULT_CAPS = { claude: 2, codex: 3, gemini: 1, grok: 2 };
4084
4432
  }
4085
4433
  });
4086
4434
 
4087
4435
  // src/architect/tools.ts
4436
+ import { randomUUID as randomUUID3 } from "crypto";
4088
4437
  import { z as z2 } from "zod";
4089
4438
  async function ticketOf(ctx, wanted) {
4090
4439
  const board = await loadBoard(ctx);
@@ -4302,10 +4651,12 @@ var init_tools = __esm({
4302
4651
  agent: z2.string().nullable().optional(),
4303
4652
  priority: z2.number().optional(),
4304
4653
  status: z2.string().optional(),
4305
- epic_id: z2.string().nullable().optional()
4654
+ epic_id: z2.string().nullable().optional(),
4655
+ request_id: z2.string().optional()
4306
4656
  }),
4307
4657
  async run(ctx, args) {
4308
4658
  const ticket = await ticketOf(ctx, String(args.key));
4659
+ const operationId = String(args.request_id ?? randomUUID3());
4309
4660
  const updated = await updateTicket(ctx, ticket, {
4310
4661
  title: args.title,
4311
4662
  body: args.body_md,
@@ -4315,8 +4666,8 @@ var init_tools = __esm({
4315
4666
  priority: args.priority,
4316
4667
  status: args.status,
4317
4668
  epic: args.epic_id
4318
- });
4319
- return { key: updated.key, status: updated.status };
4669
+ }, operationId);
4670
+ return { key: updated.key, status: updated.status, request_id: operationId };
4320
4671
  }
4321
4672
  },
4322
4673
  {
@@ -4326,17 +4677,20 @@ var init_tools = __esm({
4326
4677
  schema: z2.object({
4327
4678
  key,
4328
4679
  blocked_by: z2.array(z2.string()).describe("Ticket keys."),
4329
- mode: z2.enum(["set", "add", "remove"]).optional()
4680
+ mode: z2.enum(["set", "add", "remove"]).optional(),
4681
+ request_id: z2.string().optional()
4330
4682
  }),
4331
4683
  async run(ctx, args) {
4332
4684
  const ticket = await ticketOf(ctx, String(args.key));
4685
+ const operationId = String(args.request_id ?? randomUUID3());
4333
4686
  const updated = await setBlockedBy(
4334
4687
  ctx,
4335
4688
  ticket,
4336
4689
  args.blocked_by ?? [],
4337
- args.mode ?? "set"
4690
+ args.mode ?? "set",
4691
+ operationId
4338
4692
  );
4339
- return { key: updated.key, status: updated.status, blocked_by: updated.blocked_by };
4693
+ return { key: updated.key, status: updated.status, blocked_by: updated.blocked_by, request_id: operationId };
4340
4694
  }
4341
4695
  },
4342
4696
  {
@@ -4421,20 +4775,22 @@ var init_tools = __esm({
4421
4775
  name: "take_over_ticket",
4422
4776
  access: "write",
4423
4777
  description: "Mark a ticket human-owned so the board stops dispatching it.",
4424
- schema: z2.object({ key }),
4778
+ schema: z2.object({ key, request_id: z2.string().optional() }),
4425
4779
  async run(ctx, args) {
4426
- const updated = await takeOver(ctx, await ticketOf(ctx, String(args.key)));
4427
- return { key: updated.key, human_owner: updated.human_owner };
4780
+ const operationId = String(args.request_id ?? randomUUID3());
4781
+ const updated = await takeOver(ctx, await ticketOf(ctx, String(args.key)), operationId);
4782
+ return { key: updated.key, human_owner: updated.human_owner, request_id: operationId };
4428
4783
  }
4429
4784
  },
4430
4785
  {
4431
4786
  name: "hand_back_ticket",
4432
4787
  access: "write",
4433
4788
  description: "Give a human-owned ticket back to the board.",
4434
- schema: z2.object({ key }),
4789
+ schema: z2.object({ key, request_id: z2.string().optional() }),
4435
4790
  async run(ctx, args) {
4436
- const updated = await handBack(ctx, await ticketOf(ctx, String(args.key)));
4437
- return { key: updated.key, status: updated.status };
4791
+ const operationId = String(args.request_id ?? randomUUID3());
4792
+ const updated = await handBack(ctx, await ticketOf(ctx, String(args.key)), operationId);
4793
+ return { key: updated.key, status: updated.status, request_id: operationId };
4438
4794
  }
4439
4795
  },
4440
4796
  {
@@ -4604,7 +4960,8 @@ function architectInstructions(opts) {
4604
4960
  }
4605
4961
  async function openArchitect(opts) {
4606
4962
  const { ctx } = opts;
4607
- const brain = await requireBrain();
4963
+ const fatalAuth = opts.fatalAuth ?? true;
4964
+ const brain = await requireBrain({ fatal: fatalAuth });
4608
4965
  const repo = await repoCheckout(ctx);
4609
4966
  const platformTools = toolsFor(opts.readOnly ? "read" : "all").map((tool) => ({
4610
4967
  name: tool.name,
@@ -4622,7 +4979,8 @@ async function openArchitect(opts) {
4622
4979
  tools: [...platformTools, ...buildRepoTools(repo)],
4623
4980
  sessionId: newSessionId(),
4624
4981
  history: [],
4625
- repo
4982
+ repo,
4983
+ fatalAuth
4626
4984
  };
4627
4985
  }
4628
4986
  async function architectTurn(opts) {
@@ -4644,7 +5002,7 @@ async function architectTurn(opts) {
4644
5002
  if (runError) fail(runError.message);
4645
5003
  let seq = 0;
4646
5004
  const events = [];
4647
- 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 });
4648
5006
  const flush = async () => {
4649
5007
  if (events.length) await ctx.db.from("run_events").insert(events);
4650
5008
  };
@@ -4658,14 +5016,14 @@ async function architectTurn(opts) {
4658
5016
  sessionId: session.sessionId,
4659
5017
  signal: opts.signal,
4660
5018
  onEvent: (event) => {
4661
- 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 });
4662
5020
  else if (event.kind === "tool_result") {
4663
- 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 });
4664
5022
  }
4665
5023
  opts.onEvent(event);
4666
5024
  }
4667
5025
  });
4668
- if (result.text) record("text", { text: result.text });
5026
+ if (result.text) record2("text", { text: result.text });
4669
5027
  session.history.push({ role: "user", content: opts.request });
4670
5028
  session.history.push({ role: "assistant", content: result.text });
4671
5029
  await flush();
@@ -4680,7 +5038,7 @@ async function architectTurn(opts) {
4680
5038
  return { text: result.text, runId: runRow.id, toolCalls: result.toolCalls };
4681
5039
  } catch (error) {
4682
5040
  const message = error instanceof Error ? error.message : String(error);
4683
- record("error", { error: message });
5041
+ record2("error", { error: message });
4684
5042
  await flush();
4685
5043
  await ctx.db.from("runs").update({
4686
5044
  status: "failed",
@@ -4688,7 +5046,7 @@ async function architectTurn(opts) {
4688
5046
  exit_code: 1,
4689
5047
  summary: message.slice(0, 500)
4690
5048
  }).eq("id", runRow.id);
4691
- if (error instanceof ChatGptAuthError) fail(message);
5049
+ if (error instanceof ChatGptAuthError && session.fatalAuth) fail(message);
4692
5050
  throw error;
4693
5051
  }
4694
5052
  }
@@ -4759,297 +5117,1335 @@ var init_theme2 = __esm({
4759
5117
  });
4760
5118
 
4761
5119
  // src/tui/Banner.tsx
4762
- import { useEffect, useState } from "react";
4763
- import { Box, Text } from "ink";
5120
+ import { useEffect, useMemo, useRef, useState } from "react";
5121
+ import { Box, Text, useStdout } from "ink";
4764
5122
  import { jsx } from "react/jsx-runtime";
4765
- function bannerRows(word = WORD, gap = 1) {
4766
- const rows = ["", "", "", "", ""];
4767
- 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) => {
4768
5132
  const glyph = GLYPHS[letter];
4769
- if (!glyph) continue;
4770
5133
  const width = Math.max(...glyph.map((row) => row.length));
4771
- for (let i = 0; i < rows.length; i += 1) {
4772
- 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];
4773
5157
  }
5158
+ rows.push(row);
4774
5159
  }
4775
- return rows.map((row) => row.replace(/\s+$/, ""));
5160
+ return rows;
4776
5161
  }
4777
- function Banner({ animate = true, onDone }) {
4778
- const [frame, setFrame] = useState(animate ? 0 : FLASH.length - 1);
4779
- useEffect(() => {
4780
- if (!animate) {
4781
- onDone?.();
4782
- 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) });
4783
5187
  }
4784
- 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;
4785
5216
  onDone?.();
4786
- return;
4787
5217
  }
4788
- 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);
4789
5222
  return () => clearTimeout(timer);
4790
- }, [frame, animate, onDone]);
4791
- const style = FLASH[Math.min(frame, FLASH.length - 1)];
4792
- const rows = bannerRows();
4793
- 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)) });
4794
5228
  }
4795
- var GLYPHS, WORD, FLASH, FRAME_MS;
5229
+ var WORD, GLYPHS, PIXEL_ROWS, BANNER_WIDTH, BANNER_HEIGHT, HALF, LEVELS, SETTLED, STORM_MS;
4796
5230
  var init_Banner = __esm({
4797
5231
  "src/tui/Banner.tsx"() {
4798
5232
  "use strict";
4799
5233
  init_theme2();
5234
+ WORD = "HigherDEV";
4800
5235
  GLYPHS = {
4801
- H: ["\u2588 \u2588", "\u2588 \u2588", "\u2588\u2588\u2588\u2588", "\u2588 \u2588", "\u2588 \u2588"],
4802
- i: ["\u2588", " ", "\u2588", "\u2588", "\u2588"],
4803
- " ": [" ", " ", " ", " ", " "],
4804
- g: [" ", " \u2588\u2588\u2588\u2588", "\u2588 \u2588", " \u2588\u2588\u2588\u2588", " \u2588"],
4805
- h: ["\u2588 ", "\u2588 ", "\u2588\u2588\u2588 ", "\u2588 \u2588", "\u2588 \u2588"],
4806
- e: [" ", " \u2588\u2588\u2588 ", "\u2588\u2588\u2588\u2588\u2588", "\u2588 ", " \u2588\u2588\u2588\u2588"],
4807
- r: [" ", " ", "\u2588 \u2588\u2588", "\u2588\u2588 ", "\u2588 "],
4808
- D: ["\u2588\u2588\u2588 ", "\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588", "\u2588\u2588\u2588 "],
4809
- E: ["\u2588\u2588\u2588\u2588", "\u2588 ", "\u2588\u2588\u2588 ", "\u2588 ", "\u2588\u2588\u2588\u2588"],
4810
- 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
+ " ": ["..", "..", "..", "..", "..", "..", "..", "..", "..", "..", "..", ".."]
4811
5350
  };
4812
- WORD = "HigherDEV";
4813
- FLASH = [
4814
- { color: UI.dim, dim: true, bold: false },
4815
- { color: UI.text, dim: false, bold: true },
4816
- { color: UI.dim, dim: true, bold: false },
4817
- { color: UI.text, dim: false, bold: true },
4818
- { color: UI.text, dim: false, bold: true },
4819
- { color: UI.dim, dim: true, bold: false },
4820
- { color: UI.text, dim: false, bold: true },
4821
- { 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 }
4822
5369
  ];
4823
- FRAME_MS = 70;
5370
+ SETTLED = { color: UI.text, dimColor: false, bold: false };
5371
+ STORM_MS = 24e4;
4824
5372
  }
4825
5373
  });
4826
5374
 
4827
- // 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
4828
5416
  import "react";
4829
5417
  import { Box as Box2, Text as Text2 } from "ink";
4830
5418
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
4831
- function Bubble({ message, width }) {
4832
- const style = speakerStyle(message.speaker);
4833
- const body = message.body.replace(/\s+$/, "");
4834
- 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(
4835
5424
  Box2,
4836
5425
  {
4837
- borderStyle: style.borderStyle,
4838
- borderColor: style.borderColor,
4839
- ...style.backgroundColor ? { backgroundColor: style.backgroundColor } : {},
5426
+ borderStyle: "single",
5427
+ borderColor: UI.text,
4840
5428
  flexDirection: "column",
4841
- paddingX: 1,
5429
+ paddingX: 2,
5430
+ width,
4842
5431
  children: [
4843
- style.label ? /* @__PURE__ */ jsx2(Text2, { color: UI.text, bold: true, children: style.label }) : null,
4844
- (message.steps ?? []).map((step, index) => /* @__PURE__ */ jsx2(Text2, { color: UI.dim, children: step }, index)),
4845
- 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 })
4846
5441
  ]
4847
5442
  }
4848
- ) });
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");
4849
6244
  }
4850
- var init_Bubble = __esm({
4851
- "src/tui/Bubble.tsx"() {
6245
+ var DOT3;
6246
+ var init_Panels = __esm({
6247
+ "src/tui/Panels.tsx"() {
4852
6248
  "use strict";
6249
+ init_src();
6250
+ init_format();
6251
+ init_theme();
4853
6252
  init_theme2();
6253
+ init_bounded();
6254
+ DOT3 = "\u25CF";
4854
6255
  }
4855
6256
  });
4856
6257
 
4857
- // src/tui/Help.tsx
6258
+ // src/tui/Decision.tsx
4858
6259
  import "react";
4859
- import { Box as Box3, Text as Text3 } from "ink";
4860
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
4861
- function Help({ width }) {
4862
- return /* @__PURE__ */ jsxs2(
4863
- 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,
4864
6288
  {
4865
6289
  borderStyle: "single",
4866
- borderColor: UI.text,
6290
+ borderColor: UI.warn,
4867
6291
  flexDirection: "column",
4868
- paddingX: 2,
4869
- paddingY: 1,
6292
+ paddingX: 1,
4870
6293
  width,
6294
+ marginBottom: 1,
4871
6295
  children: [
4872
- /* @__PURE__ */ jsx3(Text3, { color: UI.text, bold: true, children: "Commands" }),
4873
- /* @__PURE__ */ jsx3(Box3, { height: 1 }),
4874
- COMMANDS.map((command) => /* @__PURE__ */ jsxs2(Box3, { children: [
4875
- /* @__PURE__ */ jsx3(Box3, { width: 18, children: /* @__PURE__ */ jsxs2(Text3, { color: UI.text, children: [
4876
- command.name,
4877
- 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
+ ")"
4878
6309
  ] }) }),
4879
- /* @__PURE__ */ jsx3(Text3, { color: UI.dim, children: command.help })
4880
- ] }, command.name)),
4881
- /* @__PURE__ */ jsx3(Box3, { height: 1 }),
4882
- /* @__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" })
4883
6313
  ]
4884
6314
  }
4885
6315
  );
4886
6316
  }
4887
- var COMMANDS;
4888
- var init_Help = __esm({
4889
- "src/tui/Help.tsx"() {
6317
+ var DECISION_CHROME;
6318
+ var init_Decision = __esm({
6319
+ "src/tui/Decision.tsx"() {
4890
6320
  "use strict";
6321
+ init_format();
6322
+ init_Panels();
6323
+ init_commands3();
4891
6324
  init_theme2();
4892
- COMMANDS = [
4893
- { name: "/architect", help: "talk to your own model, which can do anything in the platform" },
4894
- { name: "/orchestrator", help: "talk to the agent that gets work in flight finished" },
4895
- { name: "/browse", help: "stop talking, look around" },
4896
- { name: "/workspace", args: "[slug]", help: "switch workspace, or list the ones you can reach" },
4897
- { name: "/board", help: "the kanban board" },
4898
- { name: "/agents", help: "every agent and what it is doing" },
4899
- { name: "/feed", help: "what just happened" },
4900
- { name: "/inbox", help: "decisions and messages waiting on you" },
4901
- { name: "/ticket", args: "HD-12", help: "open one ticket" },
4902
- { name: "/refresh", help: "reload the board now" },
4903
- { name: "/help", help: "this list" },
4904
- { name: "/exit", help: "leave" }
4905
- ];
6325
+ DECISION_CHROME = 5;
4906
6326
  }
4907
6327
  });
4908
6328
 
4909
- // src/tui/Panels.tsx
4910
- import "react";
4911
- import { Box as Box4, Text as Text4 } from "ink";
4912
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
4913
- function Panel({ title, children }) {
4914
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, children: [
4915
- /* @__PURE__ */ jsx4(Text4, { color: UI.text, bold: true, children: title }),
4916
- children
4917
- ] });
4918
- }
4919
- function AgentsPanel({ board }) {
4920
- return /* @__PURE__ */ jsx4(Panel, { title: "Agents", children: board.agents.map((agent) => {
4921
- const run5 = board.runs.find((row) => row.agent_id === agent.id && row.status === "running");
4922
- const ticket = run5?.ticket_id ? board.tickets.find((item) => item.id === run5.ticket_id) : void 0;
4923
- const availability = board.availability.find((row) => row.provider === agent.provider);
4924
- const blocked = availability && !availability.available ? availabilityLabel(availability) : "";
4925
- const tone2 = !agent.enabled ? "muted" : run5 ? "blue" : blocked ? "warning" : "muted";
4926
- return /* @__PURE__ */ jsxs3(Box4, { children: [
4927
- /* @__PURE__ */ jsxs3(Text4, { color: inkColor(tone2), children: [
4928
- DOT2,
4929
- " "
4930
- ] }),
4931
- /* @__PURE__ */ jsx4(Box4, { width: 14, children: /* @__PURE__ */ jsx4(Text4, { color: UI.text, children: truncate(agent.display_name, 13) }) }),
4932
- /* @__PURE__ */ jsx4(Box4, { width: 13, children: /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: agent.role }) }),
4933
- /* @__PURE__ */ jsx4(Box4, { width: 24, children: /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: truncate(agent.model, 23) }) }),
4934
- /* @__PURE__ */ jsx4(Box4, { width: 9, children: /* @__PURE__ */ jsx4(Text4, { color: UI.text, children: run5 ? "running" : blocked ? "blocked" : "idle" }) }),
4935
- /* @__PURE__ */ jsxs3(Text4, { color: UI.dim, children: [
4936
- ticket ? `${ticket.key} ` : "",
4937
- run5 ? elapsed(run5.started_at ?? run5.created_at) : blocked
4938
- ] })
4939
- ] }, agent.id);
4940
- }) });
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;
4941
6364
  }
4942
- function BoardPanel({ board, cursor }) {
4943
- const active = BOARD_COLUMNS.filter(
4944
- (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
4945
6372
  );
4946
- if (active.length === 0) {
4947
- return /* @__PURE__ */ jsx4(Panel, { title: "Board", children: /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: "No tickets yet." }) });
4948
- }
4949
- return /* @__PURE__ */ jsx4(Panel, { title: "Board", children: active.map((status) => {
4950
- const rows = board.tickets.filter((ticket) => ticket.status === status);
4951
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, children: [
4952
- /* @__PURE__ */ jsxs3(Text4, { color: inkColor(statusTone(status)), children: [
4953
- statusLabel(status),
4954
- " ",
4955
- /* @__PURE__ */ jsxs3(Text4, { color: UI.dim, children: [
4956
- "(",
4957
- rows.length,
4958
- ")"
4959
- ] })
4960
- ] }),
4961
- rows.map((ticket) => /* @__PURE__ */ jsx4(TicketLine, { ticket, selected: ticket.id === cursor }, ticket.id))
4962
- ] }, status);
4963
- }) });
4964
- }
4965
- function TicketLine({ ticket, selected }) {
4966
- return /* @__PURE__ */ jsxs3(Box4, { children: [
4967
- /* @__PURE__ */ jsx4(Text4, { color: selected ? UI.accent : UI.dim, children: selected ? " > " : " " }),
4968
- /* @__PURE__ */ jsx4(Box4, { width: 8, children: /* @__PURE__ */ jsx4(Text4, { color: UI.text, bold: true, children: ticket.key }) }),
4969
- /* @__PURE__ */ jsx4(Box4, { width: 44, children: /* @__PURE__ */ jsx4(Text4, { color: UI.text, children: truncate(ticket.title, 43) }) }),
4970
- /* @__PURE__ */ jsx4(Box4, { width: 14, children: /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: ticket.agent_name ?? "" }) }),
4971
- ticket.stuck ? /* @__PURE__ */ jsx4(Text4, { color: UI.warn, children: truncate(ticket.stuck, 30) }) : null
4972
- ] });
6373
+ return next.length > limit ? next.slice(next.length - limit) : next;
4973
6374
  }
4974
- function FeedPanel({
4975
- rows
4976
- }) {
4977
- return /* @__PURE__ */ jsxs3(Panel, { title: "Feed", children: [
4978
- rows.length === 0 ? /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: "Nothing yet." }) : null,
4979
- rows.slice(0, 12).map((row, index) => /* @__PURE__ */ jsxs3(Box4, { children: [
4980
- /* @__PURE__ */ jsx4(Box4, { width: 10, children: /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: relativeTime(row.at) }) }),
4981
- /* @__PURE__ */ jsx4(Box4, { width: 18, children: /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: row.kind ?? "" }) }),
4982
- /* @__PURE__ */ jsx4(Text4, { color: UI.text, children: truncate(row.summary ?? "", 60) })
4983
- ] }, row.id ?? index))
4984
- ] });
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));
4985
6388
  }
4986
- function InboxPanel({ board }) {
4987
- if (board.decisions.length === 0) {
4988
- 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
+ });
4989
6400
  }
4990
- return /* @__PURE__ */ jsx4(Panel, { title: `Decisions (${board.decisions.length})`, children: board.decisions.map((decision) => {
4991
- const ticket = board.tickets.find((row) => row.id === decision.ticket_id);
4992
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, children: [
4993
- /* @__PURE__ */ jsxs3(Text4, { color: UI.warn, children: [
4994
- decision.id.slice(0, 8),
4995
- ticket ? ` ${ticket.key}` : ""
4996
- ] }),
4997
- /* @__PURE__ */ jsx4(Text4, { color: UI.text, children: truncate(decision.question_md, 100) })
4998
- ] }, decision.id);
4999
- }) });
5000
- }
5001
- function EpicsPanel({ board }) {
5002
- const rows = epicProgress(board).filter((row) => row.epic.status !== "done");
5003
- if (rows.length === 0) return null;
5004
- return /* @__PURE__ */ jsx4(Panel, { title: "Epics", children: rows.map((row) => /* @__PURE__ */ jsxs3(Box4, { children: [
5005
- /* @__PURE__ */ jsx4(Box4, { width: 40, children: /* @__PURE__ */ jsx4(Text4, { color: UI.text, children: truncate(row.epic.title, 39) }) }),
5006
- /* @__PURE__ */ jsx4(Text4, { color: UI.dim, children: epicProgressCaption(row.merged, row.total, row.cancelled) })
5007
- ] }, row.epic.id)) });
5008
- }
5009
- function TicketPanel({ ticket }) {
5010
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, children: [
5011
- /* @__PURE__ */ jsxs3(Text4, { color: UI.text, bold: true, children: [
5012
- ticket.key,
5013
- " ",
5014
- ticket.title
5015
- ] }),
5016
- /* @__PURE__ */ jsxs3(Box4, { children: [
5017
- /* @__PURE__ */ jsx4(Text4, { color: inkColor(statusTone(ticket.status)), children: statusLabel(ticket.status) }),
5018
- /* @__PURE__ */ jsxs3(Text4, { color: UI.dim, children: [
5019
- ticket.area ? ` ${ticket.area}` : "",
5020
- ticket.agent_name ? ` ${ticket.agent_name}` : " unassigned",
5021
- ticket.attempts ? ` attempt ${ticket.attempts}` : ""
5022
- ] })
5023
- ] }),
5024
- ticket.stuck ? /* @__PURE__ */ jsxs3(Text4, { color: UI.warn, children: [
5025
- "why: ",
5026
- ticket.stuck
5027
- ] }) : null,
5028
- ticket.blocker_keys.length ? /* @__PURE__ */ jsxs3(Text4, { color: UI.dim, children: [
5029
- "blocked by ",
5030
- ticket.blocker_keys.join(", ")
5031
- ] }) : null,
5032
- ticket.pr_url ? /* @__PURE__ */ jsx4(Text4, { color: UI.accent, children: ticket.pr_url }) : null,
5033
- 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
5034
- ] });
6401
+ return map;
5035
6402
  }
5036
- var DOT2;
5037
- var init_Panels = __esm({
5038
- "src/tui/Panels.tsx"() {
6403
+ var STREAM_LIMIT;
6404
+ var init_stream = __esm({
6405
+ "src/tui/stream.ts"() {
5039
6406
  "use strict";
5040
6407
  init_src();
5041
- init_format();
5042
- init_theme();
5043
- 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";
5044
6438
  init_theme2();
5045
- DOT2 = "\u25CF";
5046
6439
  }
5047
6440
  });
5048
6441
 
5049
6442
  // src/tui/TextInput.tsx
5050
- import { useEffect as useEffect2, useState as useState2 } from "react";
5051
- import { Text as Text5, useInput, usePaste } from "ink";
5052
- 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
+ }
5053
6449
  function TextInput({
5054
6450
  value,
5055
6451
  onChange,
@@ -5063,12 +6459,19 @@ function TextInput({
5063
6459
  color
5064
6460
  }) {
5065
6461
  const [cursor, setCursor] = useState2(value.length);
6462
+ const ours = useRef2(value);
5066
6463
  useEffect2(() => {
5067
- setCursor((c2) => Math.min(c2, value.length));
6464
+ if (value === ours.current) return;
6465
+ ours.current = value;
6466
+ setCursor(value.length);
5068
6467
  }, [value]);
6468
+ const change = (next) => {
6469
+ ours.current = next;
6470
+ onChange(next);
6471
+ };
5069
6472
  const insert = (text) => {
5070
6473
  const next = value.slice(0, cursor) + text + value.slice(cursor);
5071
- onChange(next);
6474
+ change(next);
5072
6475
  setCursor(cursor + text.length);
5073
6476
  };
5074
6477
  usePaste((text) => {
@@ -5080,18 +6483,31 @@ function TextInput({
5080
6483
  if (key2.escape) return onCancel?.();
5081
6484
  if (key2.upArrow) return onUp?.();
5082
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
+ }
5083
6499
  if (key2.leftArrow) return setCursor((c2) => Math.max(0, c2 - 1));
5084
6500
  if (key2.rightArrow) return setCursor((c2) => Math.min(value.length, c2 + 1));
5085
6501
  if (key2.home) return setCursor(0);
5086
6502
  if (key2.end) return setCursor(value.length);
5087
6503
  if (key2.backspace) {
5088
6504
  if (cursor === 0) return;
5089
- onChange(value.slice(0, cursor - 1) + value.slice(cursor));
6505
+ change(value.slice(0, cursor - 1) + value.slice(cursor));
5090
6506
  return setCursor(cursor - 1);
5091
6507
  }
5092
6508
  if (key2.delete) {
5093
6509
  if (cursor >= value.length) return;
5094
- return onChange(value.slice(0, cursor) + value.slice(cursor + 1));
6510
+ return change(value.slice(0, cursor) + value.slice(cursor + 1));
5095
6511
  }
5096
6512
  if (key2.ctrl) {
5097
6513
  switch (input) {
@@ -5100,14 +6516,14 @@ function TextInput({
5100
6516
  case "e":
5101
6517
  return setCursor(value.length);
5102
6518
  case "u":
5103
- onChange(value.slice(cursor));
6519
+ change(value.slice(cursor));
5104
6520
  return setCursor(0);
5105
6521
  case "k":
5106
- return onChange(value.slice(0, cursor));
6522
+ return change(value.slice(0, cursor));
5107
6523
  case "w": {
5108
6524
  const head = value.slice(0, cursor);
5109
6525
  const trimmed = head.replace(/\s*\S*$/, "");
5110
- onChange(trimmed + value.slice(cursor));
6526
+ change(trimmed + value.slice(cursor));
5111
6527
  return setCursor(trimmed.length);
5112
6528
  }
5113
6529
  default:
@@ -5115,7 +6531,7 @@ function TextInput({
5115
6531
  }
5116
6532
  }
5117
6533
  if (key2.meta || !input) return;
5118
- const printable = input.replace(/[\x00-\x1f\x7f]/g, "");
6534
+ const printable = printableOf(input);
5119
6535
  if (printable) insert(printable);
5120
6536
  },
5121
6537
  { isActive }
@@ -5124,15 +6540,15 @@ function TextInput({
5124
6540
  const before = value.slice(0, cursor);
5125
6541
  const at = value[cursor] ?? " ";
5126
6542
  const after = value.slice(cursor + 1);
5127
- return /* @__PURE__ */ jsxs4(Text5, { children: [
6543
+ return /* @__PURE__ */ jsxs9(Text9, { children: [
5128
6544
  prompt2,
5129
- showPlaceholder ? /* @__PURE__ */ jsxs4(Fragment, { children: [
5130
- isActive ? /* @__PURE__ */ jsx5(Text5, { inverse: true, children: placeholder[0] }) : /* @__PURE__ */ jsx5(Text5, { children: placeholder[0] }),
5131
- /* @__PURE__ */ jsx5(Text5, { color: UI.dim, children: placeholder.slice(1) })
5132
- ] }) : /* @__PURE__ */ jsxs4(Fragment, { children: [
5133
- /* @__PURE__ */ jsx5(Text5, { color, children: before }),
5134
- isActive ? /* @__PURE__ */ jsx5(Text5, { inverse: true, color, children: at }) : /* @__PURE__ */ jsx5(Text5, { color, children: at === " " ? "" : at }),
5135
- /* @__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 })
5136
6552
  ] })
5137
6553
  ] });
5138
6554
  }
@@ -5161,13 +6577,42 @@ function parseLine(raw) {
5161
6577
  case "agents":
5162
6578
  case "feed":
5163
6579
  case "inbox":
5164
- case "help":
6580
+ case "settings":
5165
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
+ }
5166
6594
  case "workspace":
5167
- 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
+ }
5168
6604
  return { kind: "workspace", slug: argument || null };
6605
+ }
5169
6606
  case "ticket":
5170
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
+ }
5171
6616
  case "refresh":
5172
6617
  return { kind: "refresh" };
5173
6618
  case "exit":
@@ -5183,15 +6628,40 @@ var init_parse = __esm({
5183
6628
  }
5184
6629
  });
5185
6630
 
6631
+ // src/tui/workspace-load.ts
6632
+ var WorkspaceLoads;
6633
+ var init_workspace_load = __esm({
6634
+ "src/tui/workspace-load.ts"() {
6635
+ "use strict";
6636
+ WorkspaceLoads = class {
6637
+ #workspaceId;
6638
+ #generation = 0;
6639
+ constructor(workspaceId) {
6640
+ this.#workspaceId = workspaceId;
6641
+ }
6642
+ start(workspaceId) {
6643
+ return { workspaceId, generation: this.#generation };
6644
+ }
6645
+ switchTo(workspaceId) {
6646
+ this.#workspaceId = workspaceId;
6647
+ this.#generation += 1;
6648
+ }
6649
+ isCurrent(token) {
6650
+ return token.workspaceId === this.#workspaceId && token.generation === this.#generation;
6651
+ }
6652
+ };
6653
+ }
6654
+ });
6655
+
5186
6656
  // src/tui/App.tsx
5187
6657
  var App_exports = {};
5188
6658
  __export(App_exports, {
5189
6659
  App: () => App,
5190
6660
  COMMANDS: () => COMMANDS
5191
6661
  });
5192
- import { useCallback, useEffect as useEffect3, useMemo, useRef, useState as useState3 } from "react";
5193
- import { Box as Box5, Text as Text6, useApp, useInput as useInput2, useStdout } from "ink";
5194
- 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";
5195
6665
  function App({
5196
6666
  ctx,
5197
6667
  workspace,
@@ -5199,8 +6669,9 @@ function App({
5199
6669
  brainLabel
5200
6670
  }) {
5201
6671
  const { exit } = useApp();
5202
- const { stdout } = useStdout();
6672
+ const { stdout } = useStdout2();
5203
6673
  const columns = stdout?.columns && stdout.columns > 0 ? stdout.columns : 80;
6674
+ const rows = stdout?.rows && stdout.rows > 0 ? stdout.rows : 24;
5204
6675
  const width = Math.max(48, Math.min(columns - 1, 120));
5205
6676
  const [current, setCurrent] = useState3(workspace);
5206
6677
  const [available, setAvailable] = useState3(workspaces);
@@ -5216,41 +6687,170 @@ function App({
5216
6687
  const [notice, setNotice] = useState3(null);
5217
6688
  const [ticketKey, setTicketKey] = useState3(null);
5218
6689
  const [ready, setReady] = useState3(false);
5219
- const workspaceCtx = useMemo(() => ({ ...ctx, workspace: current }), [ctx, current]);
5220
- const session = useRef(null);
5221
- const history = useRef([]);
5222
- 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);
5223
6703
  const say = useCallback((speaker, body, steps) => {
5224
- setMessages((prior) => [...prior, { id: nextId(), speaker, body, steps }]);
6704
+ setMessages((prior) => [...prior, { id: nextId(), speaker, body, steps, done: true }]);
5225
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
+ );
5226
6771
  const refresh = useCallback(async () => {
6772
+ const loadToken = workspaceLoads.current.start(workspaceCtx.workspace.id);
5227
6773
  try {
5228
6774
  const [next, activity, paused] = await Promise.all([
5229
6775
  loadBoard(workspaceCtx),
5230
6776
  loadActivity(workspaceCtx, 20),
5231
6777
  loadGlobalPause(workspaceCtx)
5232
6778
  ]);
6779
+ if (!workspaceLoads.current.isCurrent(loadToken)) return;
5233
6780
  setBoard(next);
5234
6781
  setFeed(activity);
5235
6782
  setPausedAll2(paused);
5236
6783
  } catch (error) {
6784
+ if (!workspaceLoads.current.isCurrent(loadToken)) return;
5237
6785
  setNotice(error instanceof Error ? error.message : String(error));
5238
6786
  }
5239
6787
  }, [workspaceCtx]);
6788
+ refreshRef.current = refresh;
5240
6789
  useEffect3(() => {
5241
6790
  void refresh();
5242
6791
  const repaint = debounce(() => void refresh(), 250);
5243
6792
  const sub = subscribe({
5244
6793
  db: ctx.db,
5245
- tables: ["tickets", "runs", "messages", "decisions", "agents", "hosts", "provider_pauses", "runtime"],
5246
- onChange: () => repaint.trigger(),
6794
+ tables: [
6795
+ "tickets",
6796
+ "runs",
6797
+ "run_events",
6798
+ "messages",
6799
+ "decisions",
6800
+ "agents",
6801
+ "hosts",
6802
+ "provider_pauses",
6803
+ "runtime"
6804
+ ],
6805
+ filter: {
6806
+ tickets: `workspace_id=eq.${current.id}`,
6807
+ runs: `workspace_id=eq.${current.id}`,
6808
+ messages: `workspace_id=eq.${current.id}`,
6809
+ decisions: `workspace_id=eq.${current.id}`,
6810
+ agents: `workspace_id=eq.${current.id}`
6811
+ },
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
+ },
5247
6822
  onState: setLive
5248
6823
  });
5249
6824
  return () => {
5250
6825
  repaint.cancel();
5251
6826
  void sub.close();
5252
6827
  };
5253
- }, [ctx, refresh]);
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]);
5254
6854
  useEffect3(() => {
5255
6855
  session.current = null;
5256
6856
  }, [current.id]);
@@ -5261,6 +6861,14 @@ function App({
5261
6861
  setNotice(`No workspace ${slug}. You can reach: ${available.map((r) => r.slug).join(", ")}.`);
5262
6862
  return;
5263
6863
  }
6864
+ workspaceLoads.current.switchTo(found.id);
6865
+ session.current = null;
6866
+ setBoard(null);
6867
+ setFeed([]);
6868
+ setPausedAll2(false);
6869
+ setStream([]);
6870
+ setCursor(null);
6871
+ setNotice(null);
5264
6872
  setCurrent(found);
5265
6873
  setView("home");
5266
6874
  setTicketKey(null);
@@ -5277,7 +6885,7 @@ function App({
5277
6885
  setMessages((prior) => [...prior, { id, speaker: "architect", body: "", steps, pending: true }]);
5278
6886
  try {
5279
6887
  if (!session.current) {
5280
- session.current = await openArchitect({ ctx: workspaceCtx, readOnly: false });
6888
+ session.current = await openArchitect({ ctx: workspaceCtx, readOnly: false, fatalAuth: false });
5281
6889
  }
5282
6890
  let streamed = "";
5283
6891
  const result = await architectTurn({
@@ -5309,6 +6917,7 @@ function App({
5309
6917
  (prior) => prior.map((m) => m.id === id ? { ...m, body: message, pending: false } : m)
5310
6918
  );
5311
6919
  } finally {
6920
+ setMessages((prior) => prior.map((m) => m.id === id ? { ...m, done: true } : m));
5312
6921
  setBusy(false);
5313
6922
  }
5314
6923
  },
@@ -5337,6 +6946,7 @@ function App({
5337
6946
  const message = error instanceof Error ? error.message : String(error);
5338
6947
  setMessages((prior) => prior.map((m) => m.id === id ? { ...m, body: message, pending: false } : m));
5339
6948
  } finally {
6949
+ setMessages((prior) => prior.map((m) => m.id === id ? { ...m, done: true } : m));
5340
6950
  setBusy(false);
5341
6951
  }
5342
6952
  },
@@ -5345,7 +6955,41 @@ function App({
5345
6955
  const run5 = useCallback(
5346
6956
  async (raw) => {
5347
6957
  const text = raw.trim();
5348
- 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
+ }
5349
6993
  history.current.push(text);
5350
6994
  historyAt.current = -1;
5351
6995
  setDraft("");
@@ -5361,14 +7005,105 @@ function App({
5361
7005
  switch (action.kind) {
5362
7006
  case "mode":
5363
7007
  setMode(action.mode);
5364
- if (action.mode !== "browse") setView("home");
5365
- say(
5366
- "system",
5367
- 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."
5368
- );
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
+ });
5369
7029
  return;
5370
7030
  case "view":
5371
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
+ ]);
5372
7107
  return;
5373
7108
  case "workspace":
5374
7109
  if (!action.slug) {
@@ -5393,85 +7128,183 @@ function App({
5393
7128
  return;
5394
7129
  }
5395
7130
  },
5396
- [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
+ ]
5397
7151
  );
5398
7152
  useInput2((input, key2) => {
5399
7153
  if (key2.ctrl && input === "c") exit();
5400
7154
  });
5401
7155
  const ticket = ticketKey ? board?.tickets.find((row) => row.key === ticketKey) : null;
5402
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", width, children: [
5403
- /* @__PURE__ */ jsx6(Banner, { onDone: () => setReady(true) }),
5404
- ready && view === "home" && messages.length === 0 ? /* @__PURE__ */ jsx6(Help, { width }) : null,
5405
- /* @__PURE__ */ jsxs5(Box5, { children: [
5406
- /* @__PURE__ */ jsx6(Text6, { color: UI.text, bold: true, children: current.slug }),
5407
- /* @__PURE__ */ jsxs5(Text6, { color: UI.dim, children: [
5408
- " ",
5409
- current.repo,
5410
- " "
5411
- ] }),
5412
- /* @__PURE__ */ jsx6(Text6, { color: live === "live" ? UI.accent : UI.dim, children: "\u25CF " }),
5413
- /* @__PURE__ */ jsxs5(Text6, { color: UI.dim, children: [
5414
- live,
5415
- " "
5416
- ] }),
5417
- /* @__PURE__ */ jsx6(Text6, { color: UI.dim, children: board ? `${board.runs.filter((r) => r.status === "running").length} running ` : "" }),
5418
- board?.decisions.length ? /* @__PURE__ */ jsxs5(Text6, { color: UI.warn, children: [
5419
- board.decisions.length,
5420
- " decisions "
5421
- ] }) : null,
5422
- pausedAll || current.paused ? /* @__PURE__ */ jsx6(Text6, { color: UI.warn, children: "paused " }) : null,
5423
- /* @__PURE__ */ jsxs5(Text6, { color: UI.dim, children: [
5424
- "\xB7 ",
5425
- mode
5426
- ] })
5427
- ] }),
5428
- /* @__PURE__ */ jsx6(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { color: UI.dim, children: brainLabel }) }),
5429
- view === "help" ? /* @__PURE__ */ jsx6(Help, { width }) : null,
5430
- view === "board" && board ? /* @__PURE__ */ jsx6(BoardPanel, { board }) : null,
5431
- view === "agents" && board ? /* @__PURE__ */ jsx6(AgentsPanel, { board }) : null,
5432
- view === "feed" ? /* @__PURE__ */ jsx6(FeedPanel, { rows: feed }) : null,
5433
- view === "inbox" && board ? /* @__PURE__ */ jsx6(InboxPanel, { board }) : null,
5434
- view === "ticket" ? ticket ? /* @__PURE__ */ jsx6(TicketPanel, { ticket }) : /* @__PURE__ */ jsxs5(Text6, { color: UI.warn, children: [
5435
- "No ticket ",
5436
- ticketKey,
5437
- " here."
5438
- ] }) : null,
5439
- view === "home" && board ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
5440
- /* @__PURE__ */ jsx6(AgentsPanel, { board }),
5441
- /* @__PURE__ */ jsx6(EpicsPanel, { board })
5442
- ] }) : null,
5443
- messages.map((message) => /* @__PURE__ */ jsx6(Bubble, { message, width }, message.id)),
5444
- notice ? /* @__PURE__ */ jsx6(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { color: UI.warn, children: notice }) }) : null,
5445
- /* @__PURE__ */ jsx6(Box5, { children: /* @__PURE__ */ jsx6(
5446
- 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,
5447
7189
  {
5448
- value: draft,
5449
- onChange: setDraft,
5450
- onSubmit: (value) => void run5(value),
5451
- isActive: !busy,
5452
- placeholder: busy ? "working\u2026" : "message, or /help",
5453
- prompt: /* @__PURE__ */ jsxs5(Text6, { color: mode === "browse" ? UI.dim : UI.cream, children: [
5454
- 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,
5455
7232
  " "
5456
7233
  ] }),
5457
- color: UI.text,
5458
- onUp: () => {
5459
- if (history.current.length === 0) return;
5460
- historyAt.current = historyAt.current < 0 ? history.current.length - 1 : Math.max(0, historyAt.current - 1);
5461
- setDraft(history.current[historyAt.current] ?? "");
5462
- },
5463
- onDown: () => {
5464
- if (historyAt.current < 0) return;
5465
- historyAt.current += 1;
5466
- if (historyAt.current >= history.current.length) {
5467
- historyAt.current = -1;
5468
- setDraft("");
5469
- 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] ?? "");
5470
7304
  }
5471
- setDraft(history.current[historyAt.current] ?? "");
5472
7305
  }
5473
- }
5474
- ) })
7306
+ ) })
7307
+ ] })
5475
7308
  ] });
5476
7309
  }
5477
7310
  function promptFor(mode) {
@@ -5499,12 +7332,23 @@ var init_App = __esm({
5499
7332
  init_plan();
5500
7333
  init_commands3();
5501
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();
5502
7345
  init_Bubble();
5503
7346
  init_Help();
5504
7347
  init_Panels();
5505
7348
  init_TextInput();
5506
7349
  init_parse();
5507
7350
  init_theme2();
7351
+ init_workspace_load();
5508
7352
  messageSeq = 0;
5509
7353
  nextId = () => `m${messageSeq++}`;
5510
7354
  }
@@ -5516,20 +7360,21 @@ __export(launch_exports, {
5516
7360
  launchApp: () => launchApp
5517
7361
  });
5518
7362
  async function launchApp(slug) {
7363
+ if (!canLaunchApp()) return;
5519
7364
  const ctx = await requireWorkspace(slug);
5520
- const [{ render }, React7, { App: App2 }] = await Promise.all([
7365
+ const [{ render }, React12, { App: App2 }] = await Promise.all([
5521
7366
  import("ink"),
5522
7367
  import("react"),
5523
7368
  Promise.resolve().then(() => (init_App(), App_exports))
5524
7369
  ]);
5525
7370
  const { data: workspaces } = await ctx.db.from("workspaces").select("*").order("name");
5526
- const brain = await requireBrain();
7371
+ const config = await loadConfig();
5527
7372
  const instance = render(
5528
- React7.createElement(App2, {
7373
+ React12.createElement(App2, {
5529
7374
  ctx,
5530
7375
  workspace: ctx.workspace,
5531
7376
  workspaces: workspaces ?? [],
5532
- brainLabel: describeBrain(brain)
7377
+ brainLabel: describeBrain(config.brain ?? DEFAULT_BRAIN)
5533
7378
  }),
5534
7379
  { exitOnCtrlC: false }
5535
7380
  );
@@ -5542,7 +7387,9 @@ var init_launch = __esm({
5542
7387
  "src/tui/launch.ts"() {
5543
7388
  "use strict";
5544
7389
  init_context();
7390
+ init_config();
5545
7391
  init_brain();
7392
+ init_capability();
5546
7393
  }
5547
7394
  });
5548
7395
 
@@ -5688,7 +7535,7 @@ function loadConfig2(env = process.env) {
5688
7535
  stopTimeoutMs: Number(env.RUNNER_STOP_TIMEOUT_MS ?? 9e4)
5689
7536
  };
5690
7537
  }
5691
- var init_config2 = __esm({
7538
+ var init_config3 = __esm({
5692
7539
  "../runner/src/config.ts"() {
5693
7540
  "use strict";
5694
7541
  }
@@ -6005,7 +7852,7 @@ var init_init = __esm({
6005
7852
  "../runner/src/init.ts"() {
6006
7853
  "use strict";
6007
7854
  init_auth_check();
6008
- init_config2();
7855
+ init_config3();
6009
7856
  init_db();
6010
7857
  init_heartbeat();
6011
7858
  init_providers();
@@ -6055,6 +7902,7 @@ init_theme();
6055
7902
  init_json();
6056
7903
  init_supabase();
6057
7904
  init_connection();
7905
+ init_capability();
6058
7906
  import { createInterface as createInterface2 } from "readline/promises";
6059
7907
  var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6060
7908
  var CODE_RE = /^\d{6}$/;
@@ -6094,7 +7942,7 @@ async function login(opts) {
6094
7942
  if (codex.state !== "connected") {
6095
7943
  out(`${c.dim("codex")} ${c.yellow(connectionSummary(codex))} ${c.dim(howToFix(codex) ?? "")}`);
6096
7944
  }
6097
- if (opts.launch !== false && process.stdin.isTTY && process.stdout.isTTY) {
7945
+ if (opts.launch !== false && canLaunchApp()) {
6098
7946
  const { launchApp: launchApp2 } = await Promise.resolve().then(() => (init_launch(), launch_exports));
6099
7947
  await launchApp2();
6100
7948
  }
@@ -6140,6 +7988,7 @@ init_commands2();
6140
7988
  init_context();
6141
7989
  init_json();
6142
7990
  init_theme();
7991
+ init_argv_parsers();
6143
7992
  init_format();
6144
7993
 
6145
7994
  // src/live/types.ts
@@ -6319,7 +8168,7 @@ function registerLiveCommands(program) {
6319
8168
  render: async () => agentsView(await loadBoard(ctx))
6320
8169
  });
6321
8170
  });
6322
- 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() {
6323
8172
  const ctx = await requireWorkspace(slugOf4(this));
6324
8173
  const limit = Number(this.opts().limit) || 40;
6325
8174
  if (isJsonMode()) {
@@ -6358,7 +8207,9 @@ init_group();
6358
8207
  init_editor();
6359
8208
  init_epics();
6360
8209
  init_tickets2();
8210
+ init_argv_parsers();
6361
8211
  import { readFile as readFile6 } from "fs/promises";
8212
+ import { randomUUID as randomUUID4 } from "crypto";
6362
8213
  function slugOf5(command) {
6363
8214
  return command.optsWithGlobals().workspace;
6364
8215
  }
@@ -6371,8 +8222,11 @@ async function findTicket(ctx, key2) {
6371
8222
  function splitKeys(value) {
6372
8223
  return (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
6373
8224
  }
8225
+ function requestId(command) {
8226
+ return String(command.opts().requestId ?? randomUUID4());
8227
+ }
6374
8228
  function registerWriteCommands(program) {
6375
- 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) {
6376
8230
  const opts = this.opts();
6377
8231
  const ctx = await requireWorkspace(slugOf5(this));
6378
8232
  let title = titleWords.join(" ").trim();
@@ -6416,7 +8270,7 @@ function registerWriteCommands(program) {
6416
8270
  });
6417
8271
  ok(`${c.bold(ticket.key)} ${ticket.title} ${c.dim(`(${ticket.status})`)}`, { ticket });
6418
8272
  });
6419
- 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").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) {
6420
8274
  const opts = this.opts();
6421
8275
  const ctx = await requireWorkspace(slugOf5(this));
6422
8276
  const ticket = await findTicket(ctx, key2);
@@ -6442,38 +8296,45 @@ function registerWriteCommands(program) {
6442
8296
  patch.body = parsed.body;
6443
8297
  patch.acceptance = parsed.acceptance;
6444
8298
  }
6445
- const updated = await updateTicket(ctx, ticket, patch);
6446
- ok(`${c.bold(updated.key)} updated ${c.dim(`(${updated.status})`)}`, { ticket: updated });
8299
+ const operationId = requestId(this);
8300
+ const updated = await updateTicket(ctx, ticket, patch, operationId);
8301
+ ok(`${c.bold(updated.key)} updated ${c.dim(`(${updated.status})`)} ${c.dim(`request ${operationId}`)}`, { ticket: updated, request_id: operationId });
6447
8302
  });
6448
- program.command("block").argument("<key>", "ticket key").argument("<blockers...>", "keys it should wait for").description("make a ticket wait for others").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) {
6449
8304
  const ctx = await requireWorkspace(slugOf5(this));
6450
8305
  const ticket = await findTicket(ctx, key2);
6451
- const updated = await setBlockedBy(ctx, ticket, blockers, "add");
6452
- ok(`${c.bold(updated.key)} is ${updated.status}`, { ticket: updated });
8306
+ const operationId = requestId(this);
8307
+ const updated = await setBlockedBy(ctx, ticket, blockers, "add", operationId);
8308
+ ok(`${c.bold(updated.key)} is ${updated.status} ${c.dim(`request ${operationId}`)}`, { ticket: updated, request_id: operationId });
6453
8309
  });
6454
- program.command("unblock").argument("<key>", "ticket key").argument("[blockers...]", "keys to drop; omit to clear them all").description("drop dependencies").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) {
6455
8311
  const ctx = await requireWorkspace(slugOf5(this));
6456
8312
  const ticket = await findTicket(ctx, key2);
6457
- const updated = blockers.length ? await setBlockedBy(ctx, ticket, blockers, "remove") : await setBlockedBy(ctx, ticket, [], "set");
6458
- ok(`${c.bold(updated.key)} is ${updated.status}`, { ticket: updated });
8313
+ const operationId = requestId(this);
8314
+ const updated = blockers.length ? await setBlockedBy(ctx, ticket, blockers, "remove", operationId) : await setBlockedBy(ctx, ticket, [], "set", operationId);
8315
+ ok(`${c.bold(updated.key)} is ${updated.status} ${c.dim(`request ${operationId}`)}`, { ticket: updated, request_id: operationId });
6459
8316
  });
6460
- program.command("take").argument("<key>", "ticket key").description("take a ticket over so the board stops dispatching it").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) {
6461
8318
  const ctx = await requireWorkspace(slugOf5(this));
6462
- const updated = await takeOver(ctx, await findTicket(ctx, key2));
8319
+ const operationId = requestId(this);
8320
+ const updated = await takeOver(ctx, await findTicket(ctx, key2), operationId);
6463
8321
  ok(`${c.bold(updated.key)} is yours. Hand it back with \`hd handback ${updated.key}\`.`, {
6464
- ticket: updated
8322
+ ticket: updated,
8323
+ request_id: operationId
6465
8324
  });
6466
8325
  });
6467
- program.command("handback").argument("<key>", "ticket key").description("give a ticket back to the board").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) {
6468
8327
  const ctx = await requireWorkspace(slugOf5(this));
6469
- const updated = await handBack(ctx, await findTicket(ctx, key2));
6470
- ok(`${c.bold(updated.key)} handed back ${c.dim(`(${updated.status})`)}`, { ticket: updated });
8328
+ const operationId = requestId(this);
8329
+ const updated = await handBack(ctx, await findTicket(ctx, key2), operationId);
8330
+ ok(`${c.bold(updated.key)} handed back ${c.dim(`(${updated.status})`)} ${c.dim(`request ${operationId}`)}`, { ticket: updated, request_id: operationId });
6471
8331
  });
6472
- program.command("cancel").argument("<key>", "ticket key").description("cancel a ticket").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) {
6473
8333
  const ctx = await requireWorkspace(slugOf5(this));
6474
8334
  const ticket = await findTicket(ctx, key2);
6475
- const updated = await updateTicket(ctx, ticket, { status: "cancelled" });
6476
- ok(`${c.bold(updated.key)} cancelled`, { ticket: updated });
8335
+ const operationId = requestId(this);
8336
+ const updated = await updateTicket(ctx, ticket, { status: "cancelled" }, operationId);
8337
+ ok(`${c.bold(updated.key)} cancelled ${c.dim(`request ${operationId}`)}`, { ticket: updated, request_id: operationId });
6477
8338
  });
6478
8339
  program.command("kill").argument("<target>", "run id prefix, or a ticket key to kill its live run").description("stop a run").action(async function(target) {
6479
8340
  const ctx = await requireWorkspace(slugOf5(this));
@@ -6502,7 +8363,7 @@ function registerWriteCommands(program) {
6502
8363
  { epic: created }
6503
8364
  );
6504
8365
  });
6505
- 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) {
6506
8367
  const opts = this.opts();
6507
8368
  const ctx = await requireWorkspace(slugOf5(this));
6508
8369
  const found = resolveEpic(await loadBoard(ctx), id);
@@ -6565,7 +8426,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6565
8426
  import { z as z3 } from "zod";
6566
8427
 
6567
8428
  // src/version.ts
6568
- var VERSION = "0.2.0";
8429
+ var VERSION = "0.2.1";
6569
8430
 
6570
8431
  // src/architect/mcp.ts
6571
8432
  init_tools();
@@ -6640,6 +8501,7 @@ init_brain();
6640
8501
  init_config();
6641
8502
  init_connection();
6642
8503
  init_tools();
8504
+ init_argv_parsers();
6643
8505
  function slugOf6(command) {
6644
8506
  return command.optsWithGlobals().workspace;
6645
8507
  }
@@ -6738,7 +8600,7 @@ function registerArchitectCommands(program) {
6738
8600
  }
6739
8601
  await serveMcp({ readOnly, workspace: slugOf6(this) });
6740
8602
  });
6741
- 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() {
6742
8604
  const opts = this.opts();
6743
8605
  if (opts.model || opts.effort) {
6744
8606
  const next = await setBrain({
@@ -6821,11 +8683,12 @@ init_queries();
6821
8683
  init_commands2();
6822
8684
  init_views();
6823
8685
  init_theme();
8686
+ init_capability();
6824
8687
  function registerDashboard(program) {
6825
8688
  program.action(async function() {
6826
8689
  const slug = this.optsWithGlobals().workspace;
6827
8690
  const ctx = await requireWorkspace(slug);
6828
- if (isJsonMode() || !process.stdout.isTTY) {
8691
+ if (!canLaunchApp()) {
6829
8692
  const [board, pausedAll] = await Promise.all([loadBoard(ctx), loadGlobalPause(ctx)]);
6830
8693
  emit(
6831
8694
  { workspace: ctx.workspace.slug, ...statusPayload(board, pausedAll) },