@cabane/companion 0.6.7 → 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 +106 -49
  2. package/dist/runtime.js +100 -43
  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";
@@ -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";
@@ -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.7",
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",