@cabane/companion 0.6.7 → 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -79,7 +79,20 @@ var prepareHookSchema = z.object({
79
79
  // Wall-clock cap for the hook. Provisioning is slow (minutes), so the
80
80
  // default is generous; a hook that hangs past this is killed and the turn
81
81
  // fails with a clear timeout message rather than pinning the companion.
82
- timeoutMs: z.number().int().positive().optional()
82
+ timeoutMs: z.number().int().positive().optional(),
83
+ // CT943: re-invoke the hook on turns where a prepared result already exists,
84
+ // in VALIDATE mode — the cached result is handed back as `prepared` and the
85
+ // hook's stdout is ignored, so the cwd provably cannot move. Its only power
86
+ // is to exit non-zero and fail the turn.
87
+ //
88
+ // Why it exists: the hook runs exactly once per (conversation, agent), so a
89
+ // later message asking for a different environment can't be honoured. Without
90
+ // this, it is silently ignored — the host knows and says nothing, which is
91
+ // the worst available outcome. An opt-in hook can now refuse it out loud.
92
+ //
93
+ // Default OFF, and deliberately: an operator hook that provisions
94
+ // unconditionally must not start running on every turn because it upgraded.
95
+ validateCached: z.boolean().optional()
83
96
  }).strict();
84
97
  var DEFAULT_TIMEOUT_MS = 10 * 6e4;
85
98
  var prepareResultSchema = z.object({
@@ -143,7 +156,12 @@ var runPrepareHook = (hook, input) => {
143
156
  // CT317: newline-joined — workspace paths never contain a newline, so a
144
157
  // hook splits on `\n` unambiguously. Empty string when none.
145
158
  CABANE_TRIGGER_ENTRY_PATHS: input.triggerEntryPaths.join("\n"),
146
- CABANE_CONVERSATION_TITLE: input.title ?? ""
159
+ CABANE_CONVERSATION_TITLE: input.title ?? "",
160
+ // CT943: the dispatching message's text, and — in validate mode only —
161
+ // the prepared result the hook is being asked to check this dispatch
162
+ // against. Both empty/absent on an ordinary first run.
163
+ CABANE_TRIGGER_MESSAGE: input.messageBody ?? "",
164
+ ...input.prepared ? { CABANE_PREPARED: JSON.stringify(input.prepared) } : {}
147
165
  }
148
166
  });
149
167
  } catch (err) {
@@ -181,6 +199,10 @@ var runPrepareHook = (hook, input) => {
181
199
  );
182
200
  return;
183
201
  }
202
+ if (input.prepared) {
203
+ resolve(input.prepared);
204
+ return;
205
+ }
184
206
  try {
185
207
  resolve(parsePrepareOutput(stdout));
186
208
  } catch (err) {
@@ -2902,8 +2924,7 @@ var claudeCodeDialectSchema = z9.object({
2902
2924
  display: z9.enum(["summarized", "omitted"]).optional()
2903
2925
  }),
2904
2926
  z9.object({ type: z9.literal("disabled") })
2905
- ]).optional(),
2906
- hostAccess: z9.boolean().optional()
2927
+ ]).optional()
2907
2928
  }).loose();
2908
2929
  function readThinking(runtimeOptions) {
2909
2930
  const dialect = runtimeOptions?.["claude-code"];
@@ -2981,8 +3002,7 @@ function buildClaudeCodeOptions(req, augment) {
2981
3002
  alwaysLoad: true
2982
3003
  };
2983
3004
  }
2984
- const dialect = claudeCodeDialectSchema.safeParse(config.runtimeOptions?.["claude-code"] ?? {});
2985
- const useCodingPreset = dialect.success ? dialect.data.hostAccess ?? false : false;
3005
+ const useCodingPreset = policy.hostFs;
2986
3006
  const cabaneGlob = `mcp__${CABANE_MCP_SERVER}__*`;
2987
3007
  const extraServerGlobs = Object.keys(req.extra.mcpServers).map((name) => `mcp__${name}__*`);
