runcastle 1.2.9 → 1.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/runcastle.js CHANGED
@@ -147,7 +147,6 @@ var init_config = __esm(() => {
147
147
  models: z.array(ModelEntry).default([]),
148
148
  sandbox: z.enum(["docker", "podman", "noSandbox"]).default("docker"),
149
149
  sessionMcp: z.enum(["inherit", "runcastleOnly"]).default("inherit"),
150
- mainBranch: z.string().default("main"),
151
150
  sandboxImage: z.string().optional(),
152
151
  burnConcurrency: z.number().int().min(1).max(8).default(3),
153
152
  burnCpus: z.number().positive().max(256).optional(),
@@ -277,7 +276,7 @@ var init_schemas = __esm(() => {
277
276
  id: z2.string(),
278
277
  name: z2.string(),
279
278
  repoPath: z2.string(),
280
- mainBranch: z2.string(),
279
+ sessionBranch: z2.string().optional(),
281
280
  devCommand: z2.string().optional(),
282
281
  model: z2.string().optional(),
283
282
  setupCommand: z2.string().optional(),
@@ -544,7 +543,7 @@ var init_db_schema = __esm(() => {
544
543
  id: text("id").primaryKey(),
545
544
  name: text("name").notNull(),
546
545
  repoPath: text("repo_path").notNull(),
547
- mainBranch: text("main_branch").notNull(),
546
+ sessionBranch: text("session_branch"),
548
547
  devCommand: text("dev_command"),
549
548
  model: text("model"),
550
549
  sandbox: text("sandbox"),
@@ -817,8 +816,6 @@ function loadConfig(env = process.env) {
817
816
  overrides.sandbox = env.RUNCASTLE_SANDBOX;
818
817
  if (env.RUNCASTLE_SESSION_MCP)
819
818
  overrides.sessionMcp = env.RUNCASTLE_SESSION_MCP;
820
- if (env.RUNCASTLE_MAIN_BRANCH)
821
- overrides.mainBranch = env.RUNCASTLE_MAIN_BRANCH;
822
819
  if (env.RUNCASTLE_SANDBOX_IMAGE)
823
820
  overrides.sandboxImage = env.RUNCASTLE_SANDBOX_IMAGE;
824
821
  num("burnConcurrency", env.RUNCASTLE_BURN_CONCURRENCY);
@@ -1760,7 +1757,7 @@ function rowToProject(row) {
1760
1757
  id: row.id,
1761
1758
  name: row.name,
1762
1759
  repoPath: row.repoPath,
1763
- mainBranch: row.mainBranch,
1760
+ sessionBranch: row.sessionBranch ?? undefined,
1764
1761
  devCommand: row.devCommand ?? undefined,
1765
1762
  model: row.model ?? undefined,
1766
1763
  setupCommand: row.setupCommand ?? undefined,
@@ -2498,11 +2495,12 @@ function markVerified(ctx, projectId, keys, sha) {
2498
2495
  async function listFindings(ctx, project) {
2499
2496
  const rows = ctx.db.select().from(projectFindings).where(eq4(projectFindings.projectId, project.id)).all();
2500
2497
  const distances = new Map;
2501
- for (const row of rows) {
2502
- const sha = row.establishedSha;
2503
- if (!sha || distances.has(sha))
2504
- continue;
2505
- distances.set(sha, await commitsSince(project.repoPath, sha, project.mainBranch));
2498
+ const shas = new Set(rows.flatMap((row) => row.establishedSha ? [row.establishedSha] : []));
2499
+ if (shas.size > 0) {
2500
+ const mainLine = await detectMainBranch(project.repoPath);
2501
+ for (const sha of shas) {
2502
+ distances.set(sha, await commitsSince(project.repoPath, sha, mainLine));
2503
+ }
2506
2504
  }
2507
2505
  const order = new Map(PREPARED_KEYS.map((k, i) => [k, i]));
2508
2506
  return rows.filter((row) => isPreparedKey(row.key)).sort((a, b) => (order.get(a.key) ?? 0) - (order.get(b.key) ?? 0)).map((row) => {
@@ -2571,8 +2569,11 @@ function migrationPaths(paths) {
2571
2569
  function featureBranch(slug) {
2572
2570
  return `feature/${slug}`;
2573
2571
  }
2574
- function mergeTarget(project, feature) {
2575
- return feature.baseBranch ?? project.mainBranch;
2572
+ function featureBase(feature) {
2573
+ if (!feature.baseBranch) {
2574
+ throw new GateError(`feature ${feature.slug} has no recorded base branch \u2014 it has not been cut from one yet`);
2575
+ }
2576
+ return feature.baseBranch;
2576
2577
  }
2577
2578
  function git(repoPath) {
2578
2579
  return simpleGit(repoPath);
@@ -2622,9 +2623,31 @@ async function detectMainBranch(repoPath) {
2622
2623
  } catch {}
2623
2624
  return "main";
2624
2625
  }
2626
+ async function resolveSessionBranch(project) {
2627
+ const stored = project.sessionBranch?.trim();
2628
+ if (!stored)
2629
+ return detectMainBranch(project.repoPath);
2630
+ const local = await git(project.repoPath).branchLocal();
2631
+ if (!local.all.includes(stored)) {
2632
+ throw new GateError(`the configured session landing branch \`${stored}\` no longer exists in ${project.repoPath} \u2014 pick where this chat's work should land`);
2633
+ }
2634
+ return stored;
2635
+ }
2636
+ async function sessionBranchView(project) {
2637
+ const stored = project.sessionBranch?.trim() || null;
2638
+ const detected = await detectMainBranch(project.repoPath);
2639
+ return { stored, effective: stored ?? detected, detected };
2640
+ }
2641
+ async function currentCheckoutBranch(project) {
2642
+ const local = await git(project.repoPath).branchLocal();
2643
+ if (local.detached || !local.current) {
2644
+ throw new InvalidInputError(`${project.repoPath} has no branch checked out (detached HEAD) \u2014 name the branch to fork from`);
2645
+ }
2646
+ return local.current;
2647
+ }
2625
2648
  async function createFeatureBranch(project, slug, base) {
2626
2649
  const branch = featureBranch(slug);
2627
- const from = base?.trim() || project.mainBranch;
2650
+ const from = base.trim();
2628
2651
  const g = git(project.repoPath);
2629
2652
  const branches = await g.branchLocal();
2630
2653
  if (branches.all.includes(branch))
@@ -2638,7 +2661,9 @@ async function createFeatureBranch(project, slug, base) {
2638
2661
  async function listBranches(project) {
2639
2662
  const g = git(project.repoPath);
2640
2663
  const local = await g.branchLocal();
2641
- const branches = local.all.filter((name) => !name.startsWith("feature/"));
2664
+ const detachedRef = local.detached ? local.current : null;
2665
+ const current = local.detached ? "" : local.current;
2666
+ const branches = local.all.filter((name) => !name.startsWith("feature/") && name !== detachedRef);
2642
2667
  const localSet = new Set(local.all);
2643
2668
  let remoteBranches = [];
2644
2669
  try {
@@ -2648,7 +2673,7 @@ async function listBranches(project) {
2648
2673
  return !short.startsWith("feature/") && !localSet.has(short);
2649
2674
  });
2650
2675
  } catch {}
2651
- return { current: local.current, mainBranch: project.mainBranch, branches, remoteBranches };
2676
+ return { current, branches, remoteBranches };
2652
2677
  }
2653
2678
  async function resolveBaseBranch(project, base) {
2654
2679
  const g = git(project.repoPath);
@@ -2674,7 +2699,7 @@ async function ensureTalkWorktree(project, feature) {
2674
2699
  const worktreePath = worktreeDir(project.id, feature.slug);
2675
2700
  const branch = featureBranch(feature.slug);
2676
2701
  const g = git(project.repoPath);
2677
- await ensureBranchExists(g, branch, feature.baseBranch ?? project.mainBranch);
2702
+ await ensureBranchExists(g, branch, featureBase(feature));
2678
2703
  if (await worktreeIsValid(g, worktreePath, branch))
2679
2704
  return worktreePath;
2680
2705
  if (existsSync6(worktreePath) && (await registeredWorktrees(g)).has(canon(worktreePath))) {
@@ -2756,8 +2781,8 @@ function slash(path) {
2756
2781
  async function ensureProjectWorktree(project, onLanded) {
2757
2782
  const worktreePath = worktreeDir(project.id, PROJECT_WORKTREE_SLUG);
2758
2783
  const g = git(project.repoPath);
2759
- const base = project.mainBranch;
2760
- const landed = await landProjectBranch(project);
2784
+ const base = await resolveSessionBranch(project);
2785
+ const landed = await landProjectBranch(project, base);
2761
2786
  if (landed)
2762
2787
  onLanded?.(landed);
2763
2788
  const stillAhead = await branchCommitsAhead(project.repoPath, base, PROJECT_BRANCH);
@@ -2766,12 +2791,13 @@ async function ensureProjectWorktree(project, onLanded) {
2766
2791
  await g.raw(["branch", PROJECT_BRANCH, base]);
2767
2792
  }
2768
2793
  if (await worktreeIsValid(g, worktreePath, PROJECT_BRANCH))
2769
- return worktreePath;
2794
+ return { worktreePath, base };
2770
2795
  if (existsSync6(worktreePath) && (await registeredWorktrees(g)).has(canon(worktreePath))) {
2771
2796
  if (await checkoutInWorktree(worktreePath, PROJECT_BRANCH))
2772
- return worktreePath;
2797
+ return { worktreePath, base };
2773
2798
  }
2774
- return addWorktree(g, worktreePath, PROJECT_BRANCH, "project worktree");
2799
+ const added = await addWorktree(g, worktreePath, PROJECT_BRANCH, "project worktree");
2800
+ return { worktreePath: added, base };
2775
2801
  }
2776
2802
  async function checkoutInWorktree(worktreePath, branch) {
2777
2803
  const gw = git(worktreePath);
@@ -2935,7 +2961,7 @@ async function branchCommitsAhead(repoPath, base, branch) {
2935
2961
  }
2936
2962
  }
2937
2963
  async function reviewCommitCount(project, feature) {
2938
- const base = mergeTarget(project, feature);
2964
+ const base = featureBase(feature);
2939
2965
  const branch = featureBranch(feature.slug);
2940
2966
  try {
2941
2967
  const out = (await git(project.repoPath).raw(["rev-list", "--count", `${base}..${branch}`])).trim();
@@ -2946,7 +2972,7 @@ async function reviewCommitCount(project, feature) {
2946
2972
  }
2947
2973
  }
2948
2974
  async function featureBranchDelta(project, feature) {
2949
- const base = mergeTarget(project, feature);
2975
+ const base = featureBase(feature);
2950
2976
  const branch = featureBranch(feature.slug);
2951
2977
  try {
2952
2978
  const out = await git(project.repoPath).raw(["diff", "--stat", `${base}...${branch}`]);
@@ -3121,12 +3147,13 @@ async function deleteBranchDetachingWorktrees(g, repoPath, branch) {
3121
3147
  return false;
3122
3148
  }
3123
3149
  }
3124
- async function landProjectBranch(project) {
3125
- const ahead = await branchCommitsAhead(project.repoPath, project.mainBranch, PROJECT_BRANCH);
3150
+ async function landProjectBranch(project, base) {
3151
+ const ahead = await branchCommitsAhead(project.repoPath, base, PROJECT_BRANCH);
3126
3152
  if (ahead.length === 0)
3127
3153
  return null;
3128
- const res = await mergeTempBranch(project.repoPath, project.mainBranch, PROJECT_BRANCH);
3154
+ const res = await mergeTempBranch(project.repoPath, base, PROJECT_BRANCH);
3129
3155
  return {
3156
+ base,
3130
3157
  commits: ahead.length,
3131
3158
  landed: res.ok,
3132
3159
  ...res.conflict ? { conflict: true } : {},
@@ -3507,7 +3534,7 @@ async function stopDryRun(ctx, project, state) {
3507
3534
  });
3508
3535
  const { participating, failure } = dryRunVerdict(project, state);
3509
3536
  if (!failure && participating.length > 0) {
3510
- const sha = await headSha(project.repoPath, project.mainBranch) ?? null;
3537
+ const sha = await headSha(project.repoPath, await detectMainBranch(project.repoPath)) ?? null;
3511
3538
  markVerified(ctx, project.id, participating, sha);
3512
3539
  emitProject(ctx, project.id, {
3513
3540
  type: "prep.dryrun.verified",
@@ -3669,7 +3696,7 @@ async function detectDbDrift(ctx, project, feature, previousBranch, driveBranch)
3669
3696
  return drift;
3670
3697
  }
3671
3698
  async function mergeFeature(project, feature) {
3672
- const target = mergeTarget(project, feature);
3699
+ const target = featureBase(feature);
3673
3700
  if (testDriveState) {
3674
3701
  throw new GateError("Cannot merge while a test drive is active \u2014 stop it first");
3675
3702
  }
@@ -4273,7 +4300,7 @@ function renderDriveFixPrompt(brief) {
4273
4300
  `);
4274
4301
  }
4275
4302
  function renderProjectPrompt(brief, runtime = DEFAULT_RUNTIME) {
4276
- const { project, branch, worktreePath } = brief;
4303
+ const { project, branch, worktreePath, base } = brief;
4277
4304
  return [
4278
4305
  `# runcastle \u2014 ${project.name} (project session)`,
4279
4306
  "",
@@ -4299,8 +4326,8 @@ function renderProjectPrompt(brief, runtime = DEFAULT_RUNTIME) {
4299
4326
  "## Where you are working \u2014 and where your writes go",
4300
4327
  `- Working directory: \`${worktreePath}\` \u2014 a runcastle-owned worktree, NOT the human's`,
4301
4328
  ` checkout at \`${project.repoPath}\`.`,
4302
- `- Branch: \`${branch}\`, cut from \`${project.mainBranch}\` when this session launched.`,
4303
- `- **Your commits land on \`${project.mainBranch}\` when this terminal closes**, arriving`,
4329
+ `- Branch: \`${branch}\`, cut from \`${base}\` when this session launched.`,
4330
+ `- **Your commits land on \`${base}\` when this terminal closes**, arriving`,
4304
4331
  " in the human's checkout the way a `git pull` does. Write real code and real docs \u2014",
4305
4332
  " there is no sandbox here \u2014 and commit everything you write before you finish.",
4306
4333
  " A session that edits without committing leaves a dirty tree, which is exactly what",
@@ -4587,10 +4614,23 @@ var init_claude = __esm(() => {
4587
4614
  };
4588
4615
  });
4589
4616
 
4590
- // src/doctor/doctor.ts
4617
+ // src/services/codex-auth.ts
4591
4618
  import { existsSync as existsSync8 } from "fs";
4592
4619
  import { homedir as homedir2 } from "os";
4593
4620
  import { join as join9 } from "path";
4621
+ function codexHomeDir(env = process.env) {
4622
+ return env.CODEX_HOME ?? join9(env.HOME ?? env.USERPROFILE ?? homedir2(), ".codex");
4623
+ }
4624
+ function codexAuthFile(env = process.env) {
4625
+ return join9(codexHomeDir(env), "auth.json");
4626
+ }
4627
+ function codexLoggedIn(env = process.env, fileExists = existsSync8) {
4628
+ return fileExists(codexAuthFile(env));
4629
+ }
4630
+ var init_codex_auth = () => {};
4631
+
4632
+ // src/doctor/doctor.ts
4633
+ import { existsSync as existsSync9 } from "fs";
4594
4634
  async function versionProbe(exec, id, label, bin, fix) {
4595
4635
  const out = await exec(bin, ["--version"]);
4596
4636
  if (out.ok && out.code === 0) {
@@ -4622,9 +4662,6 @@ async function nodeProbe(exec) {
4622
4662
  async function gitProbe(exec) {
4623
4663
  return versionProbe(exec, "git", "Git", "git", "Install Git: winget install Git.Git / brew install git / apt install git");
4624
4664
  }
4625
- function codexAuthFile(env) {
4626
- return join9(env.CODEX_HOME ?? join9(env.HOME ?? env.USERPROFILE ?? homedir2(), ".codex"), "auth.json");
4627
- }
4628
4665
  function statusUnsupported(out) {
4629
4666
  return /unknown command|unrecognized subcommand|unexpected argument|invalid subcommand/i.test(`${out.stderr} ${out.stdout}`);
4630
4667
  }
@@ -4656,23 +4693,38 @@ async function runtimeAuthProbe(spec, exec, env, fileExists, severity) {
4656
4693
  fix: spec.installFix
4657
4694
  };
4658
4695
  }
4696
+ const authFile = spec.authFile?.(env);
4697
+ if (authFile) {
4698
+ const present = fileExists(authFile);
4699
+ const statusCommand = `${spec.bin} ${spec.authStatusArgs.join(" ")}`;
4700
+ let coda;
4701
+ if (statusUnsupported(out)) {
4702
+ coda = ` \u2014 this ${spec.bin} cannot report login state`;
4703
+ } else if (out.code === 0 === present) {
4704
+ coda = ` \u2014 \`${statusCommand}\` agrees`;
4705
+ } else if (present) {
4706
+ coda = `, but \`${statusCommand}\` reports logged out \u2014 burns borrow the file, so this host counts as signed in`;
4707
+ } else {
4708
+ coda = `, but \`${statusCommand}\` reports a login \u2014 a burn borrows the file, and it is not there`;
4709
+ }
4710
+ return present ? { ...base, status: "ok", detail: `credentials found at ${authFile}${coda}` } : { ...loggedOut, detail: `no credentials at ${authFile}${coda}` };
4711
+ }
4659
4712
  if (out.code === 0)
4660
4713
  return { ...base, status: "ok", detail: `${spec.bin} reports a login` };
4661
4714
  if (!statusUnsupported(out))
4662
4715
  return loggedOut;
4663
- const authFile = spec.authFile?.(env);
4664
- if (!authFile) {
4665
- return {
4666
- ...base,
4667
- status: "ok",
4668
- detail: `this ${spec.bin} cannot report login state \u2014 it prompts at launch`
4669
- };
4670
- }
4671
- return fileExists(authFile) ? { ...base, status: "ok", detail: `credentials found at ${authFile}` } : { ...loggedOut, detail: `no credentials at ${authFile}` };
4716
+ return {
4717
+ ...base,
4718
+ status: "ok",
4719
+ detail: `this ${spec.bin} cannot report login state \u2014 it prompts at launch`
4720
+ };
4672
4721
  }
4673
4722
  function runtimeAfkKeyProbe(spec, env, severity = "error") {
4723
+ const id = spec.ids.afkKey;
4724
+ if (!id)
4725
+ return;
4674
4726
  const base = {
4675
- id: spec.ids.afkKey,
4727
+ id,
4676
4728
  label: `${spec.label} AFK ${spec.afkNoun} (${spec.afkKey})`,
4677
4729
  tier: 2,
4678
4730
  severity,
@@ -4691,11 +4743,12 @@ function runtimeAfkKeyProbe(spec, env, severity = "error") {
4691
4743
  };
4692
4744
  }
4693
4745
  async function runtimeProbes(spec, deps) {
4694
- const fileExists = deps.fileExists ?? existsSync8;
4746
+ const fileExists = deps.fileExists ?? existsSync9;
4747
+ const afkKeyProbe = runtimeAfkKeyProbe(spec, deps.env, deps.severity);
4695
4748
  return [
4696
4749
  await runtimeBinaryProbe(spec, deps.exec, deps.severity),
4697
4750
  await runtimeAuthProbe(spec, deps.exec, deps.env, fileExists, deps.severity),
4698
- runtimeAfkKeyProbe(spec, deps.env, deps.severity)
4751
+ ...afkKeyProbe ? [afkKeyProbe] : []
4699
4752
  ];
4700
4753
  }
4701
4754
  async function gitIdentityProbe(exec, cwd) {
@@ -4849,6 +4902,7 @@ function exitCodeFor(report, mode) {
4849
4902
  var RUNTIME_SPECS;
4850
4903
  var init_doctor = __esm(() => {
4851
4904
  init_src();
4905
+ init_codex_auth();
4852
4906
  RUNTIME_SPECS = {
4853
4907
  "claude-code": {
4854
4908
  runtime: "claude-code",
@@ -4868,13 +4922,13 @@ var init_doctor = __esm(() => {
4868
4922
  runtime: "codex",
4869
4923
  label: "Codex",
4870
4924
  bin: "codex",
4871
- ids: { binary: "codex", auth: "codex-auth", afkKey: "codex-api-key" },
4925
+ ids: { binary: "codex", auth: "codex-auth" },
4872
4926
  authStatusArgs: ["login", "status"],
4873
4927
  loginCommand: "codex login",
4874
4928
  loginArgs: ["login"],
4875
4929
  afkKey: "CODEX_API_KEY",
4876
4930
  afkNoun: "API key",
4877
- afkFix: "Create an OpenAI API key (platform.openai.com/api-keys) and put CODEX_API_KEY in ~/.runcastle/.env",
4931
+ afkFix: "Set CODEX_API_KEY in ~/.runcastle/.env to bill an OpenAI API key instead of your ChatGPT login",
4878
4932
  installFix: "Install Codex: npm install -g @openai/codex (or: brew install codex)",
4879
4933
  binOverrideEnv: "RUNCASTLE_CODEX_BIN",
4880
4934
  authFile: codexAuthFile
@@ -4885,31 +4939,33 @@ var init_doctor = __esm(() => {
4885
4939
  // src/launcher/runtimes/codex.ts
4886
4940
  import {
4887
4941
  copyFileSync,
4888
- existsSync as existsSync9,
4942
+ existsSync as existsSync10,
4889
4943
  mkdirSync as mkdirSync4,
4890
4944
  readFileSync as readFileSync4,
4891
4945
  readdirSync as readdirSync2,
4892
4946
  statSync,
4893
4947
  writeFileSync as writeFileSync3
4894
4948
  } from "fs";
4895
- import { homedir as homedir3 } from "os";
4896
4949
  import { basename, dirname as dirname3, join as join10, resolve as resolve7 } from "path";
4897
- function codexHomeDir(sessionId) {
4950
+ function codexHomeDir2(sessionId) {
4898
4951
  return join10(sessionDir(sessionId), "codex-home");
4899
4952
  }
4900
4953
  function realCodexAuthFile(env = process.env) {
4901
- return CODEX.authFile?.(env) ?? join10(homedir3(), ".codex", "auth.json");
4954
+ return codexAuthFile(env);
4902
4955
  }
4903
4956
  function toml(value) {
4904
4957
  return JSON.stringify(value);
4905
4958
  }
4959
+ function approvalPolicyFor(permissionMode) {
4960
+ return permissionMode === undefined || permissionMode === "acceptEdits" ? "never" : "untrusted";
4961
+ }
4906
4962
  function renderCodexConfig(input) {
4907
- const { session, config: config2, model, worktreePath, inheritedMcpServers } = input;
4963
+ const { session, config: config2, model, worktreePath, inheritedMcpServers, permissionMode } = input;
4908
4964
  return [
4909
4965
  `# Generated by runcastle for session ${session.id} \u2014 rewritten at every launch.`,
4910
4966
  `model = ${toml(model)}`,
4911
4967
  'sandbox_mode = "workspace-write"',
4912
- 'approval_policy = "never"',
4968
+ `approval_policy = ${toml(approvalPolicyFor(permissionMode))}`,
4913
4969
  "",
4914
4970
  `[projects.${toml(worktreePath)}]`,
4915
4971
  'trust_level = "trusted"',
@@ -4941,7 +4997,7 @@ function mcpServerTables(userConfigToml) {
4941
4997
  }
4942
4998
  function readInheritedMcpServers(env = process.env) {
4943
4999
  const path = join10(dirname3(realCodexAuthFile(env)), "config.toml");
4944
- if (!existsSync9(path))
5000
+ if (!existsSync10(path))
4945
5001
  return "";
4946
5002
  try {
4947
5003
  return mcpServerTables(readFileSync4(path, "utf8"));
@@ -4965,15 +5021,15 @@ function renderCodexHooks(hookClient, kind, platform = process.platform) {
4965
5021
  };
4966
5022
  }
4967
5023
  function writeCodexSkills(worktreePath, packDir) {
4968
- if (!existsSync9(worktreePath))
5024
+ if (!existsSync10(worktreePath))
4969
5025
  return [];
4970
5026
  const source = join10(packDir, "skills");
4971
- if (!existsSync9(source))
5027
+ if (!existsSync10(source))
4972
5028
  return [];
4973
5029
  const written = [];
4974
5030
  for (const name of readdirSync2(source)) {
4975
5031
  const skill = join10(source, name, "SKILL.md");
4976
- if (!existsSync9(skill))
5032
+ if (!existsSync10(skill))
4977
5033
  continue;
4978
5034
  const target = join10(worktreePath, CODEX_SKILLS_REL, name);
4979
5035
  mkdirSync4(target, { recursive: true });
@@ -4992,7 +5048,7 @@ function excludeFromGit(worktreePath, entry) {
4992
5048
  return;
4993
5049
  mkdirSync4(info, { recursive: true });
4994
5050
  const path = join10(info, "exclude");
4995
- const existing = existsSync9(path) ? readFileSync4(path, "utf8") : "";
5051
+ const existing = existsSync10(path) ? readFileSync4(path, "utf8") : "";
4996
5052
  if (existing.split(/\r?\n/).some((line) => line.trim() === entry))
4997
5053
  return;
4998
5054
  const head = existing.length > 0 && !existing.endsWith(`
@@ -5004,7 +5060,7 @@ function excludeFromGit(worktreePath, entry) {
5004
5060
  }
5005
5061
  function gitInfoDir(worktreePath) {
5006
5062
  const dotGit = join10(worktreePath, ".git");
5007
- if (!existsSync9(dotGit))
5063
+ if (!existsSync10(dotGit))
5008
5064
  return null;
5009
5065
  if (statSync(dotGit).isDirectory())
5010
5066
  return join10(dotGit, "info");
@@ -5025,6 +5081,7 @@ var CODEX_BIN = "codex", CODEX, CODEX_SKILLS_REL, CODEX_SKILLS_IGNORE = ".agents
5025
5081
  var init_codex = __esm(() => {
5026
5082
  init_paths();
5027
5083
  init_doctor();
5084
+ init_codex_auth();
5028
5085
  init_resolve_executable();
5029
5086
  init_artifacts();
5030
5087
  init_edit_guard();
@@ -5047,7 +5104,7 @@ var init_codex = __esm(() => {
5047
5104
  doctorHint: 'Run `runcastle doctor` and fix its "Codex CLI" probe, then restart runcastle from a ' + "terminal where `codex --version` works."
5048
5105
  };
5049
5106
  }
5050
- if (!existsSync9(realCodexAuthFile())) {
5107
+ if (!existsSync10(realCodexAuthFile())) {
5051
5108
  return {
5052
5109
  ok: false,
5053
5110
  reason: "Codex is installed but not logged in \u2014 runcastle copies your own credentials into " + `each session's home, and there are none at ${realCodexAuthFile()}`,
@@ -5057,7 +5114,7 @@ var init_codex = __esm(() => {
5057
5114
  return { ok: true };
5058
5115
  },
5059
5116
  async writeArtifacts(input) {
5060
- const home = codexHomeDir(input.session.id);
5117
+ const home = codexHomeDir2(input.session.id);
5061
5118
  mkdirSync4(home, { recursive: true });
5062
5119
  const configPath2 = join10(home, "config.toml");
5063
5120
  const hooksPath = join10(home, "hooks.json");
@@ -5067,13 +5124,14 @@ var init_codex = __esm(() => {
5067
5124
  config: input.config,
5068
5125
  model: input.model,
5069
5126
  worktreePath: input.worktreePath,
5127
+ ...input.permissionMode ? { permissionMode: input.permissionMode } : {},
5070
5128
  ...input.config.sessionMcp === "inherit" ? { inheritedMcpServers: readInheritedMcpServers() } : {}
5071
5129
  }), "utf8");
5072
5130
  writeFileSync3(hooksPath, JSON.stringify(renderCodexHooks(hookClientPath(), input.session.kind), null, 2), "utf8");
5073
5131
  writeFileSync3(agentsPath, renderSessionPrompt({ ...input, runtime: this.id }), "utf8");
5074
5132
  const files = [configPath2, hooksPath, agentsPath];
5075
5133
  const auth = realCodexAuthFile();
5076
- if (existsSync9(auth)) {
5134
+ if (existsSync10(auth)) {
5077
5135
  const copied = join10(home, "auth.json");
5078
5136
  copyFileSync(auth, copied);
5079
5137
  files.push(copied);
@@ -5375,20 +5433,18 @@ function landProjectSession(ctx, session) {
5375
5433
  const project = getProjectById(ctx, session.projectId);
5376
5434
  if (!project)
5377
5435
  return;
5378
- const landing = landProjectBranch(project).then((res) => {
5379
- if (res)
5380
- reportProjectLanding(ctx, project, res, { sessionId: session.id });
5381
- }).catch((e) => {
5436
+ const landFailed = (base, e) => {
5437
+ const error = e instanceof Error ? e.message : String(e);
5382
5438
  emitProject(ctx, project.id, {
5383
5439
  type: "project.land_failed",
5384
- message: `could not land the project session's work onto ${project.mainBranch}: ${e instanceof Error ? e.message : String(e)} \u2014 ${LANDING_KEPT}`,
5385
- data: {
5386
- sessionId: session.id,
5387
- branch: PROJECT_BRANCH,
5388
- error: e instanceof Error ? e.message : String(e)
5389
- }
5440
+ message: `could not land the project session's work${base ? ` onto ${base}` : ""}: ${error} \u2014 ${LANDING_KEPT}`,
5441
+ data: { sessionId: session.id, branch: PROJECT_BRANCH, error }
5390
5442
  });
5391
- });
5443
+ };
5444
+ const landing = resolveSessionBranch(project).then((base) => landProjectBranch(project, base).then((res) => {
5445
+ if (res)
5446
+ reportProjectLanding(ctx, project, res, { sessionId: session.id });
5447
+ }, (e) => landFailed(base, e)), (e) => landFailed(null, e));
5392
5448
  inFlightLandings.add(landing);
5393
5449
  landing.finally(() => inFlightLandings.delete(landing));
5394
5450
  }
@@ -5401,11 +5457,11 @@ function reportProjectLanding(ctx, project, res, data = {}) {
5401
5457
  });
5402
5458
  };
5403
5459
  if (res.landed) {
5404
- report("project.landed", `landed ${res.commits} commit(s) from the project session onto ${project.mainBranch}`, { commits: res.commits });
5460
+ report("project.landed", `landed ${res.commits} commit(s) from the project session onto ${res.base}`, { commits: res.commits });
5405
5461
  } else if (res.conflict) {
5406
- report("project.land_conflict", `the project session's ${res.commits} commit(s) conflict with ${project.mainBranch} \u2014 nothing was overwritten; ${LANDING_KEPT}`, { commits: res.commits, files: res.files ?? [] });
5462
+ report("project.land_conflict", `the project session's ${res.commits} commit(s) conflict with ${res.base} \u2014 nothing was overwritten; ${LANDING_KEPT}`, { commits: res.commits, files: res.files ?? [] });
5407
5463
  } else {
5408
- report("project.land_failed", `could not land the project session's ${res.commits} commit(s) onto ${project.mainBranch}: ${res.error ?? "unknown error"} \u2014 ${LANDING_KEPT}`, { commits: res.commits, error: res.error ?? null });
5464
+ report("project.land_failed", `could not land the project session's ${res.commits} commit(s) onto ${res.base}: ${res.error ?? "unknown error"} \u2014 ${LANDING_KEPT}`, { commits: res.commits, error: res.error ?? null });
5409
5465
  }
5410
5466
  }
5411
5467
  function markSessionEnded(ctx, id) {
@@ -5746,7 +5802,7 @@ var init_tickets = __esm(() => {
5746
5802
  });
5747
5803
 
5748
5804
  // src/services/gates.ts
5749
- import { existsSync as existsSync10 } from "fs";
5805
+ import { existsSync as existsSync11 } from "fs";
5750
5806
  import { and as and7, desc as desc4, eq as eq7, inArray as inArray3 } from "drizzle-orm";
5751
5807
  function checkGate(ctx, check, feature) {
5752
5808
  switch (check) {
@@ -5796,7 +5852,7 @@ function notYetTerminal(noun, statuses, order) {
5796
5852
  }
5797
5853
  function fileGate(ctx, feature, fileName, reason) {
5798
5854
  const project = projectForFeature(ctx, feature);
5799
- return existsSync10(featureDocPath(project, feature, fileName)) ? { satisfied: true } : { satisfied: false, reason };
5855
+ return existsSync11(featureDocPath(project, feature, fileName)) ? { satisfied: true } : { satisfied: false, reason };
5800
5856
  }
5801
5857
  function docGate(ctx, feature, fileName, reason) {
5802
5858
  const file = fileGate(ctx, feature, fileName, reason);
@@ -5860,14 +5916,14 @@ var init_gates = __esm(() => {
5860
5916
  });
5861
5917
 
5862
5918
  // src/services/knowledge.ts
5863
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, writeFileSync as writeFileSync4 } from "fs";
5919
+ import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, writeFileSync as writeFileSync4 } from "fs";
5864
5920
  import { join as join11, relative as relative2, resolve as resolve8, sep } from "path";
5865
5921
  function scaffoldDocs(ctx, feature, opts = {}) {
5866
5922
  const project = projectForFeature(ctx, feature);
5867
5923
  const dir = featureDocsDir(project, feature);
5868
5924
  mkdirSync5(dir, { recursive: true });
5869
5925
  const briefPath = featureDocPath(project, feature, "brief.md");
5870
- if (!existsSync11(briefPath)) {
5926
+ if (!existsSync12(briefPath)) {
5871
5927
  const override = opts.brief?.trim();
5872
5928
  const created = new Date().toISOString();
5873
5929
  const brief = override ? `${override}
@@ -5894,7 +5950,7 @@ function scaffoldDocs(ctx, feature, opts = {}) {
5894
5950
  function scaffoldMapDoc(project, feature, seed) {
5895
5951
  mkdirSync5(featureDocsDir(project, feature), { recursive: true });
5896
5952
  const mapPath = featureDocPath(project, feature, "map.md");
5897
- if (existsSync11(mapPath))
5953
+ if (existsSync12(mapPath))
5898
5954
  return;
5899
5955
  writeFileSync4(mapPath, mapDocBody(feature, seed), "utf8");
5900
5956
  }
@@ -5915,7 +5971,7 @@ function mapDocBody(feature, seed) {
5915
5971
  }
5916
5972
  function readCharter(repoRoot) {
5917
5973
  const path = join11(repoRoot, CHARTER_FILE);
5918
- if (!existsSync11(path))
5974
+ if (!existsSync12(path))
5919
5975
  return;
5920
5976
  try {
5921
5977
  return readFileSync5(path, "utf8");
@@ -5925,7 +5981,7 @@ function readCharter(repoRoot) {
5925
5981
  }
5926
5982
  function listLiveAdrs(repoRoot) {
5927
5983
  const dir = join11(repoRoot, ADR_DIR_REL);
5928
- if (!existsSync11(dir))
5984
+ if (!existsSync12(dir))
5929
5985
  return [];
5930
5986
  const docs2 = [];
5931
5987
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
@@ -5946,7 +6002,7 @@ function listLiveAdrs(repoRoot) {
5946
6002
  function listDocs(ctx, feature) {
5947
6003
  const project = projectForFeature(ctx, feature);
5948
6004
  const dir = featureDocsDir(project, feature);
5949
- if (!existsSync11(dir))
6005
+ if (!existsSync12(dir))
5950
6006
  return [];
5951
6007
  return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => ({ relPath: e.name, title: readTitle(join11(dir, e.name), e.name) })).sort((a, b) => a.relPath.localeCompare(b.relPath));
5952
6008
  }
@@ -5959,7 +6015,7 @@ function readDoc(ctx, feature, relPath) {
5959
6015
  if (rel === "" || rel.startsWith("..") || rel.split(sep).includes("..")) {
5960
6016
  throw new InvalidInputError(`doc path escapes the feature docs dir: ${relPath}`);
5961
6017
  }
5962
- if (!existsSync11(target))
6018
+ if (!existsSync12(target))
5963
6019
  throw new NotFoundError(`doc not found: ${relPath}`);
5964
6020
  return { content: readFileSync5(target, "utf8") };
5965
6021
  }
@@ -6017,12 +6073,12 @@ var init_end_session = __esm(() => {
6017
6073
  });
6018
6074
 
6019
6075
  // src/services/settings.ts
6020
- import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
6076
+ import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
6021
6077
  import { dirname as dirname4 } from "path";
6022
6078
  import * as z3 from "zod";
6023
6079
  import { eq as eq8 } from "drizzle-orm";
6024
6080
  function readRawConfig(configFile) {
6025
- if (!existsSync12(configFile))
6081
+ if (!existsSync13(configFile))
6026
6082
  return {};
6027
6083
  try {
6028
6084
  const parsed = JSON.parse(readFileSync6(configFile, "utf8"));
@@ -6052,6 +6108,7 @@ function projectOverrides(ctx, projectId) {
6052
6108
  const row = ctx.db.select({
6053
6109
  model: projects.model,
6054
6110
  sandbox: projects.sandbox,
6111
+ sessionBranch: projects.sessionBranch,
6055
6112
  devCommand: projects.devCommand,
6056
6113
  setupCommand: projects.setupCommand,
6057
6114
  verifyCommands: projects.verifyCommands,
@@ -6063,6 +6120,7 @@ function projectOverrides(ctx, projectId) {
6063
6120
  return {
6064
6121
  model: row?.model ?? null,
6065
6122
  sandbox: row?.sandbox ?? null,
6123
+ sessionBranch: row?.sessionBranch ?? null,
6066
6124
  devCommand: row?.devCommand ?? null,
6067
6125
  setupCommand: row?.setupCommand ?? null,
6068
6126
  verifyCommands: row?.verifyCommands ?? null,
@@ -6341,16 +6399,15 @@ var init_settings = __esm(() => {
6341
6399
  parseEnv: idEnv
6342
6400
  },
6343
6401
  {
6344
- key: "mainBranch",
6345
- configKey: "mainBranch",
6346
- envVar: "RUNCASTLE_MAIN_BRANCH",
6402
+ key: "devCommand",
6403
+ projectColumn: "devCommand",
6347
6404
  restartRequired: false,
6348
6405
  valueSchema: z3.string().min(1),
6349
6406
  parseEnv: idEnv
6350
6407
  },
6351
6408
  {
6352
- key: "devCommand",
6353
- projectColumn: "devCommand",
6409
+ key: "sessionBranch",
6410
+ projectColumn: "sessionBranch",
6354
6411
  restartRequired: false,
6355
6412
  valueSchema: z3.string().min(1),
6356
6413
  parseEnv: idEnv
@@ -6382,7 +6439,7 @@ var init_settings = __esm(() => {
6382
6439
  });
6383
6440
 
6384
6441
  // src/services/setup.ts
6385
- import { copyFileSync as copyFileSync2, existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync7, readdirSync as readdirSync4, writeFileSync as writeFileSync6 } from "fs";
6442
+ import { copyFileSync as copyFileSync2, existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync7, readdirSync as readdirSync4, writeFileSync as writeFileSync6 } from "fs";
6386
6443
  import { dirname as dirname5, join as join12 } from "path";
6387
6444
  import { fileURLToPath as fileURLToPath4 } from "url";
6388
6445
  async function writeGitIdentity(exec, identity) {
@@ -6490,7 +6547,7 @@ function resolveSandcastleBin() {
6490
6547
  let dir = dirname5(fileURLToPath4(mainUrl));
6491
6548
  for (let hops = 0;hops < 6; hops++) {
6492
6549
  const manifestPath = join12(dir, "package.json");
6493
- if (existsSync13(manifestPath)) {
6550
+ if (existsSync14(manifestPath)) {
6494
6551
  const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
6495
6552
  if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
6496
6553
  return join12(dir, manifest.bin.sandcastle);
@@ -6526,7 +6583,7 @@ function sandcastleTemplateDir() {
6526
6583
  }
6527
6584
  function scaffoldSandcastleConfig(templateDir, targetDir) {
6528
6585
  const dir = join12(targetDir, ".sandcastle");
6529
- if (existsSync13(dir))
6586
+ if (existsSync14(dir))
6530
6587
  return { scaffolded: false, dir };
6531
6588
  mkdirSync7(dir, { recursive: true });
6532
6589
  for (const name of readdirSync4(templateDir)) {
@@ -6573,7 +6630,7 @@ function createCredentialVerifier(exec, runtime = DEFAULT_RUNTIME) {
6573
6630
  function fileAfkTokenIo(exec, runtime = DEFAULT_RUNTIME) {
6574
6631
  const path = envPath();
6575
6632
  return {
6576
- read: () => existsSync13(path) ? readFileSync7(path, "utf8") : "",
6633
+ read: () => existsSync14(path) ? readFileSync7(path, "utf8") : "",
6577
6634
  write: (content) => {
6578
6635
  mkdirSync7(dirname5(path), { recursive: true });
6579
6636
  writeFileSync6(path, content, "utf8");
@@ -6597,7 +6654,7 @@ var init_setup = __esm(() => {
6597
6654
  };
6598
6655
  RUNTIME_AUTH_SETUP_HINT = {
6599
6656
  "claude-code": RUNTIME_SPECS["claude-code"].afkFix,
6600
- codex: RUNTIME_SPECS.codex.afkFix
6657
+ codex: `Run \`${RUNTIME_SPECS.codex.loginCommand}\` on this host, then burn again`
6601
6658
  };
6602
6659
  LOGIN_TERMINAL_KIND = {
6603
6660
  "claude-code": "claude-login",
@@ -6754,7 +6811,7 @@ var init_burn_guard = __esm(() => {
6754
6811
  });
6755
6812
 
6756
6813
  // src/workflows/review-ticket.ts
6757
- import { existsSync as existsSync14, mkdirSync as mkdirSync8, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
6814
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
6758
6815
  import { delimiter, join as join13 } from "path";
6759
6816
  import { run } from "@ai-hero/sandcastle";
6760
6817
  import { noSandbox } from "@ai-hero/sandcastle/sandboxes/no-sandbox";
@@ -6770,7 +6827,7 @@ function findOnPath(bin, env = process.env, platform = process.platform) {
6770
6827
  for (const dir of dirs) {
6771
6828
  for (const suffix of suffixes) {
6772
6829
  const candidate = join13(dir, `${bin}${suffix}`);
6773
- if (existsSync14(candidate))
6830
+ if (existsSync15(candidate))
6774
6831
  return candidate;
6775
6832
  }
6776
6833
  }
@@ -6798,6 +6855,9 @@ async function releaseDriveQuietly() {
6798
6855
  }
6799
6856
  async function executeReviewTicket(ctx, ticket, deps) {
6800
6857
  const { project, feature } = ctx;
6858
+ if (!feature.baseBranch) {
6859
+ return couldNotReview(ticket, `feature ${feature.slug} has no recorded base branch, so there is nothing to diff \`${feature.branch}\` against.`);
6860
+ }
6801
6861
  if (!findOnPath(AGENT_BROWSER_BIN)) {
6802
6862
  return couldNotReview(ticket, `\`${AGENT_BROWSER_BIN}\` is not on this machine's PATH, so there is no way to drive the app. Install it and re-burn this ticket.`);
6803
6863
  }
@@ -6808,7 +6868,7 @@ async function executeReviewTicket(ctx, ticket, deps) {
6808
6868
  DOCS_DIGEST: deps.docsDigest,
6809
6869
  LAP_DIGESTS: buildLapDigestsBlock(deps.lapDigests),
6810
6870
  FEATURE_BRANCH: feature.branch,
6811
- BASE_BRANCH: project.mainBranch,
6871
+ BASE_BRANCH: feature.baseBranch,
6812
6872
  DIGEST_PATH: artifacts.digestPath,
6813
6873
  BLOCKED_PATH: artifacts.blockedPath,
6814
6874
  WALKTHROUGH_PATH: artifacts.walkthroughPath
@@ -6890,13 +6950,18 @@ var init_review_ticket = __esm(() => {
6890
6950
  });
6891
6951
 
6892
6952
  // src/workflows/ticket-burner.ts
6893
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync9, readdirSync as readdirSync5, rmSync as rmSync3 } from "fs";
6953
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9, readFileSync as readFileSync9, readdirSync as readdirSync5, rmSync as rmSync3 } from "fs";
6894
6954
  import { dirname as dirname6, join as join14, posix as posixPath, win32 as winPath } from "path";
6895
6955
  import { fileURLToPath as fileURLToPath5 } from "url";
6896
6956
  import { claudeCode, codex, run as run2 } from "@ai-hero/sandcastle";
6897
6957
  import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
6898
6958
  import { podman } from "@ai-hero/sandcastle/sandboxes/podman";
6899
6959
  import { noSandbox as noSandbox2 } from "@ai-hero/sandcastle/sandboxes/no-sandbox";
6960
+ function burnAuthReady(runtime, token, loggedIn = codexLoggedIn) {
6961
+ if (token !== undefined)
6962
+ return true;
6963
+ return runtime === "codex" && loggedIn();
6964
+ }
6900
6965
  function isReviewTicket(ticket) {
6901
6966
  return ticket.kind === "review";
6902
6967
  }
@@ -7037,10 +7102,10 @@ _(The "${DROPPED_MAP_SECTIONS.join('" and "')}" sections are omitted here: they
7037
7102
  }
7038
7103
  function buildProjectStandards(repoPath) {
7039
7104
  const present = [];
7040
- if (existsSync15(join14(repoPath, AGENT_CONVENTIONS_FILE))) {
7105
+ if (existsSync16(join14(repoPath, AGENT_CONVENTIONS_FILE))) {
7041
7106
  present.push("`CLAUDE.md` \u2014 this repo's agent-facing conventions, and the highest authority on them");
7042
7107
  }
7043
- if (existsSync15(join14(repoPath, CHARTER_FILE))) {
7108
+ if (existsSync16(join14(repoPath, CHARTER_FILE))) {
7044
7109
  present.push(`\`${CHARTER_FILE}\` \u2014 the project charter: what this codebase is for`);
7045
7110
  }
7046
7111
  const adrs = listLiveAdrs(repoPath).map((a) => a.relPath);
@@ -7071,7 +7136,7 @@ function buildDriveNotes(settings) {
7071
7136
  add("stop", settings.driveStopCommand);
7072
7137
  add("dev server", settings.devCommand);
7073
7138
  add("db reset", settings.dbResetCommand);
7074
- const hasDir = existsSync15(join14(settings.repoPath, ".runcastle"));
7139
+ const hasDir = existsSync16(join14(settings.repoPath, ".runcastle"));
7075
7140
  if (configured.length === 0 && !hasDir) {
7076
7141
  return [
7077
7142
  "This project has no test-drive machinery configured, so there is nothing here for you to keep in step \u2014 do not invent any.",
@@ -7207,6 +7272,18 @@ function cacheMountFor(pm, hostPath) {
7207
7272
  const sandboxPath = PM_CACHE_SANDBOX_PATHS[pm];
7208
7273
  return sandboxPath ? { hostPath, sandboxPath } : undefined;
7209
7274
  }
7275
+ function codexAuthMountFor(runtime, sandbox, env = process.env, loggedIn = codexLoggedIn) {
7276
+ if (runtime !== "codex" || sandbox === "noSandbox" || !loggedIn(env))
7277
+ return;
7278
+ return { hostPath: codexHomeDir(env), sandboxPath: CODEX_HOST_MOUNT_PATH, readonly: true };
7279
+ }
7280
+ function buildCodexAuthCopyCommand() {
7281
+ return `mkdir -p "$HOME/.codex" && cp "${CODEX_HOST_MOUNT_PATH}/auth.json" "$HOME/.codex/auth.json"`;
7282
+ }
7283
+ function chainSetupCommands(...steps) {
7284
+ const present = steps.filter((step) => !!step);
7285
+ return present.length > 0 ? present.join(" && ") : undefined;
7286
+ }
7210
7287
  function resolveBurnWorkspaceMode(config2, platform = process.platform) {
7211
7288
  if (config2.sandbox === "noSandbox")
7212
7289
  return "mounted";
@@ -7269,7 +7346,7 @@ function attachedNoteIds(context) {
7269
7346
  return ids2;
7270
7347
  }
7271
7348
  function attachmentSources(context) {
7272
- return attachedNoteIds(context).map(annotationPath).filter((p) => existsSync15(p));
7349
+ return attachedNoteIds(context).map(annotationPath).filter((p) => existsSync16(p));
7273
7350
  }
7274
7351
  function buildAttachmentCopyCommands(sources, platform = process.platform) {
7275
7352
  if (sources.length === 0)
@@ -7312,7 +7389,7 @@ function buildVerifyNotes(config2) {
7312
7389
  }
7313
7390
  function readRepoToolchain(repoPath) {
7314
7391
  const pkgPath = join14(repoPath, "package.json");
7315
- const hasPackageJson = existsSync15(pkgPath);
7392
+ const hasPackageJson = existsSync16(pkgPath);
7316
7393
  let packageManagerField;
7317
7394
  if (hasPackageJson) {
7318
7395
  try {
@@ -7325,10 +7402,10 @@ function readRepoToolchain(repoPath) {
7325
7402
  hasPackageJson,
7326
7403
  ...packageManagerField !== undefined ? { packageManagerField } : {},
7327
7404
  lockfiles: {
7328
- bun: existsSync15(join14(repoPath, "bun.lock")) || existsSync15(join14(repoPath, "bun.lockb")),
7329
- pnpm: existsSync15(join14(repoPath, "pnpm-lock.yaml")),
7330
- yarn: existsSync15(join14(repoPath, "yarn.lock")),
7331
- npm: existsSync15(join14(repoPath, "package-lock.json"))
7405
+ bun: existsSync16(join14(repoPath, "bun.lock")) || existsSync16(join14(repoPath, "bun.lockb")),
7406
+ pnpm: existsSync16(join14(repoPath, "pnpm-lock.yaml")),
7407
+ yarn: existsSync16(join14(repoPath, "yarn.lock")),
7408
+ npm: existsSync16(join14(repoPath, "package-lock.json"))
7332
7409
  }
7333
7410
  };
7334
7411
  }
@@ -7704,6 +7781,19 @@ ${e.digest.trim()}`).join(`
7704
7781
 
7705
7782
  `);
7706
7783
  }
7784
+ function gateTicketAuth(deps) {
7785
+ const authMissing = deps.ticketAuthMissing;
7786
+ if (!authMissing || deps.config.sandbox === "noSandbox")
7787
+ return deps.executeTicketRun;
7788
+ return async (ctx, ticket, run3) => {
7789
+ const runtime = authMissing(ticket);
7790
+ if (!runtime)
7791
+ return deps.executeTicketRun(ctx, ticket, run3);
7792
+ const hint = `${runtime} is not authenticated \u2014 ${RUNTIME_AUTH_SETUP_HINT[runtime]}`;
7793
+ ctx.emitEvent({ type: AUTH_MISSING_EVENT, message: hint, ticketId: ticket.id });
7794
+ return { status: "failed", error: hint };
7795
+ };
7796
+ }
7707
7797
  async function burnRun(ctx, deps) {
7708
7798
  const tickets2 = ctx.tickets;
7709
7799
  const burnable = tickets2.filter((t) => t.status !== "cancelled");
@@ -7723,7 +7813,7 @@ async function burnRun(ctx, deps) {
7723
7813
  });
7724
7814
  return { status: "failed", summary: `dependency cycle: ${path}` };
7725
7815
  }
7726
- const { done, digests } = await burnTickets(ctx, tickets2, deps.executeTicketRun, deps.concurrency);
7816
+ const { done, digests } = await burnTickets(ctx, tickets2, gateTicketAuth(deps), deps.concurrency);
7727
7817
  const summary = cancelled > 0 ? `${done}/${total} tickets done (${cancelled} cancelled)` : `${done}/${total} tickets done`;
7728
7818
  ctx.emitEvent({ type: "burn.summary", message: summary, data: { done, total, cancelled } });
7729
7819
  const digest = composeRunDigest(digests);
@@ -7760,11 +7850,11 @@ function readDocsDigest(projectId, slug) {
7760
7850
  ...missing ? { missing } : {}
7761
7851
  });
7762
7852
  const worktree = worktreeDir(projectId, slug);
7763
- if (!existsSync15(worktree)) {
7853
+ if (!existsSync16(worktree)) {
7764
7854
  return done("_No talk worktree on disk \u2014 docs digest skipped._", "no-worktree");
7765
7855
  }
7766
7856
  const docsDir = join14(worktree, ...docsRel.split("/"));
7767
- if (!existsSync15(docsDir)) {
7857
+ if (!existsSync16(docsDir)) {
7768
7858
  return done("_No docs/features dir in the talk worktree \u2014 docs digest skipped._", "no-docs-dir");
7769
7859
  }
7770
7860
  const names = listFeatureDocs(docsDir).sort((a, b) => agentDigestDocOrder(a) - agentDigestDocOrder(b) || a.localeCompare(b));
@@ -7827,7 +7917,7 @@ function readAgentFile(dirs, file) {
7827
7917
  if (!dir)
7828
7918
  continue;
7829
7919
  const p = join14(dir, file);
7830
- if (existsSync15(p)) {
7920
+ if (existsSync16(p)) {
7831
7921
  try {
7832
7922
  return readFileSync9(p, "utf8");
7833
7923
  } catch {}
@@ -7962,8 +8052,11 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
7962
8052
  mounts.push(mount);
7963
8053
  }
7964
8054
  }
8055
+ const codexAuthMount = codexAuthMountFor(model.runtime, config2.sandbox);
8056
+ if (codexAuthMount)
8057
+ mounts.push(codexAuthMount);
7965
8058
  const guardInstall = guardInstalled ? buildGuardInstallCommand(model.runtime) : undefined;
7966
- const withGuard = (setup) => guardInstall === undefined ? setup : setup ? `${guardInstall} && ${setup}` : guardInstall;
8059
+ const withPrelude = (setup) => chainSetupCommands(codexAuthMount ? buildCodexAuthCopyCommand() : undefined, guardInstall, setup);
7967
8060
  const agentOptions = guardInstalled ? { bypassHookTrust: true } : {};
7968
8061
  mkdirSync9(logsDir(), { recursive: true });
7969
8062
  const logFilePath = join14(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
@@ -8023,7 +8116,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
8023
8116
  OTHER_SIDE: buildOtherSideBlock(otherSide),
8024
8117
  MERGE_COMMAND: resolveMergeCommand(workspaceMode, feature.branch)
8025
8118
  });
8026
- const hookCommand = withGuard(workspaceMode === "isolated" ? buildIsolatedSetupCommand(resolveBranch, setupCommand, pm) : setupCommand);
8119
+ const hookCommand = withPrelude(workspaceMode === "isolated" ? buildIsolatedSetupCommand(resolveBranch, setupCommand, pm) : setupCommand);
8027
8120
  appendTranscript(ticket.id, {
8028
8121
  kind: "text",
8029
8122
  text: `
@@ -8158,7 +8251,7 @@ ${blocked.trim()}`
8158
8251
  let tempBranch = "";
8159
8252
  for (let attempt = 1;; attempt++) {
8160
8253
  tempBranch = ticketBranchName(feature.slug, ticket.seq, newId("b").slice(2, 10));
8161
- const hookCommand = withGuard(workspaceMode === "isolated" ? buildIsolatedSetupCommand(tempBranch, setupCommand, pm) : setupCommand);
8254
+ const hookCommand = withPrelude(workspaceMode === "isolated" ? buildIsolatedSetupCommand(tempBranch, setupCommand, pm) : setupCommand);
8162
8255
  if (hookCommand && attempt === 1) {
8163
8256
  ctx.emitEvent({
8164
8257
  type: "burn.setup",
@@ -8337,14 +8430,24 @@ function resolveBurnDeps(ctx) {
8337
8430
  dbResetCommand: ctx.project.dbResetCommand
8338
8431
  })
8339
8432
  };
8433
+ const ticketCredentials = (ticket) => {
8434
+ const ticketModel = resolveTicketModel(config2, ctx.project, ctx.modelOverride, ticket);
8435
+ return {
8436
+ model: ticketModel,
8437
+ token: ticketModel.runtime === model.runtime ? token : readTokenFromEnvFile(envPath(), ticketModel.runtime)
8438
+ };
8439
+ };
8340
8440
  return {
8341
8441
  config: config2,
8342
8442
  runtime: model.runtime,
8343
- hasAuthToken: token !== undefined,
8443
+ hasAuthToken: burnAuthReady(model.runtime, token),
8444
+ ticketAuthMissing: (ticket) => {
8445
+ const { model: ticketModel, token: ticketToken } = ticketCredentials(ticket);
8446
+ return burnAuthReady(ticketModel.runtime, ticketToken) ? undefined : ticketModel.runtime;
8447
+ },
8344
8448
  concurrency: config2.burnConcurrency,
8345
8449
  executeTicketRun: (c, ticket, run3) => {
8346
- const ticketModel = resolveTicketModel(config2, ctx.project, ctx.modelOverride, ticket);
8347
- const ticketToken = ticketModel.runtime === model.runtime ? token : readTokenFromEnvFile(envPath(), ticketModel.runtime);
8450
+ const { model: ticketModel, token: ticketToken } = ticketCredentials(ticket);
8348
8451
  return isReviewTicket(ticket) ? executeReviewTicket(c, ticket, {
8349
8452
  config: config2,
8350
8453
  token: ticketToken,
@@ -8358,7 +8461,7 @@ function resolveBurnDeps(ctx) {
8358
8461
  function readTokenFromEnvFile(path, runtime) {
8359
8462
  const key = RUNTIME_AUTH_KEY[runtime];
8360
8463
  let fromFile;
8361
- if (existsSync15(path)) {
8464
+ if (existsSync16(path)) {
8362
8465
  try {
8363
8466
  fromFile = parseEnvFile2(readFileSync9(path, "utf8"))[key];
8364
8467
  } catch {
@@ -8368,12 +8471,13 @@ function readTokenFromEnvFile(path, runtime) {
8368
8471
  const token = fromFile && fromFile.length > 0 ? fromFile : process.env[key];
8369
8472
  return token && token.length > 0 ? token : undefined;
8370
8473
  }
8371
- var AUTH_MISSING_EVENT = "auth.missing", DROPPED_MAP_SECTIONS, AGENT_CONVENTIONS_FILE = "CLAUDE.md", PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, SANDBOX_WORKSPACE_PATH = "/home/agent/workspace", ISOLATED_REPO_PATH = "/home/agent/repo", RUN, BASH_PATTERNS, TOOL_NAME_CATEGORY, MAX_ATTRIBUTABLE_GAP_MS, BURN_WORKTREE_PATH, WORKTREE_REMOVAL_FAILURES, FATAL_ERROR_PATTERNS, RUNTIME_FATAL_ERROR_PATTERNS, RETRYABLE_ERROR_PATTERNS, RUNTIME_RETRYABLE_ERROR_PATTERNS, activeTicketAborts, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
8474
+ var AUTH_MISSING_EVENT = "auth.missing", DROPPED_MAP_SECTIONS, AGENT_CONVENTIONS_FILE = "CLAUDE.md", PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, CODEX_HOST_MOUNT_PATH = "/mnt/host-codex", SANDBOX_WORKSPACE_PATH = "/home/agent/workspace", ISOLATED_REPO_PATH = "/home/agent/repo", RUN, BASH_PATTERNS, TOOL_NAME_CATEGORY, MAX_ATTRIBUTABLE_GAP_MS, BURN_WORKTREE_PATH, WORKTREE_REMOVAL_FAILURES, FATAL_ERROR_PATTERNS, RUNTIME_FATAL_ERROR_PATTERNS, RETRYABLE_ERROR_PATTERNS, RUNTIME_RETRYABLE_ERROR_PATTERNS, activeTicketAborts, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
8372
8475
  var init_ticket_burner = __esm(() => {
8373
8476
  init_src();
8374
8477
  init_config_load();
8375
8478
  init_paths();
8376
8479
  init_skills_root();
8480
+ init_codex_auth();
8377
8481
  init_knowledge();
8378
8482
  init_setup();
8379
8483
  init_agent_stream();
@@ -8432,7 +8536,10 @@ var init_ticket_burner = __esm(() => {
8432
8536
  /\b401\b|\b403\b/,
8433
8537
  /insufficient_quota|exceeded your current quota/i,
8434
8538
  /model_not_found|does not exist or you do not have access/i,
8435
- /CODEX_API_KEY/
8539
+ /CODEX_API_KEY/,
8540
+ /not logged in/i,
8541
+ /\bauth (failed|required)\b/i,
8542
+ /refresh token/i
8436
8543
  ]
8437
8544
  };
8438
8545
  RETRYABLE_ERROR_PATTERNS = [
@@ -8545,14 +8652,16 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
8545
8652
  mkdirSync10(logsDir(), { recursive: true });
8546
8653
  const logFilePath = join15(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
8547
8654
  const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
8655
+ const codexAuthMount = codexAuthMountFor(model.runtime, config2.sandbox);
8548
8656
  const runOptions = {
8549
8657
  agent: buildBurnAgent(config2, token, model),
8550
- sandbox: selectSandbox(config2),
8658
+ sandbox: selectSandbox(config2, codexAuthMount ? [codexAuthMount] : []),
8551
8659
  cwd: project.repoPath,
8552
8660
  prompt,
8553
8661
  branchStrategy: { type: "branch", branch: tempBranch, baseBranch: feature.branch },
8554
8662
  signal: ctx.signal,
8555
8663
  name: `research-${waypoint.seq}`,
8664
+ ...codexAuthMount ? { hooks: { sandbox: { onSandboxReady: [{ command: buildCodexAuthCopyCommand() }] } } } : {},
8556
8665
  logging: { type: "file", path: logFilePath, onAgentStreamEvent: throttle.onEvent }
8557
8666
  };
8558
8667
  let result;
@@ -8600,7 +8709,7 @@ function resolveResearchDeps(ctx) {
8600
8709
  return {
8601
8710
  config: config2,
8602
8711
  runtime: model.runtime,
8603
- hasAuthToken: token !== undefined,
8712
+ hasAuthToken: burnAuthReady(model.runtime, token),
8604
8713
  executeResearchRun: (c, waypoint) => realExecuteResearchRun(c, waypoint, config2, token, model)
8605
8714
  };
8606
8715
  }
@@ -8786,8 +8895,7 @@ async function createFeature(ctx, input) {
8786
8895
  const project = requireProjectById(ctx, input.projectId);
8787
8896
  const slug = uniqueSlug(ctx, project.id, input.title);
8788
8897
  const branch = `feature/${slug}`;
8789
- const requestedBase = input.baseBranch?.trim() || project.mainBranch;
8790
- const cut = input.draft ? null : await ensureFeatureBranch(project, slug, requestedBase);
8898
+ const cut = input.draft ? null : await ensureFeatureBranch(project, slug, await requestedBase(project, input.baseBranch));
8791
8899
  const row = {
8792
8900
  id: newId("feat"),
8793
8901
  projectId: project.id,
@@ -8837,8 +8945,8 @@ async function startDraft(ctx, featureId, opts = {}) {
8837
8945
  throw new GateError(`feature ${feature.slug} is not a draft \u2014 it has already been started`);
8838
8946
  }
8839
8947
  const project = projectForFeature(ctx, feature);
8840
- const requestedBase = opts.baseBranch?.trim() || project.mainBranch;
8841
- const { branchReady, baseBranch } = await ensureFeatureBranch(project, feature.slug, requestedBase);
8948
+ const base = await requestedBase(project, opts.baseBranch);
8949
+ const { branchReady, baseBranch } = await ensureFeatureBranch(project, feature.slug, base);
8842
8950
  ctx.db.update(features).set({ status: "active", baseBranch }).where(eq10(features.id, featureId)).run();
8843
8951
  const started = { ...feature, status: "active", baseBranch };
8844
8952
  emit(ctx, featureId, {
@@ -8898,8 +9006,8 @@ async function quickChange(ctx, input) {
8898
9006
  throw new InvalidInputError("a quick change needs a sentence describing the change");
8899
9007
  const slug = uniqueSlug(ctx, project.id, title);
8900
9008
  const branch = `feature/${slug}`;
8901
- const requestedBase = input.baseBranch?.trim() || project.mainBranch;
8902
- const { branchReady, baseBranch } = await ensureFeatureBranch(project, slug, requestedBase);
9009
+ const base = await requestedBase(project, input.baseBranch);
9010
+ const { branchReady, baseBranch } = await ensureFeatureBranch(project, slug, base);
8903
9011
  const inserted = ctx.db.insert(features).values({
8904
9012
  id: newId("feat"),
8905
9013
  projectId: project.id,
@@ -8948,14 +9056,17 @@ async function quickChange(ctx, input) {
8948
9056
  }
8949
9057
  return feature;
8950
9058
  }
8951
- async function ensureFeatureBranch(project, slug, requestedBase) {
9059
+ async function requestedBase(project, picked) {
9060
+ return picked?.trim() || await currentCheckoutBranch(project);
9061
+ }
9062
+ async function ensureFeatureBranch(project, slug, base) {
8952
9063
  try {
8953
- const baseBranch = await resolveBaseBranch(project, requestedBase);
9064
+ const baseBranch = await resolveBaseBranch(project, base);
8954
9065
  await createFeatureBranch(project, slug, baseBranch);
8955
9066
  return { branchReady: true, baseBranch };
8956
9067
  } catch (e) {
8957
9068
  if (isNotImplemented(e))
8958
- return { branchReady: false, baseBranch: requestedBase };
9069
+ return { branchReady: false, baseBranch: base };
8959
9070
  throw e;
8960
9071
  }
8961
9072
  }
@@ -9298,7 +9409,7 @@ var init_features = __esm(() => {
9298
9409
  });
9299
9410
 
9300
9411
  // src/services/test-notes.ts
9301
- import { existsSync as existsSync17, mkdirSync as mkdirSync11, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
9412
+ import { existsSync as existsSync18, mkdirSync as mkdirSync11, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
9302
9413
  import { dirname as dirname8 } from "path";
9303
9414
  import { asc as asc4, eq as eq11 } from "drizzle-orm";
9304
9415
  function rowToNote(row) {
@@ -9311,7 +9422,7 @@ function rowToNote(row) {
9311
9422
  author: row.author,
9312
9423
  ticketId: row.ticketId ?? undefined,
9313
9424
  videoTimestamp: row.videoTimestamp ?? undefined,
9314
- screenshotUrl: existsSync17(annotationPath(row.id)) ? noteScreenshotUrl(row.id) : undefined,
9425
+ screenshotUrl: existsSync18(annotationPath(row.id)) ? noteScreenshotUrl(row.id) : undefined,
9315
9426
  createdAt: row.createdAt,
9316
9427
  updatedAt: row.updatedAt
9317
9428
  });
@@ -9519,7 +9630,7 @@ var init_test_notes = __esm(() => {
9519
9630
  });
9520
9631
 
9521
9632
  // src/mcp/server.ts
9522
- import { existsSync as existsSync18, readFileSync as readFileSync11 } from "fs";
9633
+ import { existsSync as existsSync19, readFileSync as readFileSync11 } from "fs";
9523
9634
  import { join as join16, relative as relative3, resolve as resolve9, sep as sep2 } from "path";
9524
9635
  import { StreamableHTTPTransport } from "@hono/mcp";
9525
9636
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -9873,7 +9984,7 @@ async function toolCreateFeature(ctx, session, input) {
9873
9984
  phase: feature.phase
9874
9985
  };
9875
9986
  }
9876
- function toolGetProjectContext(ctx, session) {
9987
+ async function toolGetProjectContext(ctx, session) {
9877
9988
  const project = requireProject(ctx, session);
9878
9989
  const charter = readCharter(session.worktreePath);
9879
9990
  return {
@@ -9881,7 +9992,18 @@ function toolGetProjectContext(ctx, session) {
9881
9992
  ...charter !== undefined ? { charter } : {},
9882
9993
  adrs: listLiveAdrs(session.worktreePath).map(adrRef),
9883
9994
  adrsNote: ADRS_NOTE,
9884
- featureIndex: list(ctx, project.id).map(featureIndexLine)
9995
+ featureIndex: list(ctx, project.id).map(featureIndexLine),
9996
+ baseBranches: await readBaseBranches(project)
9997
+ };
9998
+ }
9999
+ async function readBaseBranches(project) {
10000
+ const { current: current2, branches, remoteBranches } = await listBranches(project);
10001
+ const selectable = [...branches, ...remoteBranches];
10002
+ return {
10003
+ current: current2,
10004
+ currentIsSelectable: selectable.includes(current2),
10005
+ selectable,
10006
+ detectedMain: await detectMainBranch(project.repoPath)
9885
10007
  };
9886
10008
  }
9887
10009
  function adrRef(adr) {
@@ -9903,7 +10025,7 @@ function toolReadAdr(ctx, session, input) {
9903
10025
  if (rel === "" || rel.startsWith("..") || rel.split(sep2).includes("..")) {
9904
10026
  throw new InvalidInputError(`adr path escapes ${ADR_DIR_REL}: ${input.relPath}`);
9905
10027
  }
9906
- if (!existsSync18(target))
10028
+ if (!existsSync19(target))
9907
10029
  throw new NotFoundError(`adr not found: ${input.relPath}`);
9908
10030
  return { relPath: `${ADR_DIR_REL}/${name}`, content: readFileSync11(target, "utf8") };
9909
10031
  }
@@ -10100,7 +10222,7 @@ function buildMcpServer(audience) {
10100
10222
  inputSchema: {
10101
10223
  title: z4.string().min(1).describe("The feature\u2019s name; its slug and branch derive from this."),
10102
10224
  oneLiner: z4.string().optional().describe("One line saying what this feature is. Required for every shape EXCEPT `tickets`, " + "where the first ticket\u2019s prose becomes the one-liner and anything sent here is " + "ignored."),
10103
- baseBranch: z4.string().optional().describe("Branch to cut from; defaults to the project\u2019s main branch."),
10225
+ baseBranch: z4.string().optional().describe("Branch to cut from \u2014 always pass it explicitly, naming the base you told the " + "human you would use (see `get_project_context`\u2019s `baseBranches`). Omitted, it " + "falls back to whatever the project checkout happens to be on. Drafts pass none: " + "a parked feature picks its base at Start."),
10104
10226
  brief: z4.string().optional().describe("The reasoning you just worked out with the human \u2014 why this feature exists and " + "what it must NOT swallow. Becomes brief.md verbatim, and without it that " + "reasoning is lost. Unused by the `tickets` shape, whose brief IS the proses."),
10105
10227
  draft: z4.boolean().optional().describe("PARK it instead of starting it: a row and its brief, no branch and no files, " + "until the human clicks Start. Ask them per feature whether to start or park."),
10106
10228
  tickets: z4.array(z4.string().min(1)).min(1).optional().describe("The quick-change shape: work too small to deserve a grill. Creates ONE feature at " + "the implementation phase carrying a ticket per prose (plus the review ticket " + "every batch closes with). Send every sentence of one quick change in a SINGLE " + "call \u2014 calling this once per ticket makes one feature per ticket.")
@@ -10115,13 +10237,13 @@ function buildMcpServer(audience) {
10115
10237
  if (wants("get_project_context")) {
10116
10238
  server.registerTool("get_project_context", {
10117
10239
  title: "Get project context",
10118
- description: "The project as it stands: the project row, the charter (CONTEXT.md) in full, an INDEX " + "of every live ADR (superseded ones omitted) and a one-line index of every feature. " + "ADR bodies are not inlined \u2014 read the ones your work touches with `read_adr`. Shipped " + "features carry their one-liner and docs path (readable on disk); in-flight ones carry " + "slug, phase, lap and ticket counts but no docs path, because their docs live on an " + "unmerged branch. Use the slug with `get_work_record` to see what one actually did.",
10240
+ description: "The project as it stands: the project row, the charter (CONTEXT.md) in full, an INDEX " + "of every live ADR (superseded ones omitted) and a one-line index of every feature. " + "ADR bodies are not inlined \u2014 read the ones your work touches with `read_adr`. Shipped " + "features carry their one-liner and docs path (readable on disk); in-flight ones carry " + "slug, phase, lap and ticket counts but no docs path, because their docs live on an " + "unmerged branch. Use the slug with `get_work_record` to see what one actually did. " + "Plus `baseBranches` \u2014 the branch a new feature would cut from: the checkout\u2019s `current` " + "branch, `currentIsSelectable` (false mid test drive or on a detached HEAD, the one case " + "with no default), every `selectable` base, and the `detectedMain` line to suggest then.",
10119
10241
  inputSchema: {}
10120
10242
  }, async (_args, extra) => {
10121
10243
  const rs = await resolveCtxSession(extra);
10122
10244
  if (!rs)
10123
10245
  return noSession();
10124
- return ok(toolGetProjectContext(rs.ctx, rs.session));
10246
+ return ok(await toolGetProjectContext(rs.ctx, rs.session));
10125
10247
  });
10126
10248
  }
10127
10249
  if (wants("read_adr")) {
@@ -10943,7 +11065,7 @@ var init_stream = __esm(() => {
10943
11065
  });
10944
11066
 
10945
11067
  // src/routes/web.ts
10946
- import { existsSync as existsSync19, statSync as statSync4 } from "fs";
11068
+ import { existsSync as existsSync20, statSync as statSync4 } from "fs";
10947
11069
  import { readFile } from "fs/promises";
10948
11070
  import { extname, join as join17, normalize, resolve as resolve10, sep as sep3 } from "path";
10949
11071
  function contentType(path) {
@@ -10996,7 +11118,7 @@ function mountWebApp(app, distDir) {
10996
11118
  }
10997
11119
  function mountWebAppIfBuilt(app) {
10998
11120
  const dist = resolveWebDist();
10999
- if (existsSync19(join17(dist, "index.html")))
11121
+ if (existsSync20(join17(dist, "index.html")))
11000
11122
  mountWebApp(app, dist);
11001
11123
  }
11002
11124
  var MIME;
@@ -11047,12 +11169,12 @@ var init_version = () => {};
11047
11169
 
11048
11170
  // src/services/install-id.ts
11049
11171
  import { randomUUID } from "crypto";
11050
- import { existsSync as existsSync20, mkdirSync as mkdirSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
11172
+ import { existsSync as existsSync21, mkdirSync as mkdirSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
11051
11173
  import { dirname as dirname10 } from "path";
11052
11174
  function getInstallId() {
11053
11175
  const file = installIdPath();
11054
11176
  try {
11055
- if (existsSync20(file)) {
11177
+ if (existsSync21(file)) {
11056
11178
  const existing = readFileSync13(file, "utf8").trim();
11057
11179
  if (UUID_RE.test(existing))
11058
11180
  return existing;
@@ -11252,7 +11374,7 @@ var init_events2 = __esm(() => {
11252
11374
  });
11253
11375
 
11254
11376
  // src/launcher/launcher.ts
11255
- import { existsSync as existsSync21 } from "fs";
11377
+ import { existsSync as existsSync22 } from "fs";
11256
11378
  import { join as join19 } from "path";
11257
11379
  import { and as and9, eq as eq13 } from "drizzle-orm";
11258
11380
  function ptyExitMessage(exitCode) {
@@ -11638,7 +11760,7 @@ async function launchProjectSession(ctx, input, opts = {}) {
11638
11760
  const model = resolveModelEntry("project", ctx.config, project);
11639
11761
  const runtime = runtimeAdapterFor(model.runtime);
11640
11762
  assertRuntimeReady(runtime, opts);
11641
- const worktreePath = await ensureProjectWorktree(project, (res) => reportProjectLanding(ctx, project, res, { retried: true }));
11763
+ const { worktreePath, base } = await ensureProjectWorktree(project, (res) => reportProjectLanding(ctx, project, res, { retried: true }));
11642
11764
  const session = createSessionRow(ctx, {
11643
11765
  projectId: project.id,
11644
11766
  kind: "project",
@@ -11691,7 +11813,7 @@ async function launchProjectSession(ctx, input, opts = {}) {
11691
11813
  session,
11692
11814
  project,
11693
11815
  config: ctx.config,
11694
- projectBrief: { project, branch: PROJECT_BRANCH, worktreePath },
11816
+ projectBrief: { project, branch: PROJECT_BRANCH, worktreePath, base },
11695
11817
  worktreePath,
11696
11818
  serverUrl: serverUrlFor(ctx.config),
11697
11819
  model: model.id,
@@ -11725,7 +11847,7 @@ async function buildPrepareBrief(ctx, project) {
11725
11847
  };
11726
11848
  }
11727
11849
  function probePrepareHost(repoPath) {
11728
- const has = (rel) => existsSync21(join19(repoPath, rel));
11850
+ const has = (rel) => existsSync22(join19(repoPath, rel));
11729
11851
  const lock = LOCKFILES.find(([file]) => has(file));
11730
11852
  return {
11731
11853
  platform: process.platform,
@@ -12075,10 +12197,10 @@ var init_feature = __esm(() => {
12075
12197
  });
12076
12198
 
12077
12199
  // src/services/fsbrowse.ts
12078
- import { existsSync as existsSync22, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
12079
- import { homedir as homedir4 } from "os";
12200
+ import { existsSync as existsSync23, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
12201
+ import { homedir as homedir3 } from "os";
12080
12202
  import { basename as basename3, dirname as dirname11, isAbsolute as isAbsolute2, join as join21, resolve as resolve11, sep as sep4 } from "path";
12081
- function expandPath(input, home = homedir4()) {
12203
+ function expandPath(input, home = homedir3()) {
12082
12204
  const trimmed = input.trim();
12083
12205
  if (trimmed === "")
12084
12206
  return trimmed;
@@ -12091,31 +12213,31 @@ function expandPath(input, home = homedir4()) {
12091
12213
  }
12092
12214
  function looksLikeRepo(dir) {
12093
12215
  try {
12094
- return existsSync22(join21(dir, ".git"));
12216
+ return existsSync23(join21(dir, ".git"));
12095
12217
  } catch {
12096
12218
  return false;
12097
12219
  }
12098
12220
  }
12099
- function listRoots(home = homedir4()) {
12221
+ function listRoots(home = homedir3()) {
12100
12222
  const roots = [];
12101
12223
  if (process.platform === "win32") {
12102
12224
  for (let code = 67;code <= 90; code++) {
12103
12225
  const drive = `${String.fromCharCode(code)}:${sep4}`;
12104
12226
  try {
12105
- if (existsSync22(drive))
12227
+ if (existsSync23(drive))
12106
12228
  roots.push({ label: drive.replace(/\\$/, ""), path: drive, kind: "drive" });
12107
12229
  } catch {}
12108
12230
  }
12109
12231
  } else {
12110
12232
  roots.push({ label: "/", path: "/", kind: "drive" });
12111
12233
  }
12112
- if (existsSync22(home))
12234
+ if (existsSync23(home))
12113
12235
  roots.push({ label: "Home", path: home, kind: "home" });
12114
12236
  const seen = new Set(roots.map(pathKey));
12115
12237
  for (const name of COMMON_DIRS) {
12116
12238
  const candidate = join21(home, name);
12117
12239
  const key = pathKey({ path: candidate });
12118
- if (seen.has(key) || !existsSync22(candidate))
12240
+ if (seen.has(key) || !existsSync23(candidate))
12119
12241
  continue;
12120
12242
  seen.add(key);
12121
12243
  roots.push({ label: name, path: candidate, kind: "common" });
@@ -12141,12 +12263,12 @@ function parentOf(dir) {
12141
12263
  const parent = dirname11(dir);
12142
12264
  return parent === dir ? null : parent;
12143
12265
  }
12144
- function browseDir(input, showHidden = false, home = homedir4()) {
12266
+ function browseDir(input, showHidden = false, home = homedir3()) {
12145
12267
  const target = input && input.trim() !== "" ? expandPath(input, home) : home;
12146
12268
  if (!isAbsolute2(target)) {
12147
12269
  throw new InvalidInputError(`path is not absolute: ${target}`);
12148
12270
  }
12149
- if (!existsSync22(target)) {
12271
+ if (!existsSync23(target)) {
12150
12272
  throw new InvalidInputError(`path does not exist: ${target}`);
12151
12273
  }
12152
12274
  let dir = target;
@@ -12371,7 +12493,7 @@ var init_conversations = __esm(() => {
12371
12493
  });
12372
12494
 
12373
12495
  // src/services/projects.ts
12374
- import { existsSync as existsSync23 } from "fs";
12496
+ import { existsSync as existsSync24 } from "fs";
12375
12497
  import { basename as basename4, join as join22 } from "path";
12376
12498
  import { eq as eq14, isNull } from "drizzle-orm";
12377
12499
  function listProjects(ctx) {
@@ -12391,15 +12513,14 @@ function warnIfTranslatedMount(ctx, projectId, repoPath) {
12391
12513
  async function openProject(ctx, rawPath) {
12392
12514
  const repoPath = expandPath(rawPath);
12393
12515
  await assertRepoTolerant(ctx, repoPath);
12394
- const mainBranch = await detectMainBranchTolerant(ctx, repoPath);
12395
12516
  const name = basename4(repoPath) || repoPath;
12396
12517
  const existing = getProjectByRepoPath(ctx, repoPath) ?? findProjectByCanonPath(ctx, repoPath);
12397
12518
  if (existing) {
12398
- ctx.db.update(projects).set({ mainBranch, repoPath, closedAt: null }).where(eq14(projects.id, existing.id)).run();
12399
- const reopened = { ...existing, mainBranch, repoPath, closedAt: undefined };
12519
+ ctx.db.update(projects).set({ repoPath, closedAt: null }).where(eq14(projects.id, existing.id)).run();
12520
+ const reopened = { ...existing, repoPath, closedAt: undefined };
12400
12521
  emitProject(ctx, existing.id, {
12401
12522
  type: "project.opened",
12402
- message: `project ${existing.name} re-opened at ${repoPath} (${mainBranch})`
12523
+ message: `project ${existing.name} re-opened at ${repoPath}`
12403
12524
  });
12404
12525
  warnIfTranslatedMount(ctx, existing.id, repoPath);
12405
12526
  return reopened;
@@ -12408,7 +12529,6 @@ async function openProject(ctx, rawPath) {
12408
12529
  id: newId("proj"),
12409
12530
  name,
12410
12531
  repoPath,
12411
- mainBranch,
12412
12532
  devCommand: null,
12413
12533
  closedAt: null
12414
12534
  };
@@ -12416,7 +12536,7 @@ async function openProject(ctx, rawPath) {
12416
12536
  const project = rowToProject(inserted);
12417
12537
  emitProject(ctx, project.id, {
12418
12538
  type: "project.opened",
12419
- message: `project ${name} at ${repoPath} (${mainBranch})`
12539
+ message: `project ${name} at ${repoPath}`
12420
12540
  });
12421
12541
  warnIfTranslatedMount(ctx, project.id, repoPath);
12422
12542
  return project;
@@ -12453,20 +12573,11 @@ async function assertRepoTolerant(ctx, repoPath) {
12453
12573
  } catch (e) {
12454
12574
  if (!isNotImplemented(e))
12455
12575
  throw e;
12456
- if (!existsSync23(join22(repoPath, ".git"))) {
12576
+ if (!existsSync24(join22(repoPath, ".git"))) {
12457
12577
  throw new InvalidInputError(`not a git repository: ${repoPath}`);
12458
12578
  }
12459
12579
  }
12460
12580
  }
12461
- async function detectMainBranchTolerant(ctx, repoPath) {
12462
- try {
12463
- return await detectMainBranch(repoPath);
12464
- } catch (e) {
12465
- if (!isNotImplemented(e))
12466
- throw e;
12467
- return ctx.config.mainBranch;
12468
- }
12469
- }
12470
12581
  var init_projects = __esm(() => {
12471
12582
  init_src();
12472
12583
  init_schema();
@@ -12496,6 +12607,7 @@ var init_project = __esm(() => {
12496
12607
  roots: publicProcedure.query(() => listRoots()),
12497
12608
  browse: publicProcedure.input(z8.object({ dir: z8.string().optional(), showHidden: z8.boolean().optional() }).optional()).query(({ input }) => browseDir(input?.dir, input?.showHidden ?? false)),
12498
12609
  branches: publicProcedure.input(z8.object({ projectId: z8.string() })).query(({ ctx, input }) => listBranches(requireProjectById(ctx, input.projectId))),
12610
+ sessionBranch: publicProcedure.input(z8.object({ projectId: z8.string() })).query(({ ctx, input }) => sessionBranchView(requireProjectById(ctx, input.projectId))),
12499
12611
  open: publicProcedure.input(z8.object({ repoPath: z8.string().min(1) })).mutation(({ ctx, input }) => openProject(ctx, input.repoPath)),
12500
12612
  close: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ ctx, input }) => closeProject(ctx, input.projectId)),
12501
12613
  rename: publicProcedure.input(z8.object({ projectId: z8.string(), name: ProjectName })).mutation(({ ctx, input }) => renameProject(ctx, input.projectId, input.name)),
@@ -12913,7 +13025,7 @@ __export(exports_cli, {
12913
13025
  resolveDoctorEnv: () => resolveDoctorEnv,
12914
13026
  parseMode: () => parseMode
12915
13027
  });
12916
- import { existsSync as existsSync24, readFileSync as readFileSync15 } from "fs";
13028
+ import { existsSync as existsSync25, readFileSync as readFileSync15 } from "fs";
12917
13029
  function parseMode(argv) {
12918
13030
  return argv.includes("--gate") || argv.includes("--boot") ? "gate" : "diagnostic";
12919
13031
  }
@@ -12921,7 +13033,7 @@ function envWithAfkCredentials() {
12921
13033
  const merged = { ...process.env };
12922
13034
  try {
12923
13035
  const path = envPath();
12924
- if (existsSync24(path)) {
13036
+ if (existsSync25(path)) {
12925
13037
  const fromFile = parseEnvFile2(readFileSync15(path, "utf8"));
12926
13038
  for (const runtime of AGENT_RUNTIMES) {
12927
13039
  const key = RUNTIME_SPECS[runtime].afkKey;