@cabane/companion 0.6.13 → 0.6.14

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
@@ -6278,7 +6278,7 @@ var ConnectorHealthStore = class {
6278
6278
 
6279
6279
  // src/dispatcher.ts
6280
6280
  import { randomUUID } from "crypto";
6281
- import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync10 } from "fs";
6281
+ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync10, statSync as statSync2 } from "fs";
6282
6282
  import { join as join13 } from "path";
6283
6283
 
6284
6284
  // src/summon.ts
@@ -6620,7 +6620,7 @@ async function writeCodexInstructionsFile(contents) {
6620
6620
  }
6621
6621
 
6622
6622
  // src/prepared.ts
6623
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
6623
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
6624
6624
  import { join as join10 } from "path";
6625
6625
  function dirFor(workspaceId) {
6626
6626
  return join10(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
@@ -6655,6 +6655,9 @@ function writePrepared(workspaceId, conversationId, agentId, result) {
6655
6655
  "utf8"
6656
6656
  );
6657
6657
  }
6658
+ function clearPrepared(workspaceId, conversationId, agentId) {
6659
+ rmSync4(pathFor3(workspaceId, conversationId, agentId), { force: true });
6660
+ }
6658
6661
 
6659
6662
  // src/secrets.ts
6660
6663
  import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
@@ -6748,7 +6751,7 @@ function resolveMcpSecrets(mcpServers, store) {
6748
6751
  }
6749
6752
 
6750
6753
  // src/transcript-writer.ts
6751
- import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync4 } from "fs";
6754
+ import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync5 } from "fs";
6752
6755
  import { join as join12 } from "path";