2988
3008
  const allowedTools = dedupe([
@@ -3859,7 +3879,7 @@ var OPENCODE_HOST_TOOLS = [
3859
3879
  var OPENCODE_WEB_TOOLS = ["webfetch"];
3860
3880
  var OPENCODE_SUBAGENT_TOOLS = ["task"];
3861
3881
  var OPENCODE_SCHEDULING_TOOLS = [];
3862
- var OPENCODE_UI_PROMPT_TOOLS = [];
3882
+ var OPENCODE_UI_PROMPT_TOOLS = ["question"];
3863
3883
  function opencodeToolPolicy(policy) {
3864
3884
  const tools = {};
3865
3885
  const deny = (names) => {
@@ -4898,10 +4918,11 @@ function parseCodexModel(model) {
4898
4918
  var CABANE_MCP_SERVER3 = "cabane";
4899
4919
  var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
4900
4920
  var ACTIVE_CONVERSATION_HEADER4 = "x-cabane-active-conversation";
4901
- function buildRunSpec2(req, resumeThreadId) {
4921
+ function buildRunSpec2(req, resumeThreadId, instructionsFile = null) {
4902
4922
  const { policy, config } = req;
4903
4923
  const directory = req.local.cwd ?? "";
4904
4924
  const dialect = readCodexDialect(config.runtimeOptions);
4925
+ const baseInstructionsFile = !policy.hostFs && instructionsFile ? instructionsFile : null;
4905
4926
  const model = config.model ? parseCodexModel(config.model) : null;
4906
4927
  if (model === null) {
4907
4928
  console.warn(
@@ -4915,19 +4936,21 @@ function buildRunSpec2(req, resumeThreadId) {
4915
4936
  policy: codexToolPolicy(policy),
4916
4937
  skipGitRepoCheck: true,
4917
4938
  ...dialect.modelReasoningEffort ? { modelReasoningEffort: dialect.modelReasoningEffort } : {},
4918
- input: buildInput(req),
4919
- config: buildConfig(req)
4939
+ baseInstructionsFile,
4940
+ input: buildInput(req, baseInstructionsFile !== null),
4941
+ config: buildConfig(req, baseInstructionsFile)
4920
4942
  };
4921
4943
  }
4922
- function buildInput(req) {
4944
+ function buildInput(req, promptRidesInstructionsFile) {
4923
4945
  const userText = req.content.filter((b) => b.type === "text").map((b) => b.text).join("\n\n");
4924
4946
  const body = userText.trim().length > 0 ? userText : req.prompt;
4947
+ if (promptRidesInstructionsFile) return body;
4925
4948
  const system = req.systemPrompt.trim();
4926
4949
  return system.length > 0 ? `${system}
4927
4950
 
4928
4951
  ${body}` : body;
4929
4952
  }
4930
- function buildConfig(req) {
4953
+ function buildConfig(req, baseInstructionsFile) {
4931
4954
  const mcp_servers = {};
4932
4955
  for (const [name, server] of Object.entries(req.local.mcpServers ?? {})) {
4933
4956
  if ("url" in server) {
@@ -4985,6 +5008,10 @@ function buildConfig(req) {
4985
5008
  return {
4986
5009
  mcp_servers,
4987
5010
  experimental_use_rmcp_client: true,
5011
+ // CT871: the no-host-access prompt replacement — Cabane's composed prompt
5012
+ // becomes Codex's base instructions, and Codex's own environment preamble goes
5013
+ // with the harness prompt it belonged to.
5014
+ ...baseInstructionsFile ? { model_instructions_file: baseInstructionsFile, include_environment_context: false } : {},
4988
5015
  ...tmpDir ? { shell_environment_policy: { set: { TMPDIR: tmpDir } } } : {},
4989
5016
  ...policy.permissionProfile ? {
4990
5017
  // CT733: named permission profiles are Codex's split-filesystem path.
@@ -5603,19 +5630,48 @@ function createCodexAdapter(deps = {}) {
5603
5630
  );
5604
5631
  }
5605
5632
  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
- });
5633
+ let instructions = null;
5634
+ if (!req.policy.hostFs) {
5635
+ if (!deps.writeInstructionsFile) {
5636
+ deps.onWarn?.(
5637
+ "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)"
5638
+ );
5639
+ } else {
5640
+ try {
5641
+ instructions = await deps.writeInstructionsFile(req.systemPrompt);
5642
+ } catch (err) {
5643
+ deps.onWarn?.(
5644
+ "codex adapter: failed to write the per-turn instructions file \u2014 falling back to the input preamble (CT871)",
5645
+ { err: err instanceof Error ? err.message : String(err) }
5646
+ );
5647
+ }
5648
+ }
5649
+ }
5650
+ try {
5651
+ const spec = buildRunSpec2(req, resumeThreadId, instructions?.path ?? null);
5652
+ const result = await transport.run(spec, signal);
5653
+ if (signal.aborted) return;
5654
+ yield* decodeCodexStream(result.events, {
5655
+ signal,
5656
+ cwd: req.local.cwd,
5657
+ resumedThreadId: resumeThreadId,
5658
+ degraded,
5659
+ // CT601: Codex reports no model in-stream, so record what the thread was
5660
+ // started with (null on the default token). Same for the reasoning effort.
5661
+ resolvedModel: spec.model,
5662
+ resolvedReasoningEffort: spec.modelReasoningEffort ?? null
5663
+ });
5664
+ } finally {
5665
+ if (instructions) {
5666
+ try {
5667
+ await instructions.cleanup();
5668
+ } catch (err) {
5669
+ deps.onWarn?.("codex adapter: instructions-file cleanup failed (CT871)", {
5670
+ err: err instanceof Error ? err.message : String(err)
5671
+ });
5672
+ }
5673
+ }
5674
+ }
5619
5675
  }
5620
5676
  };
5621
5677
  }
@@ -6223,7 +6279,7 @@ var ConnectorHealthStore = class {
6223
6279
  // src/dispatcher.ts
6224
6280
  import { randomUUID } from "crypto";
6225
6281
  import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync10 } from "fs";
6226
- import { join as join12 } from "path";
6282
+ import { join as join13 } from "path";
6227
6283
 
6228
6284
  // src/summon.ts
6229
6285
  import { z as z12 } from "zod";
@@ -6502,17 +6558,34 @@ function trimSlash3(s) {
6502
6558
  return s.endsWith("/") ? s.slice(0, -1) : s;
6503
6559
  }
6504
6560
 
6561
+ // src/codex-instructions.ts
6562
+ import { mkdtemp, rm, writeFile } from "fs/promises";
6563
+ import { tmpdir } from "os";
6564
+ import { join as join9 } from "path";
6565
+ var PREFIX = "cabane-codex-instructions-";
6566
+ async function writeCodexInstructionsFile(contents) {
6567
+ const dir2 = await mkdtemp(join9(tmpdir(), PREFIX));
6568
+ const path3 = join9(dir2, "instructions.md");
6569
+ await writeFile(path3, contents, { encoding: "utf8", mode: 384 });
6570
+ return {
6571
+ path: path3,
6572
+ cleanup: async () => {
6573
+ await rm(dir2, { recursive: true, force: true });
6574
+ }
6575
+ };
6576
+ }
6577
+
6505
6578
  // src/prepared.ts
6506
6579
  import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
6507
- import { join as join9 } from "path";
6580
+ import { join as join10 } from "path";
6508
6581
  function dirFor(workspaceId) {
6509
- return join9(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
6582
+ return join10(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
6510
6583
  }
6511
6584
  function conversationDir(workspaceId, conversationId) {
6512
- return join9(dirFor(workspaceId), encodeURIComponent(conversationId));
6585
+ return join10(dirFor(workspaceId), encodeURIComponent(conversationId));
6513
6586
  }
6514
6587
  function pathFor3(workspaceId, conversationId, agentId) {
6515
- return join9(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
6588
+ return join10(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
6516
6589
  }
6517
6590
  function readPrepared(workspaceId, conversationId, agentId) {
6518
6591
  const path3 = pathFor3(workspaceId, conversationId, agentId);
@@ -6541,11 +6614,11 @@ function writePrepared(workspaceId, conversationId, agentId, result) {
6541
6614
 
6542
6615
  // src/secrets.ts
6543
6616
  import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
6544
- import { join as join10 } from "path";
6617
+ import { join as join11 } from "path";
6545
6618
  import { z as z13 } from "zod";
6546
6619
  var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
6547
6620
  function secretsPath() {
6548
- return join10(cabaneDir(), "secrets.json");
6621
+ return join11(cabaneDir(), "secrets.json");
6549
6622
  }
6550
6623
  var secretStoreSchema = z13.record(z13.string(), z13.string());
6551
6624
  function loadSecretStore() {
@@ -6632,9 +6705,9 @@ function resolveMcpSecrets(mcpServers, store) {
6632
6705
 
6633
6706
  // src/transcript-writer.ts
6634
6707
  import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync4 } from "fs";
6635
- import { join as join11 } from "path";
6708
+ import { join as join12 } from "path";
6636
6709
  function transcriptsDir() {
6637
- return join11(cabaneDir(), "transcripts");
6710
+ return join12(cabaneDir(), "transcripts");
6638
6711
  }
6639
6712
  var RETAIN = 200;
6640
6713
  var TranscriptWriter = class {
@@ -6643,7 +6716,7 @@ var TranscriptWriter = class {
6643
6716
  onWarn;
6644
6717
  constructor(dir2, meta, onWarn) {
6645
6718
  this.onWarn = onWarn;
6646
- this.path = join11(dir2, fileName(meta));
6719
+ this.path = join12(dir2, fileName(meta));
6647
6720
  try {
6648
6721
  mkdirSync9(dir2, { recursive: true });
6649
6722
  try {
@@ -6702,7 +6775,7 @@ function pruneOld(dir2, retain) {
6702
6775
  const drop = files.sort().slice(0, files.length - retain);
6703
6776
  for (const f of drop) {
6704
6777
  try {
6705
- rmSync4(join11(dir2, f), { force: true });
6778
+ rmSync4(join12(dir2, f), { force: true });
6706
6779
  } catch {
6707
6780
  }
6708
6781
  }
@@ -7174,6 +7247,41 @@ var Dispatcher = class {
7174
7247
  if (cached2) {
7175
7248
  effectiveCwd = cached2.cwd;
7176
7249
  hookEnv = cached2.env;
7250
+ if (prepareHook.validateCached) {
7251
+ const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
7252
+ try {
7253
+ await runHook(prepareHook, {
7254
+ workspaceId,
7255
+ conversationId: payload.conversationId,
7256
+ agentId: payload.agentId,
7257
+ agentUsername: this.opts.agentUsername,
7258
+ runtime: turnContext.runtime,
7259
+ triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
7260
+ title: turnContext.conversation.title,
7261
+ messageBody: message.body,
7262
+ prepared: cached2
7263
+ });
7264
+ } catch (err) {
7265
+ const reason = err instanceof Error ? err.message : String(err);
7266
+ turnLog.error({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
7267
+ try {
7268
+ await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7269
+ body: `${PREPARE_FAILED_PREFIX}
7270
+
7271
+ ${reason}`,
7272
+ kind: "final",
7273
+ turnId,
7274
+ parentMessageId: payload.messageId
7275
+ });
7276
+ } catch (postErr) {
7277
+ turnLog.warn(
7278
+ { err: postErr instanceof Error ? postErr.message : String(postErr) },
7279
+ "dispatcher: prepare-rejection post failed"
7280
+ );
7281
+ }
7282
+ return this.concludeBeforeRun(payload, turnLog, startedAt, `prepare_failed: ${reason}`);
7283
+ }
7284
+ }
7177
7285
  } else {
7178
7286
  const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
7179
7287
  let preparingStarted = false;
@@ -7212,7 +7320,12 @@ var Dispatcher = class {
7212
7320
  // tasker prepare hook keys its per-task env off. Defaults to `[]` for
7213
7321
  // an older API. The conversation anchor is gone (CT319).
7214
7322
  triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
7215
- title: turnContext.conversation.title
7323
+ title: turnContext.conversation.title,
7324
+ // CT943: the dispatching message's text — where an `env:` directive
7325
+ // rides. The server has always sent the trigger body on the turn
7326
+ // context (for the live feed); this is the first thing to read it as
7327
+ // an INPUT, so a dispatch can ask for its environment in words.
7328
+ messageBody: message.body
7216
7329
  });
7217
7330
  clearTimeout(preparingTimer);
7218
7331
  if (preparingStarted) reportPreparing("done");
@@ -7247,7 +7360,7 @@ ${reason}`,
7247
7360
  }
7248
7361
  let turnEnv = hookEnv;
7249
7362
  if (effectiveCwd && turnContext.runtime === "codex") {
7250
- const tmpDir = join12(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
7363
+ const tmpDir = join13(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
7251
7364
  try {
7252
7365
  mkdirSync10(tmpDir, { recursive: true });
7253
7366
  turnEnv = { ...hookEnv, TMPDIR: tmpDir };
@@ -7346,7 +7459,13 @@ ${reason}`,
7346
7459
  adapters.push(createOpencodeAdapter({ serverUrl: this.opts.opencodeServerUrl, onWarn }));
7347
7460
  }
7348
7461
  if (this.opts.codexEnabled) {
7349
- adapters.push(createCodexAdapter({ enabled: true, onWarn }));
7462
+ adapters.push(
7463
+ createCodexAdapter({
7464
+ enabled: true,
7465
+ onWarn,
7466
+ writeInstructionsFile: writeCodexInstructionsFile
7467
+ })
7468
+ );
7350
7469
  }
7351
7470
  const registry = createAdapterRegistry(adapters);
7352
7471
  let adapter;
@@ -7389,10 +7508,10 @@ ${reason}`,
7389
7508
  );
7390
7509
  if (effectiveCwd) {
7391
7510
  try {
7392
- const diagnosticDir = join12(effectiveCwd, ".git", "cabane");
7511
+ const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
7393
7512
  mkdirSync10(diagnosticDir, { recursive: true });
7394
7513
  appendFileSync2(
7395
- join12(diagnosticDir, "readiness.jsonl"),
7514
+ join13(diagnosticDir, "readiness.jsonl"),
7396
7515
  `${JSON.stringify({
7397
7516
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7398
7517
  taskId: hookEnv.CABANE_TASK_ID,
@@ -7976,7 +8095,7 @@ import {
7976
8095
  rmSync as rmSync5,
7977
8096
  writeFileSync as writeFileSync7
7978
8097
  } from "fs";
7979
- import { join as join13 } from "path";
8098
+ import { join as join14 } from "path";
7980
8099
  var MAX_ENTRIES = 2e3;
7981
8100
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
7982
8101
  var Outbox = class {
@@ -7989,10 +8108,10 @@ var Outbox = class {
7989
8108
  // Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
7990
8109
  // cases route writes at the right tmpdir.
7991
8110
  dir() {
7992
- return join13(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
8111
+ return join14(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
7993
8112
  }
7994
8113
  fileFor(turnId, seq) {
7995
- return join13(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
8114
+ return join14(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
7996
8115
  }
7997
8116
  // Persist a commit for later draining. Atomic (temp file + rename) so a
7998
8117
  // concurrent `list()` never reads a half-written entry, then enforces the
@@ -8034,7 +8153,7 @@ var Outbox = class {
8034
8153
  const entries = [];
8035
8154
  for (const name of names) {
8036
8155
  if (!name.endsWith(".json")) continue;
8037
- const full = join13(dir2, name);
8156
+ const full = join14(dir2, name);
8038
8157
  try {
8039
8158
  const parsed = JSON.parse(readFileSync8(full, "utf8"));
8040
8159
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
@@ -9141,9 +9260,9 @@ function handleUncaught(log, err, origin) {
9141
9260
 
9142
9261
  // src/crash-marker.ts
9143
9262
  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";
9263
+ import { join as join15 } from "path";
9145
9264
  function crashMarkerPath() {
9146
- return join14(cabaneDir(), "last-error.json");
9265
+ return join15(cabaneDir(), "last-error.json");
9147
9266
  }
9148
9267
  function recordCrash(rec2) {
9149
9268
  try {
@@ -9497,7 +9616,7 @@ function isAlive(kill, pid) {
9497
9616
 
9498
9617
  // src/commands/transcript.ts
9499
9618
  import { existsSync as existsSync12, readFileSync as readFileSync10, readdirSync as readdirSync4 } from "fs";
9500
- import { isAbsolute, join as join15 } from "path";
9619
+ import { isAbsolute, join as join16 } from "path";
9501
9620
  async function transcript(opts = {}) {
9502
9621
  const dir2 = transcriptsDir();
9503
9622
  if (opts.follow) {
@@ -9514,7 +9633,7 @@ async function transcript(opts = {}) {
9514
9633
  process.stdout.write(emptyMessage(dir2));
9515
9634
  return;
9516
9635
  }
9517
- process.stdout.write(renderFile(join15(dir2, newest)) + "\n");
9636
+ process.stdout.write(renderFile(join16(dir2, newest)) + "\n");
9518
9637
  return;
9519
9638
  }
9520
9639
  printList(dir2);
@@ -9597,7 +9716,7 @@ function isComplete(content) {
9597
9716
  async function followTranscripts(dir2) {
9598
9717
  const follower = new TranscriptFollower({
9599
9718
  listFiles: () => listFiles(dir2),
9600
- read: (f) => readFileSync10(join15(dir2, f), "utf8"),
9719
+ read: (f) => readFileSync10(join16(dir2, f), "utf8"),
9601
9720
  write: (s) => process.stdout.write(s),
9602
9721
  // CSI: cursor up `n` lines, then erase from cursor to end of screen.
9603
9722
  clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
@@ -9637,7 +9756,7 @@ function printList(dir2) {
9637
9756
 
9638
9757
  `);
9639
9758
  for (const f of files.slice(0, 20)) {
9640
- const { meta, outcome } = peek(join15(dir2, f));
9759
+ const { meta, outcome } = peek(join16(dir2, f));
9641
9760
  const when = fmtTime(rec(meta)?.ts);
9642
9761
  const ws = str2(rec(meta)?.workspaceSlug);
9643
9762
  const o = rec(outcome);
@@ -9674,10 +9793,10 @@ function resolveTarget(dir2, target) {
9674
9793
  if (existsSync12(target)) return target;
9675
9794
  throw new CompanionError(`no transcript at ${target}.`);
9676
9795
  }
9677
- const exact = join15(dir2, target);
9796
+ const exact = join16(dir2, target);
9678
9797
  if (existsSync12(exact)) return exact;
9679
9798
  const matches = listFiles(dir2).filter((f) => f.includes(target));
9680
- if (matches.length === 1) return join15(dir2, matches[0]);
9799
+ if (matches.length === 1) return join16(dir2, matches[0]);
9681
9800
  if (matches.length === 0) {
9682
9801
  throw new CompanionError(
9683
9802
  `no transcript matching "${target}" in ${dir2}. Run \`cabane-companion transcript\` to list them.`
@@ -63,7 +63,20 @@ var prepareHookSchema = z.object({
63
63
  // Wall-clock cap for the hook. Provisioning is slow (minutes), so the
64
64
  // default is generous; a hook that hangs past this is killed and the turn
65
65
  // fails with a clear timeout message rather than pinning the companion.
66
- timeoutMs: z.number().int().positive().optional()
66
+ timeoutMs: z.number().int().positive().optional(),
67
+ // CT943: re-invoke the hook on turns where a prepared result already exists,
68
+ // in VALIDATE mode — the cached result is handed back as `prepared` and the
69
+ // hook's stdout is ignored, so the cwd provably cannot move. Its only power
70
+ // is to exit non-zero and fail the turn.
71
+ //
72
+ // Why it exists: the hook runs exactly once per (conversation, agent), so a
73
+ // later message asking for a different environment can't be honoured. Without
74
+ // this, it is silently ignored — the host knows and says nothing, which is
75
+ // the worst available outcome. An opt-in hook can now refuse it out loud.
76
+ //
77
+ // Default OFF, and deliberately: an operator hook that provisions
78
+ // unconditionally must not start running on every turn because it upgraded.
79
+ validateCached: z.boolean().optional()
67
80
  }).strict();
68
81
  var DEFAULT_TIMEOUT_MS = 10 * 6e4;
69
82
  var prepareResultSchema = z.object({
package/dist/runtime.js CHANGED
@@ -72,7 +72,20 @@ var prepareHookSchema = z.object({
72
72
  // Wall-clock cap for the hook. Provisioning is slow (minutes), so the
73
73
  // default is generous; a hook that hangs past this is killed and the turn
74
74
  // fails with a clear timeout message rather than pinning the companion.
75
- timeoutMs: z.number().int().positive().optional()
75
+ timeoutMs: z.number().int().positive().optional(),
76
+ // CT943: re-invoke the hook on turns where a prepared result already exists,
77
+ // in VALIDATE mode — the cached result is handed back as `prepared` and the
78
+ // hook's stdout is ignored, so the cwd provably cannot move. Its only power
79
+ // is to exit non-zero and fail the turn.
80
+ //
81
+ // Why it exists: the hook runs exactly once per (conversation, agent), so a
82
+ // later message asking for a different environment can't be honoured. Without
83
+ // this, it is silently ignored — the host knows and says nothing, which is
84
+ // the worst available outcome. An opt-in hook can now refuse it out loud.
85
+ //
86
+ // Default OFF, and deliberately: an operator hook that provisions
87
+ // unconditionally must not start running on every turn because it upgraded.
88
+ validateCached: z.boolean().optional()
76
89
  }).strict();
77
90
  var DEFAULT_TIMEOUT_MS = 10 * 6e4;
78
91
  var prepareResultSchema = z.object({
@@ -136,7 +149,12 @@ var runPrepareHook = (hook, input) => {
136
149
  // CT317: newline-joined — workspace paths never contain a newline, so a
137
150
  // hook splits on `\n` unambiguously. Empty string when none.
138
151
  CABANE_TRIGGER_ENTRY_PATHS: input.triggerEntryPaths.join("\n"),
139
- CABANE_CONVERSATION_TITLE: input.title ?? ""
152
+ CABANE_CONVERSATION_TITLE: input.title ?? "",
153
+ // CT943: the dispatching message's text, and — in validate mode only —
154
+ // the prepared result the hook is being asked to check this dispatch
155
+ // against. Both empty/absent on an ordinary first run.
156
+ CABANE_TRIGGER_MESSAGE: input.messageBody ?? "",
157
+ ...input.prepared ? { CABANE_PREPARED: JSON.stringify(input.prepared) } : {}
140
158
  }
141
159
  });
142
160
  } catch (err) {
@@ -174,6 +192,10 @@ var runPrepareHook = (hook, input) => {
174
192
  );
175
193
  return;
176
194
  }
195
+ if (input.prepared) {
196
+ resolve(input.prepared);
197
+ return;
198
+ }
177
199
  try {
178
200
  resolve(parsePrepareOutput(stdout));
179
201
  } catch (err) {
@@ -2555,8 +2577,7 @@ var claudeCodeDialectSchema = z9.object({
2555
2577
  display: z9.enum(["summarized", "omitted"]).optional()
2556
2578
  }),
2557
2579
  z9.object({ type: z9.literal("disabled") })
2558
- ]).optional(),
2559
- hostAccess: z9.boolean().optional()
2580
+ ]).optional()
2560
2581
  }).loose();
2561
2582
  function readThinking(runtimeOptions) {
2562
2583
  const dialect = runtimeOptions?.["claude-code"];
@@ -2634,8 +2655,7 @@ function buildClaudeCodeOptions(req, augment) {
2634
2655
  alwaysLoad: true
2635
2656
  };
2636
2657
  }
2637
- const dialect = claudeCodeDialectSchema.safeParse(config.runtimeOptions?.["claude-code"] ?? {});
2638
- const useCodingPreset = dialect.success ? dialect.data.hostAccess ?? false : false;
2658
+ const useCodingPreset = policy.hostFs;
2639
2659
  const cabaneGlob = `mcp__${CABANE_MCP_SERVER}__*`;
2640
2660
  const extraServerGlobs = Object.keys(req.extra.mcpServers).map((name) => `mcp__${name}__*`);
2641
2661
  const allowedTools = dedupe([
@@ -3512,7 +3532,7 @@ var OPENCODE_HOST_TOOLS = [
3512
3532
  var OPENCODE_WEB_TOOLS = ["webfetch"];
3513
3533
  var OPENCODE_SUBAGENT_TOOLS = ["task"];
3514
3534
  var OPENCODE_SCHEDULING_TOOLS = [];
3515
- var OPENCODE_UI_PROMPT_TOOLS = [];
3535
+ var OPENCODE_UI_PROMPT_TOOLS = ["question"];
3516
3536
  function opencodeToolPolicy(policy) {
3517
3537
  const tools = {};
3518
3538
  const deny = (names) => {
@@ -4551,10 +4571,11 @@ function parseCodexModel(model) {
4551
4571
  var CABANE_MCP_SERVER3 = "cabane";
4552
4572
  var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
4553
4573
  var ACTIVE_CONVERSATION_HEADER4 = "x-cabane-active-conversation";
4554
- function buildRunSpec2(req, resumeThreadId) {
4574
+ function buildRunSpec2(req, resumeThreadId, instructionsFile = null) {
4555
4575
  const { policy, config } = req;
4556
4576
  const directory = req.local.cwd ?? "";
4557
4577
  const dialect = readCodexDialect(config.runtimeOptions);
4578
+ const baseInstructionsFile = !policy.hostFs && instructionsFile ? instructionsFile : null;
4558
4579
  const model = config.model ? parseCodexModel(config.model) : null;
4559
4580
  if (model === null) {
4560
4581
  console.warn(
@@ -4568,19 +4589,21 @@ function buildRunSpec2(req, resumeThreadId) {
4568
4589
  policy: codexToolPolicy(policy),
4569
4590
  skipGitRepoCheck: true,
4570
4591
  ...dialect.modelReasoningEffort ? { modelReasoningEffort: dialect.modelReasoningEffort } : {},
4571
- input: buildInput(req),
4572
- config: buildConfig(req)
4592
+ baseInstructionsFile,
4593
+ input: buildInput(req, baseInstructionsFile !== null),
4594
+ config: buildConfig(req, baseInstructionsFile)
4573
4595
  };
4574
4596
  }
4575
- function buildInput(req) {
4597
+ function buildInput(req, promptRidesInstructionsFile) {
4576
4598
  const userText = req.content.filter((b) => b.type === "text").map((b) => b.text).join("\n\n");
4577
4599
  const body = userText.trim().length > 0 ? userText : req.prompt;
4600
+ if (promptRidesInstructionsFile) return body;
4578
4601
  const system = req.systemPrompt.trim();
4579
4602
  return system.length > 0 ? `${system}
4580
4603
 
4581
4604
  ${body}` : body;
4582
4605
  }
4583
- function buildConfig(req) {
4606
+ function buildConfig(req, baseInstructionsFile) {
4584
4607
  const mcp_servers = {};
4585
4608
  for (const [name, server] of Object.entries(req.local.mcpServers ?? {})) {
4586
4609
  if ("url" in server) {
@@ -4638,6 +4661,10 @@ function buildConfig(req) {
4638
4661
  return {
4639
4662
  mcp_servers,
4640
4663
  experimental_use_rmcp_client: true,
4664
+ // CT871: the no-host-access prompt replacement — Cabane's composed prompt
4665
+ // becomes Codex's base instructions, and Codex's own environment preamble goes
4666
+ // with the harness prompt it belonged to.
4667
+ ...baseInstructionsFile ? { model_instructions_file: baseInstructionsFile, include_environment_context: false } : {},
4641
4668
  ...tmpDir ? { shell_environment_policy: { set: { TMPDIR: tmpDir } } } : {},
4642
4669
  ...policy.permissionProfile ? {
4643
4670
  // CT733: named permission profiles are Codex's split-filesystem path.
@@ -5256,19 +5283,48 @@ function createCodexAdapter(deps = {}) {
5256
5283
  );
5257
5284
  }
5258
5285
  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
- });
5286
+ let instructions = null;
5287
+ if (!req.policy.hostFs) {
5288
+ if (!deps.writeInstructionsFile) {
5289
+ deps.onWarn?.(
5290
+ "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)"
5291
+ );
5292
+ } else {
5293
+ try {
5294
+ instructions = await deps.writeInstructionsFile(req.systemPrompt);
5295
+ } catch (err) {
5296
+ deps.onWarn?.(
5297
+ "codex adapter: failed to write the per-turn instructions file \u2014 falling back to the input preamble (CT871)",
5298
+ { err: err instanceof Error ? err.message : String(err) }
5299
+ );
5300
+ }
5301
+ }
5302
+ }
5303
+ try {
5304
+ const spec = buildRunSpec2(req, resumeThreadId, instructions?.path ?? null);
5305
+ const result = await transport.run(spec, signal);
5306
+ if (signal.aborted) return;
5307
+ yield* decodeCodexStream(result.events, {
5308
+ signal,
5309
+ cwd: req.local.cwd,
5310
+ resumedThreadId: resumeThreadId,
5311
+ degraded,
5312
+ // CT601: Codex reports no model in-stream, so record what the thread was
5313
+ // started with (null on the default token). Same for the reasoning effort.
5314
+ resolvedModel: spec.model,
5315
+ resolvedReasoningEffort: spec.modelReasoningEffort ?? null
5316
+ });
5317
+ } finally {
5318
+ if (instructions) {
5319
+ try {
5320
+ await instructions.cleanup();
5321
+ } catch (err) {
5322
+ deps.onWarn?.("codex adapter: instructions-file cleanup failed (CT871)", {
5323
+ err: err instanceof Error ? err.message : String(err)
5324
+ });
5325
+ }
5326
+ }
5327
+ }
5272
5328
  }
5273
5329
  };
5274
5330
  }
@@ -5876,7 +5932,7 @@ var ConnectorHealthStore = class {
5876
5932
  // src/dispatcher.ts
5877
5933
  import { randomUUID } from "crypto";
5878
5934
  import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9 } from "fs";
5879
- import { join as join12 } from "path";
5935
+ import { join as join13 } from "path";
5880
5936
 
5881
5937
  // src/summon.ts
5882
5938
  import { z as z12 } from "zod";
@@ -6155,17 +6211,34 @@ function trimSlash3(s) {
6155
6211
  return s.endsWith("/") ? s.slice(0, -1) : s;
6156
6212
  }
6157
6213
 
6214
+ // src/codex-instructions.ts
6215
+ import { mkdtemp, rm, writeFile } from "fs/promises";
6216
+ import { tmpdir } from "os";
6217
+ import { join as join9 } from "path";
6218
+ var PREFIX = "cabane-codex-instructions-";
6219
+ async function writeCodexInstructionsFile(contents) {
6220
+ const dir2 = await mkdtemp(join9(tmpdir(), PREFIX));
6221
+ const path3 = join9(dir2, "instructions.md");
6222
+ await writeFile(path3, contents, { encoding: "utf8", mode: 384 });
6223
+ return {
6224
+ path: path3,
6225
+ cleanup: async () => {
6226
+ await rm(dir2, { recursive: true, force: true });
6227
+ }
6228
+ };
6229
+ }
6230
+
6158
6231
  // src/prepared.ts
6159
6232
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
6160
- import { join as join9 } from "path";
6233
+ import { join as join10 } from "path";
6161
6234
  function dirFor(workspaceId) {
6162
- return join9(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
6235
+ return join10(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
6163
6236
  }
6164
6237
  function conversationDir(workspaceId, conversationId) {
6165
- return join9(dirFor(workspaceId), encodeURIComponent(conversationId));
6238
+ return join10(dirFor(workspaceId), encodeURIComponent(conversationId));
6166
6239
  }
6167
6240
  function pathFor3(workspaceId, conversationId, agentId) {
6168
- return join9(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
6241
+ return join10(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
6169
6242
  }
6170
6243
  function readPrepared(workspaceId, conversationId, agentId) {
6171
6244
  const path3 = pathFor3(workspaceId, conversationId, agentId);
@@ -6194,11 +6267,11 @@ function writePrepared(workspaceId, conversationId, agentId, result) {
6194
6267
 
6195
6268
  // src/secrets.ts
6196
6269
  import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
6197
- import { join as join10 } from "path";
6270
+ import { join as join11 } from "path";
6198
6271
  import { z as z13 } from "zod";
6199
6272
  var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
6200
6273
  function secretsPath() {
6201
- return join10(cabaneDir(), "secrets.json");
6274
+ return join11(cabaneDir(), "secrets.json");
6202
6275
  }
6203
6276
  var secretStoreSchema = z13.record(z13.string(), z13.string());
6204
6277
  function loadSecretStore() {
@@ -6285,9 +6358,9 @@ function resolveMcpSecrets(mcpServers, store) {
6285
6358
 
6286
6359
  // src/transcript-writer.ts
6287
6360
  import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync4 } from "fs";
6288
- import { join as join11 } from "path";
6361
+ import { join as join12 } from "path";
6289
6362
  function transcriptsDir() {
6290
- return join11(cabaneDir(), "transcripts");
6363
+ return join12(cabaneDir(), "transcripts");
6291
6364
  }
6292
6365
  var RETAIN = 200;
6293
6366
  var TranscriptWriter = class {
@@ -6296,7 +6369,7 @@ var TranscriptWriter = class {
6296
6369
  onWarn;
6297
6370
  constructor(dir2, meta, onWarn) {
6298
6371
  this.onWarn = onWarn;
6299
- this.path = join11(dir2, fileName(meta));
6372
+ this.path = join12(dir2, fileName(meta));
6300
6373
  try {
6301
6374
  mkdirSync8(dir2, { recursive: true });
6302
6375
  try {
@@ -6355,7 +6428,7 @@ function pruneOld(dir2, retain) {
6355
6428
  const drop = files.sort().slice(0, files.length - retain);
6356
6429
  for (const f of drop) {
6357
6430
  try {
6358
- rmSync4(join11(dir2, f), { force: true });
6431
+ rmSync4(join12(dir2, f), { force: true });
6359
6432
  } catch {
6360
6433
  }
6361
6434
  }
@@ -6827,6 +6900,41 @@ var Dispatcher = class {
6827
6900
  if (cached2) {
6828
6901
  effectiveCwd = cached2.cwd;
6829
6902
  hookEnv = cached2.env;
6903
+ if (prepareHook.validateCached) {
6904
+ const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
6905
+ try {
6906
+ await runHook(prepareHook, {
6907
+ workspaceId,
6908
+ conversationId: payload.conversationId,
6909
+ agentId: payload.agentId,
6910
+ agentUsername: this.opts.agentUsername,
6911
+ runtime: turnContext.runtime,
6912
+ triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
6913
+ title: turnContext.conversation.title,
6914
+ messageBody: message.body,
6915
+ prepared: cached2
6916
+ });
6917
+ } catch (err) {
6918
+ const reason = err instanceof Error ? err.message : String(err);
6919
+ turnLog.error({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
6920
+ try {
6921
+ await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
6922
+ body: `${PREPARE_FAILED_PREFIX}
6923
+
6924
+ ${reason}`,
6925
+ kind: "final",
6926
+ turnId,
6927
+ parentMessageId: payload.messageId
6928
+ });
6929
+ } catch (postErr) {
6930
+ turnLog.warn(
6931
+ { err: postErr instanceof Error ? postErr.message : String(postErr) },
6932
+ "dispatcher: prepare-rejection post failed"
6933
+ );
6934
+ }
6935
+ return this.concludeBeforeRun(payload, turnLog, startedAt, `prepare_failed: ${reason}`);
6936
+ }
6937
+ }
6830
6938
  } else {
6831
6939
  const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
6832
6940
  let preparingStarted = false;
@@ -6865,7 +6973,12 @@ var Dispatcher = class {
6865
6973
  // tasker prepare hook keys its per-task env off. Defaults to `[]` for
6866
6974
  // an older API. The conversation anchor is gone (CT319).
6867
6975
  triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
6868
- title: turnContext.conversation.title
6976
+ title: turnContext.conversation.title,
6977
+ // CT943: the dispatching message's text — where an `env:` directive
6978
+ // rides. The server has always sent the trigger body on the turn
6979
+ // context (for the live feed); this is the first thing to read it as
6980
+ // an INPUT, so a dispatch can ask for its environment in words.
6981
+ messageBody: message.body
6869
6982
  });
6870
6983
  clearTimeout(preparingTimer);
6871
6984
  if (preparingStarted) reportPreparing("done");
@@ -6900,7 +7013,7 @@ ${reason}`,
6900
7013
  }
6901
7014
  let turnEnv = hookEnv;
6902
7015
  if (effectiveCwd && turnContext.runtime === "codex") {
6903
- const tmpDir = join12(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
7016
+ const tmpDir = join13(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
6904
7017
  try {
6905
7018
  mkdirSync9(tmpDir, { recursive: true });
6906
7019
  turnEnv = { ...hookEnv, TMPDIR: tmpDir };
@@ -6999,7 +7112,13 @@ ${reason}`,
6999
7112
  adapters.push(createOpencodeAdapter({ serverUrl: this.opts.opencodeServerUrl, onWarn }));
7000
7113
  }
7001
7114
  if (this.opts.codexEnabled) {
7002
- adapters.push(createCodexAdapter({ enabled: true, onWarn }));
7115
+ adapters.push(
7116
+ createCodexAdapter({
7117
+ enabled: true,
7118
+ onWarn,
7119
+ writeInstructionsFile: writeCodexInstructionsFile
7120
+ })
7121
+ );
7003
7122
  }
7004
7123
  const registry = createAdapterRegistry(adapters);
7005
7124
  let adapter;
@@ -7042,10 +7161,10 @@ ${reason}`,
7042
7161
  );
7043
7162
  if (effectiveCwd) {
7044
7163
  try {
7045
- const diagnosticDir = join12(effectiveCwd, ".git", "cabane");
7164
+ const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
7046
7165
  mkdirSync9(diagnosticDir, { recursive: true });
7047
7166
  appendFileSync2(
7048
- join12(diagnosticDir, "readiness.jsonl"),
7167
+ join13(diagnosticDir, "readiness.jsonl"),
7049
7168
  `${JSON.stringify({
7050
7169
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7051
7170
  taskId: hookEnv.CABANE_TASK_ID,
@@ -7629,7 +7748,7 @@ import {
7629
7748
  rmSync as rmSync5,
7630
7749
  writeFileSync as writeFileSync7
7631
7750
  } from "fs";
7632
- import { join as join13 } from "path";
7751
+ import { join as join14 } from "path";
7633
7752
  var MAX_ENTRIES = 2e3;
7634
7753
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
7635
7754
  var Outbox = class {
@@ -7642,10 +7761,10 @@ var Outbox = class {
7642
7761
  // Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
7643
7762
  // cases route writes at the right tmpdir.
7644
7763
  dir() {
7645
- return join13(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
7764
+ return join14(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
7646
7765
  }
7647
7766
  fileFor(turnId, seq) {
7648
- return join13(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
7767
+ return join14(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
7649
7768
  }
7650
7769
  // Persist a commit for later draining. Atomic (temp file + rename) so a
7651
7770
  // concurrent `list()` never reads a half-written entry, then enforces the
@@ -7687,7 +7806,7 @@ var Outbox = class {
7687
7806
  const entries = [];
7688
7807
  for (const name of names) {
7689
7808
  if (!name.endsWith(".json")) continue;
7690
- const full = join13(dir2, name);
7809
+ const full = join14(dir2, name);
7691
7810
  try {
7692
7811
  const parsed = JSON.parse(readFileSync8(full, "utf8"));
7693
7812
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
@@ -8794,9 +8913,9 @@ function handleUncaught(log, err, origin) {
8794
8913
 
8795
8914
  // src/crash-marker.ts
8796
8915
  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";
8916
+ import { join as join15 } from "path";
8798
8917
  function crashMarkerPath() {
8799
- return join14(cabaneDir(), "last-error.json");
8918
+ return join15(cabaneDir(), "last-error.json");
8800
8919
  }
8801
8920
  function recordCrash(rec) {
8802
8921
  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.9",
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",