@higherdev/cli 0.1.2 → 0.1.3

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 (3) hide show
  1. package/README.md +6 -4
  2. package/dist/index.js +231 -58
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -56,15 +56,17 @@ hd ask "why is HD-12 stuck?"
56
56
  hd msg HD-12 --to builder "also rename the old column" --interrupt
57
57
  ```
58
58
 
59
- `hd plan` runs the **architect** here on your machine, on your ChatGPT
60
- subscription. It reads the repo, decides what should be built, files an epic,
59
+ `hd plan` talks to **your own model**, here on your machine, on your ChatGPT
60
+ subscription. It is not a worker in a workspace and not a row in the agent
61
+ registry: nothing on the runner dispatches to it, and it reaches the whole
62
+ platform. `hd brain` shows which model it is and connects the subscription the
63
+ first time. It reads the repo, decides what should be built, files an epic,
61
64
  and can tune the platform itself: models, effort, routing notes, prompt addenda,
62
65
  conventions, budgets. Creating an epic fires `epic.created`, which the
63
66
  orchestrator already triggers on, so the handoff needs no glue.
64
67
 
65
68
  ```bash
66
- hd connect # ChatGPT OAuth, once per machine
67
- hd doctor # what a run would cost, before it runs
69
+ hd brain # which model, and connect it the first time
68
70
  hd plan # a conversation: type, it answers, keep going