6753
6756
  function transcriptsDir() {
6754
6757
  return join12(cabaneDir(), "transcripts");
@@ -6819,7 +6822,7 @@ function pruneOld(dir2, retain) {
6819
6822
  const drop = files.sort().slice(0, files.length - retain);
6820
6823
  for (const f of drop) {
6821
6824
  try {
6822
- rmSync4(join12(dir2, f), { force: true });
6825
+ rmSync5(join12(dir2, f), { force: true });
6823
6826
  } catch {
6824
6827
  }
6825
6828
  }
@@ -7117,6 +7120,21 @@ var SKIPPED_MARKER_BODY = "(skipped)";
7117
7120
  var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
7118
7121
  var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
7119
7122
  var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
7123
+ function checkoutState(cwd) {
7124
+ if (!cwd) return { ok: false, reason: "no checkout was resolved for this turn" };
7125
+ if (!existsSync9(cwd)) return { ok: false, reason: `the checkout directory is gone (${cwd})` };
7126
+ const gitPath = join13(cwd, ".git");
7127
+ if (!existsSync9(gitPath)) return { ok: false, reason: `${cwd} holds no git metadata` };
7128
+ let stat;
7129
+ try {
7130
+ stat = statSync2(gitPath);
7131
+ } catch (error) {
7132
+ return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
7133
+ }
7134
+ if (stat.isDirectory() && !existsSync9(join13(gitPath, "HEAD")))
7135
+ return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
7136
+ return { ok: true, reason: "usable" };
7137
+ }
7120
7138
  function runKey(conversationId, agentId) {
7121
7139
  return `${conversationId}|${agentId}`;
7122
7140
  }
@@ -7286,7 +7304,15 @@ var Dispatcher = class {
7286
7304
  const triggerIsPrepareFailure = message.body.startsWith(PREPARE_FAILED_PREFIX);
7287
7305
  const prepareFailureDispatch = turnContext.dispatchedByAgentId && !triggerIsPrepareFailure ? { dispatch: turnContext.dispatchedByAgentId } : {};
7288
7306
  if (prepareHook) {
7289
- const cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
7307
+ let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
7308
+ if (cached2 && !checkoutState(cached2.cwd).ok) {
7309
+ turnLog.warn(
7310
+ { cwd: cached2.cwd, reason: checkoutState(cached2.cwd).reason },
7311
+ "dispatcher: the prepared checkout is no longer usable \u2014 re-running the prepare hook to restore it"
7312
+ );
7313
+ clearPrepared(workspaceId, payload.conversationId, payload.agentId);
7314
+ cached2 = null;
7315
+ }
7290
7316
  if (cached2) {
7291
7317
  effectiveCwd = cached2.cwd;
7292
7318
  hookEnv = cached2.env;
@@ -7547,11 +7573,12 @@ ${reason}`,
7547
7573
  ...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
7548
7574
  harnessFingerprint: turnContext.runtime
7549
7575
  });
7550
- turnLog[proof.ok ? "info" : "error"](
7551
- { workspaceProof: proof, checkout: effectiveCwd ?? null },
7552
- `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}`
7576
+ const checkout = checkoutState(effectiveCwd);
7577
+ turnLog[proof.ok && checkout.ok ? "info" : "error"](
7578
+ { workspaceProof: proof, checkout: effectiveCwd ?? null, checkoutState: checkout },
7579
+ `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout ${checkout.ok ? "usable" : checkout.reason}`
7553
7580
  );
7554
- if (effectiveCwd) {
7581
+ if (effectiveCwd && checkout.ok) {
7555
7582
  try {
7556
7583
  const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
7557
7584
  mkdirSync10(diagnosticDir, { recursive: true });
@@ -7576,6 +7603,23 @@ ${reason}`,
7576
7603
  );
7577
7604
  }
7578
7605
  }
7606
+ if (!checkout.ok) {
7607
+ const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
7608
+ try {
7609
+ await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7610
+ body: `**Couldn't prepare your environment.** ${reason}`,
7611
+ kind: "final",
7612
+ turnId,
7613
+ parentMessageId: payload.messageId
7614
+ });
7615
+ } catch (postErr) {
7616
+ turnLog.warn(
7617
+ { err: postErr instanceof Error ? postErr.message : String(postErr) },
7618
+ "dispatcher: checkout-missing notice post failed"
7619
+ );
7620
+ }
7621
+ return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
7622
+ }
7579
7623
  if (!proof.ok) {
7580
7624
  const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd ?? "unknown"}; runtime=${adapter.name}; recovery=restart the connector after restoring the Cabane workspace tool mount`;
7581
7625
  try {
@@ -8140,7 +8184,7 @@ import {
8140
8184
  readdirSync as readdirSync2,
8141
8185
  readFileSync as readFileSync8,
8142
8186
  renameSync as renameSync3,
8143
- rmSync as rmSync5,
8187
+ rmSync as rmSync6,
8144
8188
  writeFileSync as writeFileSync7
8145
8189
  } from "fs";
8146
8190
  import { join as join14 } from "path";
@@ -8174,7 +8218,7 @@ var Outbox = class {
8174
8218
  renameSync3(tmp, target);
8175
8219
  } catch (err) {
8176
8220
  try {
8177
- rmSync5(tmp, { force: true });
8221
+ rmSync6(tmp, { force: true });
8178
8222
  } catch {
8179
8223
  }
8180
8224
  this.log?.warn(
@@ -8221,7 +8265,7 @@ var Outbox = class {
8221
8265
  // Remove a delivered (or terminally-discarded) entry. No-op if already gone.
8222
8266
  remove(turnId, seq) {
8223
8267
  try {
8224
- rmSync5(this.fileFor(turnId, seq), { force: true });
8268
+ rmSync6(this.fileFor(turnId, seq), { force: true });
8225
8269
  } catch {
8226
8270
  }
8227
8271
  }
@@ -8240,7 +8284,7 @@ var Outbox = class {
8240
8284
  "companion outbox: dropping unreadable entry"
8241
8285
  );
8242
8286
  try {
8243
- rmSync5(full, { force: true });
8287
+ rmSync6(full, { force: true });
8244
8288
  } catch {
8245
8289
  }
8246
8290
  }
@@ -9307,7 +9351,7 @@ function handleUncaught(log, err, origin) {
9307
9351
  }
9308
9352
 
9309
9353
  // src/crash-marker.ts
9310
- import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
9354
+ import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "fs";
9311
9355
  import { join as join15 } from "path";
9312
9356
  function crashMarkerPath() {
9313
9357
  return join15(cabaneDir(), "last-error.json");
@@ -9322,7 +9366,7 @@ function recordCrash(rec2) {
9322
9366
  function clearCrash() {
9323
9367
  try {
9324
9368
  const path3 = crashMarkerPath();
9325
- if (existsSync11(path3)) rmSync6(path3, { force: true });
9369
+ if (existsSync11(path3)) rmSync7(path3, { force: true });
9326
9370
  } catch {
9327
9371
  }
9328
9372
  }
package/dist/runtime.js CHANGED
@@ -5931,7 +5931,7 @@ var ConnectorHealthStore = class {
5931
5931
 
5932
5932
  // src/dispatcher.ts
5933
5933
  import { randomUUID } from "crypto";
5934
- import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9 } from "fs";
5934
+ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, statSync as statSync2 } from "fs";
5935
5935
  import { join as join13 } from "path";
5936
5936
 
5937
5937
  // src/summon.ts
@@ -6273,7 +6273,7 @@ async function writeCodexInstructionsFile(contents) {
6273
6273
  }
6274
6274
 
6275
6275
  // src/prepared.ts
6276
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
6276
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
6277
6277
  import { join as join10 } from "path";
6278
6278
  function dirFor(workspaceId) {
6279
6279
  return join10(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
@@ -6308,6 +6308,9 @@ function writePrepared(workspaceId, conversationId, agentId, result) {
6308
6308
  "utf8"
6309
6309
  );
6310
6310
  }
6311
+ function clearPrepared(workspaceId, conversationId, agentId) {
6312
+ rmSync4(pathFor3(workspaceId, conversationId, agentId), { force: true });
6313
+ }
6311
6314
 
6312
6315
  // src/secrets.ts
6313
6316
  import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
@@ -6401,7 +6404,7 @@ function resolveMcpSecrets(mcpServers, store) {
6401
6404
  }
6402
6405
 
6403
6406
  // src/transcript-writer.ts
6404
- import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync4 } from "fs";
6407
+ import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync5 } from "fs";
6405
6408
  import { join as join12 } from "path";
6406
6409
  function transcriptsDir() {
6407
6410
  return join12(cabaneDir(), "transcripts");
@@ -6472,7 +6475,7 @@ function pruneOld(dir2, retain) {
6472
6475
  const drop = files.sort().slice(0, files.length - retain);
6473
6476
  for (const f of drop) {
6474
6477
  try {
6475
- rmSync4(join12(dir2, f), { force: true });
6478
+ rmSync5(join12(dir2, f), { force: true });
6476
6479
  } catch {
6477
6480
  }
6478
6481
  }
@@ -6770,6 +6773,21 @@ var SKIPPED_MARKER_BODY = "(skipped)";
6770
6773
  var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
6771
6774
  var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
6772
6775
  var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
6776
+ function checkoutState(cwd) {
6777
+ if (!cwd) return { ok: false, reason: "no checkout was resolved for this turn" };
6778
+ if (!existsSync9(cwd)) return { ok: false, reason: `the checkout directory is gone (${cwd})` };
6779
+ const gitPath = join13(cwd, ".git");
6780
+ if (!existsSync9(gitPath)) return { ok: false, reason: `${cwd} holds no git metadata` };
6781
+ let stat;
6782
+ try {
6783
+ stat = statSync2(gitPath);
6784
+ } catch (error) {
6785
+ return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
6786
+ }
6787
+ if (stat.isDirectory() && !existsSync9(join13(gitPath, "HEAD")))
6788
+ return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
6789
+ return { ok: true, reason: "usable" };
6790
+ }
6773
6791
  function runKey(conversationId, agentId) {
6774
6792
  return `${conversationId}|${agentId}`;
6775
6793
  }
@@ -6939,7 +6957,15 @@ var Dispatcher = class {
6939
6957
  const triggerIsPrepareFailure = message.body.startsWith(PREPARE_FAILED_PREFIX);
6940
6958
  const prepareFailureDispatch = turnContext.dispatchedByAgentId && !triggerIsPrepareFailure ? { dispatch: turnContext.dispatchedByAgentId } : {};
6941
6959
  if (prepareHook) {
6942
- const cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
6960
+ let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
6961
+ if (cached2 && !checkoutState(cached2.cwd).ok) {
6962
+ turnLog.warn(
6963
+ { cwd: cached2.cwd, reason: checkoutState(cached2.cwd).reason },
6964
+ "dispatcher: the prepared checkout is no longer usable \u2014 re-running the prepare hook to restore it"
6965
+ );
6966
+ clearPrepared(workspaceId, payload.conversationId, payload.agentId);
6967
+ cached2 = null;
6968
+ }
6943
6969
  if (cached2) {
6944
6970
  effectiveCwd = cached2.cwd;
6945
6971
  hookEnv = cached2.env;
@@ -7200,11 +7226,12 @@ ${reason}`,
7200
7226
  ...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
