@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 +172 -53
- package/dist/pairing-config.js +14 -1
- package/dist/runtime.js +166 -47
- package/package.json +1 -1
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
|
|
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
|
-
|
|
4919
|
-
|
|
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
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
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
|
|
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
|
|
6580
|
+
import { join as join10 } from "path";
|
|
6508
6581
|
function dirFor(workspaceId) {
|
|
6509
|
-
return
|
|
6582
|
+
return join10(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
6510
6583
|
}
|
|
6511
6584
|
function conversationDir(workspaceId, conversationId) {
|
|
6512
|
-
return
|
|
6585
|
+
return join10(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
6513
6586
|
}
|
|
6514
6587
|
function pathFor3(workspaceId, conversationId, agentId) {
|
|
6515
|
-
return
|
|
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
|
|
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
|
|
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
|
|
6708
|
+
import { join as join12 } from "path";
|
|
6636
6709
|
function transcriptsDir() {
|
|
6637
|
-
return
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
7511
|
+
const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
|
|
7393
7512
|
mkdirSync10(diagnosticDir, { recursive: true });
|
|
7394
7513
|
appendFileSync2(
|
|
7395
|
-
|
|
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
|
|
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
|
|
8111
|
+
return join14(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
7993
8112
|
}
|
|
7994
8113
|
fileFor(turnId, seq) {
|
|
7995
|
-
return
|
|
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 =
|
|
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
|
|
9263
|
+
import { join as join15 } from "path";
|
|
9145
9264
|
function crashMarkerPath() {
|
|
9146
|
-
return
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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.`
|
package/dist/pairing-config.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
4572
|
-
|
|
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
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
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
|
|
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
|
|
6233
|
+
import { join as join10 } from "path";
|
|
6161
6234
|
function dirFor(workspaceId) {
|
|
6162
|
-
return
|
|
6235
|
+
return join10(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
6163
6236
|
}
|
|
6164
6237
|
function conversationDir(workspaceId, conversationId) {
|
|
6165
|
-
return
|
|
6238
|
+
return join10(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
6166
6239
|
}
|
|
6167
6240
|
function pathFor3(workspaceId, conversationId, agentId) {
|
|
6168
|
-
return
|
|
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
|
|
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
|
|
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
|
|
6361
|
+
import { join as join12 } from "path";
|
|
6289
6362
|
function transcriptsDir() {
|
|
6290
|
-
return
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
7164
|
+
const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
|
|
7046
7165
|
mkdirSync9(diagnosticDir, { recursive: true });
|
|
7047
7166
|
appendFileSync2(
|
|
7048
|
-
|
|
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
|
|
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
|
|
7764
|
+
return join14(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
7646
7765
|
}
|
|
7647
7766
|
fileFor(turnId, seq) {
|
|
7648
|
-
return
|
|
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 =
|
|
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
|
|
8916
|
+
import { join as join15 } from "path";
|
|
8798
8917
|
function crashMarkerPath() {
|
|
8799
|
-
return
|
|
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.
|
|
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",
|