@jpcasabiancai/cayenne 0.5.6 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +866 -421
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -31,7 +31,7 @@ var init_cloud = __esm({
31
31
  CLOUD_SUPABASE_ANON_KEY = assertPublishable(
32
32
  "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5kcXJrcXh3a3p4Z25oa3ZjdW5yIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODg3MTMyNzYsImV4cCI6MjEwNDI4OTI3Nn0.Dlsxw91E1QYZ4eoRDjP80Rf4feY77Z-T-fiIwUDY2k0"
33
33
  );
34
- CLOUD_APP_ORIGIN = "https://cayenne-pink.vercel.app";
34
+ CLOUD_APP_ORIGIN = "https://app.cayenne.sh";
35
35
  }
36
36
  });
37
37
 
@@ -1325,7 +1325,7 @@ var init_local = __esm({
1325
1325
 
1326
1326
  // ../../packages/shared/src/protocol/runner.ts
1327
1327
  import { z as z14 } from "zod";
1328
- var ProviderModel, ProviderCapability, UNAVAILABLE, RunnerCapabilities, CODEX_TOOL_NAMES;
1328
+ var ProviderModel, ProviderAuthMode, ProviderCapability, UNAVAILABLE, RunnerCapabilities, CODEX_TOOL_NAMES;
1329
1329
  var init_runner = __esm({
1330
1330
  "../../packages/shared/src/protocol/runner.ts"() {
1331
1331
  "use strict";
@@ -1338,13 +1338,21 @@ var init_runner = __esm({
1338
1338
  /** From the app-server's `supportedReasoningEfforts`; empty means the control is hidden. */
1339
1339
  efforts: z14.array(Effort).default([]),
1340
1340
  /** The effort a new chat on this model opens at, sent back on `session.start`. */
1341
- defaultEffort: Effort.optional()
1341
+ defaultEffort: Effort.optional(),
1342
+ /** One line about the model in the provider's own words (Claude's `ModelInfo.description`). */
1343
+ description: z14.string().min(1).max(200).optional()
1342
1344
  });
1345
+ ProviderAuthMode = z14.enum(["subscription", "api", "third_party"]);
1343
1346
  ProviderCapability = z14.object({
1344
1347
  available: z14.boolean(),
1345
1348
  reason: z14.enum(["not_installed", "not_logged_in", "error"]).optional(),
1346
1349
  version: z14.string().max(50).optional(),
1347
- models: z14.array(ProviderModel).max(50).default([])
1350
+ models: z14.array(ProviderModel).max(50).default([]),
1351
+ /**
1352
+ * How the provider is signed in, when the runner can tell without opening a chat (Claude's
1353
+ * `accountInfo`, protocol §9). Absent means unknown. Optional so an older row still parses.
1354
+ */
1355
+ authMode: ProviderAuthMode.optional()
1348
1356
  });
1349
1357
  UNAVAILABLE = { available: false, reason: "not_installed", models: [] };
1350
1358
  RunnerCapabilities = z14.object({
@@ -2624,13 +2632,200 @@ var init_version = __esm({
2624
2632
  }
2625
2633
  });
2626
2634
 
2635
+ // src/providers/env.ts
2636
+ function definedEnv(env) {
2637
+ const out = {};
2638
+ for (const [key, value] of Object.entries(env)) if (value !== void 0) out[key] = value;
2639
+ return out;
2640
+ }
2641
+ var init_env = __esm({
2642
+ "src/providers/env.ts"() {
2643
+ "use strict";
2644
+ }
2645
+ });
2646
+
2647
+ // src/claude/auth-mode.ts
2648
+ function authModeFrom(info) {
2649
+ if (info.apiProvider && info.apiProvider !== "firstParty") return "third_party";
2650
+ if (info.apiKeySource && info.apiKeySource !== "none") return "api";
2651
+ if (info.subscriptionType) return "subscription";
2652
+ return null;
2653
+ }
2654
+ var init_auth_mode = __esm({
2655
+ "src/claude/auth-mode.ts"() {
2656
+ "use strict";
2657
+ }
2658
+ });
2659
+
2660
+ // src/claude/pushable.ts
2661
+ var Pushable;
2662
+ var init_pushable = __esm({
2663
+ "src/claude/pushable.ts"() {
2664
+ "use strict";
2665
+ Pushable = class {
2666
+ queue = [];
2667
+ waiters = [];
2668
+ ended = false;
2669
+ push(value) {
2670
+ if (this.ended) throw new Error("Pushable already ended");
2671
+ const waiter = this.waiters.shift();
2672
+ if (waiter) waiter({ value, done: false });
2673
+ else this.queue.push(value);
2674
+ }
2675
+ end() {
2676
+ this.ended = true;
2677
+ for (const waiter of this.waiters.splice(0)) waiter({ value: void 0, done: true });
2678
+ }
2679
+ [Symbol.asyncIterator]() {
2680
+ return {
2681
+ next: () => {
2682
+ const value = this.queue.shift();
2683
+ if (value !== void 0) return Promise.resolve({ value, done: false });
2684
+ if (this.ended) return Promise.resolve({ value: void 0, done: true });
2685
+ return new Promise((resolve6) => this.waiters.push(resolve6));
2686
+ }
2687
+ };
2688
+ }
2689
+ };
2690
+ }
2691
+ });
2692
+
2693
+ // src/claude/detect.ts
2694
+ import { readFileSync as readFileSync4 } from "fs";
2695
+ import { createRequire as createRequire2 } from "module";
2696
+ import { dirname as dirname3, join as join8 } from "path";
2697
+ import {
2698
+ query as sdkQuery
2699
+ } from "@anthropic-ai/claude-agent-sdk";
2700
+ function claudeCodeVersion(resolve6 = createRequire2(import.meta.url).resolve) {
2701
+ try {
2702
+ const entry = resolve6("@anthropic-ai/claude-agent-sdk");
2703
+ const pkg = JSON.parse(readFileSync4(join8(dirname3(entry), "package.json"), "utf8"));
2704
+ return typeof pkg.claudeCodeVersion === "string" && pkg.claudeCodeVersion ? pkg.claudeCodeVersion.slice(0, MAX_VERSION) : void 0;
2705
+ } catch {
2706
+ return void 0;
2707
+ }
2708
+ }
2709
+ async function probeClaude(opts) {
2710
+ const queryFn = opts.queryFn ?? sdkQuery;
2711
+ const prompt = new Pushable();
2712
+ const abortController = new AbortController();
2713
+ let timer;
2714
+ let q;
2715
+ try {
2716
+ q = queryFn({
2717
+ prompt,
2718
+ options: {
2719
+ cwd: opts.cwd,
2720
+ settingSources: opts.settingSources,
2721
+ strictMcpConfig: true,
2722
+ mcpServers: {},
2723
+ settings: { disableAllHooks: true },
2724
+ abortController,
2725
+ stderr: () => {
2726
+ },
2727
+ env: definedEnv({ ...process.env, ...opts.env })
2728
+ }
2729
+ });
2730
+ const init = q.initializationResult();
2731
+ init.catch(() => {
2732
+ });
2733
+ const timeoutMs = opts.timeoutMs ?? PROBE_TIMEOUT_MS;
2734
+ const timeout = new Promise((_, reject) => {
2735
+ timer = setTimeout(() => {
2736
+ abortController.abort();
2737
+ reject(new Error(`Claude probe timed out after ${timeoutMs} ms`));
2738
+ }, timeoutMs);
2739
+ });
2740
+ const res = await Promise.race([init, timeout]);
2741
+ return { account: res.account ?? {}, models: res.models ?? [] };
2742
+ } finally {
2743
+ clearTimeout(timer);
2744
+ prompt.end();
2745
+ try {
2746
+ q?.close();
2747
+ } catch {
2748
+ }
2749
+ }
2750
+ }
2751
+ function toProviderModel(m, id, isDefault) {
2752
+ const levels = m.supportedEffortLevels ?? [];
2753
+ const efforts = levels.filter((e) => Effort.options.includes(e));
2754
+ const description = (m.description ?? "").slice(0, MAX_NAME);
2755
+ return {
2756
+ id: id.slice(0, MAX_NAME),
2757
+ // `||`, not `??`: an empty display name would fail `label.min(1)` and take the whole
2758
+ // capability with it.
2759
+ label: (m.displayName || id).slice(0, MAX_NAME),
2760
+ ...isDefault ? { default: true } : {},
2761
+ ...description ? { description } : {},
2762
+ efforts
2763
+ };
2764
+ }
2765
+ function toProviderModels(models) {
2766
+ const fallback = models.find((m) => m.value === "default");
2767
+ const resolved = fallback?.resolvedModel;
2768
+ const rows = models.filter((m) => m.value !== "default");
2769
+ const match = resolved ? rows.findIndex((m) => (m.resolvedModel ?? m.value) === resolved) : -1;
2770
+ const mapped = rows.map((m, i) => toProviderModel(m, m.value ?? "", i === match));
2771
+ if (fallback && resolved && match === -1) {
2772
+ mapped.unshift(toProviderModel(fallback, resolved, true));
2773
+ }
2774
+ const seen = /* @__PURE__ */ new Set();
2775
+ return mapped.filter((m) => !seen.has(m.id) && seen.add(m.id)).slice(0, MAX_MODELS).filter((m) => ProviderModel.safeParse(m).success);
2776
+ }
2777
+ async function detectClaude(deps) {
2778
+ let version;
2779
+ try {
2780
+ version = deps.version();
2781
+ } catch {
2782
+ version = void 0;
2783
+ }
2784
+ const withVersion = version ? { version } : {};
2785
+ let res;
2786
+ try {
2787
+ res = await deps.probe();
2788
+ } catch (err) {
2789
+ const message = err instanceof Error ? err.message : String(err);
2790
+ if (/native binary not found/i.test(message)) {
2791
+ return { available: false, reason: "not_installed", ...withVersion, models: [] };
2792
+ }
2793
+ return { available: true, ...withVersion, models: [] };
2794
+ }
2795
+ const account = res.account ?? {};
2796
+ const authMode = authModeFrom(account);
2797
+ if (authMode === null && account.tokenSource === "none") {
2798
+ return { available: false, reason: "not_logged_in", ...withVersion, models: [] };
2799
+ }
2800
+ return {
2801
+ available: true,
2802
+ ...withVersion,
2803
+ models: toProviderModels(res.models ?? []),
2804
+ ...authMode ? { authMode } : {}
2805
+ };
2806
+ }
2807
+ var PROBE_TIMEOUT_MS, MAX_MODELS, MAX_NAME, MAX_VERSION;
2808
+ var init_detect = __esm({
2809
+ "src/claude/detect.ts"() {
2810
+ "use strict";
2811
+ init_src();
2812
+ init_env();
2813
+ init_auth_mode();
2814
+ init_pushable();
2815
+ PROBE_TIMEOUT_MS = 15e3;
2816
+ MAX_MODELS = 50;
2817
+ MAX_NAME = 200;
2818
+ MAX_VERSION = 50;
2819
+ }
2820
+ });
2821
+
2627
2822
  // src/exec-path.ts
2628
2823
  import { accessSync, constants, statSync as statSync3 } from "fs";
2629
- import { delimiter, isAbsolute as isAbsolute5, join as join8 } from "path";
2824
+ import { delimiter, isAbsolute as isAbsolute5, join as join9 } from "path";
2630
2825
  function findOnPath(name, path) {
2631
2826
  for (const dir of (path ?? "").split(delimiter)) {
2632
2827
  if (!isAbsolute5(dir)) continue;
2633
- const candidate = join8(dir, name);
2828
+ const candidate = join9(dir, name);
2634
2829
  try {
2635
2830
  if (!statSync3(candidate).isFile()) continue;
2636
2831
  accessSync(candidate, constants.X_OK);
@@ -2729,13 +2924,16 @@ var init_rpc = __esm({
2729
2924
  }
2730
2925
  }
2731
2926
  dispatch(line) {
2732
- let frame;
2927
+ let parsed;
2733
2928
  try {
2734
- frame = JSON.parse(line);
2929
+ parsed = JSON.parse(line);
2735
2930
  } catch {
2736
2931
  return;
2737
2932
  }
2933
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return;
2934
+ const frame = parsed;
2738
2935
  if (frame.id !== void 0 && frame.method === void 0) {
2936
+ if (typeof frame.id !== "number") return;
2739
2937
  const p = this.pending.get(frame.id);
2740
2938
  if (!p) return;
2741
2939
  this.pending.delete(frame.id);
@@ -2748,6 +2946,7 @@ var init_rpc = __esm({
2748
2946
  void (async () => {
2749
2947
  try {
2750
2948
  const result = await this.request_?.({
2949
+ id,
2751
2950
  method: frame.method,
2752
2951
  params: frame.params
2753
2952
  });
@@ -2755,7 +2954,11 @@ var init_rpc = __esm({
2755
2954
  } catch (err) {
2756
2955
  this.write({
2757
2956
  id,
2758
- error: { code: -32603, message: err instanceof Error ? err.message : String(err) }
2957
+ error: {
2958
+ // A handler that knows the JSON-RPC code for its refusal says so; else "internal".
2959
+ code: err instanceof RpcError ? err.code : -32603,
2960
+ message: err instanceof Error ? err.message : String(err)
2961
+ }
2759
2962
  });
2760
2963
  }
2761
2964
  })();
@@ -2770,6 +2973,17 @@ var init_rpc = __esm({
2770
2973
  // src/codex/detect.ts
2771
2974
  import { execFile as execFile2, spawn } from "child_process";
2772
2975
  import { promisify } from "util";
2976
+ async function allModels(request) {
2977
+ const models = [];
2978
+ let cursor;
2979
+ do {
2980
+ const page = await request(cursor ? { cursor } : {});
2981
+ const data = page.data ?? [];
2982
+ models.push(...data);
2983
+ cursor = data.length > 0 && page.nextCursor ? page.nextCursor : void 0;
2984
+ } while (cursor && models.length < MAX_MODELS2);
2985
+ return models;
2986
+ }
2773
2987
  function findCodexOnPath(env) {
2774
2988
  return findOnPath("codex", env.PATH);
2775
2989
  }
@@ -2778,7 +2992,7 @@ function resolveCodexPath(cfg, env, onPath) {
2778
2992
  }
2779
2993
  async function runCodex(path, args) {
2780
2994
  try {
2781
- const { stdout, stderr } = await exec(path, args, { timeout: PROBE_TIMEOUT_MS });
2995
+ const { stdout, stderr } = await exec(path, args, { timeout: PROBE_TIMEOUT_MS2 });
2782
2996
  return { code: 0, stdout, stderr };
2783
2997
  } catch (err) {
2784
2998
  const e = err;
@@ -2798,30 +3012,29 @@ async function listCodexModels(path) {
2798
3012
  clientInfo: { name: "cayenne-runner", title: "Cayenne", version: "0.0.1" }
2799
3013
  });
2800
3014
  rpc.notify("initialized", {});
2801
- const res = await rpc.request("model/list", {});
2802
- return res.data ?? [];
3015
+ return await allModels((params) => rpc.request("model/list", params));
2803
3016
  } finally {
2804
3017
  clearTimeout(timer);
2805
3018
  rpc.close();
2806
3019
  child.kill();
2807
3020
  }
2808
3021
  }
2809
- function toProviderModel(m) {
2810
- const efforts = (m.supportedReasoningEfforts ?? []).map((e) => e.effort).filter((e) => Effort.options.includes(e));
3022
+ function toProviderModel2(m) {
3023
+ const efforts = (m.supportedReasoningEfforts ?? []).map((e) => e.reasoningEffort).filter((e) => Effort.options.includes(e));
2811
3024
  const fallback = Effort.options.includes(m.defaultReasoningEffort) ? m.defaultReasoningEffort : void 0;
2812
3025
  return {
2813
- id: (m.id ?? "").slice(0, MAX_NAME),
3026
+ id: (m.id ?? "").slice(0, MAX_NAME2),
2814
3027
  // `||`, not `??`: an app-server that sends `displayName: ''` would otherwise publish an
2815
3028
  // empty label, which `ProviderModel.label.min(1)` rejects — taking the whole capability
2816
3029
  // with it.
2817
- label: (m.displayName || m.id || "").slice(0, MAX_NAME),
3030
+ label: (m.displayName || m.id || "").slice(0, MAX_NAME2),
2818
3031
  ...m.isDefault ? { default: true } : {},
2819
3032
  efforts,
2820
3033
  ...fallback ? { defaultEffort: fallback } : {}
2821
3034
  };
2822
3035
  }
2823
- function toProviderModels(models) {
2824
- return models.filter((m) => m.hidden !== true).slice(0, MAX_MODELS).map(toProviderModel).filter((m) => ProviderModel.safeParse(m).success);
3036
+ function toProviderModels2(models) {
3037
+ return models.filter((m) => m.hidden !== true).slice(0, MAX_MODELS2).map(toProviderModel2).filter((m) => ProviderModel.safeParse(m).success);
2825
3038
  }
2826
3039
  async function detectCodex(deps) {
2827
3040
  if (!deps.path) return { available: false, reason: "not_installed", models: [] };
@@ -2829,33 +3042,37 @@ async function detectCodex(deps) {
2829
3042
  try {
2830
3043
  const probe = await deps.run(deps.path, ["--version"]);
2831
3044
  if (probe.code !== 0) return { available: false, reason: "not_installed", models: [] };
2832
- version = probe.stdout.trim().split(/\s+/).at(-1)?.slice(0, MAX_VERSION);
3045
+ version = probe.stdout.trim().split(/\s+/).at(-1)?.slice(0, MAX_VERSION2);
2833
3046
  const login2 = await deps.run(deps.path, ["login", "status"]);
2834
3047
  if (login2.code !== 0) {
2835
3048
  return { available: false, reason: "not_logged_in", version, models: [] };
2836
3049
  }
2837
3050
  const models = await deps.listModels(deps.path);
2838
- return { available: true, version, models: toProviderModels(models) };
3051
+ return { available: true, version, models: toProviderModels2(models) };
2839
3052
  } catch {
2840
3053
  return { available: false, reason: "error", ...version ? { version } : {}, models: [] };
2841
3054
  }
2842
3055
  }
2843
3056
  async function detectProviders(deps) {
2844
- return { claude: { available: true, models: [] }, codex: await detectCodex(deps) };
3057
+ const [claude, codex] = await Promise.all([
3058
+ deps.claude().catch(() => ({ available: true, models: [] })),
3059
+ detectCodex(deps)
3060
+ ]);
3061
+ return { claude, codex };
2845
3062
  }
2846
- var exec, PROBE_TIMEOUT_MS, LIST_TIMEOUT_MS, MAX_MODELS, MAX_NAME, MAX_VERSION;
2847
- var init_detect = __esm({
3063
+ var exec, PROBE_TIMEOUT_MS2, LIST_TIMEOUT_MS, MAX_MODELS2, MAX_NAME2, MAX_VERSION2;
3064
+ var init_detect2 = __esm({
2848
3065
  "src/codex/detect.ts"() {
2849
3066
  "use strict";
2850
3067
  init_src();
2851
3068
  init_exec_path();
2852
3069
  init_rpc();
2853
3070
  exec = promisify(execFile2);
2854
- PROBE_TIMEOUT_MS = 5e3;
3071
+ PROBE_TIMEOUT_MS2 = 5e3;
2855
3072
  LIST_TIMEOUT_MS = 1e4;
2856
- MAX_MODELS = 50;
2857
- MAX_NAME = 200;
2858
- MAX_VERSION = 50;
3073
+ MAX_MODELS2 = 50;
3074
+ MAX_NAME2 = 200;
3075
+ MAX_VERSION2 = 50;
2859
3076
  }
2860
3077
  });
2861
3078
 
@@ -3499,7 +3716,7 @@ var init_source = __esm({
3499
3716
 
3500
3717
  // src/paths.ts
3501
3718
  import { lstatSync, realpathSync as realpathSync5 } from "fs";
3502
- import { dirname as dirname3, isAbsolute as isAbsolute6, join as join9, relative as relative2, sep as sep2 } from "path";
3719
+ import { dirname as dirname4, isAbsolute as isAbsolute6, join as join10, relative as relative2, sep as sep2 } from "path";
3503
3720
  function assertInsideRoots(target, roots) {
3504
3721
  return resolveInsideRoots(target, roots).real;
3505
3722
  }
@@ -3519,8 +3736,8 @@ function assertInsideRootsOrAncestor(target, roots) {
3519
3736
  assertRoots(roots);
3520
3737
  const absolute = isAbsolute6(target) ? target : `${process.cwd()}${sep2}${target}`;
3521
3738
  let walked = walk(target, absolute);
3522
- if (walked.tail.includes("..")) walked = walk(target, join9(walked.real, ...walked.tail));
3523
- const checked = walked.tail.length === 0 ? walked.real : join9(walked.real, ...walked.tail);
3739
+ if (walked.tail.includes("..")) walked = walk(target, join10(walked.real, ...walked.tail));
3740
+ const checked = walked.tail.length === 0 ? walked.real : join10(walked.real, ...walked.tail);
3524
3741
  if (!insideAny(checked, roots)) throw new Error(`ROOT_NOT_ALLOWED: ${target}`);
3525
3742
  return checked;
3526
3743
  }
@@ -3537,7 +3754,7 @@ function walk(target, absolute) {
3537
3754
  tail.push(segment);
3538
3755
  continue;
3539
3756
  }
3540
- const next = segment === ".." ? dirname3(real) : join9(real, segment);
3757
+ const next = segment === ".." ? dirname4(real) : join10(real, segment);
3541
3758
  if (!lstatSync(next, { throwIfNoEntry: false })) {
3542
3759
  tail.push(segment);
3543
3760
  continue;
@@ -3585,8 +3802,8 @@ var init_paths = __esm({
3585
3802
 
3586
3803
  // src/git/introspect.ts
3587
3804
  import { execFile as execFile3 } from "child_process";
3588
- import { readFileSync as readFileSync4, realpathSync as realpathSync6 } from "fs";
3589
- import { delimiter as delimiter2, dirname as dirname4, isAbsolute as isAbsolute7, join as join10, resolve as resolve2 } from "path";
3805
+ import { readFileSync as readFileSync5, realpathSync as realpathSync6 } from "fs";
3806
+ import { delimiter as delimiter2, dirname as dirname5, isAbsolute as isAbsolute7, join as join11, resolve as resolve2 } from "path";
3590
3807
  import { promisify as promisify2 } from "util";
3591
3808
  function parseGitHubRemote(url) {
3592
3809
  const trimmed = url.trim();
@@ -3672,10 +3889,10 @@ function insideRoot(path, root) {
3672
3889
  }
3673
3890
  }
3674
3891
  function isLinkedWorktree(gitDir, commonDir, repoRoot) {
3675
- if (gitDir === commonDir || dirname4(gitDir) !== join10(commonDir, "worktrees")) return false;
3892
+ if (gitDir === commonDir || dirname5(gitDir) !== join11(commonDir, "worktrees")) return false;
3676
3893
  try {
3677
- const back = readFileSync4(join10(gitDir, "gitdir"), "utf8").trim();
3678
- const mine = canonical(join10(repoRoot, ".git"), repoRoot);
3894
+ const back = readFileSync5(join11(gitDir, "gitdir"), "utf8").trim();
3895
+ const mine = canonical(join11(repoRoot, ".git"), repoRoot);
3679
3896
  return mine !== null && canonical(back, gitDir) === mine;
3680
3897
  } catch {
3681
3898
  return false;
@@ -3683,7 +3900,7 @@ function isLinkedWorktree(gitDir, commonDir, repoRoot) {
3683
3900
  }
3684
3901
  async function introspectRepo(cwd, run, root, { names = true } = {}) {
3685
3902
  try {
3686
- const ceiling = dirname4(root);
3903
+ const ceiling = dirname5(root);
3687
3904
  if (ceiling.includes(delimiter2)) return { ...NONE };
3688
3905
  const line = async (args) => {
3689
3906
  const { code, stdout } = await run(args, cwd, ceiling);
@@ -3780,9 +3997,9 @@ var init_introspect = __esm({
3780
3997
  });
3781
3998
 
3782
3999
  // src/repos/credentials.ts
3783
- import { chmodSync as chmodSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
4000
+ import { chmodSync as chmodSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
3784
4001
  import { devNull, homedir as homedir4 } from "os";
3785
- import { delimiter as delimiter3, isAbsolute as isAbsolute8, join as join11 } from "path";
4002
+ import { delimiter as delimiter3, isAbsolute as isAbsolute8, join as join12 } from "path";
3786
4003
  function gitWrapper(git, gitHome) {
3787
4004
  if (git === null || !isAbsolute8(git)) {
3788
4005
  return `#!/bin/sh
@@ -3797,7 +4014,7 @@ exec ${shellQuote(git)} "$@"
3797
4014
  `;
3798
4015
  }
3799
4016
  function repositoryGitHome(runDir) {
3800
- return join11(runDir, GIT_HOME);
4017
+ return join12(runDir, GIT_HOME);
3801
4018
  }
3802
4019
  function gitCredentialEnv(token, readOnlyText, opts = {}) {
3803
4020
  const env = {
@@ -3818,7 +4035,7 @@ function gitCredentialEnv(token, readOnlyText, opts = {}) {
3818
4035
  CAYENNE_READ_ONLY: readOnlyText
3819
4036
  };
3820
4037
  if (token !== null) env.GH_TOKEN = token;
3821
- if (opts.runDir !== void 0) env.GIT_SSH_COMMAND = shellQuote(join11(opts.runDir, NO_SSH));
4038
+ if (opts.runDir !== void 0) env.GIT_SSH_COMMAND = shellQuote(join12(opts.runDir, NO_SSH));
3822
4039
  if (opts.home !== void 0) env.HOME = opts.home;
3823
4040
  return env;
3824
4041
  }
@@ -3830,14 +4047,14 @@ function repositoryChatEnv(input) {
3830
4047
  SSH_AUTH_SOCK: void 0,
3831
4048
  GIT_CONFIG_PARAMETERS: void 0,
3832
4049
  ...Object.fromEntries(TRACE_AND_TLS_VARS.map((key) => [key, void 0])),
3833
- GH_CONFIG_DIR: join11(input.runDir, GH_CONFIG),
4050
+ GH_CONFIG_DIR: join12(input.runDir, GH_CONFIG),
3834
4051
  GH_PROMPT_DISABLED: "1",
3835
4052
  // A replace ref another chat planted in the shared clone must not change what a sha reads as.
3836
4053
  GIT_NO_REPLACE_OBJECTS: "1",
3837
4054
  // Only git runs with HOME at the git home. Go, SwiftPM and python-requests read `$NETRC` first:
3838
4055
  // this one never exists. curl reads the first `.curlrc` of `$CURL_HOME`, then `$HOME`: the git
3839
4056
  // home's is empty, so the owner's (`netrc`, `user = …`) never applies.
3840
- NETRC: join11(repositoryGitHome(input.runDir), NETRC_NAME),
4057
+ NETRC: join12(repositoryGitHome(input.runDir), NETRC_NAME),
3841
4058
  CURL_HOME: repositoryGitHome(input.runDir),
3842
4059
  // With it and CLAUDE_CONFIG_DIR both set, the CLI names its project dir by it rather than by
3843
4060
  // the cwd's slug: every chat's tool outputs would share one dir the worktree bound lets it read.
@@ -3852,8 +4069,8 @@ function repositoryChatEnv(input) {
3852
4069
  };
3853
4070
  if (input.token === null) env.GH_TOKEN = void 0;
3854
4071
  const bins = [
3855
- ...input.token === null ? [join11(input.runDir, READONLY_BIN)] : [],
3856
- join11(input.runDir, GIT_BIN)
4072
+ ...input.token === null ? [join12(input.runDir, READONLY_BIN)] : [],
4073
+ join12(input.runDir, GIT_BIN)
3857
4074
  ];
3858
4075
  env.PATH = [...bins, ...input.path ? [input.path] : []].join(delimiter3);
3859
4076
  for (const key of Object.keys(input.inherited ?? process.env)) {
@@ -3864,17 +4081,17 @@ function repositoryChatEnv(input) {
3864
4081
  }
3865
4082
  function prepareRunDir(runDir, opts) {
3866
4083
  mkdirSync4(runDir, { recursive: true, mode: 448 });
3867
- freshDir(join11(runDir, GH_CONFIG));
4084
+ freshDir(join12(runDir, GH_CONFIG));
3868
4085
  freshDir(repositoryGitHome(runDir));
3869
- writeFileSync4(join11(repositoryGitHome(runDir), CURLRC_NAME), "", { mode: 384, flag: "wx" });
3870
- chmodSync4(join11(repositoryGitHome(runDir), CURLRC_NAME), 384);
3871
- const bin = join11(runDir, READONLY_BIN);
4086
+ writeFileSync4(join12(repositoryGitHome(runDir), CURLRC_NAME), "", { mode: 384, flag: "wx" });
4087
+ chmodSync4(join12(repositoryGitHome(runDir), CURLRC_NAME), 384);
4088
+ const bin = join12(runDir, READONLY_BIN);
3872
4089
  freshDir(bin);
3873
- writeShim(join11(bin, "gh"), GH_SHIM);
3874
- writeShim(join11(runDir, NO_SSH), NO_SSH_SHIM);
3875
- const gitBin = join11(runDir, GIT_BIN);
4090
+ writeShim(join12(bin, "gh"), GH_SHIM);
4091
+ writeShim(join12(runDir, NO_SSH), NO_SSH_SHIM);
4092
+ const gitBin = join12(runDir, GIT_BIN);
3876
4093
  freshDir(gitBin);
3877
- writeShim(join11(gitBin, "git"), gitWrapper(opts.git, repositoryGitHome(runDir)));
4094
+ writeShim(join12(gitBin, "git"), gitWrapper(opts.git, repositoryGitHome(runDir)));
3878
4095
  }
3879
4096
  function freshDir(path) {
3880
4097
  rmSync3(path, { recursive: true, force: true });
@@ -3882,12 +4099,12 @@ function freshDir(path) {
3882
4099
  chmodSync4(path, 448);
3883
4100
  }
3884
4101
  function githubNetrcFiles(home = homedir4(), env = process.env) {
3885
- const candidates = [env.NETRC, join11(home, ".netrc")].filter(
4102
+ const candidates = [env.NETRC, join12(home, ".netrc")].filter(
3886
4103
  (path) => typeof path === "string" && path !== ""
3887
4104
  );
3888
4105
  return [...new Set(candidates)].filter((path) => {
3889
4106
  try {
3890
- return netrcCoversGitHub(readFileSync5(path, "utf8"));
4107
+ return netrcCoversGitHub(readFileSync6(path, "utf8"));
3891
4108
  } catch {
3892
4109
  return false;
3893
4110
  }
@@ -3968,13 +4185,13 @@ exit 1
3968
4185
 
3969
4186
  // src/git/commands.ts
3970
4187
  import { lstatSync as lstatSync2, realpathSync as realpathSync7, statSync as statSync4 } from "fs";
3971
- import { dirname as dirname5, isAbsolute as isAbsolute9, join as join12, relative as relative3, resolve as resolve3, sep as sep3 } from "path";
4188
+ import { dirname as dirname6, isAbsolute as isAbsolute9, join as join13, relative as relative3, resolve as resolve3, sep as sep3 } from "path";
3972
4189
  async function scopeRepo(cwd, run, root) {
3973
4190
  const identity = await introspectRepo(cwd, run, root, { names: false });
3974
4191
  if (identity.repo_root === null) {
3975
4192
  throw new Error(`VALIDATION: ${cwd} is not inside a git repository under an allowed root`);
3976
4193
  }
3977
- return { repoRoot: identity.repo_root, ceiling: dirname5(root) };
4194
+ return { repoRoot: identity.repo_root, ceiling: dirname6(root) };
3978
4195
  }
3979
4196
  function fold(letter) {
3980
4197
  switch (letter) {
@@ -4134,13 +4351,13 @@ async function gitDirs(scope, run) {
4134
4351
  }
4135
4352
  }
4136
4353
  function isGitDirShape(dir) {
4137
- const head = lstatSync2(join12(dir, "HEAD"), { throwIfNoEntry: false });
4354
+ const head = lstatSync2(join13(dir, "HEAD"), { throwIfNoEntry: false });
4138
4355
  if (!head || !(head.isFile() || head.isSymbolicLink())) return false;
4139
- const commondir = lstatSync2(join12(dir, "commondir"), { throwIfNoEntry: false });
4356
+ const commondir = lstatSync2(join13(dir, "commondir"), { throwIfNoEntry: false });
4140
4357
  if (commondir?.isFile()) return true;
4141
4358
  const isDir = (name) => {
4142
4359
  try {
4143
- return statSync4(join12(dir, name), { throwIfNoEntry: false })?.isDirectory() === true;
4360
+ return statSync4(join13(dir, name), { throwIfNoEntry: false })?.isDirectory() === true;
4144
4361
  } catch {
4145
4362
  return false;
4146
4363
  }
@@ -4161,13 +4378,13 @@ function enclosesHome(repoRoot, home) {
4161
4378
  } catch {
4162
4379
  return false;
4163
4380
  }
4164
- for (let cur = real; ; cur = dirname5(cur)) {
4381
+ for (let cur = real; ; cur = dirname6(cur)) {
4165
4382
  try {
4166
4383
  const st = statSync4(cur, { bigint: true, throwIfNoEntry: false });
4167
4384
  if (st && st.dev === root.dev && st.ino === root.ino) return true;
4168
4385
  } catch {
4169
4386
  }
4170
- if (dirname5(cur) === cur) return false;
4387
+ if (dirname6(cur) === cur) return false;
4171
4388
  }
4172
4389
  }
4173
4390
  function protectedTest(paths, exempt = []) {
@@ -4195,10 +4412,10 @@ function protectedTest(paths, exempt = []) {
4195
4412
  return (path) => {
4196
4413
  if (exempts.length === 0 && entries2.some((e) => within(path, e.real))) return true;
4197
4414
  let exempted = false;
4198
- for (let cur = path; ; cur = dirname5(cur)) {
4415
+ for (let cur = path; ; cur = dirname6(cur)) {
4199
4416
  if (!exempted && isOneOf(cur, entries2)) return true;
4200
4417
  if (exempts.length > 0 && isOneOf(cur, exempts)) exempted = true;
4201
- if (dirname5(cur) === cur) return false;
4418
+ if (dirname6(cur) === cur) return false;
4202
4419
  }
4203
4420
  };
4204
4421
  }
@@ -4215,7 +4432,7 @@ function identities(paths) {
4215
4432
  return out;
4216
4433
  }
4217
4434
  function insideOtherGitDir(real, repoRoot) {
4218
- for (let dir = dirname5(real); within(dir, repoRoot) && dir !== repoRoot; dir = dirname5(dir)) {
4435
+ for (let dir = dirname6(real); within(dir, repoRoot) && dir !== repoRoot; dir = dirname6(dir)) {
4219
4436
  if (isGitDirShape(dir)) return true;
4220
4437
  }
4221
4438
  return false;
@@ -4225,7 +4442,7 @@ async function gitDiff(scope, run, opts, guard = {}) {
4225
4442
  const guarded = protectedTest(guard.protectedPaths ?? [], guard.exemptPaths ?? []);
4226
4443
  const isProtected = (p) => guarded?.(p) === true;
4227
4444
  if (opts.path) {
4228
- const named = join12(scope.repoRoot, opts.path);
4445
+ const named = join13(scope.repoRoot, opts.path);
4229
4446
  let resolved = null;
4230
4447
  try {
4231
4448
  resolved = realpathSync7.native(named);
@@ -4255,7 +4472,7 @@ async function gitDiff(scope, run, opts, guard = {}) {
4255
4472
  `VALIDATION: untracked files are not diffed in a repository at or above the home directory (${scope.repoRoot})`
4256
4473
  );
4257
4474
  }
4258
- const absolute = join12(scope.repoRoot, opts.path);
4475
+ const absolute = join13(scope.repoRoot, opts.path);
4259
4476
  if (!lstatSync2(absolute, { throwIfNoEntry: false })) {
4260
4477
  try {
4261
4478
  assertInsideRootsOrAncestor(absolute, [scope.repoRoot]);
@@ -4298,7 +4515,7 @@ async function gitDiff(scope, run, opts, guard = {}) {
4298
4515
  }
4299
4516
  const diff = splitDiff(first.stdout);
4300
4517
  if (guarded === null) return diff;
4301
- const keep = (p) => p === void 0 || !isProtected(join12(scope.repoRoot, p));
4518
+ const keep = (p) => p === void 0 || !isProtected(join13(scope.repoRoot, p));
4302
4519
  return { ...diff, files: diff.files.filter((f) => keep(f.path) && keep(f.oldPath)) };
4303
4520
  }
4304
4521
  function parseBranches(raw, head) {
@@ -4367,16 +4584,16 @@ import {
4367
4584
  lstatSync as lstatSync3,
4368
4585
  mkdirSync as mkdirSync5,
4369
4586
  openSync,
4370
- readFileSync as readFileSync6,
4587
+ readFileSync as readFileSync7,
4371
4588
  realpathSync as realpathSync8,
4372
4589
  statSync as statSync5,
4373
4590
  writeSync
4374
4591
  } from "fs";
4375
4592
  import { readdir } from "fs/promises";
4376
- import { dirname as dirname6, isAbsolute as isAbsolute10, join as join13, relative as relative4, sep as sep4 } from "path";
4593
+ import { dirname as dirname7, isAbsolute as isAbsolute10, join as join14, relative as relative4, sep as sep4 } from "path";
4377
4594
  import { setImmediate } from "timers/promises";
4378
4595
  function worktreePathFor(repoRoot, name) {
4379
- return join13(repoRoot, WORKTREES_DIR, name);
4596
+ return join14(repoRoot, WORKTREES_DIR, name);
4380
4597
  }
4381
4598
  async function listedWorktrees(scope, run) {
4382
4599
  const r = await run(["worktree", "list", "--porcelain"], scope.repoRoot, scope.ceiling);
@@ -4399,7 +4616,7 @@ async function excludeCayenneDir(scope, run) {
4399
4616
  );
4400
4617
  const common = r.stdout.trim();
4401
4618
  if (r.code !== 0 || !isAbsolute10(common)) return;
4402
- const dotGit = join13(scope.repoRoot, ".git");
4619
+ const dotGit = join14(scope.repoRoot, ".git");
4403
4620
  let dotGitIsRealDir;
4404
4621
  try {
4405
4622
  dotGitIsRealDir = lstatSync3(dotGit).isDirectory();
@@ -4416,16 +4633,16 @@ async function excludeCayenneDir(scope, run) {
4416
4633
  return;
4417
4634
  }
4418
4635
  if (commonReal !== dotGitReal) return;
4419
- const exclude = join13(commonReal, "info", "exclude");
4636
+ const exclude = join14(commonReal, "info", "exclude");
4420
4637
  let checkedExclude;
4421
4638
  try {
4422
4639
  checkedExclude = assertInsideRootsOrAncestor(exclude, [scope.repoRoot]);
4423
4640
  } catch {
4424
4641
  return;
4425
4642
  }
4426
- const existing = existsSync4(checkedExclude) ? readFileSync6(checkedExclude, "utf8") : "";
4643
+ const existing = existsSync4(checkedExclude) ? readFileSync7(checkedExclude, "utf8") : "";
4427
4644
  if (existing.split("\n").includes(".cayenne/")) return;
4428
- mkdirSync5(dirname6(checkedExclude), { recursive: true });
4645
+ mkdirSync5(dirname7(checkedExclude), { recursive: true });
4429
4646
  const toAppend = `${existing.endsWith("\n") || existing === "" ? "" : "\n"}.cayenne/
4430
4647
  `;
4431
4648
  let fd;
@@ -4451,7 +4668,7 @@ function asNotADirectory(path, err) {
4451
4668
  return err instanceof Error ? err : new Error(String(err));
4452
4669
  }
4453
4670
  function canonicalWorktreesDir(scope) {
4454
- const dir = join13(scope.repoRoot, WORKTREES_DIR);
4671
+ const dir = join14(scope.repoRoot, WORKTREES_DIR);
4455
4672
  try {
4456
4673
  assertInsideRootsOrAncestor(dir, [scope.repoRoot]);
4457
4674
  } catch (err) {
@@ -4474,7 +4691,7 @@ async function createWorktree(scope, run, name, base) {
4474
4691
  throw new Error(`VALIDATION: ${name} is not a worktree name`);
4475
4692
  }
4476
4693
  const worktreesDir = canonicalWorktreesDir(scope);
4477
- const path = join13(worktreesDir, name);
4694
+ const path = join14(worktreesDir, name);
4478
4695
  if (lstatSync3(path, { throwIfNoEntry: false })) {
4479
4696
  try {
4480
4697
  resolveInsideRoots(path, [scope.repoRoot]);
@@ -4523,7 +4740,7 @@ async function findNestedRepository(top, limits = NESTED_WALK_LIMITS) {
4523
4740
  const lower = entry.name.toLowerCase();
4524
4741
  if (lower === ".git" && !(depth === 0 && entry.name === ".git")) return "repository";
4525
4742
  if (lower === "head") hasHead = true;
4526
- const path = join13(dir, entry.name);
4743
+ const path = join14(dir, entry.name);
4527
4744
  let isDir;
4528
4745
  try {
4529
4746
  isDir = entry.isDirectory() || // Some filesystems report no type; lstat decides, still without following.
@@ -4559,7 +4776,7 @@ async function worktreeIdentity(scope, run) {
4559
4776
  }
4560
4777
  }
4561
4778
  async function removeWorktree(scope, run, worktreePath, force) {
4562
- const worktreesDir = join13(scope.repoRoot, WORKTREES_DIR);
4779
+ const worktreesDir = join14(scope.repoRoot, WORKTREES_DIR);
4563
4780
  let base;
4564
4781
  try {
4565
4782
  base = realpathSync8.native(worktreesDir);
@@ -4664,10 +4881,10 @@ var init_ops = __esm({
4664
4881
 
4665
4882
  // src/git/worktree-owners.ts
4666
4883
  import { createHash, randomBytes } from "crypto";
4667
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync7, renameSync as renameSync2, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
4668
- import { join as join14 } from "path";
4884
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync8, renameSync as renameSync2, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
4885
+ import { join as join15 } from "path";
4669
4886
  function createWorktreeOwners(dir) {
4670
- const fileFor = (path) => join14(dir, `${createHash("sha256").update(path).digest("hex")}.json`);
4887
+ const fileFor = (path) => join15(dir, `${createHash("sha256").update(path).digest("hex")}.json`);
4671
4888
  return {
4672
4889
  record(path, rec) {
4673
4890
  const body = {
@@ -4710,7 +4927,7 @@ function writeRecordFile(dir, file, body) {
4710
4927
  }
4711
4928
  function readRecordFile(file) {
4712
4929
  try {
4713
- const parsed = JSON.parse(readFileSync7(file, "utf8"));
4930
+ const parsed = JSON.parse(readFileSync8(file, "utf8"));
4714
4931
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
4715
4932
  } catch {
4716
4933
  return null;
@@ -4724,9 +4941,9 @@ var init_worktree_owners = __esm({
4724
4941
 
4725
4942
  // src/git/session-worktrees.ts
4726
4943
  import { rmSync as rmSync5 } from "fs";
4727
- import { join as join15 } from "path";
4944
+ import { join as join16 } from "path";
4728
4945
  function createSessionWorktrees(dir) {
4729
- const fileFor = (sessionId) => UUID2.test(sessionId) ? join15(dir, `${sessionId}.json`) : null;
4946
+ const fileFor = (sessionId) => UUID2.test(sessionId) ? join16(dir, `${sessionId}.json`) : null;
4730
4947
  return {
4731
4948
  record(rec) {
4732
4949
  const file = fileFor(rec.sessionId);
@@ -5546,16 +5763,16 @@ import {
5546
5763
  mkdirSync as mkdirSync7,
5547
5764
  openSync as openSync2,
5548
5765
  readdirSync,
5549
- readFileSync as readFileSync8,
5766
+ readFileSync as readFileSync9,
5550
5767
  realpathSync as realpathSync9,
5551
5768
  renameSync as renameSync3,
5552
5769
  rmSync as rmSync6,
5553
5770
  statSync as statSync6,
5554
5771
  writeSync as writeSync2
5555
5772
  } from "fs";
5556
- import { dirname as dirname7, join as join16 } from "path";
5773
+ import { dirname as dirname8, join as join17 } from "path";
5557
5774
  function managedReposRoot(profileDir2) {
5558
- return join16(profileDir2, "repos");
5775
+ return join17(profileDir2, "repos");
5559
5776
  }
5560
5777
  function githubRemoteUrl(repo) {
5561
5778
  return `https://github.com/${repo.owner}/${repo.name}.git`;
@@ -5590,9 +5807,9 @@ function createManagedRepos(deps) {
5590
5807
  }
5591
5808
  };
5592
5809
  async function cloneInto(repo, env) {
5593
- const ownerDir = join16(root, repo.owner);
5594
- const path = join16(ownerDir, repo.name);
5595
- const tmp = join16(ownerDir, `.${repo.name}.clone-${randomBytes2(4).toString("hex")}`);
5810
+ const ownerDir = join17(root, repo.owner);
5811
+ const path = join17(ownerDir, repo.name);
5812
+ const tmp = join17(ownerDir, `.${repo.name}.clone-${randomBytes2(4).toString("hex")}`);
5596
5813
  inflight.add(tmp);
5597
5814
  try {
5598
5815
  mkdirSync7(ownerDir, { recursive: true, mode: 448 });
@@ -5637,7 +5854,7 @@ function createManagedRepos(deps) {
5637
5854
  const listed = await run(
5638
5855
  ["config", "--local", "--no-includes", "--list", "-z"],
5639
5856
  path,
5640
- dirname7(path)
5857
+ dirname8(path)
5641
5858
  );
5642
5859
  const entries2 = listed.code === 0 ? parseConfigList(listed.stdout) : [];
5643
5860
  const mark = entries2.filter((e) => e.section === "cayenne" && e.subsection === null && e.name === "repositoryid").at(-1)?.value;
@@ -5662,8 +5879,8 @@ function createManagedRepos(deps) {
5662
5879
  }
5663
5880
  async function sync(repo, tok) {
5664
5881
  const slug = `${repo.owner}/${repo.name}`;
5665
- const ownerDir = join16(root, repo.owner);
5666
- const path = join16(ownerDir, repo.name);
5882
+ const ownerDir = join17(root, repo.owner);
5883
+ const path = join17(ownerDir, repo.name);
5667
5884
  assertRealDirs(ownerDir, path);
5668
5885
  const env = gitCredentialEnv(tok.token, "", { home: deps.gitHome });
5669
5886
  const run = makeGitRun({ timeoutMs: GIT_SLOW_TIMEOUT_MS, env });
@@ -5703,9 +5920,9 @@ function createManagedRepos(deps) {
5703
5920
  return { clonePath: real, defaultBranch: branch, baseSha };
5704
5921
  }
5705
5922
  function excludePlans(path) {
5706
- const gitDir = join16(path, ".git");
5707
- const info = join16(gitDir, "info");
5708
- const file = join16(info, "exclude");
5923
+ const gitDir = join17(path, ".git");
5924
+ const info = join17(gitDir, "info");
5925
+ const file = join17(info, "exclude");
5709
5926
  try {
5710
5927
  if (!lstatSync4(gitDir).isDirectory()) throw new Error(".git is not a directory");
5711
5928
  const infoStat = lstatSync4(info, { throwIfNoEntry: false });
@@ -5721,7 +5938,7 @@ function createManagedRepos(deps) {
5721
5938
  420
5722
5939
  );
5723
5940
  try {
5724
- const text = readFileSync8(fd, "utf8");
5941
+ const text = readFileSync9(fd, "utf8");
5725
5942
  const lines = text.split("\n");
5726
5943
  const isOld = (line) => line.replace(/\r$/, "") === OLD_PLANS_EXCLUDE_LINE;
5727
5944
  const has = lines.some((line) => line.replace(/\r$/, "") === PLANS_EXCLUDE_LINE);
@@ -5762,9 +5979,9 @@ function createManagedRepos(deps) {
5762
5979
  }
5763
5980
  async function markOf(path) {
5764
5981
  const { code, stdout } = await scanRun(
5765
- ["config", "--file", join16(path, ".git", "config"), "--get", ID_KEY],
5982
+ ["config", "--file", join17(path, ".git", "config"), "--get", ID_KEY],
5766
5983
  path,
5767
- dirname7(path)
5984
+ dirname8(path)
5768
5985
  );
5769
5986
  const text = stdout.trim();
5770
5987
  if (code !== 0 || !ID_TEXT.test(text)) return null;
@@ -5791,8 +6008,8 @@ function createManagedRepos(deps) {
5791
6008
  });
5792
6009
  },
5793
6010
  verifyConfig(repo) {
5794
- const ownerDir = join16(root, repo.owner);
5795
- const path = join16(ownerDir, repo.name);
6011
+ const ownerDir = join17(root, repo.owner);
6012
+ const path = join17(ownerDir, repo.name);
5796
6013
  const check = async () => {
5797
6014
  assertRealDirs(ownerDir, path);
5798
6015
  await checkConfig(repo, path, scanRun);
@@ -5813,12 +6030,12 @@ function createManagedRepos(deps) {
5813
6030
  const candidates = [];
5814
6031
  for (const owner of entries(root)) {
5815
6032
  if (!owner.isDirectory()) continue;
5816
- const ownerDir = join16(root, owner.name);
6033
+ const ownerDir = join17(root, owner.name);
5817
6034
  for (const entry of entries(ownerDir)) {
5818
- const path = join16(ownerDir, entry.name);
6035
+ const path = join17(ownerDir, entry.name);
5819
6036
  if (TEMP_CLONE.test(entry.name)) {
5820
6037
  if (inflight.has(path)) continue;
5821
- if (!present(join16(path, ".cayenne"))) {
6038
+ if (!present(join17(path, ".cayenne"))) {
5822
6039
  rmSync6(path, { recursive: true, force: true });
5823
6040
  deps.log.info({ path }, "removed an interrupted managed clone");
5824
6041
  continue;
@@ -5894,7 +6111,7 @@ function keyText(e) {
5894
6111
  return `${e.section}.${subsection}.${e.name}`;
5895
6112
  }
5896
6113
  function recency(path) {
5897
- for (const file of [join16(path, ".git", "FETCH_HEAD"), join16(path, ".git")]) {
6114
+ for (const file of [join17(path, ".git", "FETCH_HEAD"), join17(path, ".git")]) {
5898
6115
  try {
5899
6116
  const stat = statSync6(file, { throwIfNoEntry: false });
5900
6117
  if (stat) return stat.mtimeMs;
@@ -6023,7 +6240,7 @@ var init_managed = __esm({
6023
6240
 
6024
6241
  // src/repos/sweep.ts
6025
6242
  import { lstatSync as lstatSync5, realpathSync as realpathSync10 } from "fs";
6026
- import { isAbsolute as isAbsolute12, join as join17, relative as relative5, sep as sep7 } from "path";
6243
+ import { isAbsolute as isAbsolute12, join as join18, relative as relative5, sep as sep7 } from "path";
6027
6244
  function createRepoWorktreeSweep(deps) {
6028
6245
  const { log } = deps;
6029
6246
  const said3 = /* @__PURE__ */ new Set();
@@ -6038,8 +6255,8 @@ function createRepoWorktreeSweep(deps) {
6038
6255
  return null;
6039
6256
  }
6040
6257
  const leaf = worktreeName(sessionId);
6041
- const repoRoot = join17(deps.managedRoot, owner, name);
6042
- if (leaf === null || rec.path !== join17(repoRoot, WORKTREES_DIR, leaf)) return null;
6258
+ const repoRoot = join18(deps.managedRoot, owner, name);
6259
+ if (leaf === null || rec.path !== join18(repoRoot, WORKTREES_DIR, leaf)) return null;
6043
6260
  return { sessionId, path: rec.path, repoRoot, record: rec };
6044
6261
  }
6045
6262
  async function readRows(ids) {
@@ -6481,7 +6698,7 @@ var init_roster = __esm({
6481
6698
 
6482
6699
  // src/sessions/attachments.ts
6483
6700
  import { readdirSync as readdirSync2, rmSync as rmSync7 } from "fs";
6484
- import { join as join18 } from "path";
6701
+ import { join as join19 } from "path";
6485
6702
  async function loadAttachments(db, scope, attachments) {
6486
6703
  const prefix = `${scope.workspaceId.toLowerCase()}/${scope.sessionId.toLowerCase()}/`;
6487
6704
  const named = attachments.map(({ storagePath }, i) => {
@@ -6518,13 +6735,13 @@ async function loadAttachments(db, scope, attachments) {
6518
6735
  return loaded;
6519
6736
  }
6520
6737
  function attachmentsTempDir(home, runnerId, sessionId) {
6521
- return join18(home, "tmp", "attachments", runnerId, sessionId);
6738
+ return join19(home, "tmp", "attachments", runnerId, sessionId);
6522
6739
  }
6523
6740
  function assertStagingInsideHome(home, dir) {
6524
6741
  return assertInsideRootsOrAncestor(dir, [home]);
6525
6742
  }
6526
6743
  function clearAttachmentsTemp(home, runnerId, heldBy) {
6527
- clearRunnerStaging(home, join18(home, "tmp", "attachments"), runnerId, heldBy);
6744
+ clearRunnerStaging(home, join19(home, "tmp", "attachments"), runnerId, heldBy);
6528
6745
  }
6529
6746
  function clearRunnerStaging(home, dir, runnerId, heldBy = (id) => holderOf({ root: home, runnerId: id }) !== null) {
6530
6747
  const proved = assertStagingInsideHome(home, dir);
@@ -6535,9 +6752,9 @@ function clearRunnerStaging(home, dir, runnerId, heldBy = (id) => holderOf({ roo
6535
6752
  return;
6536
6753
  }
6537
6754
  for (const name of names) {
6538
- assertStagingInsideHome(home, join18(proved, name));
6755
+ assertStagingInsideHome(home, join19(proved, name));
6539
6756
  if (name !== runnerId && heldBy(name)) continue;
6540
- rmSync7(join18(proved, name), { recursive: true, force: true });
6757
+ rmSync7(join19(proved, name), { recursive: true, force: true });
6541
6758
  }
6542
6759
  }
6543
6760
  var ATTACHMENTS_BUCKET, TYPE_OF_EXT, refuse;
@@ -6557,7 +6774,7 @@ var init_attachments2 = __esm({
6557
6774
 
6558
6775
  // src/fs/list.ts
6559
6776
  import { readdirSync as readdirSync3, realpathSync as realpathSync11, statSync as statSync7 } from "fs";
6560
- import { join as join19, relative as relative6, sep as sep8 } from "path";
6777
+ import { join as join20, relative as relative6, sep as sep8 } from "path";
6561
6778
  function order(a, b) {
6562
6779
  const aDir = a.kind === "dir" ? 0 : 1;
6563
6780
  const bDir = b.kind === "dir" ? 0 : 1;
@@ -6598,7 +6815,7 @@ function listDirectory(dir, root, depth) {
6598
6815
  return;
6599
6816
  }
6600
6817
  const level = dirents.map((dirent) => {
6601
- const path = join19(current, dirent.name);
6818
+ const path = join20(current, dirent.name);
6602
6819
  const link = dirent.isSymbolicLink();
6603
6820
  const resolved = link ? symlinkKind(path, root) : null;
6604
6821
  const isDir = link ? resolved?.kind === "dir" : dirent.isDirectory();
@@ -7043,7 +7260,7 @@ var init_broadcast = __esm({
7043
7260
 
7044
7261
  // src/documents/paths.ts
7045
7262
  import { realpathSync as realpathSync12 } from "fs";
7046
- import { isAbsolute as isAbsolute13, join as join20, relative as relative7, sep as sep10 } from "path";
7263
+ import { isAbsolute as isAbsolute13, join as join21, relative as relative7, sep as sep10 } from "path";
7047
7264
  function contained(dir, candidate) {
7048
7265
  const rel = relative7(dir, candidate);
7049
7266
  return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep10}`) && !isAbsolute13(rel);
@@ -7080,7 +7297,7 @@ var init_paths2 = __esm({
7080
7297
  "src/documents/paths.ts"() {
7081
7298
  "use strict";
7082
7299
  init_src();
7083
- docsDir = (cwd) => join20(cwd, ...DOCUMENTS_DIR.split("/"));
7300
+ docsDir = (cwd) => join21(cwd, ...DOCUMENTS_DIR.split("/"));
7084
7301
  }
7085
7302
  });
7086
7303
 
@@ -7160,10 +7377,10 @@ async function syncDocuments(deps, pass) {
7160
7377
  if (error2) throw new Error(error2.message);
7161
7378
  const row = data;
7162
7379
  if (!row?.id) throw new Error("the insert returned no row");
7163
- const failed4 = await publish2(row, doc);
7164
- if (failed4 !== null) {
7380
+ const failed5 = await publish2(row, doc);
7381
+ if (failed5 !== null) {
7165
7382
  log.warn(
7166
- { err: failed4, relPath: doc.relPath, sessionId: deps.sessionId },
7383
+ { err: failed5, relPath: doc.relPath, sessionId: deps.sessionId },
7167
7384
  "could not upload a document; removing the row so the next pass retries it"
7168
7385
  );
7169
7386
  await takeBack(row.id, doc.relPath);
@@ -7176,10 +7393,10 @@ async function syncDocuments(deps, pass) {
7176
7393
  const restated = before.status !== "present" || before.title !== doc.title || before.kind !== doc.kind || before.size_bytes !== doc.size;
7177
7394
  if (!rewritten && !restated) continue;
7178
7395
  if (rewritten) {
7179
- const failed4 = await upload(before.storage_path, doc);
7180
- if (failed4 !== null) {
7396
+ const failed5 = await upload(before.storage_path, doc);
7397
+ if (failed5 !== null) {
7181
7398
  log.warn(
7182
- { err: failed4, relPath: doc.relPath, sessionId: deps.sessionId },
7399
+ { err: failed5, relPath: doc.relPath, sessionId: deps.sessionId },
7183
7400
  "could not overwrite a document; its row still describes the bytes in the bucket"
7184
7401
  );
7185
7402
  continue;
@@ -7257,10 +7474,10 @@ import {
7257
7474
  lstatSync as lstatSync6,
7258
7475
  opendirSync,
7259
7476
  openSync as openSync3,
7260
- readFileSync as readFileSync9
7477
+ readFileSync as readFileSync10
7261
7478
  } from "fs";
7262
7479
  import { open } from "fs/promises";
7263
- import { join as join21 } from "path";
7480
+ import { join as join22 } from "path";
7264
7481
  function isNew(baseline, relPath, contentHash) {
7265
7482
  const before = baseline.hashes.get(relPath);
7266
7483
  if (before === void 0) return baseline.complete;
@@ -7347,7 +7564,7 @@ function walk2(st, dir, rel, depth) {
7347
7564
  for (const entry of entries2) {
7348
7565
  if (st.stop) return;
7349
7566
  if (entry.name.startsWith(".")) continue;
7350
- const path = join21(dir, entry.name);
7567
+ const path = join22(dir, entry.name);
7351
7568
  const relPath = rel === "" ? entry.name : `${rel}/${entry.name}`;
7352
7569
  if (entry.isSymbolicLink()) {
7353
7570
  st.log.warn({ path, relPath }, `document skipped: ${SYMLINK_SKIPPED}`);
@@ -7458,7 +7675,7 @@ function baselineOf(root, log) {
7458
7675
  continue;
7459
7676
  }
7460
7677
  try {
7461
- hashes.set(file.relPath, sha256(readFileSync9(fd)));
7678
+ hashes.set(file.relPath, sha256(readFileSync10(fd)));
7462
7679
  } catch {
7463
7680
  hashes.set(file.relPath, UNREADABLE);
7464
7681
  } finally {
@@ -7580,31 +7797,6 @@ var init_service = __esm({
7580
7797
  }
7581
7798
  });
7582
7799
 
7583
- // src/providers/env.ts
7584
- function definedEnv(env) {
7585
- const out = {};
7586
- for (const [key, value] of Object.entries(env)) if (value !== void 0) out[key] = value;
7587
- return out;
7588
- }
7589
- var init_env = __esm({
7590
- "src/providers/env.ts"() {
7591
- "use strict";
7592
- }
7593
- });
7594
-
7595
- // src/claude/auth-mode.ts
7596
- function authModeFrom(info) {
7597
- if (info.apiProvider && info.apiProvider !== "firstParty") return "third_party";
7598
- if (info.apiKeySource && info.apiKeySource !== "none") return "api";
7599
- if (info.subscriptionType) return "subscription";
7600
- return null;
7601
- }
7602
- var init_auth_mode = __esm({
7603
- "src/claude/auth-mode.ts"() {
7604
- "use strict";
7605
- }
7606
- });
7607
-
7608
7800
  // src/claude/normalize.ts
7609
7801
  function normalizeSdkMessage(msg) {
7610
7802
  switch (msg.type) {
@@ -7708,43 +7900,10 @@ var init_normalize = __esm({
7708
7900
  }
7709
7901
  });
7710
7902
 
7711
- // src/claude/pushable.ts
7712
- var Pushable;
7713
- var init_pushable = __esm({
7714
- "src/claude/pushable.ts"() {
7715
- "use strict";
7716
- Pushable = class {
7717
- queue = [];
7718
- waiters = [];
7719
- ended = false;
7720
- push(value) {
7721
- if (this.ended) throw new Error("Pushable already ended");
7722
- const waiter = this.waiters.shift();
7723
- if (waiter) waiter({ value, done: false });
7724
- else this.queue.push(value);
7725
- }
7726
- end() {
7727
- this.ended = true;
7728
- for (const waiter of this.waiters.splice(0)) waiter({ value: void 0, done: true });
7729
- }
7730
- [Symbol.asyncIterator]() {
7731
- return {
7732
- next: () => {
7733
- const value = this.queue.shift();
7734
- if (value !== void 0) return Promise.resolve({ value, done: false });
7735
- if (this.ended) return Promise.resolve({ value: void 0, done: true });
7736
- return new Promise((resolve6) => this.waiters.push(resolve6));
7737
- }
7738
- };
7739
- }
7740
- };
7741
- }
7742
- });
7743
-
7744
7903
  // src/claude/session.ts
7745
7904
  import { randomUUID as randomUUID3 } from "crypto";
7746
7905
  import { EventEmitter } from "events";
7747
- import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
7906
+ import { query as sdkQuery2 } from "@anthropic-ai/claude-agent-sdk";
7748
7907
  async function askGuard(guard, toolName2, input) {
7749
7908
  try {
7750
7909
  const refusal = await guard(toolName2, input);
@@ -7805,7 +7964,7 @@ var init_session2 = __esm({
7805
7964
  stopAsked = false;
7806
7965
  start() {
7807
7966
  if (this.q) throw new Error("VALIDATION: session already started");
7808
- const queryFn = this.opts.queryFn ?? sdkQuery;
7967
+ const queryFn = this.opts.queryFn ?? sdkQuery2;
7809
7968
  const guard = this.opts.toolGuard;
7810
7969
  const canUseTool = async (toolName2, input, options) => {
7811
7970
  if (guard) {
@@ -8047,15 +8206,37 @@ ${this.stderrTail.join("\n")}` : "";
8047
8206
  });
8048
8207
 
8049
8208
  // src/codex/normalize.ts
8209
+ function moveTarget(kind) {
8210
+ const named = [kind.move_path, kind.movePath].filter((target) => target != null);
8211
+ if (named.length === 0) return void 0;
8212
+ const [first] = named;
8213
+ return typeof first === "string" && named.every((target) => target === first) ? first : "";
8214
+ }
8215
+ function fileChanges(item) {
8216
+ if (!Array.isArray(item.changes)) return [];
8217
+ return item.changes.map((entry) => {
8218
+ const change = isRecord(entry) ? entry : {};
8219
+ const kind = isRecord(change.kind) ? change.kind : {};
8220
+ const movePath = moveTarget(kind);
8221
+ return {
8222
+ path: typeof change.path === "string" ? change.path : "",
8223
+ ...typeof kind.type === "string" ? { kind: kind.type } : {},
8224
+ ...typeof change.diff === "string" ? { diff: change.diff } : {},
8225
+ ...movePath === void 0 ? {} : { movePath }
8226
+ };
8227
+ });
8228
+ }
8050
8229
  function normalizeCodexItem(item, phase) {
8051
8230
  const base = { providerUuid: item.id, parentToolUseId: null, raw: item };
8052
- if (item.type === "agent_message") {
8231
+ if (item.type === "agentMessage") {
8053
8232
  if (phase !== "completed") return null;
8054
8233
  return { role: "assistant", content: [{ type: "text", text: item.text ?? "" }], ...base };
8055
8234
  }
8056
8235
  if (item.type === "reasoning") {
8057
8236
  if (phase !== "completed") return null;
8058
- return { role: "assistant", content: [{ type: "thinking", text: item.text ?? "" }], ...base };
8237
+ const text = (Array.isArray(item.summary) ? item.summary : []).join("\n\n");
8238
+ if (!text) return null;
8239
+ return { role: "assistant", content: [{ type: "thinking", text }], ...base };
8059
8240
  }
8060
8241
  const name = toolName(item);
8061
8242
  if (!name) return null;
@@ -8063,12 +8244,11 @@ function normalizeCodexItem(item, phase) {
8063
8244
  const block2 = { type: "tool_use", id: item.id, name, input: toolInput(item) };
8064
8245
  return { role: "assistant", content: [block2], ...base };
8065
8246
  }
8066
- const isError2 = item.status === "failed" || item.exit_code !== void 0 && item.exit_code !== 0;
8067
8247
  const block = {
8068
8248
  type: "tool_result",
8069
8249
  toolUseId: item.id,
8070
8250
  content: resultText(item),
8071
- isError: isError2
8251
+ isError: failed4(item)
8072
8252
  };
8073
8253
  return { role: "tool", content: [block], ...base };
8074
8254
  }
@@ -8089,20 +8269,21 @@ function deltaEvents(_itemId, kind, text, first) {
8089
8269
  function stopEvents() {
8090
8270
  return [{ type: "content_block_stop", index: 0 }, { type: "message_stop" }];
8091
8271
  }
8092
- var toolName, toolInput, resultText;
8272
+ var isRecord, toolName, toolInput, resultText, failed4;
8093
8273
  var init_normalize2 = __esm({
8094
8274
  "src/codex/normalize.ts"() {
8095
8275
  "use strict";
8096
8276
  init_src();
8277
+ isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
8097
8278
  toolName = (item) => {
8098
8279
  switch (item.type) {
8099
- case "command_execution":
8280
+ case "commandExecution":
8100
8281
  return CODEX_TOOL_NAMES.command;
8101
- case "file_change":
8282
+ case "fileChange":
8102
8283
  return CODEX_TOOL_NAMES.fileChange;
8103
- case "web_search":
8284
+ case "webSearch":
8104
8285
  return CODEX_TOOL_NAMES.webSearch;
8105
- case "mcp_tool_call":
8286
+ case "mcpToolCall":
8106
8287
  return `codex.mcp.${(item.server ?? "server").replace(/[^A-Za-z0-9_.-]/g, "")}.${(item.tool ?? "tool").replace(/[^A-Za-z0-9_.-]/g, "")}`.slice(
8107
8288
  0,
8108
8289
  64
@@ -8113,21 +8294,25 @@ var init_normalize2 = __esm({
8113
8294
  };
8114
8295
  toolInput = (item) => {
8115
8296
  switch (item.type) {
8116
- case "command_execution":
8297
+ case "commandExecution":
8117
8298
  return { command: item.command ?? "", ...item.cwd ? { cwd: item.cwd } : {} };
8118
- case "file_change":
8119
- return { changes: item.changes ?? [] };
8120
- case "web_search":
8299
+ case "fileChange":
8300
+ return { changes: fileChanges(item) };
8301
+ case "webSearch":
8121
8302
  return { query: item.query ?? "" };
8122
8303
  default:
8123
- return item.arguments ?? {};
8304
+ if (isRecord(item.arguments)) return item.arguments;
8305
+ return item.arguments === void 0 ? {} : { arguments: item.arguments };
8124
8306
  }
8125
8307
  };
8126
8308
  resultText = (item) => {
8127
- if (item.type === "command_execution") return item.aggregated_output ?? "";
8128
- if (item.type === "file_change") return JSON.stringify(item.changes ?? []);
8309
+ if (item.type === "commandExecution") return item.aggregatedOutput ?? "";
8310
+ if (item.type === "fileChange") return JSON.stringify(fileChanges(item));
8311
+ if (item.type === "webSearch") return JSON.stringify(item.action ?? null);
8312
+ if (item.error?.message) return item.error.message;
8129
8313
  return typeof item.result === "string" ? item.result : JSON.stringify(item.result ?? null);
8130
8314
  };
8315
+ failed4 = (item) => item.status === "failed" || item.status === "declined" || Boolean(item.error) || typeof item.exitCode === "number" && item.exitCode !== 0;
8131
8316
  }
8132
8317
  });
8133
8318
 
@@ -8142,6 +8327,9 @@ function sandboxFor(mode, cwd) {
8142
8327
  return { type: "workspaceWrite", writableRoots: [cwd], networkAccess: false };
8143
8328
  }
8144
8329
  }
8330
+ function sandboxMode(policy) {
8331
+ return SANDBOX_MODES[policy.type];
8332
+ }
8145
8333
  function autoDecide(mode, kind) {
8146
8334
  if (mode === "bypassPermissions") return "accept";
8147
8335
  if (mode === "dontAsk") return "decline";
@@ -8149,9 +8337,15 @@ function autoDecide(mode, kind) {
8149
8337
  if (mode === "acceptEdits" && kind === "file_change") return "accept";
8150
8338
  return "ask";
8151
8339
  }
8340
+ var SANDBOX_MODES;
8152
8341
  var init_policy = __esm({
8153
8342
  "src/codex/policy.ts"() {
8154
8343
  "use strict";
8344
+ SANDBOX_MODES = {
8345
+ readOnly: "read-only",
8346
+ workspaceWrite: "workspace-write",
8347
+ dangerFullAccess: "danger-full-access"
8348
+ };
8155
8349
  }
8156
8350
  });
8157
8351
 
@@ -8160,7 +8354,8 @@ import { spawn as spawn2 } from "child_process";
8160
8354
  import { randomUUID as randomUUID4 } from "crypto";
8161
8355
  import { EventEmitter as EventEmitter2 } from "events";
8162
8356
  import { chmodSync as chmodSync5, lstatSync as lstatSync7, mkdirSync as mkdirSync8, rmdirSync, rmSync as rmSync8, writeFileSync as writeFileSync6 } from "fs";
8163
- import { dirname as dirname8, join as join22 } from "path";
8357
+ import { dirname as dirname9, join as join23 } from "path";
8358
+ import { z as z18 } from "zod";
8164
8359
  function newStop() {
8165
8360
  let resolve6;
8166
8361
  let reject;
@@ -8174,10 +8369,35 @@ function newStop() {
8174
8369
  function removeFiles(files) {
8175
8370
  for (const file of files) rmSync8(file, { force: true });
8176
8371
  }
8372
+ function readQuestions(params) {
8373
+ const asked = AskedQuestions.safeParse(params);
8374
+ if (!asked.success) return null;
8375
+ const { questions } = asked.data;
8376
+ const ids = questions.map((q) => q.id);
8377
+ if (new Set(ids).size !== ids.length || questions.some((q) => q.isSecret)) return null;
8378
+ const input = CodexQuestions.safeParse({
8379
+ questions: questions.map((q) => ({
8380
+ text: q.question,
8381
+ ...q.options?.length ? { options: q.options.map((o) => o.label) } : {}
8382
+ }))
8383
+ });
8384
+ return input.success ? { ids, input: input.data } : null;
8385
+ }
8177
8386
  function scrubSecrets(text) {
8178
8387
  return SECRET_PATTERNS.reduce((acc, [pattern, to]) => acc.replace(pattern, to), text);
8179
8388
  }
8180
- var STDERR_TAIL_MAX, STOP_WAIT_MS, SIGKILL_AFTER_MS, SECRET_PATTERNS, APPROVAL_METHODS, CodexSession;
8389
+ function failureText(error) {
8390
+ const message = error?.message;
8391
+ if (typeof message !== "string" || !message.trim()) return null;
8392
+ let text = message;
8393
+ try {
8394
+ const inner = JSON.parse(message)?.error?.message;
8395
+ if (typeof inner === "string" && inner.trim()) text = inner;
8396
+ } catch {
8397
+ }
8398
+ return scrubSecrets(text).trim().slice(0, TURN_ERROR_MAX);
8399
+ }
8400
+ var decided, NO_ANSWERS, REFUSALS, METHOD_NOT_FOUND, AskedQuestions, STDERR_TAIL_MAX, STOP_WAIT_MS, TURN_ERROR_MAX, SIGKILL_AFTER_MS, SECRET_PATTERNS, APPROVAL_METHODS, CodexSession;
8181
8401
  var init_session3 = __esm({
8182
8402
  "src/codex/session.ts"() {
8183
8403
  "use strict";
@@ -8187,8 +8407,27 @@ var init_session3 = __esm({
8187
8407
  init_normalize2();
8188
8408
  init_policy();
8189
8409
  init_rpc();
8410
+ decided = (decision) => ({ decision });
8411
+ NO_ANSWERS = { answers: {} };
8412
+ REFUSALS = {
8413
+ "item/permissions/requestApproval": { permissions: {} },
8414
+ "mcpServer/elicitation/request": { action: "decline" },
8415
+ "item/tool/call": { contentItems: [], success: false }
8416
+ };
8417
+ METHOD_NOT_FOUND = -32601;
8418
+ AskedQuestions = z18.object({
8419
+ questions: z18.array(
8420
+ z18.object({
8421
+ id: z18.string().min(1),
8422
+ question: z18.string(),
8423
+ isSecret: z18.boolean().nullish(),
8424
+ options: z18.array(z18.object({ label: z18.string() })).nullish()
8425
+ })
8426
+ )
8427
+ });
8190
8428
  STDERR_TAIL_MAX = 2 * 1024;
8191
8429
  STOP_WAIT_MS = 2e3;
8430
+ TURN_ERROR_MAX = 500;
8192
8431
  SIGKILL_AFTER_MS = 5e3;
8193
8432
  SECRET_PATTERNS = [
8194
8433
  [
@@ -8234,8 +8473,17 @@ var init_session3 = __esm({
8234
8473
  resolvers = /* @__PURE__ */ new Map();
8235
8474
  /** Monotonic, so no two held requests can ever share a local id. */
8236
8475
  approvalSeq = 0;
8237
- /** How many questions each pending `codex.user_input` asked, so answers can be counted. */
8238
- questionCounts = /* @__PURE__ */ new Map();
8476
+ /**
8477
+ * The app-server's id of each question a pending `codex.user_input` asked, in the order the
8478
+ * card shows them: an answer goes back under its question's id.
8479
+ */
8480
+ questionIds = /* @__PURE__ */ new Map();
8481
+ /**
8482
+ * The files of each `fileChange` item in progress, by item id. `item/fileChange/requestApproval`
8483
+ * names the item and not its files: those came with `item/started`, which the app-server sends
8484
+ * first (live 2026-10-13), and with any `item/fileChange/patchUpdated` after.
8485
+ */
8486
+ fileChanges = /* @__PURE__ */ new Map();
8239
8487
  start() {
8240
8488
  if (this.child) throw new Error("VALIDATION: session already started");
8241
8489
  const spawnFn = this.opts.spawnFn ?? ((p, a, env) => spawn2(p, a, { stdio: "pipe", env }));
@@ -8255,13 +8503,19 @@ var init_session3 = __esm({
8255
8503
  ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
8256
8504
  const error = this.stopping || code === 0 ? null : `PROVIDER_ERROR: codex exited with ${code}${tail}`;
8257
8505
  this.removeAllFiles();
8506
+ this.withdrawAll();
8258
8507
  this.emit("exit", { error });
8259
8508
  });
8260
8509
  const rpc = new JsonRpc(child.stdin, child.stdout);
8261
8510
  this.rpc = rpc;
8262
- rpc.onNotification((n) => this.onNotification(n.method, n.params));
8263
- rpc.onRequest((r) => this.onRequest(r.method, r.params));
8511
+ rpc.onNotification(
8512
+ (n) => this.onNotification(n.method, n.params ?? {})
8513
+ );
8514
+ rpc.onRequest(
8515
+ (r) => this.onRequest(r.id, r.method, r.params ?? {})
8516
+ );
8264
8517
  void this.handshake().catch((err) => {
8518
+ this.withdrawAll();
8265
8519
  this.emit("exit", {
8266
8520
  error: `PROVIDER_ERROR: ${err instanceof Error ? err.message : String(err)}`
8267
8521
  });
@@ -8277,17 +8531,24 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
8277
8531
  const params = {
8278
8532
  cwd: this.opts.cwd,
8279
8533
  approvalPolicy: "on-request",
8280
- sandbox: this.sandbox,
8534
+ // The mode word: opening a thread refuses a policy object (`unknown variant `type``, live
8535
+ // 2026-10-13). The policy itself, with its writable root, goes with every `turn/start`.
8536
+ sandbox: sandboxMode(this.sandbox),
8281
8537
  ...this.pendingModel ? { model: this.pendingModel } : {}
8282
8538
  };
8283
8539
  const res = this.opts.resume ? await rpc.request("thread/resume", {
8284
8540
  threadId: this.opts.resume,
8285
8541
  ...params
8286
- }) : await rpc.request("thread/start", params);
8287
- this.providerSessionId = res.threadId ?? this.opts.resume ?? null;
8542
+ }) : await rpc.request("thread/start", {
8543
+ ...params,
8544
+ ...this.opts.ephemeral ? { ephemeral: true } : {}
8545
+ });
8546
+ const threadId = res?.thread?.id;
8547
+ if (!threadId) throw new Error("codex app-server opened no thread");
8548
+ this.providerSessionId = threadId;
8288
8549
  this.emit("init", {
8289
- providerSessionId: this.providerSessionId ?? "",
8290
- model: res.model ?? this.pendingModel ?? "",
8550
+ providerSessionId: threadId,
8551
+ model: res?.model ?? this.pendingModel ?? "",
8291
8552
  tools: [],
8292
8553
  slashCommands: [],
8293
8554
  permissionMode: this.mode
@@ -8325,7 +8586,7 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
8325
8586
  assertStagingInsideHome(home, dir);
8326
8587
  mkdirSync8(dir, { recursive: true, mode: 448 });
8327
8588
  assertStagingInsideHome(home, dir);
8328
- for (const path of [dirname8(dir), dir]) {
8589
+ for (const path of [dirname9(dir), dir]) {
8329
8590
  const st = lstatSync7(path);
8330
8591
  if (st.isSymbolicLink() || !st.isDirectory()) {
8331
8592
  throw new Error(`${path} is not a plain directory`);
@@ -8333,7 +8594,7 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
8333
8594
  }
8334
8595
  chmodSync5(dir, 448);
8335
8596
  for (const image of images) {
8336
- const path = join22(dir, `${randomUUID4()}.${ATTACHMENT_MEDIA_TYPES[image.mediaType]}`);
8597
+ const path = join23(dir, `${randomUUID4()}.${ATTACHMENT_MEDIA_TYPES[image.mediaType]}`);
8337
8598
  writeFileSync6(path, image.data, { mode: 384, flag: "wx" });
8338
8599
  written.push(path);
8339
8600
  }
@@ -8353,6 +8614,7 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
8353
8614
  if (!turn) return;
8354
8615
  this.turn = null;
8355
8616
  removeFiles(turn.files);
8617
+ this.fileChanges.clear();
8356
8618
  turn.stop?.resolve();
8357
8619
  }
8358
8620
  removeAllFiles() {
@@ -8399,7 +8661,11 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
8399
8661
  isError: true,
8400
8662
  durationMs: 0,
8401
8663
  numTurns: 1,
8402
- stopReason: err instanceof Error ? err.message : String(err)
8664
+ // The app-server's own words, broadcast like a failed turn's: scrubbed and kept short.
8665
+ stopReason: scrubSecrets(err instanceof Error ? err.message : String(err)).slice(
8666
+ 0,
8667
+ TURN_ERROR_MAX
8668
+ )
8403
8669
  });
8404
8670
  });
8405
8671
  }
@@ -8469,6 +8735,10 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
8469
8735
  const item = params.item;
8470
8736
  if (!item) return;
8471
8737
  const phase = method === "item/started" ? "started" : "completed";
8738
+ if (item.type === "fileChange") {
8739
+ if (phase === "started") this.setFiles(item.id, fileChanges(item));
8740
+ else this.fileChanges.delete(item.id);
8741
+ }
8472
8742
  if (phase === "completed" && this.streaming.delete(item.id)) {
8473
8743
  for (const event of stopEvents())
8474
8744
  this.emit("stream", { event, parentToolUseId: null, uuid: item.id });
@@ -8477,15 +8747,32 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
8477
8747
  if (row) this.emit("message", row);
8478
8748
  return;
8479
8749
  }
8480
- case "turn/completed":
8481
- case "turn/failed": {
8750
+ case "item/fileChange/patchUpdated":
8751
+ if (typeof params.itemId === "string" && this.fileChanges.has(params.itemId)) {
8752
+ this.setFiles(params.itemId, fileChanges(params));
8753
+ }
8754
+ return;
8755
+ case "serverRequest/resolved":
8756
+ for (const [id, entry] of [...this.approvals]) {
8757
+ if (entry.rpcId === params.requestId) this.withdraw(id);
8758
+ }
8759
+ return;
8760
+ case "turn/completed": {
8482
8761
  this.endTurn();
8483
- const status = turn?.status ?? params.status ?? (method === "turn/failed" ? "failed" : "completed");
8762
+ const ended = turn?.id ?? params.turnId;
8763
+ for (const [id, entry] of [...this.approvals]) {
8764
+ if (typeof ended !== "string" || entry.turnId === null || entry.turnId === ended) {
8765
+ this.withdraw(id);
8766
+ }
8767
+ }
8768
+ const status = turn?.status ?? params.status ?? "completed";
8769
+ const isError2 = status === "failed";
8484
8770
  this.emit("result", {
8485
- isError: status === "failed",
8771
+ isError: isError2,
8486
8772
  durationMs: turn?.durationMs ?? params.durationMs ?? 0,
8487
8773
  numTurns: 1,
8488
- stopReason: status
8774
+ // A failed turn says why, when the app-server did.
8775
+ stopReason: isError2 && failureText(turn?.error) || status
8489
8776
  });
8490
8777
  this.flush();
8491
8778
  return;
@@ -8494,6 +8781,45 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
8494
8781
  return;
8495
8782
  }
8496
8783
  }
8784
+ /**
8785
+ * The files of a `fileChange` item, as last told. A card already up for that item shows the
8786
+ * files as they were, and the roots check passed those: it is withdrawn and the approval
8787
+ * declined, since the patch an answer would now allow is not the one the owner is reading.
8788
+ */
8789
+ setFiles(itemId, changes) {
8790
+ const before = this.fileChanges.get(itemId);
8791
+ this.fileChanges.set(itemId, changes);
8792
+ if (before === void 0 || JSON.stringify(before) === JSON.stringify(changes)) return;
8793
+ for (const [id, entry] of [...this.approvals]) {
8794
+ if (entry.kind === "file_change" && entry.itemId === itemId) this.withdraw(id);
8795
+ }
8796
+ }
8797
+ /**
8798
+ * Take back a held request no answer can reach any more: nobody answered in time, its files
8799
+ * changed under the card, the app-server settled it itself, its turn ended, the session is
8800
+ * stopping, or the app-server is gone. The request is answered with its own refusal (an approval
8801
+ * declines; a question's response is its answers, of which there are none), and
8802
+ * `permissionTimeout` has the manager expire the card's row, after which an answer to it is
8803
+ * refused. One place, so no way of withdrawing can leave behind what another clears.
8804
+ */
8805
+ withdraw(id) {
8806
+ const entry = this.approvals.get(id);
8807
+ if (!entry) return;
8808
+ clearTimeout(entry.timer);
8809
+ this.approvals.delete(id);
8810
+ this.questionIds.delete(id);
8811
+ this.resolvers.get(id)?.(entry.refusal);
8812
+ this.resolvers.delete(id);
8813
+ this.emit("permissionTimeout", { id });
8814
+ }
8815
+ /**
8816
+ * Every held request, when nothing is left to answer: the session is stopping, the app-server
8817
+ * has exited, or it never opened a thread. The refusals are written if there is still a pipe to
8818
+ * write them to; into one that is gone they are dropped, without a throw (`rpc.test.ts`).
8819
+ */
8820
+ withdrawAll() {
8821
+ for (const id of [...this.approvals.keys()]) this.withdraw(id);
8822
+ }
8497
8823
  delta(params, kind, field) {
8498
8824
  const itemId = params.itemId ?? "";
8499
8825
  const text = params[field] ?? "";
@@ -8509,27 +8835,43 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
8509
8835
  * The reply is held until a human answers or the timer fires, which is what makes a Codex chat
8510
8836
  * behave like a Claude one — the SDK's `canUseTool` blocks the same way.
8511
8837
  */
8512
- onRequest(method, params) {
8513
- if (method === "tool/requestUserInput") return this.askQuestion(params);
8514
- const kind = APPROVAL_METHODS[method];
8515
- if (!kind) return "decline";
8516
- const toolName2 = kind === "command" ? CODEX_TOOL_NAMES.command : CODEX_TOOL_NAMES.fileChange;
8517
- const input = kind === "command" ? {
8518
- command: params.command ?? "",
8519
- cwd: params.cwd ?? this.opts.cwd,
8520
- ...params.reason ? { reason: params.reason } : {}
8521
- } : { changes: params.changes ?? [], ...params.reason ? { reason: params.reason } : {} };
8838
+ onRequest(rpcId, method, params) {
8839
+ const from = { rpcId, turnId: typeof params.turnId === "string" ? params.turnId : null };
8840
+ if (method === "item/tool/requestUserInput") return this.askQuestion(from, params);
8841
+ const kind = Object.hasOwn(APPROVAL_METHODS, method) ? APPROVAL_METHODS[method] : void 0;
8842
+ if (!kind) {
8843
+ if (Object.hasOwn(REFUSALS, method)) return REFUSALS[method];
8844
+ throw new RpcError(METHOD_NOT_FOUND, `cayenne does not handle ${method}`);
8845
+ }
8846
+ const itemId = typeof params.itemId === "string" ? params.itemId : null;
8847
+ const reason = params.reason ? { reason: params.reason } : {};
8848
+ let toolName2;
8849
+ let input;
8850
+ if (kind === "command") {
8851
+ toolName2 = CODEX_TOOL_NAMES.command;
8852
+ const { command, cwd } = params;
8853
+ if ((params.kind ?? "command") !== "command" || params.networkApprovalContext != null || typeof command !== "string" || command.trim() === "" || cwd != null && typeof cwd !== "string") {
8854
+ return decided("decline");
8855
+ }
8856
+ input = { command, cwd: cwd ?? this.opts.cwd, ...reason };
8857
+ } else {
8858
+ toolName2 = CODEX_TOOL_NAMES.fileChange;
8859
+ if (params.grantRoot != null) return decided("decline");
8860
+ const changes = (itemId ? this.fileChanges.get(itemId) : void 0) ?? [];
8861
+ if (changes.length === 0) return decided("decline");
8862
+ input = { changes, ...reason };
8863
+ }
8522
8864
  try {
8523
- this.opts.checkInput?.(toolName2, input);
8865
+ this.opts.checkInput(toolName2, input);
8524
8866
  } catch {
8525
- return "decline";
8867
+ return decided("decline");
8526
8868
  }
8527
8869
  const decision = autoDecide(this.mode, kind);
8528
- if (decision !== "ask") return decision;
8529
- return this.hold(this.nextApprovalId(kind), kind, "decline", {
8870
+ if (decision !== "ask") return decided(decision);
8871
+ return this.hold(this.nextApprovalId(kind), kind, decided("decline"), from, {
8530
8872
  toolName: toolName2,
8531
8873
  input,
8532
- toolUseId: params.itemId ?? null
8874
+ toolUseId: itemId
8533
8875
  });
8534
8876
  }
8535
8877
  /**
@@ -8539,15 +8881,20 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
8539
8881
  * same answer RPC. The shape is validated here, before a row exists — a malformed or oversized
8540
8882
  * ask is cancelled rather than rendered, because the questions are text the model produced and
8541
8883
  * the card is the one surface where a human is asked to trust what they read.
8542
- */
8543
- askQuestion(params) {
8544
- const parsed = CodexQuestions.safeParse(params);
8545
- if (!parsed.success) return Promise.resolve("cancel");
8884
+ *
8885
+ * The app-server's question (`{ id, header, question, options: { label, description }[] }`) is
8886
+ * not the one Cayenne stores (`CodexQuestions`: `{ text, options: string[] }`), and its answer is
8887
+ * keyed by question id where `CodexAnswers` is a list in order. The adapter translates both
8888
+ * ways, so the row, the card and the answer RPC are what they were.
8889
+ */
8890
+ askQuestion(from, params) {
8891
+ const asked = readQuestions(params);
8892
+ if (!asked) return Promise.resolve(NO_ANSWERS);
8546
8893
  const id = this.nextApprovalId("question");
8547
- this.questionCounts.set(id, parsed.data.questions.length);
8548
- return this.hold(id, "question", "cancel", {
8894
+ this.questionIds.set(id, asked.ids);
8895
+ return this.hold(id, "question", NO_ANSWERS, from, {
8549
8896
  toolName: CODEX_TOOL_NAMES.userInput,
8550
- input: parsed.data,
8897
+ input: asked.input,
8551
8898
  toolUseId: null
8552
8899
  });
8553
8900
  }
@@ -8564,20 +8911,21 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
8564
8911
  this.approvalSeq += 1;
8565
8912
  return `${kind}-${this.approvalSeq}`;
8566
8913
  }
8567
- /** Park a server request until a human answers it or the timer gives up on them. */
8568
- hold(id, kind, onTimeout, req) {
8914
+ /**
8915
+ * Park a server request until a human answers it, the timer gives up on them, or it is
8916
+ * withdrawn (`withdraw`).
8917
+ */
8918
+ hold(id, kind, onTimeout, from, req) {
8569
8919
  if (this.approvals.has(id) || this.resolvers.has(id)) {
8570
8920
  throw new Error(`VALIDATION: approval id ${id} is already held`);
8571
8921
  }
8572
8922
  return new Promise((resolve6) => {
8573
- const timer = setTimeout(() => {
8574
- this.approvals.delete(id);
8575
- this.resolvers.delete(id);
8576
- this.questionCounts.delete(id);
8577
- this.emit("permissionTimeout", { id });
8578
- resolve6(onTimeout);
8579
- }, this.opts.permissionTimeoutMs ?? PERMISSION_TIMEOUT_MS);
8580
- this.approvals.set(id, { rpcId: 0, kind, refusal: onTimeout, timer });
8923
+ const timer = setTimeout(
8924
+ () => this.withdraw(id),
8925
+ this.opts.permissionTimeoutMs ?? PERMISSION_TIMEOUT_MS
8926
+ );
8927
+ const itemId = kind === "question" ? null : req.toolUseId;
8928
+ this.approvals.set(id, { ...from, kind, refusal: onTimeout, itemId, timer });
8581
8929
  this.resolvers.set(id, resolve6);
8582
8930
  this.emit("permission", { id, ...req, suggestions: null });
8583
8931
  });
@@ -8588,20 +8936,24 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
8588
8936
  if (!entry || !resolve6) return false;
8589
8937
  let reply;
8590
8938
  if (entry.kind === "question") {
8591
- if (decision.behavior === "deny") reply = "cancel";
8939
+ if (decision.behavior === "deny") reply = NO_ANSWERS;
8592
8940
  else {
8941
+ const ids = this.questionIds.get(id) ?? [];
8593
8942
  const answers = CodexAnswers.safeParse(decision.updatedInput ?? {});
8594
- if (!answers.success || answers.data.answers.length !== this.questionCounts.get(id))
8595
- return false;
8596
- reply = { answers: answers.data.answers };
8943
+ if (!answers.success || answers.data.answers.length !== ids.length) return false;
8944
+ reply = {
8945
+ answers: Object.fromEntries(
8946
+ ids.map((questionId, n) => [questionId, { answers: [answers.data.answers[n] ?? ""] }])
8947
+ )
8948
+ };
8597
8949
  }
8598
8950
  } else {
8599
- reply = decision.behavior === "allow" ? "accept" : "decline";
8951
+ reply = decided(decision.behavior === "allow" ? "accept" : "decline");
8600
8952
  }
8601
8953
  clearTimeout(entry.timer);
8602
8954
  this.approvals.delete(id);
8603
8955
  this.resolvers.delete(id);
8604
- this.questionCounts.delete(id);
8956
+ this.questionIds.delete(id);
8605
8957
  resolve6(reply);
8606
8958
  return true;
8607
8959
  }
@@ -8657,13 +9009,7 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
8657
9009
  }
8658
9010
  async stop() {
8659
9011
  this.stopping = true;
8660
- for (const [id, entry] of [...this.approvals]) {
8661
- clearTimeout(entry.timer);
8662
- this.approvals.delete(id);
8663
- this.resolvers.get(id)?.(entry.refusal);
8664
- this.resolvers.delete(id);
8665
- this.emit("permissionTimeout", { id });
8666
- }
9012
+ this.withdrawAll();
8667
9013
  const stoppable = this.turn?.started && this.turn.id;
8668
9014
  await (stoppable ? this.interrupt().catch(() => void 0) : void 0);
8669
9015
  this.removeAllFiles();
@@ -8715,9 +9061,9 @@ var init_types = __esm({
8715
9061
 
8716
9062
  // src/skills/paths.ts
8717
9063
  import { realpathSync as realpathSync13 } from "fs";
8718
- import { isAbsolute as isAbsolute14, join as join23, relative as relative8, sep as sep11 } from "path";
9064
+ import { isAbsolute as isAbsolute14, join as join24, relative as relative8, sep as sep11 } from "path";
8719
9065
  function clearSessionSkills(home, runnerId, heldBy) {
8720
- clearRunnerStaging(home, join23(home, "session-skills"), runnerId, heldBy);
9066
+ clearRunnerStaging(home, join24(home, "session-skills"), runnerId, heldBy);
8721
9067
  }
8722
9068
  function assertInsideScopeDir(path, scopeDir) {
8723
9069
  let dirNow;
@@ -8742,8 +9088,8 @@ var init_paths3 = __esm({
8742
9088
  "use strict";
8743
9089
  init_paths();
8744
9090
  init_attachments2();
8745
- sessionSkillsRoot = (home, runnerId) => join23(home, "session-skills", runnerId);
8746
- sessionSkillsDir = (home, runnerId, sessionId) => join23(sessionSkillsRoot(home, runnerId), sessionId);
9091
+ sessionSkillsRoot = (home, runnerId) => join24(home, "session-skills", runnerId);
9092
+ sessionSkillsDir = (home, runnerId, sessionId) => join24(sessionSkillsRoot(home, runnerId), sessionId);
8747
9093
  }
8748
9094
  });
8749
9095
 
@@ -8882,7 +9228,7 @@ import {
8882
9228
  realpathSync as realpathSync14
8883
9229
  } from "fs";
8884
9230
  import { open as open2, opendir } from "fs/promises";
8885
- import { isAbsolute as isAbsolute15, join as join24, relative as relative9, sep as sep12 } from "path";
9231
+ import { isAbsolute as isAbsolute15, join as join25, relative as relative9, sep as sep12 } from "path";
8886
9232
  function inside2(root, candidate) {
8887
9233
  const rel = relative9(root, candidate);
8888
9234
  return rel === "" || !rel.startsWith(`..${sep12}`) && rel !== ".." && !isAbsolute15(rel);
@@ -8911,7 +9257,7 @@ function scopeRoots(opts) {
8911
9257
  const isGitRepo = opts.isGitRepo ?? defaultIsGitRepo;
8912
9258
  const home = canonical2(opts.home);
8913
9259
  const homes = protectedHomes(opts);
8914
- const found = [{ scope: "personal", dir: join24(home, "skills"), base: home }];
9260
+ const found = [{ scope: "personal", dir: join25(home, "skills"), base: home }];
8915
9261
  let skillsRepo;
8916
9262
  if (opts.skillsRepo !== void 0) {
8917
9263
  try {
@@ -8932,7 +9278,7 @@ function scopeRoots(opts) {
8932
9278
  continue;
8933
9279
  }
8934
9280
  if (!isGitRepo(real)) continue;
8935
- const dir = join24(real, ".cayenne", "skills");
9281
+ const dir = join25(real, ".cayenne", "skills");
8936
9282
  const shared = skillsRepo !== void 0 && real === skillsRepo ? "workspace" : "project";
8937
9283
  if (overlapsHome(canonical2(dir), homes, shared, log)) continue;
8938
9284
  if (skillsRepo !== void 0 && real === skillsRepo) {
@@ -8994,7 +9340,7 @@ async function scanSkills(opts) {
8994
9340
  } catch (err) {
8995
9341
  log.warn(
8996
9342
  {
8997
- path: join24(root.dirReal, entry.name),
9343
+ path: join25(root.dirReal, entry.name),
8998
9344
  scope: root.scope,
8999
9345
  err: err instanceof Error ? err.message : String(err)
9000
9346
  },
@@ -9039,13 +9385,13 @@ async function readAtMost2(fh, limit) {
9039
9385
  return buffer.subarray(0, filled);
9040
9386
  }
9041
9387
  async function readEntry(root, entry, log, budget) {
9042
- const path = join24(root.dirReal, entry.name);
9388
+ const path = join25(root.dirReal, entry.name);
9043
9389
  const skip = (reason) => {
9044
9390
  log.warn({ path, scope: root.scope }, `skill skipped: ${reason}`);
9045
9391
  return null;
9046
9392
  };
9047
9393
  if (entry.isDirectory()) {
9048
- const nested = existsSync5(join24(path, "SKILL.md"));
9394
+ const nested = existsSync5(join25(path, "SKILL.md"));
9049
9395
  return skip(
9050
9396
  nested ? `a directory holding SKILL.md: Cayenne's store is flat, so move it to ${entry.name}.md beside this directory` : "a directory, and the scan does not recurse"
9051
9397
  );
@@ -9205,7 +9551,7 @@ var init_scan2 = __esm({
9205
9551
  SKILL_SCAN_MAX_PER_DIR = 500;
9206
9552
  SKILL_SCAN_MAX_SKILLS = 500;
9207
9553
  SKILL_SCAN_MAX_BYTES = 32 * 1024 * 1024;
9208
- defaultIsGitRepo = (dir) => lstatSync8(join24(dir, ".git"), { throwIfNoEntry: false }) !== void 0;
9554
+ defaultIsGitRepo = (dir) => lstatSync8(join25(dir, ".git"), { throwIfNoEntry: false }) !== void 0;
9209
9555
  summariesOf = (scan) => scan.map((s) => s.summary);
9210
9556
  FRONTMATTER = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
9211
9557
  KEY = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(.*)$/;
@@ -9224,7 +9570,7 @@ import {
9224
9570
  rmSync as rmSync9
9225
9571
  } from "fs";
9226
9572
  import { open as open3, writeFile } from "fs/promises";
9227
- import { dirname as dirname9, isAbsolute as isAbsolute16, join as join25, relative as relative10, sep as sep13 } from "path";
9573
+ import { dirname as dirname10, isAbsolute as isAbsolute16, join as join26, relative as relative10, sep as sep13 } from "path";
9228
9574
  function isSkillRefusal(err) {
9229
9575
  const msg = err instanceof Error ? err.message : String(err);
9230
9576
  return msg.startsWith("ROOT_NOT_ALLOWED") || msg.startsWith(BUDGET) || msg.includes("is not a plain directory") || msg.includes("is not part of the session skills plugin");
@@ -9271,7 +9617,7 @@ async function materialiseSkills(scan, t) {
9271
9617
  }
9272
9618
  const path = plainDirectory(t.home, sessionSkillsDir(t.home, t.runnerId, t.sessionId));
9273
9619
  await writeFile(
9274
- join25(plainDirectory(t.home, join25(path, ".claude-plugin")), "plugin.json"),
9620
+ join26(plainDirectory(t.home, join26(path, ".claude-plugin")), "plugin.json"),
9275
9621
  `${JSON.stringify(
9276
9622
  {
9277
9623
  name: SKILL_PLUGIN_NAME,
@@ -9301,13 +9647,13 @@ async function materialiseSkills(scan, t) {
9301
9647
  );
9302
9648
  }
9303
9649
  try {
9304
- const dir = plainDirectory(t.home, join25(path, "skills", slug));
9305
- await writeFile(join25(dir, "SKILL.md"), bytes, { mode: 384, flag: "wx" });
9650
+ const dir = plainDirectory(t.home, join26(path, "skills", slug));
9651
+ await writeFile(join26(dir, "SKILL.md"), bytes, { mode: 384, flag: "wx" });
9306
9652
  slugs.push(slug);
9307
9653
  } catch (err) {
9308
9654
  note(log, err, { slug, scope, path: skill.path }, "skill not materialised for this session");
9309
9655
  try {
9310
- rmSync9(assertStagingInsideHome(t.home, join25(path, "skills", slug)), {
9656
+ rmSync9(assertStagingInsideHome(t.home, join26(path, "skills", slug)), {
9311
9657
  recursive: true,
9312
9658
  force: true
9313
9659
  });
@@ -9371,20 +9717,20 @@ function assertSealed(path, slugs) {
9371
9717
  const want = new Set(expected);
9372
9718
  for (const entry of entries2) {
9373
9719
  const ok = kind === "dir" ? entry.isDirectory() : entry.isFile();
9374
- if (!ok || !want.has(entry.name)) throw stray(join25(dir, entry.name));
9720
+ if (!ok || !want.has(entry.name)) throw stray(join26(dir, entry.name));
9375
9721
  }
9376
- if (entries2.length !== want.size) throw stray(join25(dir, [...want].join("|")));
9722
+ if (entries2.length !== want.size) throw stray(join26(dir, [...want].join("|")));
9377
9723
  };
9378
9724
  named(path, [".claude-plugin", "skills"], "dir");
9379
- named(join25(path, ".claude-plugin"), ["plugin.json"], "file");
9380
- named(join25(path, "skills"), slugs, "dir");
9381
- for (const slug of slugs) named(join25(path, "skills", slug), ["SKILL.md"], "file");
9725
+ named(join26(path, ".claude-plugin"), ["plugin.json"], "file");
9726
+ named(join26(path, "skills"), slugs, "dir");
9727
+ for (const slug of slugs) named(join26(path, "skills", slug), ["SKILL.md"], "file");
9382
9728
  }
9383
9729
  function plainDirectory(home, dir) {
9384
9730
  const intended = assertStagingInsideHome(home, dir);
9385
9731
  mkdirSync9(intended, { recursive: true, mode: 448 });
9386
9732
  const proved = assertStagingInsideHome(home, intended);
9387
- for (const p of [dirname9(proved), proved]) {
9733
+ for (const p of [dirname10(proved), proved]) {
9388
9734
  const st = lstatSync9(p);
9389
9735
  if (st.isSymbolicLink() || !st.isDirectory()) throw new Error(`${p} is not a plain directory`);
9390
9736
  }
@@ -9941,13 +10287,13 @@ var init_secret_files = __esm({
9941
10287
 
9942
10288
  // src/sessions/task-sessions.ts
9943
10289
  import { existsSync as existsSync6 } from "fs";
9944
- import { join as join26 } from "path";
10290
+ import { join as join27 } from "path";
9945
10291
  function createTaskSessions(dir) {
9946
10292
  const fileFor = (sessionId) => {
9947
10293
  if (!UUID3.test(sessionId)) {
9948
10294
  throw new Error(`VALIDATION: ${sessionId.slice(0, 40)} is not a session id`);
9949
10295
  }
9950
- return join26(dir, `${sessionId.toLowerCase()}.json`);
10296
+ return join27(dir, `${sessionId.toLowerCase()}.json`);
9951
10297
  };
9952
10298
  return {
9953
10299
  mark(sessionId) {
@@ -9968,10 +10314,10 @@ var init_task_sessions = __esm({
9968
10314
  });
9969
10315
 
9970
10316
  // src/sessions/user-guards.ts
9971
- import { closeSync as closeSync4, constants as constants4, fstatSync, openSync as openSync4, readFileSync as readFileSync10 } from "fs";
9972
- import { isAbsolute as isAbsolute20, join as join27 } from "path";
10317
+ import { closeSync as closeSync4, constants as constants4, fstatSync, openSync as openSync4, readFileSync as readFileSync11 } from "fs";
10318
+ import { isAbsolute as isAbsolute20, join as join28 } from "path";
9973
10319
  function readUserGuards(configDir) {
9974
- const settingsFile = join27(configDir, "settings.json");
10320
+ const settingsFile = join28(configDir, "settings.json");
9975
10321
  const none = { settingsFile, preToolUseHooks: 0, denyRules: 0 };
9976
10322
  if (!isAbsolute20(configDir)) return none;
9977
10323
  const text = readSmallFile(settingsFile);
@@ -10006,7 +10352,7 @@ function readSmallFile(path) {
10006
10352
  try {
10007
10353
  const stats = fstatSync(fd);
10008
10354
  if (!stats.isFile() || stats.size > USER_SETTINGS_MAX_BYTES) return null;
10009
- return readFileSync10(fd, "utf8");
10355
+ return readFileSync11(fd, "utf8");
10010
10356
  } catch {
10011
10357
  return null;
10012
10358
  } finally {
@@ -10040,7 +10386,7 @@ var init_user_guards = __esm({
10040
10386
  // src/sessions/manager.ts
10041
10387
  import { randomUUID as randomUUID5 } from "crypto";
10042
10388
  import { realpathSync as realpathSync17 } from "fs";
10043
- import { isAbsolute as isAbsolute21, join as join28, relative as relative14, sep as sep17 } from "path";
10389
+ import { isAbsolute as isAbsolute21, join as join29, relative as relative14, sep as sep17 } from "path";
10044
10390
  import { isDeepStrictEqual } from "util";
10045
10391
  function releaseCreds(creds) {
10046
10392
  if (creds?.state === "ok") creds.release();
@@ -10071,7 +10417,7 @@ function assertRepoOwner(ownerId, createdBy) {
10071
10417
  }
10072
10418
  function narrowToSession(suggestions) {
10073
10419
  if (!suggestions) return suggestions;
10074
- return suggestions.map((s) => isRecord(s) ? { ...s, destination: "session" } : s);
10420
+ return suggestions.map((s) => isRecord2(s) ? { ...s, destination: "session" } : s);
10075
10421
  }
10076
10422
  function shrink(event) {
10077
10423
  switch (event.type) {
@@ -10089,7 +10435,7 @@ function shrink(event) {
10089
10435
  return null;
10090
10436
  }
10091
10437
  }
10092
- var READ_ONLY_TEXT, REFRESH_WITHIN_MS, betweenTurns, UNREADABLE_MARK_CREDS, DRAINED_AFTER_MS, roundUsd, UNIQUE_VIOLATION, ROW_WRITE_TIMEOUT_MS, ORPHANED_STATUSES, RESTARTABLE_STATUSES, isAtOrBelow3, DENY_MESSAGE_MAX, EXIT_ERROR_MAX, INITIAL_PROMPT_MAX, TASK_SKILL_LOOKUP_MS, TASK_REFUSED_MODES, OWNER_DRIVEN_KINDS, REPO_OWNER_ONLY, REPO_NO_DUPLICATE, PROVIDER_SESSION_ID, REPO_WORKTREE_GONE, TASK_ASK_TOOLS, MEMORY_PREAMBLE, MEMORY_SOURCE, wrapMemory, PATH_INPUT_TOOLS, PATH_INPUT_KEYS, isRecord, errorCode, SessionManager;
10438
+ var READ_ONLY_TEXT, REFRESH_WITHIN_MS, betweenTurns, UNREADABLE_MARK_CREDS, DRAINED_AFTER_MS, roundUsd, UNIQUE_VIOLATION, ROW_WRITE_TIMEOUT_MS, ORPHANED_STATUSES, RESTARTABLE_STATUSES, isAtOrBelow3, DENY_MESSAGE_MAX, EXIT_ERROR_MAX, INITIAL_PROMPT_MAX, TASK_SKILL_LOOKUP_MS, TASK_REFUSED_MODES, OWNER_DRIVEN_KINDS, REPO_OWNER_ONLY, REPO_NO_DUPLICATE, PROVIDER_SESSION_ID, REPO_WORKTREE_GONE, TASK_ASK_TOOLS, MEMORY_PREAMBLE, MEMORY_SOURCE, wrapMemory, PATH_INPUT_TOOLS, PATH_INPUT_KEYS, isRecord2, errorCode, SessionManager;
10093
10439
  var init_manager = __esm({
10094
10440
  "src/sessions/manager.ts"() {
10095
10441
  "use strict";
@@ -10176,7 +10522,7 @@ var init_manager = __esm({
10176
10522
  "LS"
10177
10523
  ]);
10178
10524
  PATH_INPUT_KEYS = ["file_path", "path", "cwd", "notebook_path"];
10179
- isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
10525
+ isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
10180
10526
  errorCode = (message) => message.startsWith("ROOT_NOT_ALLOWED") ? "ROOT_NOT_ALLOWED" : "VALIDATION";
10181
10527
  SessionManager = class {
10182
10528
  constructor(deps) {
@@ -11173,6 +11519,7 @@ var init_manager = __esm({
11173
11519
  turns.drainedAt = Date.now();
11174
11520
  turns.early = true;
11175
11521
  turns.chased = false;
11522
+ turns.ended += 1;
11176
11523
  }
11177
11524
  /**
11178
11525
  * Put rows on this runner that claim a turn in progress back to `idle`, for the chats that have
@@ -11429,7 +11776,7 @@ var init_manager = __esm({
11429
11776
  const id = sessionId === void 0 ? null : this.live.get(sessionId)?.session.providerSessionId;
11430
11777
  if (projectDir === null || typeof id !== "string" || !PROVIDER_SESSION_ID.test(id))
11431
11778
  return [];
11432
- return [join28(projectDir, id, "tool-results")];
11779
+ return [join29(projectDir, id, "tool-results")];
11433
11780
  };
11434
11781
  const policy2 = this.settingsPolicy(createdBy, cwd, permissionMode, []);
11435
11782
  this.warnBypassWithoutUserGuards(createdBy, permissionMode, policy2, sessionId);
@@ -11477,7 +11824,7 @@ var init_manager = __esm({
11477
11824
  canonical3 = realpathSync17.native(configDir);
11478
11825
  } catch {
11479
11826
  }
11480
- return join28(canonical3, "projects", claudeProjectDirName(cwd));
11827
+ return join29(canonical3, "projects", claudeProjectDirName(cwd));
11481
11828
  }
11482
11829
  /** Whether one of `roots` contains the managed root, as an allowed root such as `~` can. */
11483
11830
  reachesManagedRoot(roots) {
@@ -11508,7 +11855,7 @@ var init_manager = __esm({
11508
11855
  return parsed.data;
11509
11856
  }
11510
11857
  taskSessions() {
11511
- return this.deps.taskSessions ?? createTaskSessions(join28(this.deps.cayenneHome ?? cayenneHome(), "task-sessions"));
11858
+ return this.deps.taskSessions ?? createTaskSessions(join29(this.deps.cayenneHome ?? cayenneHome(), "task-sessions"));
11512
11859
  }
11513
11860
  markTaskSession(sessionId) {
11514
11861
  try {
@@ -11690,15 +12037,31 @@ var init_manager = __esm({
11690
12037
  /**
11691
12038
  * A tool's path as the provider opens it: relative means relative to the session's cwd. Every
11692
12039
  * judge of a tool's path goes through here (R28's `checkUpdatedInput`, a suggested directory,
11693
- * Codex's changes and command cwd). A value the Claude CLI would rewrite before it opens it
11694
- * (surrounding whitespace, a leading `~`, a non-NFC spelling) is refused, not judged: judged as
11695
- * written, `~/.ssh/id_rsa` was `<cwd>/~/.ssh/id_rsa`, inside any root (security re-review
11696
- * 2026-09-29).
12040
+ * and Codex's paths by way of `codexPath`, which admits no relative one). A value the Claude CLI
12041
+ * would rewrite before it opens it (surrounding whitespace, a leading `~`, a non-NFC spelling)
12042
+ * is refused, not judged: judged as written, `~/.ssh/id_rsa` was `<cwd>/~/.ssh/id_rsa`, inside
12043
+ * any root (security re-review 2026-09-29).
11697
12044
  */
11698
12045
  fromCwd(cwd, value) {
11699
12046
  if (!isPlainToolPath(value)) throw new Error(notPlainPathRefusal(value));
11700
12047
  return isAbsolute21(value) ? value : `${cwd}${sep17}${value}`;
11701
12048
  }
12049
+ /**
12050
+ * A path Codex named, for the roots check: absolute, or refused. The runner cannot prove what
12051
+ * directory the app-server would resolve a relative one against: a turn's cwd is the
12052
+ * app-server's, and a command can move it. Every live run against codex-cli 0.162 named
12053
+ * absolute paths only, so a relative one is an unknown shape, refused exactly as an
12054
+ * out-of-roots path is. An absolute value still passes through `fromCwd`, which refuses one
12055
+ * that is not a plain path.
12056
+ */
12057
+ codexPath(cwd, value) {
12058
+ if (!isAbsolute21(value)) {
12059
+ throw new Error(
12060
+ `ROOT_NOT_ALLOWED: ${JSON.stringify(value)} is not an absolute path; a Codex change or command names every path by its absolute path`
12061
+ );
12062
+ }
12063
+ return this.fromCwd(cwd, value);
12064
+ }
11702
12065
  /**
11703
12066
  * §2.9: a path in `updatedInput` passes the same boundary the session's `cwd` did, and the
11704
12067
  * SDK receives the canonical path the guard proved rather than the spelling typed (S2). The
@@ -11721,6 +12084,11 @@ var init_manager = __esm({
11721
12084
  * not admit is denied before a row exists: no answer the owner could give would make it
11722
12085
  * acceptable, exactly as R28 reasons for the Claude path.
11723
12086
  *
12087
+ * A change writes its `path` and, when it moves the file, its `movePath`: both are read here,
12088
+ * each as a path of its own, so the card and the auto-accept gate judge the same set. A command
12089
+ * must name its `cwd`: absent, blank or not a string, the command is refused, not passed.
12090
+ * Every path, and that `cwd`, must be absolute (`codexPath`).
12091
+ *
11724
12092
  * When the roots reach the managed root (D50: an allowed root such as `~`), a file change is
11725
12093
  * also held to the managed `.git` guard Claude sessions get: a change under a managed clone's
11726
12094
  * git dir, or to a worktree's `.git` file, is refused with the same text. In `acceptEdits` or
@@ -11733,19 +12101,30 @@ var init_manager = __esm({
11733
12101
  throw new Error("VALIDATION: codex file change carries no changes to check");
11734
12102
  }
11735
12103
  const guarded = this.reachesManagedRoot(roots);
12104
+ const check = (path) => {
12105
+ assertInsideRootsOrAncestor(this.codexPath(cwd, path), roots);
12106
+ if (guarded && writesManagedGit(path, this.deps.managedRoot, cwd)) {
12107
+ throw new Error(MANAGED_GIT_REFUSAL);
12108
+ }
12109
+ };
11736
12110
  for (const change of changes) {
11737
- if (!isRecord(change) || typeof change.path !== "string" || change.path.trim() === "") {
12111
+ if (!isRecord2(change) || typeof change.path !== "string" || change.path.trim() === "") {
11738
12112
  throw new Error("VALIDATION: codex file change entry names no path to check");
11739
12113
  }
11740
- assertInsideRootsOrAncestor(this.fromCwd(cwd, change.path), roots);
11741
- if (guarded && writesManagedGit(change.path, this.deps.managedRoot, cwd)) {
11742
- throw new Error(MANAGED_GIT_REFUSAL);
12114
+ check(change.path);
12115
+ if (change.movePath === void 0) continue;
12116
+ if (typeof change.movePath !== "string" || change.movePath.trim() === "") {
12117
+ throw new Error("VALIDATION: codex file change entry names no move target to check");
11743
12118
  }
12119
+ check(change.movePath);
11744
12120
  }
11745
12121
  return;
11746
12122
  }
11747
- if (toolName2 === CODEX_TOOL_NAMES.command && typeof input.cwd === "string") {
11748
- assertInsideRoots(this.fromCwd(cwd, input.cwd), roots);
12123
+ if (toolName2 === CODEX_TOOL_NAMES.command) {
12124
+ if (typeof input.cwd !== "string" || input.cwd.trim() === "") {
12125
+ throw new Error("VALIDATION: codex command names no cwd to check");
12126
+ }
12127
+ assertInsideRoots(this.codexPath(cwd, input.cwd), roots);
11749
12128
  }
11750
12129
  }
11751
12130
  /**
@@ -11818,7 +12197,7 @@ var init_manager = __esm({
11818
12197
  checkUpdatedPermissions(live, entry, updates) {
11819
12198
  if (!updates) return void 0;
11820
12199
  return updates.map((update) => {
11821
- if (!isRecord(update))
12200
+ if (!isRecord2(update))
11822
12201
  throw new Error("VALIDATION: updatedPermissions entry is not an object");
11823
12202
  if (update.destination !== "session") {
11824
12203
  throw new Error(
@@ -12004,9 +12383,19 @@ var init_manager = __esm({
12004
12383
  status: "starting",
12005
12384
  echoed: /* @__PURE__ */ new Set(),
12006
12385
  permissions: /* @__PURE__ */ new Map(),
12386
+ permissionsInFlight: /* @__PURE__ */ new Set(),
12387
+ permissionsWithdrawn: /* @__PURE__ */ new Set(),
12007
12388
  cwd: opts.cwd,
12008
12389
  stopping: false,
12009
- turns: { owed: 0, begun: false, drainedAt: null, stops: 0, early: false, chased: false },
12390
+ turns: {
12391
+ owed: 0,
12392
+ begun: false,
12393
+ drainedAt: null,
12394
+ stops: 0,
12395
+ early: false,
12396
+ chased: false,
12397
+ ended: 0
12398
+ },
12010
12399
  dropping: null,
12011
12400
  inflight: /* @__PURE__ */ new Set(),
12012
12401
  writes: Promise.resolve(),
@@ -12089,7 +12478,7 @@ var init_manager = __esm({
12089
12478
  */
12090
12479
  watchWorkingCopy(sessionId, live) {
12091
12480
  const factory = this.deps.watch ?? ((cwd, onChange2, onError2) => watchChanges(cwd, onChange2, CHANGES_DEBOUNCE_MS, onError2));
12092
- let failed4 = false;
12481
+ let failed5 = false;
12093
12482
  let built = null;
12094
12483
  const closeFailed = (watcher) => {
12095
12484
  if (live.watcher === watcher) live.watcher = null;
@@ -12098,8 +12487,8 @@ var init_manager = __esm({
12098
12487
  );
12099
12488
  };
12100
12489
  const onError = (err) => {
12101
- if (failed4) return;
12102
- failed4 = true;
12490
+ if (failed5) return;
12491
+ failed5 = true;
12103
12492
  this.deps.log.warn(
12104
12493
  { err, sessionId },
12105
12494
  "working-copy watcher error; live change refresh stopped"
@@ -12107,7 +12496,7 @@ var init_manager = __esm({
12107
12496
  if (built) closeFailed(built);
12108
12497
  };
12109
12498
  const onChange = () => {
12110
- if (failed4 || this.live.get(sessionId) !== live) return;
12499
+ if (failed5 || this.live.get(sessionId) !== live) return;
12111
12500
  this.bumpChanges(sessionId, live);
12112
12501
  };
12113
12502
  try {
@@ -12116,7 +12505,7 @@ var init_manager = __esm({
12116
12505
  this.deps.log.warn({ err, sessionId }, "working-copy watcher failed to start");
12117
12506
  return null;
12118
12507
  }
12119
- if (failed4) {
12508
+ if (failed5) {
12120
12509
  closeFailed(built);
12121
12510
  return null;
12122
12511
  }
@@ -12218,89 +12607,95 @@ var init_manager = __esm({
12218
12607
  );
12219
12608
  })();
12220
12609
  });
12610
+ const raise = async (rawReq) => {
12611
+ const req = { ...rawReq, suggestions: narrowToSession(rawReq.suggestions) };
12612
+ try {
12613
+ this.checkUpdatedInput(live, req.toolName, req.input);
12614
+ this.checkCodexInput(live.cwd, live.roots, req.toolName, req.input);
12615
+ } catch (err) {
12616
+ const message = this.cap(err instanceof Error ? err.message : String(err));
12617
+ session.respondPermission(req.id, { behavior: "deny", message, interrupt: false });
12618
+ this.deps.log.warn(
12619
+ { sessionId, tool: req.toolName, err: message },
12620
+ "permission request refused before a row existed"
12621
+ );
12622
+ await this.broadcast(sessionId, { type: "error", code: errorCode(message), message });
12623
+ return;
12624
+ }
12625
+ const input = this.secrets.redact(req.input);
12626
+ const suggestions = this.secrets.redact(req.suggestions);
12627
+ const { data, error } = await this.deps.db.from("permission_requests").insert({
12628
+ session_id: sessionId,
12629
+ workspace_id: this.deps.workspaceId,
12630
+ tool_use_id: req.toolUseId,
12631
+ tool_name: req.toolName,
12632
+ input,
12633
+ suggestions
12634
+ }).select("*").single();
12635
+ if (error || !data) {
12636
+ const message = this.cap(
12637
+ `VALIDATION: could not persist the permission request for ${req.toolName}: ${error?.message ?? "no row"}`
12638
+ );
12639
+ session.respondPermission(req.id, { behavior: "deny", message, interrupt: false });
12640
+ await this.broadcast(sessionId, { type: "error", code: "VALIDATION", message });
12641
+ throw new Error(message);
12642
+ }
12643
+ if (live.permissionsWithdrawn.has(req.id)) {
12644
+ await this.expirePermission(sessionId, data.id);
12645
+ return;
12646
+ }
12647
+ live.permissions.set(data.id, {
12648
+ localId: req.id,
12649
+ toolName: req.toolName,
12650
+ input,
12651
+ suggestions
12652
+ });
12653
+ await this.setStatus(sessionId, "awaiting_permission");
12654
+ if (!live.permissions.has(data.id)) return;
12655
+ try {
12656
+ await this.broadcast(sessionId, {
12657
+ type: "permission.request",
12658
+ request: data
12659
+ });
12660
+ } catch (err) {
12661
+ if (!(err instanceof Error) || !err.message.startsWith("VALIDATION:")) throw err;
12662
+ this.deps.log.warn(
12663
+ { sessionId, requestId: data.id, err: err.message },
12664
+ "permission request too large to broadcast; clients reconcile from the row"
12665
+ );
12666
+ await this.broadcast(sessionId, {
12667
+ type: "error",
12668
+ code: "VALIDATION",
12669
+ message: err.message
12670
+ });
12671
+ }
12672
+ };
12221
12673
  session.on(
12222
12674
  "permission",
12223
12675
  (rawReq) => guard(async () => {
12224
- const req = { ...rawReq, suggestions: narrowToSession(rawReq.suggestions) };
12225
- try {
12226
- this.checkUpdatedInput(live, req.toolName, req.input);
12227
- this.checkCodexInput(live.cwd, live.roots, req.toolName, req.input);
12228
- } catch (err) {
12229
- const message = this.cap(err instanceof Error ? err.message : String(err));
12230
- session.respondPermission(req.id, { behavior: "deny", message, interrupt: false });
12231
- this.deps.log.warn(
12232
- { sessionId, tool: req.toolName, err: message },
12233
- "permission request refused before a row existed"
12234
- );
12235
- await this.broadcast(sessionId, { type: "error", code: errorCode(message), message });
12236
- return;
12237
- }
12238
- const input = this.secrets.redact(req.input);
12239
- const suggestions = this.secrets.redact(req.suggestions);
12240
- const { data, error } = await this.deps.db.from("permission_requests").insert({
12241
- session_id: sessionId,
12242
- workspace_id: this.deps.workspaceId,
12243
- tool_use_id: req.toolUseId,
12244
- tool_name: req.toolName,
12245
- input,
12246
- suggestions
12247
- }).select("*").single();
12248
- if (error || !data) {
12249
- const message = this.cap(
12250
- `VALIDATION: could not persist the permission request for ${req.toolName}: ${error?.message ?? "no row"}`
12251
- );
12252
- session.respondPermission(req.id, { behavior: "deny", message, interrupt: false });
12253
- await this.broadcast(sessionId, { type: "error", code: "VALIDATION", message });
12254
- throw new Error(message);
12255
- }
12256
- live.permissions.set(data.id, {
12257
- localId: req.id,
12258
- toolName: req.toolName,
12259
- input,
12260
- suggestions
12261
- });
12262
- await this.setStatus(sessionId, "awaiting_permission");
12676
+ live.permissionsInFlight.add(rawReq.id);
12263
12677
  try {
12264
- await this.broadcast(sessionId, {
12265
- type: "permission.request",
12266
- request: data
12267
- });
12268
- } catch (err) {
12269
- if (!(err instanceof Error) || !err.message.startsWith("VALIDATION:")) throw err;
12270
- this.deps.log.warn(
12271
- { sessionId, requestId: data.id, err: err.message },
12272
- "permission request too large to broadcast; clients reconcile from the row"
12273
- );
12274
- await this.broadcast(sessionId, {
12275
- type: "error",
12276
- code: "VALIDATION",
12277
- message: err.message
12278
- });
12678
+ await raise(rawReq);
12679
+ } finally {
12680
+ live.permissionsInFlight.delete(rawReq.id);
12681
+ live.permissionsWithdrawn.delete(rawReq.id);
12279
12682
  }
12280
12683
  })()
12281
12684
  );
12282
12685
  session.on(
12283
12686
  "permissionTimeout",
12284
12687
  ({ id }) => guard(async () => {
12688
+ const ended = live.turns.ended;
12285
12689
  const requestId = [...live.permissions.entries()].find(
12286
12690
  ([, entry]) => entry.localId === id
12287
12691
  )?.[0];
12288
- if (!requestId) return;
12289
- live.permissions.delete(requestId);
12290
- const { data, error } = await this.deps.db.from("permission_requests").update({ status: "expired" }).eq("id", requestId).eq("status", "pending").select("id");
12291
- if (error) {
12292
- this.deps.log.warn(
12293
- { requestId, err: error.message },
12294
- "could not expire permission request"
12295
- );
12296
- } else if (data && data.length > 0) {
12297
- await this.broadcast(sessionId, {
12298
- type: "permission.resolved",
12299
- requestId,
12300
- status: "expired"
12301
- });
12692
+ if (!requestId) {
12693
+ if (live.permissionsInFlight.has(id)) live.permissionsWithdrawn.add(id);
12694
+ return;
12302
12695
  }
12303
- if (live.permissions.size === 0 && !live.stopping) {
12696
+ live.permissions.delete(requestId);
12697
+ await this.expirePermission(sessionId, requestId);
12698
+ if (live.permissions.size === 0 && !live.stopping && live.status === "awaiting_permission" && live.turns.ended === ended) {
12304
12699
  await this.setStatus(sessionId, "running");
12305
12700
  }
12306
12701
  })()
@@ -12343,6 +12738,26 @@ var init_manager = __esm({
12343
12738
  })();
12344
12739
  });
12345
12740
  }
12741
+ /**
12742
+ * Expire a request the session settled itself, and say so when this write is what settled the
12743
+ * row. `permissions_expire_by_runner` is the one transition the runner may write. Filtered on
12744
+ * status = 'pending' so an owner answer in the same instant wins: the row is then terminal and
12745
+ * this matches nothing rather than failing. A write that fails is logged, never thrown.
12746
+ */
12747
+ async expirePermission(sessionId, requestId) {
12748
+ const { data, error } = await this.deps.db.from("permission_requests").update({ status: "expired" }).eq("id", requestId).eq("status", "pending").select("id");
12749
+ if (error) {
12750
+ this.deps.log.warn({ requestId, err: error.message }, "could not expire permission request");
12751
+ return;
12752
+ }
12753
+ if (data && data.length > 0) {
12754
+ await this.broadcast(sessionId, {
12755
+ type: "permission.resolved",
12756
+ requestId,
12757
+ status: "expired"
12758
+ });
12759
+ }
12760
+ }
12346
12761
  /**
12347
12762
  * Forget `live` and release its channel — once, and only if it is still the current entry.
12348
12763
  *
@@ -12584,10 +12999,10 @@ var init_manager = __esm({
12584
12999
  });
12585
13000
 
12586
13001
  // src/sessions/repo-sessions.ts
12587
- import { readdirSync as readdirSync5, readFileSync as readFileSync11 } from "fs";
12588
- import { join as join29 } from "path";
13002
+ import { readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
13003
+ import { join as join30 } from "path";
12589
13004
  function createRepoSessions(dir) {
12590
- const fileFor = (sessionId) => UUID4.test(sessionId) ? join29(dir, `${sessionId.toLowerCase()}.json`) : null;
13005
+ const fileFor = (sessionId) => UUID4.test(sessionId) ? join30(dir, `${sessionId.toLowerCase()}.json`) : null;
12591
13006
  return {
12592
13007
  mark(sessionId, mark) {
12593
13008
  const file = fileFor(sessionId);
@@ -12607,7 +13022,7 @@ function createRepoSessions(dir) {
12607
13022
  if (file === null) return null;
12608
13023
  let text;
12609
13024
  try {
12610
- text = readFileSync11(file, "utf8");
13025
+ text = readFileSync12(file, "utf8");
12611
13026
  } catch (err) {
12612
13027
  return isMissing(err) ? null : "unreadable";
12613
13028
  }
@@ -12991,9 +13406,9 @@ function createTerminalChannels({
12991
13406
  const inflight = joining.get(terminalId);
12992
13407
  if (inflight) return inflight;
12993
13408
  if (joined.has(terminalId)) return;
12994
- const join32 = attachOne(terminalId).finally(() => joining.delete(terminalId));
12995
- joining.set(terminalId, join32);
12996
- return join32;
13409
+ const join33 = attachOne(terminalId).finally(() => joining.delete(terminalId));
13410
+ joining.set(terminalId, join33);
13411
+ return join33;
12997
13412
  },
12998
13413
  flush,
12999
13414
  detach,
@@ -13054,9 +13469,9 @@ var init_channels = __esm({
13054
13469
  });
13055
13470
 
13056
13471
  // src/terminal/env.ts
13057
- import { readFileSync as readFileSync12, realpathSync as realpathSync19 } from "fs";
13472
+ import { readFileSync as readFileSync13, realpathSync as realpathSync19 } from "fs";
13058
13473
  import { lstat } from "fs/promises";
13059
- import { dirname as dirname10, isAbsolute as isAbsolute22, relative as relative15 } from "path";
13474
+ import { dirname as dirname11, isAbsolute as isAbsolute22, relative as relative15 } from "path";
13060
13475
  function terminalEnv(base, { runnerOwner, claudeConfigDir }) {
13061
13476
  const env = {};
13062
13477
  for (const [key, value] of Object.entries(base)) {
@@ -13115,7 +13530,7 @@ async function rootOwnedPath(fs, real) {
13115
13530
  return false;
13116
13531
  }
13117
13532
  if (st.isSymbolicLink() || st.uid !== 0 || (st.mode & 18) !== 0) return false;
13118
- const parent = dirname10(path);
13533
+ const parent = dirname11(path);
13119
13534
  if (parent === path) return true;
13120
13535
  path = parent;
13121
13536
  }
@@ -13154,7 +13569,7 @@ var init_env2 = __esm({
13154
13569
  nodeShellFs = {
13155
13570
  readEtcShells: () => {
13156
13571
  try {
13157
- return readFileSync12("/etc/shells", "utf8");
13572
+ return readFileSync13("/etc/shells", "utf8");
13158
13573
  } catch {
13159
13574
  return "";
13160
13575
  }
@@ -14200,7 +14615,7 @@ __export(start_exports, {
14200
14615
  start: () => start
14201
14616
  });
14202
14617
  import { homedir as homedir6, hostname } from "os";
14203
- import { join as join30 } from "path";
14618
+ import { join as join31 } from "path";
14204
14619
  function protectedPaths(claudeConfigDir, home = homedir6()) {
14205
14620
  const inHome = [
14206
14621
  ".claude",
@@ -14208,12 +14623,12 @@ function protectedPaths(claudeConfigDir, home = homedir6()) {
14208
14623
  ".codex",
14209
14624
  ".ssh",
14210
14625
  ".aws",
14211
- join30(".config", "gh"),
14626
+ join31(".config", "gh"),
14212
14627
  ".gnupg",
14213
14628
  ".git-credentials",
14214
14629
  ".npmrc",
14215
14630
  ".netrc"
14216
- ].map((p) => join30(home, p));
14631
+ ].map((p) => join31(home, p));
14217
14632
  return [cayenneHome(), ...claudeConfigDir ? [claudeConfigDir] : [], ...inHome];
14218
14633
  }
14219
14634
  function withRepoClone(repoClone) {
@@ -14229,6 +14644,16 @@ function logCodex(codex) {
14229
14644
  `codex available on ${codex.models.map((m) => m.id).join(", ") || "its default model"}`
14230
14645
  );
14231
14646
  }
14647
+ function logClaude(claude) {
14648
+ if (!claude.available) {
14649
+ logger.info({ reason: claude.reason }, `claude ${claude.reason ?? "unavailable"}`);
14650
+ return;
14651
+ }
14652
+ logger.info(
14653
+ { version: claude.version, models: claude.models.map((m) => m.id), authMode: claude.authMode },
14654
+ `claude available on ${claude.models.map((m) => m.id).join(", ") || "the curated model list"}`
14655
+ );
14656
+ }
14232
14657
  function holdLock(root, record) {
14233
14658
  let held;
14234
14659
  return {
@@ -14321,7 +14746,6 @@ async function compose(cfg, lock, host, preflight) {
14321
14746
  );
14322
14747
  });
14323
14748
  const codexPath = resolveCodexPath(cfg, process.env, () => findCodexOnPath(process.env));
14324
- const detect = () => detectProviders({ path: codexPath, run: runCodex, listModels: listCodexModels });
14325
14749
  let providers = {
14326
14750
  claude: { available: true, models: [] },
14327
14751
  codex: { available: false, reason: "not_installed", models: [] }
@@ -14329,9 +14753,23 @@ async function compose(cfg, lock, host, preflight) {
14329
14753
  const pty = await loadPty();
14330
14754
  const memberTerminalIds = memberTerminalIdsOf(cfg);
14331
14755
  const kinds2 = handledKinds({ terminal: pty !== null });
14332
- const runDir = join30(cayenneDir(), "run");
14756
+ const runDir = join31(cayenneDir(), "run");
14333
14757
  prepareRunDir(runDir, { git: findOnPath("git", process.env.PATH) });
14334
14758
  const gitHome = repositoryGitHome(runDir);
14759
+ const claudeProbeEnv = claudeEnv(claudeConfigDir);
14760
+ const detect = () => detectProviders({
14761
+ path: codexPath,
14762
+ run: runCodex,
14763
+ listModels: listCodexModels,
14764
+ claude: () => detectClaude({
14765
+ version: claudeCodeVersion,
14766
+ probe: () => probeClaude({
14767
+ env: claudeProbeEnv,
14768
+ cwd: runDir,
14769
+ settingSources: cfg.loadUserSettings ? ["user"] : []
14770
+ })
14771
+ })
14772
+ });
14335
14773
  for (const path of githubNetrcFiles()) {
14336
14774
  logger.warn(
14337
14775
  { path },
@@ -14411,6 +14849,7 @@ async function compose(cfg, lock, host, preflight) {
14411
14849
  });
14412
14850
  const registered = capabilities();
14413
14851
  let loggedCodex = null;
14852
+ let loggedClaude = null;
14414
14853
  const refresh = async () => {
14415
14854
  providers = await detect();
14416
14855
  const line = JSON.stringify(providers.codex);
@@ -14418,6 +14857,11 @@ async function compose(cfg, lock, host, preflight) {
14418
14857
  loggedCodex = line;
14419
14858
  logCodex(providers.codex);
14420
14859
  }
14860
+ const claudeLine = JSON.stringify(providers.claude);
14861
+ if (claudeLine !== loggedClaude) {
14862
+ loggedClaude = claudeLine;
14863
+ logClaude(providers.claude);
14864
+ }
14421
14865
  return capabilities();
14422
14866
  };
14423
14867
  let identity;
@@ -14478,15 +14922,15 @@ async function compose(cfg, lock, host, preflight) {
14478
14922
  const trustedRoots = trustedRepoRoots(cfg.trustRepoSettings);
14479
14923
  if (!cfg.loadUserSettings) {
14480
14924
  const guards = readUserGuards(sessionClaudeConfigDir(claudeEnv(claudeConfigDir)));
14481
- const warning = userGuardsWarning(guards, join30(cayenneDir(), "config.json"));
14925
+ const warning = userGuardsWarning(guards, join31(cayenneDir(), "config.json"));
14482
14926
  if (warning) {
14483
14927
  const { settingsFile, preToolUseHooks, denyRules } = guards;
14484
14928
  logger.warn({ settingsFile, preToolUseHooks, denyRules }, warning);
14485
14929
  }
14486
14930
  }
14487
- const repoSessions = createRepoSessions(join30(cayenneHome(), "repo-sessions"));
14488
- const worktreeOwners = createWorktreeOwners(join30(cayenneHome(), "worktrees"));
14489
- const sessionWorktrees = createSessionWorktrees(join30(cayenneHome(), "session-worktrees"));
14931
+ const repoSessions = createRepoSessions(join31(cayenneHome(), "repo-sessions"));
14932
+ const worktreeOwners = createWorktreeOwners(join31(cayenneHome(), "worktrees"));
14933
+ const sessionWorktrees = createSessionWorktrees(join31(cayenneHome(), "session-worktrees"));
14490
14934
  const git = createGitOps({
14491
14935
  protectedPaths: protectedPaths(claudeConfigDir),
14492
14936
  home: homedir6(),
@@ -14514,7 +14958,7 @@ async function compose(cfg, lock, host, preflight) {
14514
14958
  worktreeOwners,
14515
14959
  sessionWorktrees,
14516
14960
  // And which sessions were started from a task, so their secret-file guard outlives a restart.
14517
- taskSessions: createTaskSessions(join30(cayenneHome(), "task-sessions")),
14961
+ taskSessions: createTaskSessions(join31(cayenneHome(), "task-sessions")),
14518
14962
  // The paths no `git.diff` returns are paths no task session reads (protocol §2.8).
14519
14963
  protectedPaths: protectedPaths(claudeConfigDir),
14520
14964
  // D50: where repository chats run, which of them are, and their credentials' run dir. A
@@ -14873,9 +15317,10 @@ var init_start = __esm({
14873
15317
  "src/start.ts"() {
14874
15318
  "use strict";
14875
15319
  init_src();
15320
+ init_detect();
14876
15321
  init_claude_dir();
14877
15322
  init_cloud();
14878
- init_detect();
15323
+ init_detect2();
14879
15324
  init_dispatcher();
14880
15325
  init_source();
14881
15326
  init_config();
@@ -14918,9 +15363,9 @@ var init_start = __esm({
14918
15363
  });
14919
15364
 
14920
15365
  // src/cli.ts
14921
- import { existsSync as existsSync7, mkdirSync as mkdirSync10, readFileSync as readFileSync13, realpathSync as realpathSync20, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "fs";
15366
+ import { existsSync as existsSync7, mkdirSync as mkdirSync10, readFileSync as readFileSync14, realpathSync as realpathSync20, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "fs";
14922
15367
  import { homedir as homedir7, hostname as hostname2, userInfo } from "os";
14923
- import { dirname as dirname11, join as join31 } from "path";
15368
+ import { dirname as dirname12, join as join32 } from "path";
14924
15369
  import { Command } from "commander";
14925
15370
  import prompts from "prompts";
14926
15371
 
@@ -15418,14 +15863,14 @@ var configForWrite = () => {
15418
15863
  });
15419
15864
  return cfg;
15420
15865
  };
15421
- var WEB_URL = "https://cayenne-pink.vercel.app";
15866
+ var WEB_URL = "https://app.cayenne.sh";
15422
15867
  var PKG = "@jpcasabiancai/cayenne";
15423
15868
  var service = () => {
15424
15869
  const exec4 = nodeExec();
15425
15870
  return { exec: exec4, launchd: makeLaunchd(exec4, userInfo().uid), label: plistLabel(currentProfile()) };
15426
15871
  };
15427
15872
  var writePlistFile = (path, xml2) => {
15428
- mkdirSync10(dirname11(path), { recursive: true });
15873
+ mkdirSync10(dirname12(path), { recursive: true });
15429
15874
  writeFileSync7(path, xml2, { mode: 420 });
15430
15875
  };
15431
15876
  var requireMac = () => {
@@ -15574,7 +16019,7 @@ program.command("doctor").description("Check the install and say how to fix what
15574
16019
  if (checks.some((c) => !c.ok)) process.exitCode = 1;
15575
16020
  });
15576
16021
  program.command("logs").description("Show the service log").option("-f, --follow", "keep streaming").action(async (opts) => {
15577
- const file = join31(cayenneDir(), "logs", "runner.log");
16022
+ const file = join32(cayenneDir(), "logs", "runner.log");
15578
16023
  if (!existsSync7(file)) throw new Error(`no log yet at ${file}. Run \`cayenne up\` first.`);
15579
16024
  const print = (chunk) => {
15580
16025
  for (const line of chunk.split("\n")) {
@@ -15587,12 +16032,12 @@ program.command("logs").description("Show the service log").option("-f, --follow
15587
16032
  }
15588
16033
  }
15589
16034
  };
15590
- print(readFileSync13(file, "utf8"));
16035
+ print(readFileSync14(file, "utf8"));
15591
16036
  if (opts.follow) {
15592
16037
  const { watch: watch2 } = await import("fs");
15593
- let size = readFileSync13(file).length;
16038
+ let size = readFileSync14(file).length;
15594
16039
  watch2(file, () => {
15595
- const buf = readFileSync13(file);
16040
+ const buf = readFileSync14(file);
15596
16041
  if (buf.length > size) print(buf.subarray(size).toString("utf8"));
15597
16042
  size = buf.length;
15598
16043
  });
@@ -15620,7 +16065,7 @@ program.command("update").description("Re-pin the installed app and restart the
15620
16065
  cliPath: record.cliPath,
15621
16066
  profile: currentProfile(),
15622
16067
  pathEnv: record.pathEnv,
15623
- logDir: join31(dir, "logs")
16068
+ logDir: join32(dir, "logs")
15624
16069
  })
15625
16070
  );
15626
16071
  await launchd.bootout(label);
@@ -15641,7 +16086,7 @@ program.command("uninstall").description("Stop the service and remove the launch
15641
16086
  if (dir === "/" || dir === home || home.startsWith(`${dir}/`)) {
15642
16087
  throw new Error(`refusing to purge ${dir}: it is the home directory or an ancestor of it`);
15643
16088
  }
15644
- if (!existsSync7(join31(dir, "config.json")) && !existsSync7(join31(dir, "app", "install.json"))) {
16089
+ if (!existsSync7(join32(dir, "config.json")) && !existsSync7(join32(dir, "app", "install.json"))) {
15645
16090
  throw new Error(`refusing to purge ${dir}: it does not look like a Cayenne profile`);
15646
16091
  }
15647
16092
  if (!opts.yes) {