7201
7227
  harnessFingerprint: turnContext.runtime
7202
7228
  });
7203
- turnLog[proof.ok ? "info" : "error"](
7204
- { workspaceProof: proof, checkout: effectiveCwd ?? null },
7205
- `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}`
7229
+ const checkout = checkoutState(effectiveCwd);
7230
+ turnLog[proof.ok && checkout.ok ? "info" : "error"](
7231
+ { workspaceProof: proof, checkout: effectiveCwd ?? null, checkoutState: checkout },
7232
+ `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout ${checkout.ok ? "usable" : checkout.reason}`
7206
7233
  );
7207
- if (effectiveCwd) {
7234
+ if (effectiveCwd && checkout.ok) {
7208
7235
  try {
7209
7236
  const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
7210
7237
  mkdirSync9(diagnosticDir, { recursive: true });
@@ -7229,6 +7256,23 @@ ${reason}`,
7229
7256
  );
7230
7257
  }
7231
7258
  }
7259
+ if (!checkout.ok) {
7260
+ const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
7261
+ try {
7262
+ await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7263
+ body: `**Couldn't prepare your environment.** ${reason}`,
7264
+ kind: "final",
7265
+ turnId,
7266
+ parentMessageId: payload.messageId
7267
+ });
7268
+ } catch (postErr) {
7269
+ turnLog.warn(
7270
+ { err: postErr instanceof Error ? postErr.message : String(postErr) },
7271
+ "dispatcher: checkout-missing notice post failed"
7272
+ );
7273
+ }
7274
+ return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
7275
+ }
7232
7276
  if (!proof.ok) {
7233
7277
  const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd ?? "unknown"}; runtime=${adapter.name}; recovery=restart the connector after restoring the Cabane workspace tool mount`;
7234
7278
  try {
@@ -7793,7 +7837,7 @@ import {
7793
7837
  readdirSync as readdirSync2,
7794
7838
  readFileSync as readFileSync8,
7795
7839
  renameSync as renameSync3,
7796
- rmSync as rmSync5,
7840
+ rmSync as rmSync6,
7797
7841
  writeFileSync as writeFileSync7
7798
7842
  } from "fs";
7799
7843
  import { join as join14 } from "path";
@@ -7827,7 +7871,7 @@ var Outbox = class {
7827
7871
  renameSync3(tmp, target);
7828
7872
  } catch (err) {
7829
7873
  try {
7830
- rmSync5(tmp, { force: true });
7874
+ rmSync6(tmp, { force: true });
7831
7875
  } catch {
7832
7876
  }
7833
7877
  this.log?.warn(
@@ -7874,7 +7918,7 @@ var Outbox = class {
7874
7918
  // Remove a delivered (or terminally-discarded) entry. No-op if already gone.
7875
7919
  remove(turnId, seq) {
7876
7920
  try {
7877
- rmSync5(this.fileFor(turnId, seq), { force: true });
7921
+ rmSync6(this.fileFor(turnId, seq), { force: true });
7878
7922
  } catch {
7879
7923
  }
7880
7924
  }
@@ -7893,7 +7937,7 @@ var Outbox = class {
7893
7937
  "companion outbox: dropping unreadable entry"
7894
7938
  );
7895
7939
  try {
7896
- rmSync5(full, { force: true });
7940
+ rmSync6(full, { force: true });
7897
7941
  } catch {
7898
7942
  }
7899
7943
  }
@@ -8960,7 +9004,7 @@ function handleUncaught(log, err, origin) {
8960
9004
  }
8961
9005
 
8962
9006
  // src/crash-marker.ts
8963
- import { existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
9007
+ import { existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "fs";
8964
9008
  import { join as join15 } from "path";
8965
9009
  function crashMarkerPath() {
8966
9010
  return join15(cabaneDir(), "last-error.json");
@@ -8975,7 +9019,7 @@ function recordCrash(rec) {
8975
9019
  function clearCrash() {
8976
9020
  try {
8977
9021
  const path3 = crashMarkerPath();
8978
- if (existsSync11(path3)) rmSync6(path3, { force: true });
9022
+ if (existsSync11(path3)) rmSync7(path3, { force: true });
8979
9023
  } catch {
8980
9024
  }
8981
9025
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.13",
3
+ "version": "0.6.14",
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",