@cabane/companion 0.6.82 → 0.6.84

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
@@ -7576,6 +7576,9 @@ var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
7576
7576
  var SKIP_TURN_TOOL = "skip_turn";
7577
7577
  var ASK_TOOL = "ask";
7578
7578
  var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
7579
+ var WITHDRAW_ASK_TOOL = "withdraw_ask";
7580
+ var WITHDRAW_ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_ASK_TOOL}`;
7581
+ var MAX_WITHDRAW_REASON = 400;
7579
7582
  var REPLY_TO_TOOL = "reply_to";
7580
7583
  var REPLY_TO_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${REPLY_TO_TOOL}`;
7581
7584
  var MAX_ASK_ITEMS = 20;
@@ -7691,7 +7694,7 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
7691
7694
  ] : [],
7692
7695
  tool(
7693
7696
  ASK_TOOL,
7694
- `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\u2013${MAX_ASK_ITEMS} items, each \`{ headline, body?, options? }\`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 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; inline markdown only, no 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. 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 CONTEXT, the ask carries the QUESTION(S). It is written and delivered THE MOMENT you call this \u2014 the person sees it in their inbox while your turn is still running \u2014 so call it once you know the question; a second call is a second question, not an edit. The human sends ONE response: supplied answers are recorded, omissions become terminally Unanswered. Ask only when blocked \u2014 never ceremonially. After asking, finish your reply and stop; when the person replies addressed to you, the ask resolves and you resume. Targets a human only; to hand work to another AGENT use send/dispatch instead.`,
7697
+ `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\u2013${MAX_ASK_ITEMS} items, each \`{ headline, body?, options? }\`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 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; inline markdown only, no 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. 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 CONTEXT, the ask carries the QUESTION(S). It is written and delivered THE MOMENT you call this \u2014 the person sees it in their inbox while your turn is still running \u2014 so call it once you know the question; a second call is a second question, not an edit. The human sends ONE response: supplied answers are recorded, omissions become terminally Unanswered. Ask only when blocked \u2014 never ceremonially. \`replaces\` withdraws a stale ask of yours (or anyone's) in this conversation in the same write, with the reason shown to the person. Targets a human only; to hand work to another AGENT use send/dispatch instead.`,
7695
7698
  {
7696
7699
  targetUserId: z13.string().uuid().describe(
7697
7700
  "The workspace member (human) to ask \u2014 a user id, from your turn context's roster."
@@ -7713,6 +7716,14 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
7713
7716
  })
7714
7717
  ).min(1).max(MAX_ASK_ITEMS).optional().describe(
7715
7718
  `MULTI-question form: 1\u2013${MAX_ASK_ITEMS} questions to ask at once, when a plan ends with several bounded decisions. Provide EITHER this or \`question\`/\`headline\`/\`options\`, not both.`
7719
+ ),
7720
+ replaces: z13.object({
7721
+ askId: z13.string().uuid().describe("The id of the open ask to withdraw."),
7722
+ reason: z13.string().min(1).max(MAX_WITHDRAW_REASON).describe(
7723
+ "Why the old question is no longer the question \u2014 shown to the person on their Done row."
7724
+ )
7725
+ }).optional().describe(
7726
+ "Withdraw a stale ask in this conversation as part of raising this one. Both land in a single write, so the person goes from the old question to the new one and never sees neither or both."
7716
7727
  )
7717
7728
  },
