@cabane/companion 0.6.6 → 0.6.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 (3) hide show
  1. package/dist/cli.js +108 -51
  2. package/dist/runtime.js +102 -45
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2902,8 +2902,7 @@ var claudeCodeDialectSchema = z9.object({
2902
2902
  display: z9.enum(["summarized", "omitted"]).optional()
2903
2903
  }),
2904
2904
  z9.object({ type: z9.literal("disabled") })
2905
- ]).optional(),
2906
- hostAccess: z9.boolean().optional()
2905
+ ]).optional()
2907
2906
  }).loose();
2908
2907
  function readThinking(runtimeOptions) {
2909
2908
  const dialect = runtimeOptions?.["claude-code"];
@@ -2981,8 +2980,7 @@ function buildClaudeCodeOptions(req, augment) {
2981
2980
  alwaysLoad: true
2982
2981
  };
2983
2982
  }
2984
- const dialect = claudeCodeDialectSchema.safeParse(config.runtimeOptions?.["claude-code"] ?? {});
2985
- const useCodingPreset = dialect.success ? dialect.data.hostAccess ?? false : false;
2983
+ const useCodingPreset = policy.hostFs;
2986
2984
  const cabaneGlob = `mcp__${CABANE_MCP_SERVER}__*`;
2987
2985
  const extraServerGlobs = Object.keys(req.extra.mcpServers).map((name) => `mcp__${name}__*`);