69
71
  ```
70
72
 
package/dist/index.js CHANGED
@@ -11,9 +11,10 @@ var __export = (target, all) => {
11
11
 
12
12
  // ../../packages/db/src/enums.ts
13
13
  function asEffort(value) {
14
- return value === "low" || value === "high" ? value : "medium";
14
+ const raw = typeof value === "string" ? value.trim() : "";
15
+ return efforts.includes(raw) ? raw : DEFAULT_EFFORT;
15
16
  }
16
- var ticketStatuses, epicStatuses, runStatuses, runKinds, runEventTypes, agentRoles, providers, efforts, messageRoles, messageToRoles, deliveries, createdBy;
17
+ var ticketStatuses, epicStatuses, runStatuses, runKinds, runEventTypes, agentRoles, providers, efforts, messageRoles, messageToRoles, deliveries, createdBy, DEFAULT_EFFORT;
17
18
  var init_enums = __esm({
18
19
  "../../packages/db/src/enums.ts"() {
19
20
  "use strict";
@@ -43,13 +44,14 @@ var init_enums = __esm({
43
44
  "error",
44
45
  "status"
45
46
  ];
46
- agentRoles = ["orchestrator", "reviewer", "builder", "architect"];
47
+ agentRoles = ["orchestrator", "reviewer", "builder"];
47
48
  providers = ["claude", "codex", "gemini", "grok"];
48
49
  efforts = ["low", "medium", "high"];
49
50
  messageRoles = ["human", "operator", "orchestrator", "reviewer", "builder", "architect"];
50
51
  messageToRoles = [...messageRoles, "all"];
51
52
  deliveries = ["queue", "interrupt"];
52
53
  createdBy = ["human", "orchestrator", "api"];
54
+ DEFAULT_EFFORT = "high";
53
55
  }
54
56
  });
55
57
 
@@ -183,6 +185,8 @@ function auditSummary(actor, action, payload = {}) {
183
185
  return key2 ? `${actor} took over ${key2}` : `${actor} took over a ticket`;
184
186
  case "hand_back":
185
187
  return key2 ? `${actor} handed back ${key2}` : `${actor} handed back a ticket`;
188
+ case "merge_override":
189
+ return key2 ? `${actor} authorized an optional-check merge for ${key2}` : `${actor} authorized an optional-check merge`;
186
190
  case "create":
187
191
  return key2 ? `${actor} created ${key2}` : `${actor} created ${subject(payload)}`;
188
192
  case "update":
@@ -456,6 +460,36 @@ var init_cost = __esm({
456
460
  }
457
461
  });
458
462
 
463
+ // ../../packages/db/src/commands/lifecycle.ts
464
+ var lifecycleCommandRejectionCodes, lifecycleCommandResultCodes, TERMINAL_TICKET_STATUSES, OPTIONAL_CHECK_OVERRIDE_STATUSES, ticketStatusSet, terminalStatusSet, optionalCheckOverrideStatusSet;
465
+ var init_lifecycle = __esm({
466
+ "../../packages/db/src/commands/lifecycle.ts"() {
467
+ "use strict";
468
+ init_enums();
469
+ lifecycleCommandRejectionCodes = [
470
+ "illegal_transition",
471
+ "terminal_target",
472
+ "stale_state",
473
+ "malformed_intent",
474
+ "duplicate_command_identity"
475
+ ];
476
+ lifecycleCommandResultCodes = ["accepted", ...lifecycleCommandRejectionCodes];
477
+ TERMINAL_TICKET_STATUSES = ["merged", "cancelled"];
478
+ OPTIONAL_CHECK_OVERRIDE_STATUSES = ["approved", "needs_decision"];
479
+ ticketStatusSet = new Set(ticketStatuses);
480
+ terminalStatusSet = new Set(TERMINAL_TICKET_STATUSES);
481
+ optionalCheckOverrideStatusSet = new Set(OPTIONAL_CHECK_OVERRIDE_STATUSES);
482
+ }
483
+ });
484
+
485
+ // ../../packages/db/src/commands/index.ts
486
+ var init_commands = __esm({
487
+ "../../packages/db/src/commands/index.ts"() {
488
+ "use strict";
489
+ init_lifecycle();
490
+ }
491
+ });
492
+
459
493
  // ../../packages/db/src/defaults.ts
460
494
  function slugify(name) {
461
495
  const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -467,9 +501,19 @@ function hostAllowsRepo(allowedRepoOwners, repo) {
467
501
  if (!owner) return false;
468
502
  return allowedRepoOwners.some((item) => item.toLowerCase() === owner);
469
503
  }
504
+ var DEFAULT_AGENTS;
470
505
  var init_defaults = __esm({
471
506
  "../../packages/db/src/defaults.ts"() {
472
507
  "use strict";
508
+ init_enums();
509
+ DEFAULT_AGENTS = [
510
+ { role: "orchestrator", provider: "claude", model: "claude-fable-5", display_name: "Orchestrator", effort: DEFAULT_EFFORT },
511
+ { role: "reviewer", provider: "claude", model: "claude-opus-5", display_name: "Reviewer", effort: DEFAULT_EFFORT },
512
+ { role: "builder", provider: "claude", model: "claude-sonnet-5", display_name: "Claude", effort: DEFAULT_EFFORT },
513
+ { role: "builder", provider: "codex", model: "gpt-5.6-sol", display_name: "Codex", effort: DEFAULT_EFFORT },
514
+ { role: "builder", provider: "gemini", model: "gemini-3.7-flash-high", display_name: "Gemini", effort: DEFAULT_EFFORT },
515
+ { role: "builder", provider: "grok", model: "grok-4.6", display_name: "Grok", effort: DEFAULT_EFFORT }
516
+ ];
473
517
  }
474
518
  });
475
519
 
@@ -691,7 +735,7 @@ var init_schemas = __esm({
691
735
  role: z.enum(agentRoles),
692
736
  provider: z.enum(providers),
693
737
  model: z.string().min(1),
694
- effort: z.enum(efforts).default("medium"),
738
+ effort: z.enum(efforts).default(DEFAULT_EFFORT),
695
739
  display_name: z.string().min(1),
696
740
  enabled: z.boolean(),
697
741
  routing_notes: z.string().default(""),
@@ -746,6 +790,7 @@ var init_schemas = __esm({
746
790
  next_review_at: isoDateSchema.nullable(),
747
791
  created_by: z.enum(createdBy),
748
792
  last_error: z.string().nullable(),
793
+ merge_override_optional_checks: z.boolean().default(false),
749
794
  created_at: isoDateSchema,
750
795
  updated_at: isoDateSchema
751
796
  });
@@ -835,7 +880,8 @@ var init_schemas = __esm({
835
880
  "dismiss",
836
881
  "reroute",
837
882
  "takeover",
838
- "hand_back"
883
+ "hand_back",
884
+ "merge_override"
839
885
  ]);
840
886
  auditEventSchema = z.object({
841
887
  id: uuidSchema,
@@ -1036,6 +1082,32 @@ var init_templates = __esm({
1036
1082
  }
1037
1083
  });
1038
1084
 
1085
+ // ../../packages/db/src/tickets.ts
1086
+ function uncertainCreateError(error) {
1087
+ return Boolean(error && (!error.code || /^PGRST00[0-3]$/.test(error.code)));
1088
+ }
1089
+ async function createTicketRecord(db, workspaceId, fields, ticketId) {
1090
+ const requestId = ticketId ?? globalThis.crypto.randomUUID();
1091
+ const args = {
1092
+ p_ticket_id: requestId,
1093
+ p_workspace_id: workspaceId,
1094
+ p_fields: fields
1095
+ };
1096
+ for (let attempt = 0; ; attempt += 1) {
1097
+ try {
1098
+ const result = await db.rpc("create_ticket", args);
1099
+ if (!result.error || attempt === 1 || !uncertainCreateError(result.error)) return result;
1100
+ } catch (error) {
1101
+ if (attempt === 1) throw error;
1102
+ }
1103
+ }
1104
+ }
1105
+ var init_tickets = __esm({
1106
+ "../../packages/db/src/tickets.ts"() {
1107
+ "use strict";
1108
+ }
1109
+ });
1110
+
1039
1111
  // ../../packages/db/src/ticket-writes.ts
1040
1112
  async function workspaceBySlug(db, slug) {
1041
1113
  const { data, error } = await db.from("workspaces").select("*").eq("slug", slug).maybeSingle();
@@ -1134,7 +1206,13 @@ function buildTimeline(input) {
1134
1206
  id: `run:${run5.id}`,
1135
1207
  at,
1136
1208
  kind,
1137
- title: [run5.kind, run5.provider, run5.status, span].filter(Boolean).join(" "),
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(" "),
1138
1216
  detail: run5.summary ? clip(run5.summary) : void 0,
1139
1217
  durationMs: runDurationMs(run5, nowIso),
1140
1218
  endedAt: run5.ended_at
@@ -1388,6 +1466,7 @@ var init_src = __esm({
1388
1466
  init_blocked();
1389
1467
  init_budgets();
1390
1468
  init_cost();
1469
+ init_commands();
1391
1470
  init_defaults();
1392
1471
  init_enums();
1393
1472
  init_epic_progress();
@@ -1399,6 +1478,7 @@ var init_src = __esm({
1399
1478
  init_status();
1400
1479
  init_stuck();
1401
1480
  init_templates();
1481
+ init_tickets();
1402
1482
  init_ticket_writes();
1403
1483
  init_timeline();
1404
1484
  init_transcript();
@@ -4037,10 +4117,8 @@ async function createTicket(ctx, input) {
4037
4117
  assigned: Boolean(assigned.agent),
4038
4118
  blockerStatuses: blockers.statuses
4039
4119
  });
4040
- const { data, error } = await ctx.db.from("tickets").insert({
4041
- workspace_id: ctx.workspace.id,
4120
+ const { data, error } = await createTicketRecord(ctx.db, ctx.workspace.id, {
4042
4121
  epic_id: input.epic ?? null,
4043
- key: "",
4044
4122
  title,
4045
4123
  body_md: input.body ?? "",
4046
4124
  acceptance_md: acceptance,
@@ -4053,7 +4131,7 @@ async function createTicket(ctx, input) {
4053
4131
  provider_pinned: Boolean(assigned.agent),
4054
4132
  blocked_by: blockers.ids,
4055
4133
  created_by: "human"
4056
- }).select("*").single();
4134
+ });
4057
4135
  if (error) fail(error.message);
4058
4136
  await ctx.audit(ctx.workspace.id, "create", { ticket_key: data.key, subject: "a ticket" });
4059
4137
  return data;
@@ -4141,7 +4219,12 @@ async function handBack(ctx, ticket) {
4141
4219
  return data;
4142
4220
  }
4143
4221
  async function killRun(ctx, runId, ticketKey) {
4144
- const { data, error } = await ctx.db.from("runs").update({ status: "killed", ended_at: (/* @__PURE__ */ new Date()).toISOString(), summary: `Killed by ${ctx.email}.` }).eq("id", runId).eq("workspace_id", ctx.workspace.id).select("*").maybeSingle();
4222
+ const { data, error } = await ctx.db.rpc("request_run_cancellation", {
4223
+ p_run_id: runId,
4224
+ p_workspace_id: ctx.workspace.id,
4225
+ p_requested_by: ctx.email,
4226
+ p_reason: `Cancellation requested by ${ctx.email}.`
4227
+ });
4145
4228
  if (error) fail(error.message);
4146
4229
  if (!data) fail(`No run ${runId} in ${ctx.workspace.slug}.`);
4147
4230
  await ctx.audit(ctx.workspace.id, "kill", { ticket_key: ticketKey, run_id: runId });
@@ -4273,14 +4356,14 @@ function registerWriteCommands(program) {
4273
4356
  const live = runs2.find((run6) => run6.status === "running" || run6.status === "queued");
4274
4357
  if (!live) fail(`${ticket.key} has no live run.`);
4275
4358
  const killed2 = await killRun(ctx, live.id, ticket.key);
4276
- ok(`Killed ${c.dim(killed2.id.slice(0, 8))} on ${c.bold(ticket.key)}.`, { run: killed2 });
4359
+ ok(`Cancellation requested for ${c.dim(killed2.id.slice(0, 8))} on ${c.bold(ticket.key)}.`, { run: killed2 });
4277
4360
  return;
4278
4361
  }
4279
4362
  const runs = await loadRuns(ctx, { limit: 200 });
4280
4363
  const run5 = runs.find((row) => row.id.startsWith(target));
4281
4364
  if (!run5) fail(`No run matching ${target}.`);
4282
4365
  const killed = await killRun(ctx, run5.id);
4283
- ok(`Killed ${c.dim(killed.id.slice(0, 8))}.`, { run: killed });
4366
+ ok(`Cancellation requested for ${c.dim(killed.id.slice(0, 8))}.`, { run: killed });
4284
4367
  });
4285
4368
  const epic = group(program, "epic", "epics and their dependency waves");
4286
4369
  epic.command("new").argument("<title>", "epic title").argument("[spec]", "path to a markdown spec").description("create an epic; the orchestrator decomposes it").action(async function(title, spec) {
@@ -4591,7 +4674,7 @@ function registerAgentCommands(program) {
4591
4674
  row.role,
4592
4675
  row.provider,
4593
4676
  c.dim(row.model),
4594
- row.effort,
4677
+ asEffort(row.effort),
4595
4678
  row.enabled ? c.green("yes") : c.dim("no"),
4596
4679
  truncate(row.routing_notes ?? "", 44)
4597
4680
  ])
@@ -4605,7 +4688,7 @@ function registerAgentCommands(program) {
4605
4688
  found,
4606
4689
  () => [
4607
4690
  `${c.bold(found.display_name)} ${c.dim(found.role)}`,
4608
- `${c.dim("model")} ${found.provider}/${found.model} ${c.dim(`effort ${found.effort}`)}`,
4691
+ `${c.dim("model")} ${found.provider}/${found.model} ${c.dim(`effort ${asEffort(found.effort)}`)}`,
4609
4692
  `${c.dim("enabled")} ${found.enabled ? "yes" : "no"}`,
4610
4693
  `${c.dim("caps")} ${found.runs_per_hour ?? "unlimited"} runs/hour, ${found.daily_spend_usd ?? "unlimited"} usd/day`,
4611
4694
  found.routing_notes ? `
