@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 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");
@@ -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.8",
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",