2988
2986
  const allowedTools = dedupe([
@@ -4898,10 +4896,11 @@ function parseCodexModel(model) {
4898
4896
  var CABANE_MCP_SERVER3 = "cabane";
4899
4897
  var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
4900
4898
  var ACTIVE_CONVERSATION_HEADER4 = "x-cabane-active-conversation";
4901
- function buildRunSpec2(req, resumeThreadId) {
4899
+ function buildRunSpec2(req, resumeThreadId, instructionsFile = null) {
4902
4900
  const { policy, config } = req;
4903
4901
  const directory = req.local.cwd ?? "";
4904
4902
  const dialect = readCodexDialect(config.runtimeOptions);
4903
+ const baseInstructionsFile = !policy.hostFs && instructionsFile ? instructionsFile : null;
4905
4904
  const model = config.model ? parseCodexModel(config.model) : null;
4906
4905
  if (model === null) {
4907
4906
  console.warn(
@@ -4915,19 +4914,21 @@ function buildRunSpec2(req, resumeThreadId) {
4915
4914
  policy: codexToolPolicy(policy),
4916
4915
  skipGitRepoCheck: true,
4917
4916
  ...dialect.modelReasoningEffort ? { modelReasoningEffort: dialect.modelReasoningEffort } : {},
4918
- input: buildInput(req),
4919
- config: buildConfig(req)
4917
+ baseInstructionsFile,
4918
+ input: buildInput(req, baseInstructionsFile !== null),
4919
+ config: buildConfig(req, baseInstructionsFile)
4920
4920
  };
4921
4921
  }
4922
- function buildInput(req) {
4922
+ function buildInput(req, promptRidesInstructionsFile) {
4923
4923
  const userText = req.content.filter((b) => b.type === "text").map((b) => b.text).join("\n\n");
4924
4924
  const body = userText.trim().length > 0 ? userText : req.prompt;
4925
+ if (promptRidesInstructionsFile) return body;
4925
4926
  const system = req.systemPrompt.trim();
4926
4927
  return system.length > 0 ? `${system}
4927
4928
 
4928
4929
  ${body}` : body;
4929
4930
  }
4930
- function buildConfig(req) {
4931
+ function buildConfig(req, baseInstructionsFile) {
4931
4932
  const mcp_servers = {};
4932
4933
  for (const [name, server] of Object.entries(req.local.mcpServers ?? {})) {
4933
4934
  if ("url" in server) {
@@ -4985,6 +4986,10 @@ function buildConfig(req) {
4985
4986
  return {
4986
4987
  mcp_servers,
4987
4988
  experimental_use_rmcp_client: true,
4989
+ // CT871: the no-host-access prompt replacement — Cabane's composed prompt
4990
+ // becomes Codex's base instructions, and Codex's own environment preamble goes
4991
+ // with the harness prompt it belonged to.
4992
+ ...baseInstructionsFile ? { model_instructions_file: baseInstructionsFile, include_environment_context: false } : {},
4988
4993
  ...tmpDir ? { shell_environment_policy: { set: { TMPDIR: tmpDir } } } : {},
4989
4994
  ...policy.permissionProfile ? {
4990
4995
  // CT733: named permission profiles are Codex's split-filesystem path.
@@ -5603,19 +5608,48 @@ function createCodexAdapter(deps = {}) {
5603
5608
  );
5604
5609
  }
5605
5610
  const degraded = "fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session";
5606
- const spec = buildRunSpec2(req, resumeThreadId);
5607
- const result = await transport.run(spec, signal);
5608
- if (signal.aborted) return;
5609
- yield* decodeCodexStream(result.events, {
5610
- signal,
5611
- cwd: req.local.cwd,
5612
- resumedThreadId: resumeThreadId,
5613
- degraded,
5614
- // CT601: Codex reports no model in-stream, so record what the thread was
5615
- // started with (null on the default token). Same for the reasoning effort.
5616
- resolvedModel: spec.model,
5617
- resolvedReasoningEffort: spec.modelReasoningEffort ?? null
5618
- });
5611
+ let instructions = null;
5612
+ if (!req.policy.hostFs) {
5613
+ if (!deps.writeInstructionsFile) {
5614
+ deps.onWarn?.(
5615
+ "codex adapter: no instructions-file writer wired on this host \u2014 the composed prompt rides as the input preamble, so Codex keeps its own base instructions (CT871)"
5616
+ );
5617
+ } else {
5618
+ try {
5619
+ instructions = await deps.writeInstructionsFile(req.systemPrompt);
5620
+ } catch (err) {
5621
+ deps.onWarn?.(
5622
+ "codex adapter: failed to write the per-turn instructions file \u2014 falling back to the input preamble (CT871)",
5623
+ { err: err instanceof Error ? err.message : String(err) }
5624
+ );
5625
+ }
5626
+ }
5627
+ }
5628
+ try {
5629
+ const spec = buildRunSpec2(req, resumeThreadId, instructions?.path ?? null);
5630
+ const result = await transport.run(spec, signal);
5631
+ if (signal.aborted) return;
5632
+ yield* decodeCodexStream(result.events, {
5633
+ signal,
5634
+ cwd: req.local.cwd,
5635
+ resumedThreadId: resumeThreadId,
5636
+ degraded,
5637
+ // CT601: Codex reports no model in-stream, so record what the thread was
5638
+ // started with (null on the default token). Same for the reasoning effort.
5639
+ resolvedModel: spec.model,
5640
+ resolvedReasoningEffort: spec.modelReasoningEffort ?? null
5641
+ });
5642
+ } finally {
5643
+ if (instructions) {
5644
+ try {
5645
+ await instructions.cleanup();
5646
+ } catch (err) {
5647
+ deps.onWarn?.("codex adapter: instructions-file cleanup failed (CT871)", {
5648
+ err: err instanceof Error ? err.message : String(err)
5649
+ });
5650
+ }
5651
+ }
5652
+ }
5619
5653
  }
5620
5654
  };
5621
5655
  }
@@ -6223,7 +6257,7 @@ var ConnectorHealthStore = class {
6223
6257
  // src/dispatcher.ts
6224
6258
  import { randomUUID } from "crypto";
6225
6259
  import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync10 } from "fs";
6226
- import { join as join12 } from "path";
6260
+ import { join as join13 } from "path";
6227
6261
 
6228
6262
  // src/summon.ts
6229
6263
  import { z as z12 } from "zod";
@@ -6290,7 +6324,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
6290
6324
  ...askState ? [
6291
6325
  tool(
6292
6326
  ASK_TOOL,
6293
- "Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact, a go/no-go). Pass `targetUserId` (a workspace member's user id \u2014 get it from `mcp__cabane__list_members`). Two forms: a SINGLE question \u2014 a `headline` (the actual question as one clear, capitalized sentence ending in `?`, \"Do we go to prod?\") plus a short `question` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once (\"three calls before I build: A? B? C?\"), a `questions` array of 1\u20135 items, each `{ headline, body?, options? }`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 end the turn with one ask carrying every question, never pick one and bury the rest. Each question keeps the same form rules: a one-sentence `headline`, a short `body` frame (NOT a report \u2014 your status, links, and detail go in your REPLY, and the body renders inline markdown only: links/emphasis/inline code, no bulleted lists or headings), and 2\u20134 `options` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. An option can be a short button label OR a sentence that carries its own context; short/binary sets render as inline buttons, long ones stack full-width. Provide EITHER `question` (single) or `questions` (array), never both. The ask is recorded as a first-class attention item aimed at that person; your final reply carries the surrounding CONTEXT (what you found, why you're stuck), the ask carries the QUESTION(S). An open ask marks you as blocked until EVERY question is answered, so raise one only when you truly can't proceed \u2014 never ceremonially. One ask per turn (last call wins). After asking, stop \u2014 when the person replies addressed to you, the ask resolves and you resume; other people's or agents' messages may wake you but leave it open. Targets a human only; to hand work to another AGENT use summon/dispatch instead.",
6327
+ "Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact). Pass `targetUserId` (a workspace member's user id \u2014 get it from `mcp__cabane__list_members`). Two forms: a SINGLE question \u2014 a `headline` (the actual question as one clear, capitalized sentence ending in `?`, \"Do we go to prod?\") plus a short `question` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once, a `questions` array of 1\u20135 items, each `{ headline, body?, options? }`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 end the turn with one ask carrying every question, never pick one and bury the rest. Each question keeps the same form rules: a one-sentence `headline`, a short `body` frame (NOT a report \u2014 your status, links, and detail go in your REPLY, and the body renders inline markdown only: links/emphasis/inline code, no bulleted lists or headings), and 2\u20134 `options` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. An option can be a short button label or a whole sentence. Provide EITHER `question` (single) or `questions` (array), never both. The ask is a first-class attention item aimed at that person; your final reply carries the surrounding CONTEXT (what you found, why you're stuck), the ask carries the QUESTION(S). An open ask marks you as blocked until EVERY question is answered, so raise one only when you truly can't proceed \u2014 never ceremonially. One ask per turn (last call wins). After asking, stop \u2014 when the person replies addressed to you, the ask resolves and you resume; other people's or agents' messages may wake you but leave it open. Targets a human only; to hand work to another AGENT use summon/dispatch instead.",
6294
6328
  {
6295
6329
  targetUserId: z12.string().uuid().describe("The workspace member (human) to ask \u2014 a user id from `list_members`."),
6296
6330
  question: z12.string().min(1).max(400).optional().describe(
@@ -6397,7 +6431,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
6397
6431
  ...wakeState ? [
6398
6432
  tool(
6399
6433
  WAKE_ME_TOOL,
6400
- 'Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for "wait until X": when the thing you need hasn\'t happened yet (a conversation isn\'t done, a PR isn\'t merged, a human hasn\'t answered), arm a wake, end your turn, and you\'re woken later to CHECK \u2014 read the workspace, and either act or re-arm. This is the loop behind "check back in five minutes", "keep checking until {condition}", and a scheduled re-try ("re-send once that agent\'s limit resets"). Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging, a deploy landing, a throttled dispatch to re-send. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a scheduled appointment, a known reset time. A speculative far-future check-in you invented yourself \u2014 "in two weeks I\'ll see whether this feature is used" \u2014 is the one thing not to arm: if no one asked and you can\'t name both what clears the wait and why it takes that long, don\'t arm it \u2014 finish now, or raise an `ask`. Pass EXACTLY ONE of `afterSeconds` (a relative delay \u2014 `300` for five minutes) or `at` (an absolute ISO-8601 timestamp WITH a zone, e.g. `2026-07-16T09:00:00-07:00` \u2014 YOU compute it from a phrase like "tomorrow morning"; the system never parses natural-language time). `note` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check ("check whether CT441 merged yet"). The wake is armed when your turn SETTLES, not now, so the delay counts from the turn ending; one wake per turn (last call wins). This is the sanctioned way to schedule your own continuation \u2014 the ONLY one; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you\'ll be steered to raise an `ask` to the human instead. If a wake can\'t be armed you\'re re-dispatched with a note explaining why \u2014 never a silent drop.',
6434
+ "Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for \"wait until X\": when the thing you need hasn't happened yet (a PR isn't merged, a human hasn't answered), arm a wake, end your turn, and you're woken later to CHECK \u2014 read the workspace, and either act or re-arm. Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a known reset time. A speculative far-future check-in you invented yourself is the one thing not to arm: if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it \u2014 finish now, or raise an `ask`. Pass EXACTLY ONE of `afterSeconds` (a relative delay \u2014 `300` for five minutes) or `at` (an absolute ISO-8601 timestamp WITH a zone, e.g. `2026-07-16T09:00:00-07:00` \u2014 YOU compute it from a phrase like \"tomorrow morning\"; the system never parses natural-language time). `note` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check (\"check whether CT441 merged yet\"). The wake is armed when your turn SETTLES, not now, so the delay counts from the turn ending; one wake per turn (last call wins). This is the sanctioned way to schedule your own continuation \u2014 the ONLY one; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you'll be steered to raise an `ask` to the human instead. If a wake can't be armed you're re-dispatched with a note explaining why \u2014 never a silent drop.",
6401
6435
  {
6402
6436
  afterSeconds: z12.number().int().positive().optional().describe(
6403
6437
  "Relative delay in seconds from when this turn ends (e.g. 300 = five minutes). Provide EITHER this or `at`, not both. Floor 60s, horizon 14 days \u2014 enforced server-side."
@@ -6502,17 +6536,34 @@ function trimSlash3(s) {
6502
6536
  return s.endsWith("/") ? s.slice(0, -1) : s;
6503
6537
  }
6504
6538
 
6539
+ // src/codex-instructions.ts
6540
+ import { mkdtemp, rm, writeFile } from "fs/promises";
6541
+ import { tmpdir } from "os";
6542
+ import { join as join9 } from "path";
6543
+ var PREFIX = "cabane-codex-instructions-";
6544
+ async function writeCodexInstructionsFile(contents) {
6545
+ const dir2 = await mkdtemp(join9(tmpdir(), PREFIX));
6546
+ const path3 = join9(dir2, "instructions.md");
6547
+ await writeFile(path3, contents, { encoding: "utf8", mode: 384 });
6548
+ return {
6549
+ path: path3,
6550
+ cleanup: async () => {
6551
+ await rm(dir2, { recursive: true, force: true });
6552
+ }
6553
+ };
6554
+ }
6555
+
6505
6556
  // src/prepared.ts
6506
6557
  import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
6507
- import { join as join9 } from "path";
6558
+ import { join as join10 } from "path";
6508
6559
  function dirFor(workspaceId) {
6509
- return join9(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
6560
+ return join10(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
6510
6561
  }
6511
6562
  function conversationDir(workspaceId, conversationId) {
6512
- return join9(dirFor(workspaceId), encodeURIComponent(conversationId));
6563
+ return join10(dirFor(workspaceId), encodeURIComponent(conversationId));
6513
6564
  }
6514
6565
  function pathFor3(workspaceId, conversationId, agentId) {
6515
- return join9(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
6566
+ return join10(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
6516
6567
  }
6517
6568
  function readPrepared(workspaceId, conversationId, agentId) {
6518
6569
  const path3 = pathFor3(workspaceId, conversationId, agentId);
@@ -6541,11 +6592,11 @@ function writePrepared(workspaceId, conversationId, agentId, result) {
6541
6592
 
6542
6593
  // src/secrets.ts
6543
6594
  import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
6544
- import { join as join10 } from "path";
6595
+ import { join as join11 } from "path";
6545
6596
  import { z as z13 } from "zod";
6546
6597
  var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
6547
6598
  function secretsPath() {
6548
- return join10(cabaneDir(), "secrets.json");
6599
+ return join11(cabaneDir(), "secrets.json");
6549
6600
  }
6550
6601
  var secretStoreSchema = z13.record(z13.string(), z13.string());
6551
6602
  function loadSecretStore() {
@@ -6632,9 +6683,9 @@ function resolveMcpSecrets(mcpServers, store) {
6632
6683
 
6633
6684
  // src/transcript-writer.ts
6634
6685
  import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync4 } from "fs";
6635
- import { join as join11 } from "path";
6686
+ import { join as join12 } from "path";
6636
6687
  function transcriptsDir() {
6637
- return join11(cabaneDir(), "transcripts");
6688
+ return join12(cabaneDir(), "transcripts");
6638
6689
  }
6639
6690
  var RETAIN = 200;
6640
6691
  var TranscriptWriter = class {
@@ -6643,7 +6694,7 @@ var TranscriptWriter = class {
6643
6694
  onWarn;
6644
6695
  constructor(dir2, meta, onWarn) {
6645
6696
  this.onWarn = onWarn;
6646
- this.path = join11(dir2, fileName(meta));
6697
+ this.path = join12(dir2, fileName(meta));
6647
6698
  try {
6648
6699
  mkdirSync9(dir2, { recursive: true });
6649
6700
  try {
@@ -6702,7 +6753,7 @@ function pruneOld(dir2, retain) {
6702
6753
  const drop = files.sort().slice(0, files.length - retain);
6703
6754
  for (const f of drop) {
6704
6755
  try {
6705
- rmSync4(join11(dir2, f), { force: true });
6756
+ rmSync4(join12(dir2, f), { force: true });
6706
6757
  } catch {
6707
6758
  }
6708
6759
  }
@@ -7247,7 +7298,7 @@ ${reason}`,
7247
7298
  }
7248
7299
  let turnEnv = hookEnv;
7249
7300
  if (effectiveCwd && turnContext.runtime === "codex") {
7250
- const tmpDir = join12(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
7301
+ const tmpDir = join13(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
7251
7302
  try {
7252
7303
  mkdirSync10(tmpDir, { recursive: true });
7253
7304
  turnEnv = { ...hookEnv, TMPDIR: tmpDir };
@@ -7346,7 +7397,13 @@ ${reason}`,
7346
7397
  adapters.push(createOpencodeAdapter({ serverUrl: this.opts.opencodeServerUrl, onWarn }));
7347
7398
  }
7348
7399
  if (this.opts.codexEnabled) {
7349
- adapters.push(createCodexAdapter({ enabled: true, onWarn }));
7400
+ adapters.push(
7401
+ createCodexAdapter({
7402
+ enabled: true,
7403
+ onWarn,
7404
+ writeInstructionsFile: writeCodexInstructionsFile
7405
+ })
7406
+ );
7350
7407
  }
7351
7408
  const registry = createAdapterRegistry(adapters);
7352
7409
  let adapter;
@@ -7389,10 +7446,10 @@ ${reason}`,
7389
7446
  );
7390
7447
  if (effectiveCwd) {
7391
7448
  try {
7392
- const diagnosticDir = join12(effectiveCwd, ".git", "cabane");
7449
+ const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
7393
7450
  mkdirSync10(diagnosticDir, { recursive: true });
7394
7451
  appendFileSync2(
7395
- join12(diagnosticDir, "readiness.jsonl"),
7452
+ join13(diagnosticDir, "readiness.jsonl"),
7396
7453
  `${JSON.stringify({
7397
7454
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7398
7455
  taskId: hookEnv.CABANE_TASK_ID,
@@ -7976,7 +8033,7 @@ import {
7976
8033
  rmSync as rmSync5,
7977
8034
  writeFileSync as writeFileSync7
7978
8035
  } from "fs";
7979
- import { join as join13 } from "path";
8036
+ import { join as join14 } from "path";
7980
8037
  var MAX_ENTRIES = 2e3;
7981
8038
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
7982
8039
  var Outbox = class {
@@ -7989,10 +8046,10 @@ var Outbox = class {
7989
8046
  // Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
7990
8047
  // cases route writes at the right tmpdir.
7991
8048
  dir() {
7992
- return join13(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
8049
+ return join14(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
7993
8050
  }
7994
8051
  fileFor(turnId, seq) {
7995
- return join13(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
8052
+ return join14(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
7996
8053
  }
7997
8054
  // Persist a commit for later draining. Atomic (temp file + rename) so a
7998
8055
  // concurrent `list()` never reads a half-written entry, then enforces the
@@ -8034,7 +8091,7 @@ var Outbox = class {
8034
8091
  const entries = [];
8035
8092
  for (const name of names) {
8036
8093
  if (!name.endsWith(".json")) continue;
8037
- const full = join13(dir2, name);
8094
+ const full = join14(dir2, name);
8038
8095
  try {
8039
8096
  const parsed = JSON.parse(readFileSync8(full, "utf8"));
8040
8097
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
@@ -9141,9 +9198,9 @@ function handleUncaught(log, err, origin) {
9141
9198
 
9142
9199
  // src/crash-marker.ts
9143
9200
  import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
9144
- import { join as join14 } from "path";
9201
+ import { join as join15 } from "path";
9145
9202
  function crashMarkerPath() {
9146
- return join14(cabaneDir(), "last-error.json");
9203
+ return join15(cabaneDir(), "last-error.json");
9147
9204
  }
9148
9205
  function recordCrash(rec2) {
9149
9206
  try {
@@ -9497,7 +9554,7 @@ function isAlive(kill, pid) {
9497
9554
 
9498
9555
  // src/commands/transcript.ts
9499
9556
  import { existsSync as existsSync12, readFileSync as readFileSync10, readdirSync as readdirSync4 } from "fs";
9500
- import { isAbsolute, join as join15 } from "path";
9557
+ import { isAbsolute, join as join16 } from "path";
9501
9558
  async function transcript(opts = {}) {
9502
9559
  const dir2 = transcriptsDir();
9503
9560
  if (opts.follow) {
@@ -9514,7 +9571,7 @@ async function transcript(opts = {}) {
9514
9571
  process.stdout.write(emptyMessage(dir2));
9515
9572
  return;
9516
9573
  }
9517
- process.stdout.write(renderFile(join15(dir2, newest)) + "\n");
9574
+ process.stdout.write(renderFile(join16(dir2, newest)) + "\n");
9518
9575
  return;
9519
9576
  }
9520
9577
  printList(dir2);
@@ -9597,7 +9654,7 @@ function isComplete(content) {
9597
9654
  async function followTranscripts(dir2) {
9598
9655
  const follower = new TranscriptFollower({
9599
9656
  listFiles: () => listFiles(dir2),
9600
- read: (f) => readFileSync10(join15(dir2, f), "utf8"),
9657
+ read: (f) => readFileSync10(join16(dir2, f), "utf8"),
9601
9658
  write: (s) => process.stdout.write(s),
9602
9659
  // CSI: cursor up `n` lines, then erase from cursor to end of screen.
9603
9660
  clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
@@ -9637,7 +9694,7 @@ function printList(dir2) {
9637
9694
 
9638
9695
  `);
9639
9696
  for (const f of files.slice(0, 20)) {
9640
- const { meta, outcome } = peek(join15(dir2, f));
9697
+ const { meta, outcome } = peek(join16(dir2, f));
9641
9698
  const when = fmtTime(rec(meta)?.ts);
9642
9699
  const ws = str2(rec(meta)?.workspaceSlug);
9643
9700
  const o = rec(outcome);
@@ -9674,10 +9731,10 @@ function resolveTarget(dir2, target) {
9674
9731
  if (existsSync12(target)) return target;
9675
9732
  throw new CompanionError(`no transcript at ${target}.`);
9676
9733
  }
9677
- const exact = join15(dir2, target);
9734
+ const exact = join16(dir2, target);
9678
9735
  if (existsSync12(exact)) return exact;
9679
9736
  const matches = listFiles(dir2).filter((f) => f.includes(target));
9680
- if (matches.length === 1) return join15(dir2, matches[0]);
9737
+ if (matches.length === 1) return join16(dir2, matches[0]);
9681
9738
  if (matches.length === 0) {
9682
9739
  throw new CompanionError(
9683
9740
  `no transcript matching "${target}" in ${dir2}. Run \`cabane-companion transcript\` to list them.`
package/dist/runtime.js CHANGED
@@ -2555,8 +2555,7 @@ var claudeCodeDialectSchema = z9.object({
2555
2555
  display: z9.enum(["summarized", "omitted"]).optional()
2556
2556
  }),
2557
2557
  z9.object({ type: z9.literal("disabled") })
2558
- ]).optional(),
2559
- hostAccess: z9.boolean().optional()
2558
+ ]).optional()
2560
2559
  }).loose();
2561
2560
  function readThinking(runtimeOptions) {
2562
2561
  const dialect = runtimeOptions?.["claude-code"];
@@ -2634,8 +2633,7 @@ function buildClaudeCodeOptions(req, augment) {
2634
2633
  alwaysLoad: true
2635
2634
  };
2636
2635
  }
2637
- const dialect = claudeCodeDialectSchema.safeParse(config.runtimeOptions?.["claude-code"] ?? {});
2638
- const useCodingPreset = dialect.success ? dialect.data.hostAccess ?? false : false;
2636
+ const useCodingPreset = policy.hostFs;
2639
2637
  const cabaneGlob = `mcp__${CABANE_MCP_SERVER}__*`;
2640
2638
  const extraServerGlobs = Object.keys(req.extra.mcpServers).map((name) => `mcp__${name}__*`);
2641
2639
  const allowedTools = dedupe([
@@ -4551,10 +4549,11 @@ function parseCodexModel(model) {
4551
4549
  var CABANE_MCP_SERVER3 = "cabane";
4552
4550
  var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
4553
4551
  var ACTIVE_CONVERSATION_HEADER4 = "x-cabane-active-conversation";
4554
- function buildRunSpec2(req, resumeThreadId) {
4552
+ function buildRunSpec2(req, resumeThreadId, instructionsFile = null) {
4555
4553
  const { policy, config } = req;
4556
4554
  const directory = req.local.cwd ?? "";
4557
4555
  const dialect = readCodexDialect(config.runtimeOptions);
4556
+ const baseInstructionsFile = !policy.hostFs && instructionsFile ? instructionsFile : null;
4558
4557
  const model = config.model ? parseCodexModel(config.model) : null;
4559
4558
  if (model === null) {
4560
4559
  console.warn(
@@ -4568,19 +4567,21 @@ function buildRunSpec2(req, resumeThreadId) {
4568
4567
  policy: codexToolPolicy(policy),
4569
4568
  skipGitRepoCheck: true,
4570
4569
  ...dialect.modelReasoningEffort ? { modelReasoningEffort: dialect.modelReasoningEffort } : {},
4571
- input: buildInput(req),
4572
- config: buildConfig(req)
4570
+ baseInstructionsFile,
4571
+ input: buildInput(req, baseInstructionsFile !== null),
4572
+ config: buildConfig(req, baseInstructionsFile)
4573
4573
  };
4574
4574
  }
4575
- function buildInput(req) {
4575
+ function buildInput(req, promptRidesInstructionsFile) {
4576
4576
  const userText = req.content.filter((b) => b.type === "text").map((b) => b.text).join("\n\n");
4577
4577
  const body = userText.trim().length > 0 ? userText : req.prompt;
4578
+ if (promptRidesInstructionsFile) return body;
4578
4579
  const system = req.systemPrompt.trim();
4579
4580
  return system.length > 0 ? `${system}
4580
4581
 
4581
4582
  ${body}` : body;
4582
4583
  }
4583
- function buildConfig(req) {
4584
+ function buildConfig(req, baseInstructionsFile) {
4584
4585
  const mcp_servers = {};
4585
4586
  for (const [name, server] of Object.entries(req.local.mcpServers ?? {})) {
4586
4587
  if ("url" in server) {
@@ -4638,6 +4639,10 @@ function buildConfig(req) {
4638
4639
  return {
4639
4640
  mcp_servers,
4640
4641
  experimental_use_rmcp_client: true,
4642
+ // CT871: the no-host-access prompt replacement — Cabane's composed prompt
4643
+ // becomes Codex's base instructions, and Codex's own environment preamble goes
4644
+ // with the harness prompt it belonged to.
4645
+ ...baseInstructionsFile ? { model_instructions_file: baseInstructionsFile, include_environment_context: false } : {},
4641
4646
  ...tmpDir ? { shell_environment_policy: { set: { TMPDIR: tmpDir } } } : {},
4642
4647
  ...policy.permissionProfile ? {
4643
4648
  // CT733: named permission profiles are Codex's split-filesystem path.
@@ -5256,19 +5261,48 @@ function createCodexAdapter(deps = {}) {
5256
5261
  );
5257
5262
  }
5258
5263
  const degraded = "fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session";
5259
- const spec = buildRunSpec2(req, resumeThreadId);
5260
- const result = await transport.run(spec, signal);
5261
- if (signal.aborted) return;
5262
- yield* decodeCodexStream(result.events, {
5263
- signal,
5264
- cwd: req.local.cwd,
5265
- resumedThreadId: resumeThreadId,
5266
- degraded,
5267
- // CT601: Codex reports no model in-stream, so record what the thread was
5268
- // started with (null on the default token). Same for the reasoning effort.
5269
- resolvedModel: spec.model,
5270
- resolvedReasoningEffort: spec.modelReasoningEffort ?? null
5271
- });
5264
+ let instructions = null;
5265
+ if (!req.policy.hostFs) {
5266
+ if (!deps.writeInstructionsFile) {
5267
+ deps.onWarn?.(
5268
+ "codex adapter: no instructions-file writer wired on this host \u2014 the composed prompt rides as the input preamble, so Codex keeps its own base instructions (CT871)"
5269
+ );
5270
+ } else {
5271
+ try {
5272
+ instructions = await deps.writeInstructionsFile(req.systemPrompt);
5273
+ } catch (err) {
5274
+ deps.onWarn?.(
5275
+ "codex adapter: failed to write the per-turn instructions file \u2014 falling back to the input preamble (CT871)",
5276
+ { err: err instanceof Error ? err.message : String(err) }
5277
+ );
5278
+ }
5279
+ }
5280
+ }
5281
+ try {
5282
+ const spec = buildRunSpec2(req, resumeThreadId, instructions?.path ?? null);
5283
+ const result = await transport.run(spec, signal);
5284
+ if (signal.aborted) return;
5285
+ yield* decodeCodexStream(result.events, {
5286
+ signal,
5287
+ cwd: req.local.cwd,
5288
+ resumedThreadId: resumeThreadId,
5289
+ degraded,
5290
+ // CT601: Codex reports no model in-stream, so record what the thread was
5291
+ // started with (null on the default token). Same for the reasoning effort.
5292
+ resolvedModel: spec.model,
5293
+ resolvedReasoningEffort: spec.modelReasoningEffort ?? null
5294
+ });
5295
+ } finally {
5296
+ if (instructions) {
5297
+ try {
5298
+ await instructions.cleanup();
5299
+ } catch (err) {
5300
+ deps.onWarn?.("codex adapter: instructions-file cleanup failed (CT871)", {
5301
+ err: err instanceof Error ? err.message : String(err)
5302
+ });
5303
+ }
5304
+ }
5305
+ }
5272
5306
  }
5273
5307
  };
5274
5308
  }
@@ -5876,7 +5910,7 @@ var ConnectorHealthStore = class {
5876
5910
  // src/dispatcher.ts
5877
5911
  import { randomUUID } from "crypto";
5878
5912
  import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9 } from "fs";
5879
- import { join as join12 } from "path";
5913
+ import { join as join13 } from "path";
5880
5914
 
5881
5915
  // src/summon.ts
5882
5916
  import { z as z12 } from "zod";
@@ -5943,7 +5977,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
5943
5977
  ...askState ? [
5944
5978
  tool(
5945
5979
  ASK_TOOL,
5946
- "Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact, a go/no-go). Pass `targetUserId` (a workspace member's user id \u2014 get it from `mcp__cabane__list_members`). Two forms: a SINGLE question \u2014 a `headline` (the actual question as one clear, capitalized sentence ending in `?`, \"Do we go to prod?\") plus a short `question` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once (\"three calls before I build: A? B? C?\"), a `questions` array of 1\u20135 items, each `{ headline, body?, options? }`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 end the turn with one ask carrying every question, never pick one and bury the rest. Each question keeps the same form rules: a one-sentence `headline`, a short `body` frame (NOT a report \u2014 your status, links, and detail go in your REPLY, and the body renders inline markdown only: links/emphasis/inline code, no bulleted lists or headings), and 2\u20134 `options` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. An option can be a short button label OR a sentence that carries its own context; short/binary sets render as inline buttons, long ones stack full-width. Provide EITHER `question` (single) or `questions` (array), never both. The ask is recorded as a first-class attention item aimed at that person; your final reply carries the surrounding CONTEXT (what you found, why you're stuck), the ask carries the QUESTION(S). An open ask marks you as blocked until EVERY question is answered, so raise one only when you truly can't proceed \u2014 never ceremonially. One ask per turn (last call wins). After asking, stop \u2014 when the person replies addressed to you, the ask resolves and you resume; other people's or agents' messages may wake you but leave it open. Targets a human only; to hand work to another AGENT use summon/dispatch instead.",
5980
+ "Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact). Pass `targetUserId` (a workspace member's user id \u2014 get it from `mcp__cabane__list_members`). Two forms: a SINGLE question \u2014 a `headline` (the actual question as one clear, capitalized sentence ending in `?`, \"Do we go to prod?\") plus a short `question` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once, a `questions` array of 1\u20135 items, each `{ headline, body?, options? }`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 end the turn with one ask carrying every question, never pick one and bury the rest. Each question keeps the same form rules: a one-sentence `headline`, a short `body` frame (NOT a report \u2014 your status, links, and detail go in your REPLY, and the body renders inline markdown only: links/emphasis/inline code, no bulleted lists or headings), and 2\u20134 `options` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. An option can be a short button label or a whole sentence. Provide EITHER `question` (single) or `questions` (array), never both. The ask is a first-class attention item aimed at that person; your final reply carries the surrounding CONTEXT (what you found, why you're stuck), the ask carries the QUESTION(S). An open ask marks you as blocked until EVERY question is answered, so raise one only when you truly can't proceed \u2014 never ceremonially. One ask per turn (last call wins). After asking, stop \u2014 when the person replies addressed to you, the ask resolves and you resume; other people's or agents' messages may wake you but leave it open. Targets a human only; to hand work to another AGENT use summon/dispatch instead.",
5947
5981
  {
5948
5982
  targetUserId: z12.string().uuid().describe("The workspace member (human) to ask \u2014 a user id from `list_members`."),
5949
5983
  question: z12.string().min(1).max(400).optional().describe(
@@ -6050,7 +6084,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
6050
6084
  ...wakeState ? [
6051
6085
  tool(
6052
6086
  WAKE_ME_TOOL,
6053
- 'Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for "wait until X": when the thing you need hasn\'t happened yet (a conversation isn\'t done, a PR isn\'t merged, a human hasn\'t answered), arm a wake, end your turn, and you\'re woken later to CHECK \u2014 read the workspace, and either act or re-arm. This is the loop behind "check back in five minutes", "keep checking until {condition}", and a scheduled re-try ("re-send once that agent\'s limit resets"). Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging, a deploy landing, a throttled dispatch to re-send. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a scheduled appointment, a known reset time. A speculative far-future check-in you invented yourself \u2014 "in two weeks I\'ll see whether this feature is used" \u2014 is the one thing not to arm: if no one asked and you can\'t name both what clears the wait and why it takes that long, don\'t arm it \u2014 finish now, or raise an `ask`. Pass EXACTLY ONE of `afterSeconds` (a relative delay \u2014 `300` for five minutes) or `at` (an absolute ISO-8601 timestamp WITH a zone, e.g. `2026-07-16T09:00:00-07:00` \u2014 YOU compute it from a phrase like "tomorrow morning"; the system never parses natural-language time). `note` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check ("check whether CT441 merged yet"). The wake is armed when your turn SETTLES, not now, so the delay counts from the turn ending; one wake per turn (last call wins). This is the sanctioned way to schedule your own continuation \u2014 the ONLY one; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you\'ll be steered to raise an `ask` to the human instead. If a wake can\'t be armed you\'re re-dispatched with a note explaining why \u2014 never a silent drop.',
6087
+ "Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for \"wait until X\": when the thing you need hasn't happened yet (a PR isn't merged, a human hasn't answered), arm a wake, end your turn, and you're woken later to CHECK \u2014 read the workspace, and either act or re-arm. Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a known reset time. A speculative far-future check-in you invented yourself is the one thing not to arm: if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it \u2014 finish now, or raise an `ask`. Pass EXACTLY ONE of `afterSeconds` (a relative delay \u2014 `300` for five minutes) or `at` (an absolute ISO-8601 timestamp WITH a zone, e.g. `2026-07-16T09:00:00-07:00` \u2014 YOU compute it from a phrase like \"tomorrow morning\"; the system never parses natural-language time). `note` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check (\"check whether CT441 merged yet\"). The wake is armed when your turn SETTLES, not now, so the delay counts from the turn ending; one wake per turn (last call wins). This is the sanctioned way to schedule your own continuation \u2014 the ONLY one; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you'll be steered to raise an `ask` to the human instead. If a wake can't be armed you're re-dispatched with a note explaining why \u2014 never a silent drop.",
6054
6088
  {
6055
6089
  afterSeconds: z12.number().int().positive().optional().describe(
6056
6090
  "Relative delay in seconds from when this turn ends (e.g. 300 = five minutes). Provide EITHER this or `at`, not both. Floor 60s, horizon 14 days \u2014 enforced server-side."
@@ -6155,17 +6189,34 @@ function trimSlash3(s) {
6155
6189
  return s.endsWith("/") ? s.slice(0, -1) : s;
6156
6190
  }
6157
6191
 
6192
+ // src/codex-instructions.ts
6193
+ import { mkdtemp, rm, writeFile } from "fs/promises";
6194
+ import { tmpdir } from "os";
6195
+ import { join as join9 } from "path";
6196
+ var PREFIX = "cabane-codex-instructions-";
6197
+ async function writeCodexInstructionsFile(contents) {
6198
+ const dir2 = await mkdtemp(join9(tmpdir(), PREFIX));
6199
+ const path3 = join9(dir2, "instructions.md");
6200
+ await writeFile(path3, contents, { encoding: "utf8", mode: 384 });
6201
+ return {
6202
+ path: path3,
6203
+ cleanup: async () => {
6204
+ await rm(dir2, { recursive: true, force: true });
6205
+ }
6206
+ };
6207
+ }
6208
+
6158
6209
  // src/prepared.ts
6159
6210
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
6160
- import { join as join9 } from "path";
6211
+ import { join as join10 } from "path";
6161
6212
  function dirFor(workspaceId) {
6162
- return join9(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
6213
+ return join10(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
6163
6214
  }
6164
6215
  function conversationDir(workspaceId, conversationId) {
6165
- return join9(dirFor(workspaceId), encodeURIComponent(conversationId));
6216
+ return join10(dirFor(workspaceId), encodeURIComponent(conversationId));
6166
6217
  }
6167
6218
  function pathFor3(workspaceId, conversationId, agentId) {
6168
- return join9(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
6219
+ return join10(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
6169
6220
  }
6170
6221
  function readPrepared(workspaceId, conversationId, agentId) {
6171
6222
  const path3 = pathFor3(workspaceId, conversationId, agentId);
@@ -6194,11 +6245,11 @@ function writePrepared(workspaceId, conversationId, agentId, result) {
6194
6245
 
6195
6246
  // src/secrets.ts
6196
6247
  import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
6197
- import { join as join10 } from "path";
6248
+ import { join as join11 } from "path";
6198
6249
  import { z as z13 } from "zod";
6199
6250
  var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
6200
6251
  function secretsPath() {
6201
- return join10(cabaneDir(), "secrets.json");
6252
+ return join11(cabaneDir(), "secrets.json");
6202
6253
  }
6203
6254
  var secretStoreSchema = z13.record(z13.string(), z13.string());
6204
6255
  function loadSecretStore() {
@@ -6285,9 +6336,9 @@ function resolveMcpSecrets(mcpServers, store) {
6285
6336
 
6286
6337
  // src/transcript-writer.ts
6287
6338
  import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync4 } from "fs";
6288
- import { join as join11 } from "path";
6339
+ import { join as join12 } from "path";
6289
6340
  function transcriptsDir() {
6290
- return join11(cabaneDir(), "transcripts");
6341
+ return join12(cabaneDir(), "transcripts");
6291
6342
  }
6292
6343
  var RETAIN = 200;
6293
6344
  var TranscriptWriter = class {
@@ -6296,7 +6347,7 @@ var TranscriptWriter = class {
6296
6347
  onWarn;
6297
6348
  constructor(dir2, meta, onWarn) {
6298
6349
  this.onWarn = onWarn;
6299
- this.path = join11(dir2, fileName(meta));
6350
+ this.path = join12(dir2, fileName(meta));
6300
6351
  try {
6301
6352
  mkdirSync8(dir2, { recursive: true });
6302
6353
  try {
@@ -6355,7 +6406,7 @@ function pruneOld(dir2, retain) {
6355
6406
  const drop = files.sort().slice(0, files.length - retain);
6356
6407
  for (const f of drop) {
6357
6408
  try {
6358
- rmSync4(join11(dir2, f), { force: true });
6409
+ rmSync4(join12(dir2, f), { force: true });
6359
6410
  } catch {
6360
6411
  }
6361
6412
  }
@@ -6900,7 +6951,7 @@ ${reason}`,
6900
6951
  }
6901
6952
  let turnEnv = hookEnv;
6902
6953
  if (effectiveCwd && turnContext.runtime === "codex") {
6903
- const tmpDir = join12(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
6954
+ const tmpDir = join13(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
6904
6955
  try {
6905
6956
  mkdirSync9(tmpDir, { recursive: true });
6906
6957
  turnEnv = { ...hookEnv, TMPDIR: tmpDir };
@@ -6999,7 +7050,13 @@ ${reason}`,
6999
7050
  adapters.push(createOpencodeAdapter({ serverUrl: this.opts.opencodeServerUrl, onWarn }));
7000
7051
  }
7001
7052
  if (this.opts.codexEnabled) {
7002
- adapters.push(createCodexAdapter({ enabled: true, onWarn }));
7053
+ adapters.push(
7054
+ createCodexAdapter({
7055
+ enabled: true,
7056
+ onWarn,
7057
+ writeInstructionsFile: writeCodexInstructionsFile
7058
+ })
7059
+ );
7003
7060
  }
7004
7061
  const registry = createAdapterRegistry(adapters);
7005
7062
  let adapter;
@@ -7042,10 +7099,10 @@ ${reason}`,
7042
7099
  );
7043
7100
  if (effectiveCwd) {
7044
7101
  try {
7045
- const diagnosticDir = join12(effectiveCwd, ".git", "cabane");
7102
+ const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
7046
7103
  mkdirSync9(diagnosticDir, { recursive: true });
7047
7104
  appendFileSync2(
7048
- join12(diagnosticDir, "readiness.jsonl"),
7105
+ join13(diagnosticDir, "readiness.jsonl"),
7049
7106
  `${JSON.stringify({
7050
7107
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7051
7108
  taskId: hookEnv.CABANE_TASK_ID,
@@ -7629,7 +7686,7 @@ import {
7629
7686
  rmSync as rmSync5,
7630
7687
  writeFileSync as writeFileSync7
7631
7688
  } from "fs";
7632
- import { join as join13 } from "path";
7689
+ import { join as join14 } from "path";
7633
7690
  var MAX_ENTRIES = 2e3;
7634
7691
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
7635
7692
  var Outbox = class {
@@ -7642,10 +7699,10 @@ var Outbox = class {
7642
7699
  // Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
7643
7700
  // cases route writes at the right tmpdir.
7644
7701
  dir() {
7645
- return join13(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
7702
+ return join14(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
7646
7703
  }
7647
7704
  fileFor(turnId, seq) {
7648
- return join13(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
7705
+ return join14(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
7649
7706
  }
7650
7707
  // Persist a commit for later draining. Atomic (temp file + rename) so a
7651
7708
  // concurrent `list()` never reads a half-written entry, then enforces the
@@ -7687,7 +7744,7 @@ var Outbox = class {
7687
7744
  const entries = [];
7688
7745
  for (const name of names) {
7689
7746
  if (!name.endsWith(".json")) continue;
7690
- const full = join13(dir2, name);
7747
+ const full = join14(dir2, name);
7691
7748
  try {
7692
7749
  const parsed = JSON.parse(readFileSync8(full, "utf8"));
7693
7750
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
@@ -8794,9 +8851,9 @@ function handleUncaught(log, err, origin) {
8794
8851
 
8795
8852
  // src/crash-marker.ts
8796
8853
  import { existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
8797
- import { join as join14 } from "path";
8854
+ import { join as join15 } from "path";
8798
8855
  function crashMarkerPath() {
8799
- return join14(cabaneDir(), "last-error.json");
8856
+ return join15(cabaneDir(), "last-error.json");
8800
8857
  }
8801
8858
  function recordCrash(rec) {
8802
8859
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect a coding agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
6
6
  "license": "UNLICENSED",