7718
7729
  async (args) => {
@@ -7729,6 +7740,9 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
7729
7740
  return performAct(acts, {
7730
7741
  kind: "ask",
7731
7742
  callId: randomUUID(),
7743
+ // CT1350: the swap rides the ask, so the server commits both halves
7744
+ // in one transaction — the person never sees neither or both.
7745
+ ...args.replaces ? { replaces: args.replaces } : {},
7732
7746
  ask: hasArray ? {
7733
7747
  targetUserId: args.targetUserId,
7734
7748
  questions: args.questions.map((q) => ({
@@ -7783,6 +7797,29 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
7783
7797
  },
7784
7798
  { annotations: ACT_TOOL, alwaysLoad: true }
7785
7799
  ),
7800
+ // CT1350: the standalone withdrawal. Not annotated destructive — an ask
7801
+ // is a question, not data, and a harness that gated this on a live
7802
+ // operator would hang a turn nobody is watching (the `cancel_wake`
7803
+ // reasoning). Not idempotent either: a second call with the same id is
7804
+ // refused with `ask_not_open`, which is information, not a no-op.
7805
+ tool(
7806
+ WITHDRAW_ASK_TOOL,
7807
+ "Withdraw an open ask \u2014 yours or anyone's in this conversation \u2014 because the question is moot, was wrong, or has been overtaken. `reason` is required and is shown to the person and, if it wasn't your ask, to the agent who asked it. If there is a current question to put in its place, don't call this \u2014 call `ask` with `replaces` so the two land as one and the person never sees neither or both. Never withdraw to hurry an answer; to act on a deadline, arm a `wake_me` and decide then.",
7808
+ {
7809
+ askId: z13.string().uuid().describe(
7810
+ "The open ask to withdraw. Ask ids ride the conversation's message rows (`ask: { id, status, items }`), and your turn context lists the ones you have open."
7811
+ ),
7812
+ reason: z13.string().min(1).max(MAX_WITHDRAW_REASON).describe(
7813
+ 'Why this question is no longer the question ("merged via GitHub #1551"). Required, and shown to the person on their Done row \u2014 a bare "withdrawn" tells them nothing.'
7814
+ )
7815
+ },
7816
+ async (args) => performAct(acts, {
7817
+ kind: "withdraw_ask",
7818
+ callId: randomUUID(),
7819
+ withdraw: { askId: args.askId, reason: args.reason }
7820
+ }),
7821
+ { annotations: ACT_TOOL, alwaysLoad: true }
7822
+ ),
7786
7823
  // CT990: the inverse. No arguments — it can only ever stand down the
7787
7824
  // caller's own wake in the conversation it's holding a turn in.
7788
7825
  tool(
package/dist/runtime.js CHANGED
@@ -7075,6 +7075,9 @@ var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
7075
7075
  var SKIP_TURN_TOOL = "skip_turn";
7076
7076
  var ASK_TOOL = "ask";
7077
7077
  var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
7078
+ var WITHDRAW_ASK_TOOL = "withdraw_ask";
7079
+ var WITHDRAW_ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_ASK_TOOL}`;
7080
+ var MAX_WITHDRAW_REASON = 400;
7078
7081
  var REPLY_TO_TOOL = "reply_to";
7079
7082
  var REPLY_TO_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${REPLY_TO_TOOL}`;
7080
7083
  var MAX_ASK_ITEMS = 20;
@@ -7190,7 +7193,7 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
7190
7193
  ] : [],
7191
7194
  tool(
7192
7195
  ASK_TOOL,
7193
- `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\u2013${MAX_ASK_ITEMS} items, each \`{ headline, body?, options? }\`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 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; inline markdown only, no 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. 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 CONTEXT, the ask carries the QUESTION(S). It is written and delivered THE MOMENT you call this \u2014 the person sees it in their inbox while your turn is still running \u2014 so call it once you know the question; a second call is a second question, not an edit. The human sends ONE response: supplied answers are recorded, omissions become terminally Unanswered. Ask only when blocked \u2014 never ceremonially. After asking, finish your reply and stop; when the person replies addressed to you, the ask resolves and you resume. Targets a human only; to hand work to another AGENT use send/dispatch instead.`,
7196
+ `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\u2013${MAX_ASK_ITEMS} items, each \`{ headline, body?, options? }\`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 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; inline markdown only, no 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. 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 CONTEXT, the ask carries the QUESTION(S). It is written and delivered THE MOMENT you call this \u2014 the person sees it in their inbox while your turn is still running \u2014 so call it once you know the question; a second call is a second question, not an edit. The human sends ONE response: supplied answers are recorded, omissions become terminally Unanswered. Ask only when blocked \u2014 never ceremonially. \`replaces\` withdraws a stale ask of yours (or anyone's) in this conversation in the same write, with the reason shown to the person. Targets a human only; to hand work to another AGENT use send/dispatch instead.`,
7194
7197
  {
7195
7198
  targetUserId: z13.string().uuid().describe(
7196
7199
  "The workspace member (human) to ask \u2014 a user id, from your turn context's roster."
@@ -7212,6 +7215,14 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
7212
7215
  })
7213
7216
  ).min(1).max(MAX_ASK_ITEMS).optional().describe(
7214
7217
  `MULTI-question form: 1\u2013${MAX_ASK_ITEMS} questions to ask at once, when a plan ends with several bounded decisions. Provide EITHER this or \`question\`/\`headline\`/\`options\`, not both.`
7218
+ ),
7219
+ replaces: z13.object({
7220
+ askId: z13.string().uuid().describe("The id of the open ask to withdraw."),
7221
+ reason: z13.string().min(1).max(MAX_WITHDRAW_REASON).describe(
7222
+ "Why the old question is no longer the question \u2014 shown to the person on their Done row."
7223
+ )
7224
+ }).optional().describe(
7225
+ "Withdraw a stale ask in this conversation as part of raising this one. Both land in a single write, so the person goes from the old question to the new one and never sees neither or both."
7215
7226
  )
7216
7227
  },
7217
7228
  async (args) => {
@@ -7228,6 +7239,9 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
7228
7239
  return performAct(acts, {
7229
7240
  kind: "ask",
7230
7241
  callId: randomUUID(),
7242
+ // CT1350: the swap rides the ask, so the server commits both halves
7243
+ // in one transaction — the person never sees neither or both.
7244
+ ...args.replaces ? { replaces: args.replaces } : {},
7231
7245
  ask: hasArray ? {
7232
7246
  targetUserId: args.targetUserId,
7233
7247
  questions: args.questions.map((q) => ({
@@ -7282,6 +7296,29 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
7282
7296
  },
7283
7297
  { annotations: ACT_TOOL, alwaysLoad: true }
7284
7298
  ),
7299
+ // CT1350: the standalone withdrawal. Not annotated destructive — an ask
7300
+ // is a question, not data, and a harness that gated this on a live
7301
+ // operator would hang a turn nobody is watching (the `cancel_wake`
7302
+ // reasoning). Not idempotent either: a second call with the same id is
7303
+ // refused with `ask_not_open`, which is information, not a no-op.
7304
+ tool(
7305
+ WITHDRAW_ASK_TOOL,
7306
+ "Withdraw an open ask \u2014 yours or anyone's in this conversation \u2014 because the question is moot, was wrong, or has been overtaken. `reason` is required and is shown to the person and, if it wasn't your ask, to the agent who asked it. If there is a current question to put in its place, don't call this \u2014 call `ask` with `replaces` so the two land as one and the person never sees neither or both. Never withdraw to hurry an answer; to act on a deadline, arm a `wake_me` and decide then.",
7307
+ {
7308
+ askId: z13.string().uuid().describe(
7309
+ "The open ask to withdraw. Ask ids ride the conversation's message rows (`ask: { id, status, items }`), and your turn context lists the ones you have open."
7310
+ ),
7311
+ reason: z13.string().min(1).max(MAX_WITHDRAW_REASON).describe(
7312
+ 'Why this question is no longer the question ("merged via GitHub #1551"). Required, and shown to the person on their Done row \u2014 a bare "withdrawn" tells them nothing.'
7313
+ )
7314
+ },
7315
+ async (args) => performAct(acts, {
7316
+ kind: "withdraw_ask",
7317
+ callId: randomUUID(),
7318
+ withdraw: { askId: args.askId, reason: args.reason }
7319
+ }),
7320
+ { annotations: ACT_TOOL, alwaysLoad: true }
7321
+ ),
7285
7322
  // CT990: the inverse. No arguments — it can only ever stand down the
7286
7323
  // caller's own wake in the conversation it's holding a turn in.
7287
7324
  tool(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.82",
3
+ "version": "0.6.84",
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",