@@ -4660,7 +4743,7 @@ ${found.prompt_addendum}` : ""
4660
4743
  await ctx.audit(ctx.workspace.id, "configure", { subject: `agent ${data.display_name}` });
4661
4744
  ok(`${c.bold(data.display_name)} updated.`, { agent: data });
4662
4745
  });
4663
- 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", "medium").option("--notes <text>", "routing notes").action(async function(name) {
4746
+ 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) {
4664
4747
  const opts = this.opts();
4665
4748
  const ctx = await requireWorkspace(slugOf5(this));
4666
4749
  if (!providers.includes(opts.provider)) {
@@ -4745,8 +4828,13 @@ function registerWorkspaceCommands(program) {
4745
4828
  default_host: opts.host
4746
4829
  }).select("*").single();
4747
4830
  if (error) fail(error.message);
4831
+ const { error: agentsError } = await ctx.db.from("agents").insert(DEFAULT_AGENTS.map((agent) => ({ ...agent, workspace_id: data.id })));
4832
+ if (agentsError) fail(`Workspace created, but seeding its agents failed: ${agentsError.message}`);
4748
4833
  await ctx.audit(data.id, "create", { subject: `workspace ${data.slug}` });
4749
- ok(`${c.bold(data.slug)} created. Run \`hd use ${data.slug}\`.`, { workspace: data });
4834
+ ok(
4835
+ `${c.bold(data.slug)} created with ${DEFAULT_AGENTS.length} agents. Run \`hd use ${data.slug}\`.`,
4836
+ { workspace: data }
4837
+ );
4750
4838
  });
