@cabane/companion 0.6.8 → 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 +66 -4
- package/dist/pairing-config.js +14 -1
- package/dist/runtime.js +66 -4
- 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) {
|
|
@@ -3857,7 +3879,7 @@ var OPENCODE_HOST_TOOLS = [
|
|
|
3857
3879
|
var OPENCODE_WEB_TOOLS = ["webfetch"];
|
|
3858
3880
|
var OPENCODE_SUBAGENT_TOOLS = ["task"];
|
|
3859
3881
|
var OPENCODE_SCHEDULING_TOOLS = [];
|
|
3860
|
-
var OPENCODE_UI_PROMPT_TOOLS = [];
|
|
3882
|
+
var OPENCODE_UI_PROMPT_TOOLS = ["question"];
|
|
3861
3883
|
function opencodeToolPolicy(policy) {
|
|
3862
3884
|
const tools = {};
|
|
3863
3885
|
const deny = (names) => {
|
|
@@ -7225,6 +7247,41 @@ var Dispatcher = class {
|
|
|
7225
7247
|
if (cached2) {
|
|
7226
7248
|
effectiveCwd = cached2.cwd;
|
|
7227
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
|
+
}
|
|
7228
7285
|
} else {
|
|
7229
7286
|
const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
|
|
7230
7287
|
let preparingStarted = false;
|
|
@@ -7263,7 +7320,12 @@ var Dispatcher = class {
|
|
|
7263
7320
|
// tasker prepare hook keys its per-task env off. Defaults to `[]` for
|
|
7264
7321
|
// an older API. The conversation anchor is gone (CT319).
|
|
7265
7322
|
triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
|
|
7266
|
-
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
|
|
7267
7329
|
});
|
|
7268
7330
|
clearTimeout(preparingTimer);
|
|
7269
7331
|
if (preparingStarted) reportPreparing("done");
|
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) {
|
|
@@ -3510,7 +3532,7 @@ var OPENCODE_HOST_TOOLS = [
|
|
|
3510
3532
|
var OPENCODE_WEB_TOOLS = ["webfetch"];
|
|
3511
3533
|
var OPENCODE_SUBAGENT_TOOLS = ["task"];
|
|
3512
3534
|
var OPENCODE_SCHEDULING_TOOLS = [];
|
|
3513
|
-
var OPENCODE_UI_PROMPT_TOOLS = [];
|
|
3535
|
+
var OPENCODE_UI_PROMPT_TOOLS = ["question"];
|
|
3514
3536
|
function opencodeToolPolicy(policy) {
|
|
3515
3537
|
const tools = {};
|
|
3516
3538
|
const deny = (names) => {
|
|
@@ -6878,6 +6900,41 @@ var Dispatcher = class {
|
|
|
6878
6900
|
if (cached2) {
|
|
6879
6901
|
effectiveCwd = cached2.cwd;
|
|
6880
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
|
+
}
|
|
6881
6938
|
} else {
|
|
6882
6939
|
const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
|
|
6883
6940
|
let preparingStarted = false;
|
|
@@ -6916,7 +6973,12 @@ var Dispatcher = class {
|
|
|
6916
6973
|
// tasker prepare hook keys its per-task env off. Defaults to `[]` for
|
|
6917
6974
|
// an older API. The conversation anchor is gone (CT319).
|
|
6918
6975
|
triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
|
|
6919
|
-
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
|
|
6920
6982
|
});
|
|
6921
6983
|
clearTimeout(preparingTimer);
|
|
6922
6984
|
if (preparingStarted) reportPreparing("done");
|
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",
|