@cabane/companion 0.6.8 → 0.6.10
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 +76 -10
- package/dist/pairing-config.js +14 -1
- package/dist/runtime.js +76 -10
- 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) => {
|
|
@@ -6292,9 +6314,11 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6292
6314
|
tools: [
|
|
6293
6315
|
tool(
|
|
6294
6316
|
SUMMON_AGENT_TOOL,
|
|
6295
|
-
"Summon another agent into THIS conversation \u2014 dispatch a peer to reply here on your turn. Use it to hand part of the work to a teammate, or pull in an expert, without leaving the conversation. Pass the peer's `agentId`
|
|
6317
|
+
"Summon another agent into THIS conversation \u2014 dispatch a peer to reply here on your turn. Use it to hand part of the work to a teammate, or pull in an expert, without leaving the conversation. Pass the peer's `agentId` \u2014 every agent's handle and id is on the roster in your turn context. The peer is dispatched on your turn's final reply, so write the context/ask into that reply first \u2014 it receives your message + this conversation to work from. Writing `@handle` in your prose does NOT summon anyone (agent prose never dispatches); this tool is the only in-thread lever. Single target \u2014 the last call wins. Summoning yourself is a no-op. Reach for it when the human wants the peer's answer right HERE, in front of them \u2014 the reply lands in this thread, so there's no return to wire (a return is for work YOU consume, never a courtesy notification). A handoff to a DIFFERENT conversation is `cabane.conversations.create` / `cabane.conversations.post` with their `dispatch` field instead.",
|
|
6296
6318
|
{
|
|
6297
|
-
agentId: z12.string().uuid().describe(
|
|
6319
|
+
agentId: z12.string().uuid().describe(
|
|
6320
|
+
"The peer agent to summon \u2014 a workspace agent id, from your turn context's roster."
|
|
6321
|
+
)
|
|
6298
6322
|
},
|
|
6299
6323
|
async (args) => {
|
|
6300
6324
|
summonState.agentId = args.agentId;
|
|
@@ -6324,9 +6348,11 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6324
6348
|
...askState ? [
|
|
6325
6349
|
tool(
|
|
6326
6350
|
ASK_TOOL,
|
|
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
|
|
6351
|
+
"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 every person's id is on the roster in your turn context). 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.",
|
|
6328
6352
|
{
|
|
6329
|
-
targetUserId: z12.string().uuid().describe(
|
|
6353
|
+
targetUserId: z12.string().uuid().describe(
|
|
6354
|
+
"The workspace member (human) to ask \u2014 a user id, from your turn context's roster."
|
|
6355
|
+
),
|
|
6330
6356
|
question: z12.string().min(1).max(400).optional().describe(
|
|
6331
6357
|
"SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (capped, inline markdown only). Provide EITHER this or `questions`, not both. Put the crisp one-sentence question in `headline`."
|
|
6332
6358
|
),
|
|
@@ -6401,7 +6427,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6401
6427
|
"The sub-agent's opening instruction \u2014 self-contained (it starts with a fresh context window; only this prompt + the thread it lands in)."
|
|
6402
6428
|
),
|
|
6403
6429
|
agentId: z12.string().uuid().optional().describe(
|
|
6404
|
-
"Optional peer to run the sub-agent as (a workspace agent id from
|
|
6430
|
+
"Optional peer to run the sub-agent as (a workspace agent id, from your turn context's roster); omit to spawn yourself with a fresh context window."
|
|
6405
6431
|
),
|
|
6406
6432
|
title: z12.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
|
|
6407
6433
|
},
|
|
@@ -7065,7 +7091,7 @@ function describeSubAgentError(status2, body) {
|
|
|
7065
7091
|
case "callout_depth_exceeded":
|
|
7066
7092
|
return "sub_agent: this would nest sub-agents too deep (max 3 levels). Have the current worker report back rather than spawning another layer.";
|
|
7067
7093
|
case "dispatch_agent_not_found":
|
|
7068
|
-
return "sub_agent: no live agent in this workspace matches that `agentId`. Check
|
|
7094
|
+
return "sub_agent: no live agent in this workspace matches that `agentId`. Check the roster in your turn context, or omit `agentId` to spawn yourself.";
|
|
7069
7095
|
case "dispatch_return_requires_turn":
|
|
7070
7096
|
case "dispatch_return_requires_agent":
|
|
7071
7097
|
case "dispatch_return_requires_dispatch":
|
|
@@ -7225,6 +7251,41 @@ var Dispatcher = class {
|
|
|
7225
7251
|
if (cached2) {
|
|
7226
7252
|
effectiveCwd = cached2.cwd;
|
|
7227
7253
|
hookEnv = cached2.env;
|
|
7254
|
+
if (prepareHook.validateCached) {
|
|
7255
|
+
const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
|
|
7256
|
+
try {
|
|
7257
|
+
await runHook(prepareHook, {
|
|
7258
|
+
workspaceId,
|
|
7259
|
+
conversationId: payload.conversationId,
|
|
7260
|
+
agentId: payload.agentId,
|
|
7261
|
+
agentUsername: this.opts.agentUsername,
|
|
7262
|
+
runtime: turnContext.runtime,
|
|
7263
|
+
triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
|
|
7264
|
+
title: turnContext.conversation.title,
|
|
7265
|
+
messageBody: message.body,
|
|
7266
|
+
prepared: cached2
|
|
7267
|
+
});
|
|
7268
|
+
} catch (err) {
|
|
7269
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
7270
|
+
turnLog.error({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
|
|
7271
|
+
try {
|
|
7272
|
+
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
7273
|
+
body: `${PREPARE_FAILED_PREFIX}
|
|
7274
|
+
|
|
7275
|
+
${reason}`,
|
|
7276
|
+
kind: "final",
|
|
7277
|
+
turnId,
|
|
7278
|
+
parentMessageId: payload.messageId
|
|
7279
|
+
});
|
|
7280
|
+
} catch (postErr) {
|
|
7281
|
+
turnLog.warn(
|
|
7282
|
+
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
7283
|
+
"dispatcher: prepare-rejection post failed"
|
|
7284
|
+
);
|
|
7285
|
+
}
|
|
7286
|
+
return this.concludeBeforeRun(payload, turnLog, startedAt, `prepare_failed: ${reason}`);
|
|
7287
|
+
}
|
|
7288
|
+
}
|
|
7228
7289
|
} else {
|
|
7229
7290
|
const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
|
|
7230
7291
|
let preparingStarted = false;
|
|
@@ -7263,7 +7324,12 @@ var Dispatcher = class {
|
|
|
7263
7324
|
// tasker prepare hook keys its per-task env off. Defaults to `[]` for
|
|
7264
7325
|
// an older API. The conversation anchor is gone (CT319).
|
|
7265
7326
|
triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
|
|
7266
|
-
title: turnContext.conversation.title
|
|
7327
|
+
title: turnContext.conversation.title,
|
|
7328
|
+
// CT943: the dispatching message's text — where an `env:` directive
|
|
7329
|
+
// rides. The server has always sent the trigger body on the turn
|
|
7330
|
+
// context (for the live feed); this is the first thing to read it as
|
|
7331
|
+
// an INPUT, so a dispatch can ask for its environment in words.
|
|
7332
|
+
messageBody: message.body
|
|
7267
7333
|
});
|
|
7268
7334
|
clearTimeout(preparingTimer);
|
|
7269
7335
|
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) => {
|
|
@@ -5945,9 +5967,11 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
5945
5967
|
tools: [
|
|
5946
5968
|
tool(
|
|
5947
5969
|
SUMMON_AGENT_TOOL,
|
|
5948
|
-
"Summon another agent into THIS conversation \u2014 dispatch a peer to reply here on your turn. Use it to hand part of the work to a teammate, or pull in an expert, without leaving the conversation. Pass the peer's `agentId`
|
|
5970
|
+
"Summon another agent into THIS conversation \u2014 dispatch a peer to reply here on your turn. Use it to hand part of the work to a teammate, or pull in an expert, without leaving the conversation. Pass the peer's `agentId` \u2014 every agent's handle and id is on the roster in your turn context. The peer is dispatched on your turn's final reply, so write the context/ask into that reply first \u2014 it receives your message + this conversation to work from. Writing `@handle` in your prose does NOT summon anyone (agent prose never dispatches); this tool is the only in-thread lever. Single target \u2014 the last call wins. Summoning yourself is a no-op. Reach for it when the human wants the peer's answer right HERE, in front of them \u2014 the reply lands in this thread, so there's no return to wire (a return is for work YOU consume, never a courtesy notification). A handoff to a DIFFERENT conversation is `cabane.conversations.create` / `cabane.conversations.post` with their `dispatch` field instead.",
|
|
5949
5971
|
{
|
|
5950
|
-
agentId: z12.string().uuid().describe(
|
|
5972
|
+
agentId: z12.string().uuid().describe(
|
|
5973
|
+
"The peer agent to summon \u2014 a workspace agent id, from your turn context's roster."
|
|
5974
|
+
)
|
|
5951
5975
|
},
|
|
5952
5976
|
async (args) => {
|
|
5953
5977
|
summonState.agentId = args.agentId;
|
|
@@ -5977,9 +6001,11 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
5977
6001
|
...askState ? [
|
|
5978
6002
|
tool(
|
|
5979
6003
|
ASK_TOOL,
|
|
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
|
|
6004
|
+
"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 every person's id is on the roster in your turn context). 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.",
|
|
5981
6005
|
{
|
|
5982
|
-
targetUserId: z12.string().uuid().describe(
|
|
6006
|
+
targetUserId: z12.string().uuid().describe(
|
|
6007
|
+
"The workspace member (human) to ask \u2014 a user id, from your turn context's roster."
|
|
6008
|
+
),
|
|
5983
6009
|
question: z12.string().min(1).max(400).optional().describe(
|
|
5984
6010
|
"SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (capped, inline markdown only). Provide EITHER this or `questions`, not both. Put the crisp one-sentence question in `headline`."
|
|
5985
6011
|
),
|
|
@@ -6054,7 +6080,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6054
6080
|
"The sub-agent's opening instruction \u2014 self-contained (it starts with a fresh context window; only this prompt + the thread it lands in)."
|
|
6055
6081
|
),
|
|
6056
6082
|
agentId: z12.string().uuid().optional().describe(
|
|
6057
|
-
"Optional peer to run the sub-agent as (a workspace agent id from
|
|
6083
|
+
"Optional peer to run the sub-agent as (a workspace agent id, from your turn context's roster); omit to spawn yourself with a fresh context window."
|
|
6058
6084
|
),
|
|
6059
6085
|
title: z12.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
|
|
6060
6086
|
},
|
|
@@ -6718,7 +6744,7 @@ function describeSubAgentError(status, body) {
|
|
|
6718
6744
|
case "callout_depth_exceeded":
|
|
6719
6745
|
return "sub_agent: this would nest sub-agents too deep (max 3 levels). Have the current worker report back rather than spawning another layer.";
|
|
6720
6746
|
case "dispatch_agent_not_found":
|
|
6721
|
-
return "sub_agent: no live agent in this workspace matches that `agentId`. Check
|
|
6747
|
+
return "sub_agent: no live agent in this workspace matches that `agentId`. Check the roster in your turn context, or omit `agentId` to spawn yourself.";
|
|
6722
6748
|
case "dispatch_return_requires_turn":
|
|
6723
6749
|
case "dispatch_return_requires_agent":
|
|
6724
6750
|
case "dispatch_return_requires_dispatch":
|
|
@@ -6878,6 +6904,41 @@ var Dispatcher = class {
|
|
|
6878
6904
|
if (cached2) {
|
|
6879
6905
|
effectiveCwd = cached2.cwd;
|
|
6880
6906
|
hookEnv = cached2.env;
|
|
6907
|
+
if (prepareHook.validateCached) {
|
|
6908
|
+
const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
|
|
6909
|
+
try {
|
|
6910
|
+
await runHook(prepareHook, {
|
|
6911
|
+
workspaceId,
|
|
6912
|
+
conversationId: payload.conversationId,
|
|
6913
|
+
agentId: payload.agentId,
|
|
6914
|
+
agentUsername: this.opts.agentUsername,
|
|
6915
|
+
runtime: turnContext.runtime,
|
|
6916
|
+
triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
|
|
6917
|
+
title: turnContext.conversation.title,
|
|
6918
|
+
messageBody: message.body,
|
|
6919
|
+
prepared: cached2
|
|
6920
|
+
});
|
|
6921
|
+
} catch (err) {
|
|
6922
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
6923
|
+
turnLog.error({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
|
|
6924
|
+
try {
|
|
6925
|
+
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
6926
|
+
body: `${PREPARE_FAILED_PREFIX}
|
|
6927
|
+
|
|
6928
|
+
${reason}`,
|
|
6929
|
+
kind: "final",
|
|
6930
|
+
turnId,
|
|
6931
|
+
parentMessageId: payload.messageId
|
|
6932
|
+
});
|
|
6933
|
+
} catch (postErr) {
|
|
6934
|
+
turnLog.warn(
|
|
6935
|
+
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
6936
|
+
"dispatcher: prepare-rejection post failed"
|
|
6937
|
+
);
|
|
6938
|
+
}
|
|
6939
|
+
return this.concludeBeforeRun(payload, turnLog, startedAt, `prepare_failed: ${reason}`);
|
|
6940
|
+
}
|
|
6941
|
+
}
|
|
6881
6942
|
} else {
|
|
6882
6943
|
const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
|
|
6883
6944
|
let preparingStarted = false;
|
|
@@ -6916,7 +6977,12 @@ var Dispatcher = class {
|
|
|
6916
6977
|
// tasker prepare hook keys its per-task env off. Defaults to `[]` for
|
|
6917
6978
|
// an older API. The conversation anchor is gone (CT319).
|
|
6918
6979
|
triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
|
|
6919
|
-
title: turnContext.conversation.title
|
|
6980
|
+
title: turnContext.conversation.title,
|
|
6981
|
+
// CT943: the dispatching message's text — where an `env:` directive
|
|
6982
|
+
// rides. The server has always sent the trigger body on the turn
|
|
6983
|
+
// context (for the live feed); this is the first thing to read it as
|
|
6984
|
+
// an INPUT, so a dispatch can ask for its environment in words.
|
|
6985
|
+
messageBody: message.body
|
|
6920
6986
|
});
|
|
6921
6987
|
clearTimeout(preparingTimer);
|
|
6922
6988
|
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.10",
|
|
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",
|