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