@cabane/companion 0.6.6 → 0.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +108 -51
- package/dist/runtime.js +102 -45
- 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
|
|
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
|
-
|
|
4919
|
-
|
|
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
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
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
|
|
6260
|
+
import { join as join13 } from "path";
|
|
6227
6261
|
|
|
6228
6262
|
// src/summon.ts
|
|
6229
6263
|
import { z as z12 } from "zod";
|
|
@@ -6290,7 +6324,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6290
6324
|
...askState ? [
|
|
6291
6325
|
tool(
|
|
6292
6326
|
ASK_TOOL,
|
|
6293
|
-
"Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact
|
|
6327
|
+
"Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact). Pass `targetUserId` (a workspace member's user id \u2014 get it from `mcp__cabane__list_members`). Two forms: a SINGLE question \u2014 a `headline` (the actual question as one clear, capitalized sentence ending in `?`, \"Do we go to prod?\") plus a short `question` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once, a `questions` array of 1\u20135 items, each `{ headline, body?, options? }`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 end the turn with one ask carrying every question, never pick one and bury the rest. Each question keeps the same form rules: a one-sentence `headline`, a short `body` frame (NOT a report \u2014 your status, links, and detail go in your REPLY, and the body renders inline markdown only: links/emphasis/inline code, no bulleted lists or headings), and 2\u20134 `options` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. An option can be a short button label or a whole sentence. Provide EITHER `question` (single) or `questions` (array), never both. The ask is a first-class attention item aimed at that person; your final reply carries the surrounding CONTEXT (what you found, why you're stuck), the ask carries the QUESTION(S). An open ask marks you as blocked until EVERY question is answered, so raise one only when you truly can't proceed \u2014 never ceremonially. One ask per turn (last call wins). After asking, stop \u2014 when the person replies addressed to you, the ask resolves and you resume; other people's or agents' messages may wake you but leave it open. Targets a human only; to hand work to another AGENT use summon/dispatch instead.",
|
|
6294
6328
|
{
|
|
6295
6329
|
targetUserId: z12.string().uuid().describe("The workspace member (human) to ask \u2014 a user id from `list_members`."),
|
|
6296
6330
|
question: z12.string().min(1).max(400).optional().describe(
|
|
@@ -6397,7 +6431,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6397
6431
|
...wakeState ? [
|
|
6398
6432
|
tool(
|
|
6399
6433
|
WAKE_ME_TOOL,
|
|
6400
|
-
|
|
6434
|
+
"Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for \"wait until X\": when the thing you need hasn't happened yet (a PR isn't merged, a human hasn't answered), arm a wake, end your turn, and you're woken later to CHECK \u2014 read the workspace, and either act or re-arm. Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a known reset time. A speculative far-future check-in you invented yourself is the one thing not to arm: if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it \u2014 finish now, or raise an `ask`. Pass EXACTLY ONE of `afterSeconds` (a relative delay \u2014 `300` for five minutes) or `at` (an absolute ISO-8601 timestamp WITH a zone, e.g. `2026-07-16T09:00:00-07:00` \u2014 YOU compute it from a phrase like \"tomorrow morning\"; the system never parses natural-language time). `note` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check (\"check whether CT441 merged yet\"). The wake is armed when your turn SETTLES, not now, so the delay counts from the turn ending; one wake per turn (last call wins). This is the sanctioned way to schedule your own continuation \u2014 the ONLY one; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you'll be steered to raise an `ask` to the human instead. If a wake can't be armed you're re-dispatched with a note explaining why \u2014 never a silent drop.",
|
|
6401
6435
|
{
|
|
6402
6436
|
afterSeconds: z12.number().int().positive().optional().describe(
|
|
6403
6437
|
"Relative delay in seconds from when this turn ends (e.g. 300 = five minutes). Provide EITHER this or `at`, not both. Floor 60s, horizon 14 days \u2014 enforced server-side."
|
|
@@ -6502,17 +6536,34 @@ function trimSlash3(s) {
|
|
|
6502
6536
|
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
6503
6537
|
}
|
|
6504
6538
|
|
|
6539
|
+
// src/codex-instructions.ts
|
|
6540
|
+
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
6541
|
+
import { tmpdir } from "os";
|
|
6542
|
+
import { join as join9 } from "path";
|
|
6543
|
+
var PREFIX = "cabane-codex-instructions-";
|
|
6544
|
+
async function writeCodexInstructionsFile(contents) {
|
|
6545
|
+
const dir2 = await mkdtemp(join9(tmpdir(), PREFIX));
|
|
6546
|
+
const path3 = join9(dir2, "instructions.md");
|
|
6547
|
+
await writeFile(path3, contents, { encoding: "utf8", mode: 384 });
|
|
6548
|
+
return {
|
|
6549
|
+
path: path3,
|
|
6550
|
+
cleanup: async () => {
|
|
6551
|
+
await rm(dir2, { recursive: true, force: true });
|
|
6552
|
+
}
|
|
6553
|
+
};
|
|
6554
|
+
}
|
|
6555
|
+
|
|
6505
6556
|
// src/prepared.ts
|
|
6506
6557
|
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
|
|
6507
|
-
import { join as
|
|
6558
|
+
import { join as join10 } from "path";
|
|
6508
6559
|
function dirFor(workspaceId) {
|
|
6509
|
-
return
|
|
6560
|
+
return join10(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
6510
6561
|
}
|
|
6511
6562
|
function conversationDir(workspaceId, conversationId) {
|
|
6512
|
-
return
|
|
6563
|
+
return join10(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
6513
6564
|
}
|
|
6514
6565
|
function pathFor3(workspaceId, conversationId, agentId) {
|
|
6515
|
-
return
|
|
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
|
|
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
|
|
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
|
|
6686
|
+
import { join as join12 } from "path";
|
|
6636
6687
|
function transcriptsDir() {
|
|
6637
|
-
return
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
7449
|
+
const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
|
|
7393
7450
|
mkdirSync10(diagnosticDir, { recursive: true });
|
|
7394
7451
|
appendFileSync2(
|
|
7395
|
-
|
|
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
|
|
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
|
|
8049
|
+
return join14(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
7993
8050
|
}
|
|
7994
8051
|
fileFor(turnId, seq) {
|
|
7995
|
-
return
|
|
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 =
|
|
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
|
|
9201
|
+
import { join as join15 } from "path";
|
|
9145
9202
|
function crashMarkerPath() {
|
|
9146
|
-
return
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
4572
|
-
|
|
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
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
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
|
|
5913
|
+
import { join as join13 } from "path";
|
|
5880
5914
|
|
|
5881
5915
|
// src/summon.ts
|
|
5882
5916
|
import { z as z12 } from "zod";
|
|
@@ -5943,7 +5977,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
5943
5977
|
...askState ? [
|
|
5944
5978
|
tool(
|
|
5945
5979
|
ASK_TOOL,
|
|
5946
|
-
"Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact
|
|
5980
|
+
"Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact). Pass `targetUserId` (a workspace member's user id \u2014 get it from `mcp__cabane__list_members`). Two forms: a SINGLE question \u2014 a `headline` (the actual question as one clear, capitalized sentence ending in `?`, \"Do we go to prod?\") plus a short `question` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once, a `questions` array of 1\u20135 items, each `{ headline, body?, options? }`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 end the turn with one ask carrying every question, never pick one and bury the rest. Each question keeps the same form rules: a one-sentence `headline`, a short `body` frame (NOT a report \u2014 your status, links, and detail go in your REPLY, and the body renders inline markdown only: links/emphasis/inline code, no bulleted lists or headings), and 2\u20134 `options` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. An option can be a short button label or a whole sentence. Provide EITHER `question` (single) or `questions` (array), never both. The ask is a first-class attention item aimed at that person; your final reply carries the surrounding CONTEXT (what you found, why you're stuck), the ask carries the QUESTION(S). An open ask marks you as blocked until EVERY question is answered, so raise one only when you truly can't proceed \u2014 never ceremonially. One ask per turn (last call wins). After asking, stop \u2014 when the person replies addressed to you, the ask resolves and you resume; other people's or agents' messages may wake you but leave it open. Targets a human only; to hand work to another AGENT use summon/dispatch instead.",
|
|
5947
5981
|
{
|
|
5948
5982
|
targetUserId: z12.string().uuid().describe("The workspace member (human) to ask \u2014 a user id from `list_members`."),
|
|
5949
5983
|
question: z12.string().min(1).max(400).optional().describe(
|
|
@@ -6050,7 +6084,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6050
6084
|
...wakeState ? [
|
|
6051
6085
|
tool(
|
|
6052
6086
|
WAKE_ME_TOOL,
|
|
6053
|
-
|
|
6087
|
+
"Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for \"wait until X\": when the thing you need hasn't happened yet (a PR isn't merged, a human hasn't answered), arm a wake, end your turn, and you're woken later to CHECK \u2014 read the workspace, and either act or re-arm. Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a known reset time. A speculative far-future check-in you invented yourself is the one thing not to arm: if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it \u2014 finish now, or raise an `ask`. Pass EXACTLY ONE of `afterSeconds` (a relative delay \u2014 `300` for five minutes) or `at` (an absolute ISO-8601 timestamp WITH a zone, e.g. `2026-07-16T09:00:00-07:00` \u2014 YOU compute it from a phrase like \"tomorrow morning\"; the system never parses natural-language time). `note` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check (\"check whether CT441 merged yet\"). The wake is armed when your turn SETTLES, not now, so the delay counts from the turn ending; one wake per turn (last call wins). This is the sanctioned way to schedule your own continuation \u2014 the ONLY one; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you'll be steered to raise an `ask` to the human instead. If a wake can't be armed you're re-dispatched with a note explaining why \u2014 never a silent drop.",
|
|
6054
6088
|
{
|
|
6055
6089
|
afterSeconds: z12.number().int().positive().optional().describe(
|
|
6056
6090
|
"Relative delay in seconds from when this turn ends (e.g. 300 = five minutes). Provide EITHER this or `at`, not both. Floor 60s, horizon 14 days \u2014 enforced server-side."
|
|
@@ -6155,17 +6189,34 @@ function trimSlash3(s) {
|
|
|
6155
6189
|
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
6156
6190
|
}
|
|
6157
6191
|
|
|
6192
|
+
// src/codex-instructions.ts
|
|
6193
|
+
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
6194
|
+
import { tmpdir } from "os";
|
|
6195
|
+
import { join as join9 } from "path";
|
|
6196
|
+
var PREFIX = "cabane-codex-instructions-";
|
|
6197
|
+
async function writeCodexInstructionsFile(contents) {
|
|
6198
|
+
const dir2 = await mkdtemp(join9(tmpdir(), PREFIX));
|
|
6199
|
+
const path3 = join9(dir2, "instructions.md");
|
|
6200
|
+
await writeFile(path3, contents, { encoding: "utf8", mode: 384 });
|
|
6201
|
+
return {
|
|
6202
|
+
path: path3,
|
|
6203
|
+
cleanup: async () => {
|
|
6204
|
+
await rm(dir2, { recursive: true, force: true });
|
|
6205
|
+
}
|
|
6206
|
+
};
|
|
6207
|
+
}
|
|
6208
|
+
|
|
6158
6209
|
// src/prepared.ts
|
|
6159
6210
|
import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
|
|
6160
|
-
import { join as
|
|
6211
|
+
import { join as join10 } from "path";
|
|
6161
6212
|
function dirFor(workspaceId) {
|
|
6162
|
-
return
|
|
6213
|
+
return join10(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
6163
6214
|
}
|
|
6164
6215
|
function conversationDir(workspaceId, conversationId) {
|
|
6165
|
-
return
|
|
6216
|
+
return join10(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
6166
6217
|
}
|
|
6167
6218
|
function pathFor3(workspaceId, conversationId, agentId) {
|
|
6168
|
-
return
|
|
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
|
|
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
|
|
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
|
|
6339
|
+
import { join as join12 } from "path";
|
|
6289
6340
|
function transcriptsDir() {
|
|
6290
|
-
return
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
7102
|
+
const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
|
|
7046
7103
|
mkdirSync9(diagnosticDir, { recursive: true });
|
|
7047
7104
|
appendFileSync2(
|
|
7048
|
-
|
|
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
|
|
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
|
|
7702
|
+
return join14(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
7646
7703
|
}
|
|
7647
7704
|
fileFor(turnId, seq) {
|
|
7648
|
-
return
|
|
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 =
|
|
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
|
|
8854
|
+
import { join as join15 } from "path";
|
|
8798
8855
|
function crashMarkerPath() {
|
|
8799
|
-
return
|
|
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.
|
|
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",
|