4751
4839
  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() {
4752
4840
  const opts = this.opts();
@@ -4878,7 +4966,7 @@ function registerAttachCommands(program) {
4878
4966
  }
4879
4967
 
4880
4968
  // src/architect/commands.ts
4881
- import { createInterface as createInterface2 } from "readline/promises";
4969
+ import { createInterface as createInterface3 } from "readline/promises";
4882
4970
  init_json();
4883
4971
  init_theme();
4884
4972
 
@@ -4888,7 +4976,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4888
4976
  import { z as z3 } from "zod";
4889
4977
 
4890
4978
  // src/version.ts
4891
- var VERSION = "0.1.2";
4979
+ var VERSION = "0.1.3";
4892
4980
 
4893
4981
  // src/architect/tools.ts
4894
4982
  init_src();
@@ -5401,13 +5489,93 @@ async function serveMcp(opts) {
5401
5489
  init_json();
5402
5490
  init_theme();
5403
5491
  init_chatgpt();
5404
- init_connection();
5405
5492
  import { execFile as execFile2 } from "child_process";
5406
5493
  import { mkdir as mkdir2 } from "fs/promises";
5407
5494
  import { homedir as homedir3 } from "os";
5408
5495
  import path6 from "path";
5409
5496
  import { promisify as promisify2 } from "util";
5410
5497
 
5498
+ // src/architect/brain.ts
5499
+ import { createInterface as createInterface2 } from "readline/promises";
5500
+ init_json();
5501
+ init_theme();
5502
+ init_connection();
5503
+ var BRAINS = [
5504
+ {
5505
+ id: "chatgpt",
5506
+ label: "ChatGPT subscription",
5507
+ billing: "included in your ChatGPT plan, nothing per token",
5508
+ // gpt-5.6, without a suffix, is refused by this endpoint for ChatGPT accounts.
5509
+ defaultModel: "gpt-5.6-sol",
5510
+ models: ["gpt-5.6-sol", "gpt-5.5"]
5511
+ }
5512
+ ];
5513
+ var DEFAULT_BRAIN = {
5514
+ id: "chatgpt",
5515
+ model: "gpt-5.6-sol",
5516
+ effort: "high"
5517
+ };
5518
+ function brainById(id) {
5519
+ return BRAINS.find((brain) => brain.id === id) ?? null;
5520
+ }
5521
+ function describeBrain(config) {
5522
+ const brain = brainById(config.id);
5523
+ return `${brain?.label ?? config.id} (${config.model}, effort ${config.effort})`;
5524
+ }
5525
+ async function confirm(question) {
5526
+ const rl = createInterface2({ input: process.stdin, output: process.stderr });
5527
+ try {
5528
+ const answer = (await rl.question(question)).trim().toLowerCase();
5529
+ return answer === "" || answer === "y" || answer === "yes";
5530
+ } finally {
5531
+ rl.close();
5532
+ }
5533
+ }
5534
+ async function requireBrain(opts = {}) {
5535
+ const config = await loadConfig();
5536
+ const chosen = config.brain ?? DEFAULT_BRAIN;
5537
+ const interactive = opts.interactive ?? (process.stdin.isTTY && process.stdout.isTTY);
5538
+ const connection = await codexConnection();
5539
+ if (connection.state === "connected") {
5540
+ if (!config.brain) await saveConfig({ brain: chosen });
5541
+ return chosen;
5542
+ }
5543
+ if (!interactive) {
5544
+ fail(`${connectionSummary(connection)}
5545
+ ${howToFix(connection) ?? ""}`.trim());
5546
+ }
5547
+ if (connection.state === "not_installed") {
5548
+ fail(`${connectionSummary(connection)}
5549
+ ${howToFix(connection) ?? ""}`.trim());
5550
+ }
5551
+ const brain = brainById(chosen.id);
5552
+ out("");
5553
+ out(`${c.bold("HigherDEV needs your ChatGPT subscription to answer.")}`);
5554
+ out(c.dim(`${brain?.billing ?? ""}. The sign-in happens in your browser; hd never sees the token.`));
5555
+ if (!await confirm(c.dim("Press enter to sign in, or n to cancel: "))) {
5556
+ fail("Not connected. Run `hd brain` when you want to.");
5557
+ }
5558
+ const connected = await connectCodex();
5559
+ if (connected.state !== "connected") {
5560
+ fail(`${connectionSummary(connected)}
5561
+ ${howToFix(connected) ?? ""}`.trim());
5562
+ }
5563
+ out(c.green(connectionSummary(connected)));
5564
+ await saveConfig({ brain: chosen });
5565
+ return chosen;
5566
+ }
5567
+ async function setBrain(patch) {
5568
+ const config = await loadConfig();
5569
+ const next = { ...config.brain ?? DEFAULT_BRAIN, ...patch };
5570
+ const brain = brainById(next.id);
5571
+ if (!brain) fail(`Unknown brain ${next.id}. One of: ${BRAINS.map((row) => row.id).join(", ")}.`);
5572
+ if (patch.model && !brain.models.includes(patch.model)) {
5573
+ out(c.yellow(`${patch.model} is not one of the models known to work here (${brain.models.join(", ")}).`));
5574
+ }
5575
+ await saveConfig({ brain: next });
5576
+ return next;
5577
+ }
5578
+
5411
5579
  // src/architect/repo.ts
5412
5580
  import { readFile as readFile6, readdir, stat } from "fs/promises";
5413
5581
  import path5 from "path";
@@ -5564,18 +5732,6 @@ function buildRepoTools(root) {
5564
5732
 
5565
5733
  // src/architect/plan.ts
5566
5734
  var run2 = promisify2(execFile2);
5567
- var ARCHITECT_ROLE = "architect";
5568
- async function architectAgent(ctx) {
5569
- const { data, error } = await ctx.db.from("agents").select("*").eq("workspace_id", ctx.workspace.id).eq("role", ARCHITECT_ROLE).maybeSingle();
5570
- if (error) fail(error.message);
5571
- if (!data) {
5572
- fail(
5573
- "This workspace has no architect agent. Apply the migrations, or add one with:\n hd agent new Architect --provider codex --model gpt-5.6-sol"
5574
- );
5575
- }
5576
- if (!data.enabled) fail(`${data.display_name} is disabled. Enable it with \`hd agent set ${data.display_name} --enable\`.`);
5577
- return data;
5578
- }
5579
5735
  async function repoCheckout(ctx) {
5580
5736
  const here = process.cwd();
5581
5737
  try {
@@ -5632,19 +5788,12 @@ function architectInstructions(opts) {
5632
5788
  "conventions, budgets, pausing. The orchestrator has no action for any of it.",
5633
5789
  "",
5634
5790
  opts.readOnly ? "This session is read-only. Propose in your reply; the tools that change things are not loaded." : "Say what you filed and what the owner should look at.",
5635
- "Answer conversationally and briefly. Lead with the answer. Never use an em dash.",
5636
- opts.addendum ? `
5637
- ${opts.addendum}` : ""
5791
+ "Answer conversationally and briefly. Lead with the answer. Never use an em dash."
5638
5792
  ].filter(Boolean).join("\n");
5639
5793
  }
5640
5794
  async function openArchitect(opts) {
5641
5795
  const { ctx } = opts;
5642
- const agent = await architectAgent(ctx);
5643
- const connection = await codexConnection();
5644
- if (connection.state !== "connected" && !opts.allowApiBilling) {
5645
- fail(`${connectionSummary(connection)}
5646
- ${howToFix(connection) ?? ""}`.trim());
5647
- }
5796
+ const brain = await requireBrain();
5648
5797
  const repo = await repoCheckout(ctx);
5649
5798
  const platformTools = toolsFor(opts.readOnly ? "read" : "all").map((tool) => ({
5650
5799
  name: tool.name,
@@ -5653,10 +5802,9 @@ ${howToFix(connection) ?? ""}`.trim());
5653
5802
  run: (args) => tool.run(ctx, args)
5654
5803
  }));
5655
5804
  return {
5656
- agent,
5805
+ brain,
5657
5806
  instructions: architectInstructions({
5658
5807
  ctx,
5659
- addendum: agent.prompt_addendum,
5660
5808
  readOnly: opts.readOnly,
5661
5809
  hasRepo: Boolean(repo)
5662
5810
  }),
@@ -5671,10 +5819,12 @@ async function architectTurn(opts) {
5671
5819
  const { data: runRow, error: runError } = await ctx.db.from("runs").insert({
5672
5820
  workspace_id: ctx.workspace.id,
5673
5821
  ticket_id: null,
5674
- agent_id: session.agent.id,
5822
+ // No agent: the brain is the operator's own model, not a worker in this
5823
+ // workspace. The run exists so what it spends is still visible.
5824
+ agent_id: null,
5675
5825
  kind: "architect",
5676
- provider: session.agent.provider,
5677
- model: session.agent.model,
5826
+ provider: "codex",
5827
+ model: session.brain.model,
5678
5828
  status: "running",
5679
5829
  started_at: (/* @__PURE__ */ new Date()).toISOString(),
5680
5830
  session_id: session.sessionId,
@@ -5693,7 +5843,7 @@ async function architectTurn(opts) {
5693
5843
  instructions: session.instructions,
5694
5844
  history: session.history,
5695
5845
  tools: session.tools,
5696
- model: session.agent.model,
5846
+ model: session.brain.model,
5697
5847
  sessionId: session.sessionId,
5698
5848
  signal: opts.signal,
5699
5849
  onEvent: (event) => {
@@ -5715,7 +5865,7 @@ async function architectTurn(opts) {
5715
5865
  turns: result.steps,
5716
5866
  summary: result.text.slice(0, 500) || "Architect turn"
5717
5867
  }).eq("id", runRow.id);
5718
- await ctx.audit(ctx.workspace.id, "architect", { subject: session.agent.display_name });
5868
+ await ctx.audit(ctx.workspace.id, "architect", { subject: describeBrain(session.brain) });
5719
5869
  return { text: result.text, runId: runRow.id, toolCalls: result.toolCalls };
5720
5870
  } catch (error) {
5721
5871
  const message = error instanceof Error ? error.message : String(error);
@@ -5738,7 +5888,7 @@ function slugOf6(command) {
5738
5888
  return command.optsWithGlobals().workspace;
5739
5889
  }
5740
5890
  async function askLine(prompt2) {
5741
- const rl = createInterface2({ input: process.stdin, output: process.stderr });
5891
+ const rl = createInterface3({ input: process.stdin, output: process.stderr });
5742
5892
  try {
5743
5893
  return await new Promise((resolve) => {
5744
5894
  rl.once("close", () => resolve(""));
@@ -5759,16 +5909,12 @@ ${howToFix(connection) ?? ""}`.trim()
5759
5909
  );
5760
5910
  }
5761
5911
  function registerArchitectCommands(program) {
5762
- program.command("plan").argument("[request...]", "what to plan, assess, or change; omit for a conversation").description("the architect: assess the repo, shape the work, tune the platform").option("--read-only", "let it look and propose, but change nothing").option("--once", "run a single turn and exit, even without a request").option("--allow-api-billing", "run even when Codex would bill per token instead of the subscription").action(async function(request) {
5912
+ program.command("plan").argument("[request...]", "what to plan, assess, or change; omit for a conversation").description("the architect: assess the repo, shape the work, tune the platform").option("--read-only", "let it look and propose, but change nothing").option("--once", "run a single turn and exit, even without a request").action(async function(request) {
5763
5913
  const opts = this.opts();
5764
5914
  const ctx = await requireWorkspace(slugOf6(this));
5765
5915
  const first = request.join(" ").trim();
5766
5916
  const conversational = !first && !opts.once && process.stdin.isTTY && !isJsonMode();
5767
- const session = await openArchitect({
5768
- ctx,
5769
- readOnly: Boolean(opts.readOnly),
5770
- allowApiBilling: Boolean(opts.allowApiBilling)
5771
- });
5917
+ const session = await openArchitect({ ctx, readOnly: Boolean(opts.readOnly) });
5772
5918
  let wroteText = false;
5773
5919
  const onEvent = (event) => {
5774
5920
  if (isJsonMode()) return;
@@ -5811,7 +5957,7 @@ function registerArchitectCommands(program) {
5811
5957
  }
5812
5958
  out(
5813
5959
  c.dim(
5814
- `Talking to the architect (${session.agent.model}${session.repo ? ", repo readable" : ""}). Blank line or /exit leaves.`
5960
+ `${describeBrain(session.brain)}${session.repo ? ", repo readable" : ""}. Blank line or /exit leaves.`
5815
5961
  )
5816
5962
  );
5817
5963
  for (; ; ) {
@@ -5836,6 +5982,33 @@ function registerArchitectCommands(program) {
5836
5982
  }
5837
5983
  await serveMcp({ readOnly, workspace: slugOf6(this) });
5838
5984
  });
5985
+ 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() {
5986
+ const opts = this.opts();
5987
+ if (opts.model || opts.effort) {
5988
+ const next = await setBrain({
5989
+ ...opts.model ? { model: String(opts.model) } : {},
5990
+ ...opts.effort ? { effort: String(opts.effort) } : {}
5991
+ });
5992
+ ok(describeBrain(next), { brain: next });
5993
+ return;
5994
+ }
5995
+ const config = await loadConfig();
5996
+ const chosen = config.brain ?? DEFAULT_BRAIN;
5997
+ const connection = await codexConnection();
5998
+ emit(
5999
+ { brain: chosen, connected: connection.state === "connected", codex: connection },
6000
+ () => [
6001
+ `${c.bold(describeBrain(chosen))}`,
6002
+ ...BRAINS.map(
6003
+ (brain) => ` ${brain.id === chosen.id ? c.blue("*") : " "} ${brain.label.padEnd(24)} ${c.dim(brain.billing)}`
6004
+ ),
6005
+ "",
6006
+ connection.state === "connected" ? c.green(connectionSummary(connection)) : `${c.yellow(connectionSummary(connection))}
6007
+ ${howToFix(connection) ?? ""}`,
6008
+ c.dim("Models: " + (brainById(chosen.id)?.models.join(", ") ?? ""))
6009
+ ].join("\n")
6010
+ );
6011
+ });
5839
6012
  program.command("connect").description("connect Codex to your ChatGPT subscription, so the architect costs nothing per token").option("--check", "report the state without starting a login").action(async function() {
5840
6013
  let connection = await codexConnection();
5841
6014
  if (connection.state === "connected" || this.opts().check) {
@@ -5952,7 +6125,7 @@ ${runner.HD_HELP}`);
5952
6125
 
5953
6126
  // src/host/upgrade.ts
5954
6127
  import { execFile as execFile5 } from "child_process";
5955
- import { createInterface as createInterface3 } from "readline/promises";
6128
+ import { createInterface as createInterface4 } from "readline/promises";
5956
6129
  import { promisify as promisify5 } from "util";
5957
6130
  init_json();
5958
6131
  init_theme();
@@ -6001,7 +6174,7 @@ async function offerUpdate(now = Date.now()) {
6001
6174
  await saveUpdateState({ checked_at: now });
6002
6175
  if (!latest || !isNewer(latest, VERSION)) return;
6003
6176
  if (state.skipped === latest) return;
6004
- const rl = createInterface3({ input: process.stdin, output: process.stderr });
6177
+ const rl = createInterface4({ input: process.stdin, output: process.stderr });
6005
6178
  let answer = "";
6006
6179
  try {
6007
6180
  answer = (await rl.question(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@higherdev/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "HigherDEV in your terminal. Full control of the board, live.",
6
6
  "bin": {