openmausbot 0.1.86 → 0.1.88

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.
Files changed (216) hide show
  1. package/README.md +4 -3
  2. package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
  3. package/dist/assets/{arc-DW7mrI4p.js → arc-3RwkUjRe.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
  7. package/dist/assets/channel-CnRnO3tF.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
  34. package/dist/assets/index-BIMdIbaw.css +1 -0
  35. package/dist/assets/index-CHnODCr2.js +583 -0
  36. package/dist/assets/{index-h9FFNwTu.js → index-CIaHn5NS.js} +1 -1
  37. package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
  41. package/dist/assets/{linear-3su2TG93.js → linear-C2pWPSxO.js} +1 -1
  42. package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-CR-j5WQU.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/browser-proxy.js +2 -1
  62. package/dist-server/companion/src/routes.js +8 -1
  63. package/dist-server/connector-proxy.js +105 -1
  64. package/dist-server/container-mcp.js +1032 -165
  65. package/dist-server/drivers/agents-proxy.js +338 -29
  66. package/dist-server/drivers/phone-proxy.js +33 -2
  67. package/dist-server/index.js +40590 -31811
  68. package/dist-server/local-computer-proxy.js +5 -3
  69. package/dist-server/local-computer.js +53 -17
  70. package/dist-server/mcp-gate.js +2 -0
  71. package/dist-server/mcp-server.js +5 -3
  72. package/dist-server/message-search.worker.js +52 -0
  73. package/dist-server/openmausbot.js +4457 -1399
  74. package/dist-server/pair-cli.js +4457 -1399
  75. package/dist-server/permission-proxy.js +29 -9
  76. package/dist-server/server/admission.js +81 -0
  77. package/dist-server/server/aside-queue.js +294 -0
  78. package/dist-server/server/attachments.js +98 -1
  79. package/dist-server/server/auto-approve.js +4 -3
  80. package/dist-server/server/auto-vm-claims.js +1 -0
  81. package/dist-server/server/bot-attachment.js +107 -0
  82. package/dist-server/server/bot-overview.js +19 -0
  83. package/dist-server/server/bot-package.js +18 -292
  84. package/dist-server/server/bot-profile.js +5 -17
  85. package/dist-server/server/box-create-idempotency.js +3 -3
  86. package/dist-server/server/box.js +57 -55
  87. package/dist-server/server/browser-proxy.js +3 -2
  88. package/dist-server/server/browser-runtime.js +37 -1
  89. package/dist-server/server/cli-api-setup.js +1 -0
  90. package/dist-server/server/cli.js +2 -2
  91. package/dist-server/server/command-allowlist.js +154 -0
  92. package/dist-server/server/composio.js +297 -1
  93. package/dist-server/server/computer-wait.js +22 -9
  94. package/dist-server/server/config.js +84 -12
  95. package/dist-server/server/connector-advertisement.js +165 -0
  96. package/dist-server/server/connector-proxy.js +14 -2
  97. package/dist-server/server/connector-verdict.js +168 -0
  98. package/dist-server/server/container-computer.js +61 -1
  99. package/dist-server/server/contracts.js +22 -0
  100. package/dist-server/server/default-model-selection.js +9 -0
  101. package/dist-server/server/delegations.js +85 -34
  102. package/dist-server/server/drivers/acp/core.js +80 -20
  103. package/dist-server/server/drivers/acp/cursor.js +3 -7
  104. package/dist-server/server/drivers/acp/droid.js +4 -7
  105. package/dist-server/server/drivers/acp/gemini.js +4 -4
  106. package/dist-server/server/drivers/acp/grok.js +4 -9
  107. package/dist-server/server/drivers/acp/hermes.js +3 -5
  108. package/dist-server/server/drivers/acp/opencode-go.js +2 -5
  109. package/dist-server/server/drivers/acp/qwen.js +9 -15
  110. package/dist-server/server/drivers/agents-call.js +179 -13
  111. package/dist-server/server/drivers/agents-catalog.js +134 -10
  112. package/dist-server/server/drivers/boxagent.js +160 -36
  113. package/dist-server/server/drivers/builtIn.js +2 -0
  114. package/dist-server/server/drivers/chat-box-tools.js +90 -0
  115. package/dist-server/server/drivers/chat-images.js +54 -0
  116. package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
  117. package/dist-server/server/drivers/chat-tool-approval.js +70 -18
  118. package/dist-server/server/drivers/claude.js +38 -19
  119. package/dist-server/server/drivers/codex-catalog.js +14 -6
  120. package/dist-server/server/drivers/codex-identity.js +2 -1
  121. package/dist-server/server/drivers/codex-instructions.js +48 -6
  122. package/dist-server/server/drivers/codex-release.js +48 -0
  123. package/dist-server/server/drivers/codex-tool-surface.js +20 -0
  124. package/dist-server/server/drivers/codex.js +102 -55
  125. package/dist-server/server/drivers/mistral.js +115 -0
  126. package/dist-server/server/drivers/openai-chat.js +180 -33
  127. package/dist-server/server/drivers/openai-compat.js +1 -0
  128. package/dist-server/server/drivers/permission-command.js +35 -0
  129. package/dist-server/server/drivers/phone-proxy.js +47 -2
  130. package/dist-server/server/drivers/pi.js +306 -28
  131. package/dist-server/server/drivers/prompt-split.js +110 -0
  132. package/dist-server/server/drivers/session-idle.js +29 -0
  133. package/dist-server/server/env-path.js +15 -0
  134. package/dist-server/server/fleet.js +1 -1
  135. package/dist-server/server/group-thread-usage.js +103 -0
  136. package/dist-server/server/guarded-requests.js +2 -2
  137. package/dist-server/server/harness/registry.js +1 -0
  138. package/dist-server/server/harness-capabilities.js +46 -0
  139. package/dist-server/server/hosted-models.js +1 -1
  140. package/dist-server/server/index.js +2396 -505
  141. package/dist-server/server/local-computer.js +72 -23
  142. package/dist-server/server/managed-desktop.js +1 -1
  143. package/dist-server/server/mcp-bridge.js +6 -6
  144. package/dist-server/server/message-db.js +22 -42
  145. package/dist-server/server/message-file.js +9 -1
  146. package/dist-server/server/message-search-query.js +43 -0
  147. package/dist-server/server/message-search-worker.js +80 -0
  148. package/dist-server/server/message-search.worker.js +21 -0
  149. package/dist-server/server/model-requests.js +248 -0
  150. package/dist-server/server/new-bot-defaults.js +115 -0
  151. package/dist-server/server/options-card.js +21 -0
  152. package/dist-server/server/org-library.js +1065 -0
  153. package/dist-server/server/package-export.js +627 -39
  154. package/dist-server/server/package-import.js +458 -0
  155. package/dist-server/server/package-parts.js +87 -0
  156. package/dist-server/server/peer-approval.js +16 -7
  157. package/dist-server/server/peer-delivery.js +60 -0
  158. package/dist-server/server/peer-roster.js +67 -0
  159. package/dist-server/server/presets.js +492 -0
  160. package/dist-server/server/profile-requests.js +66 -39
  161. package/dist-server/server/profile-revision.js +9 -1
  162. package/dist-server/server/profile-versions.js +6 -3
  163. package/dist-server/server/provider-key-check.js +3 -2
  164. package/dist-server/server/published-teams.js +80 -0
  165. package/dist-server/server/room-handoffs.js +19 -3
  166. package/dist-server/server/routes/bot-presets.js +25 -0
  167. package/dist-server/server/routine-requests.js +35 -10
  168. package/dist-server/server/routines.js +41 -1
  169. package/dist-server/server/section-context.js +10 -0
  170. package/dist-server/server/skill-fetch.js +72 -14
  171. package/dist-server/server/skills.js +35 -61
  172. package/dist-server/server/steer-queue.js +3 -2
  173. package/dist-server/server/store.js +291 -17
  174. package/dist-server/server/surface.js +10 -4
  175. package/dist-server/server/system-prompt.js +38 -6
  176. package/dist-server/server/team-backup.js +6 -2
  177. package/dist-server/server/team-computers.js +10 -0
  178. package/dist-server/server/team-library.js +6 -2
  179. package/dist-server/server/team-manifest.js +4 -3
  180. package/dist-server/server/team-setup-requests.js +139 -27
  181. package/dist-server/server/thread-events.js +20 -2
  182. package/dist-server/server/tightening-requests.js +405 -0
  183. package/dist-server/server/tts/chatterbox.js +27 -5
  184. package/dist-server/server/turn-context.js +2 -0
  185. package/dist-server/server/turn-watchdog.js +12 -3
  186. package/dist-server/server/usage-ledger.js +19 -2
  187. package/dist-server/server/vps-computer.js +139 -51
  188. package/dist-server/server/vps-container-mcp.js +7 -0
  189. package/dist-server/server/vps-ssh.js +36 -7
  190. package/dist-server/server/workspace-backup-policy.js +12 -0
  191. package/dist-server/server/workspace-backup.js +42 -5
  192. package/dist-server/server/workspace-backup.worker.js +10 -0
  193. package/dist-server/server/workspace.js +1 -1
  194. package/dist-server/shared/approval-mode.js +1 -1
  195. package/dist-server/shared/ask-question.js +207 -12
  196. package/dist-server/shared/bot-profile.js +15 -0
  197. package/dist-server/shared/command-allowlist.js +1 -0
  198. package/dist-server/shared/credential-request.js +6 -2
  199. package/dist-server/shared/line-diff.js +23 -0
  200. package/dist-server/shared/model-request.js +1 -0
  201. package/dist-server/shared/options-card.js +51 -0
  202. package/dist-server/shared/package-format.js +967 -0
  203. package/dist-server/shared/profile-request.js +5 -4
  204. package/dist-server/shared/skill-md.js +64 -0
  205. package/dist-server/shared/team-backup.js +7 -0
  206. package/dist-server/shared/tightening-request.js +87 -0
  207. package/dist-server/shared/wire.js +4 -0
  208. package/dist-server/src/lib/composer-attachments.js +25 -3
  209. package/dist-server/vps-container-mcp.js +1132 -171
  210. package/dist-server/workspace-backup.worker.js +19474 -0
  211. package/enterprise/README.md +1 -1
  212. package/package.json +1 -1
  213. package/dist/assets/channel-BXFkgus9.js +0 -1
  214. package/dist/assets/index-BGgqlWPJ.css +0 -1
  215. package/dist/assets/index-BtHo8ZKl.js +0 -577
  216. package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
@@ -29,7 +29,7 @@ function text(value, limit) {
29
29
  if (typeof value !== "string")
30
30
  return undefined;
31
31
  const trimmed = value.trim();
32
- return trimmed ? trimmed.slice(0, limit) : undefined;
32
+ return trimmed ? trimmed.slice(0, limit).trim() : undefined;
33
33
  }
34
34
  function isRecord(value) {
35
35
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -94,6 +94,55 @@ export function parseAskQuestions(input) {
94
94
  }
95
95
  return questions.length ? questions : null;
96
96
  }
97
+ /** The fenced block a turn-boundary agent ends its run with when it wants
98
+ * to ask the person something (the BoxAgent transport): the harness cannot
99
+ * pause mid-run, so the questions ride the final output and OMB parses them
100
+ * at settle. The block is model-authored — untrusted input like any tool
101
+ * call — so its body runs through parseAskQuestions and the same caps. */
102
+ const OMB_ASK_FENCE = /(^|\n)[ \t]{0,3}(`{3,}|~{3,})[ \t]*omb-ask[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]{0,3}\2[ \t]*(?=\r?\n|$)/;
103
+ const OMB_ASK_FENCE_GLOBAL = new RegExp(OMB_ASK_FENCE.source, OMB_ASK_FENCE.flags + "g");
104
+ /** The questions inside the first fenced omb-ask block in an output, or
105
+ * null when there is nothing worth showing (no fence, invalid JSON, or no
106
+ * entry that parses as a question). The first fence wins: a second block
107
+ * in the same output is ignored, not merged. */
108
+ export function parseOmbAskQuestions(output) {
109
+ const match = OMB_ASK_FENCE.exec(output);
110
+ if (!match)
111
+ return null;
112
+ try {
113
+ return parseAskQuestions(JSON.parse(match[3]));
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ }
119
+ /** The output with its omb-ask block(s) removed, for display: the person
120
+ * reads the prose around the ask, never the raw protocol JSON. Text
121
+ * without a block is returned untouched. */
122
+ export function stripOmbAskBlock(output) {
123
+ if (!OMB_ASK_FENCE.test(output))
124
+ return output;
125
+ return output.replace(OMB_ASK_FENCE_GLOBAL, "").replace(/\n{3,}/g, "\n\n").trim();
126
+ }
127
+ /** The prompt contract that teaches a model the turn-held ask transport: end
128
+ * the reply with a fenced `omb-ask` block carrying the questions, and the
129
+ * answers come back on the next prompt. A harness that cannot pause mid-run
130
+ * (the BoxAgent transport) appends this to every prompt; the caps in the text
131
+ * are the constants above, so the taught contract and the parser cannot
132
+ * drift apart. */
133
+ export function ombAskProtocolPrompt() {
134
+ return [
135
+ "",
136
+ "## Asking the person a question",
137
+ "When a decision belongs to the person, end your reply with a fenced block exactly like this:",
138
+ "",
139
+ "```omb-ask",
140
+ '{"questions":[{"question":"Ship the release now?","header":"Release","options":[{"label":"Ship now"},{"label":"Wait for the QA signoff"}]}]}',
141
+ "```",
142
+ "",
143
+ `The block must be the last thing in your reply. You may ask up to ${MAX_QUESTIONS} questions at once, each with up to ${MAX_OPTIONS} options; the person can always answer in their own words. Their answers arrive on your next prompt as \`Q:\`/\`A:\` lines — never invent them.`,
144
+ ].join("\n");
145
+ }
97
146
  /** The one line the card subtitle and a spoken prompt show. */
98
147
  export function askQuestionSummary(questions) {
99
148
  const first = questions[0]?.question ?? "";
@@ -139,6 +188,61 @@ export function questionChoices(questions) {
139
188
  return undefined;
140
189
  return only.options.map((option) => option.label);
141
190
  }
191
+ /** The chat runtime's own ask tool: the OpenAI-compatible engines'
192
+ * counterpart of ASK_USER_QUESTION. Named ask_user so the ASKS_A_PERSON
193
+ * backstop already covers it — no auto mode may answer a question. */
194
+ export const ASK_USER_TOOL = "ask_user";
195
+ /** The ask_user definition shared by every chat-runtime engine, shaped for
196
+ * an OpenAI function-calling tool list. The schema states the caps; the
197
+ * parser enforces them again on arrival, because a model-authored argument
198
+ * is a request to follow the schema, not a guarantee that it did. */
199
+ export const ASK_USER_TOOL_DEFINITION = {
200
+ type: "function",
201
+ function: {
202
+ name: ASK_USER_TOOL,
203
+ description: "Ask the person questions when the decision is theirs. Offer concrete options where they exist; they can always answer in their own words. Ask up to six questions in one call rather than one at a time.",
204
+ parameters: {
205
+ type: "object",
206
+ properties: {
207
+ questions: {
208
+ type: "array",
209
+ minItems: 1,
210
+ maxItems: MAX_QUESTIONS,
211
+ items: {
212
+ type: "object",
213
+ properties: {
214
+ question: { type: "string", description: "The question to show the person." },
215
+ header: { type: "string", description: "A short label for the question's tab." },
216
+ multiSelect: { type: "boolean", description: "Whether several options may be picked." },
217
+ options: {
218
+ type: "array",
219
+ maxItems: MAX_OPTIONS,
220
+ description: "Suggested answers; the person may always write their own.",
221
+ items: {
222
+ type: "object",
223
+ properties: {
224
+ label: { type: "string" },
225
+ description: { type: "string" },
226
+ },
227
+ required: ["label"],
228
+ additionalProperties: false,
229
+ },
230
+ },
231
+ },
232
+ required: ["question"],
233
+ additionalProperties: false,
234
+ },
235
+ },
236
+ },
237
+ required: ["questions"],
238
+ additionalProperties: false,
239
+ },
240
+ },
241
+ };
242
+ /** The synthetic tool string for an ask parsed out of model-authored final
243
+ * output (the BoxAgent turn-held transport): there is no tool call to name,
244
+ * but the event and the ASKS_A_PERSON backstop need one string. */
245
+ export const OMB_ASK_TOOL = "omb-ask";
142
246
  /** The lead-in on a formatted answer. It exists for the model — the answer
143
247
  * is delivered on the deny channel, so it has to say what it is — and the
144
248
  * card strips it back off when it shows the person what they sent. */
@@ -164,6 +268,23 @@ export function formatQuestionAnswers(questions, answers) {
164
268
  export function answerWithoutPreamble(answer) {
165
269
  return answer.startsWith(`${ANSWER_PREAMBLE}\n\n`) ? answer.slice(ANSWER_PREAMBLE.length + 2) : answer;
166
270
  }
271
+ /** The longest formatted answer OMB will echo back into a follow-up prompt
272
+ * (the BoxAgent continuation). formatQuestionAnswers itself does not
273
+ * truncate — the tool-result channel has no stated limit — but a prompt is
274
+ * not the place to find one: six answers at the custom-answer cap is the
275
+ * most a legitimate reply weighs, so that is the ceiling. */
276
+ export const MAX_ANSWER_ECHO = 6 * MAX_CUSTOM_ANSWER;
277
+ /** Cap a formatted answer for echoing into a follow-up prompt. An over-cap
278
+ * echo is cut back to the last whole block so no partial answer reads as
279
+ * one, and says it was truncated. */
280
+ export function capAnswerEcho(answer, limit = MAX_ANSWER_ECHO) {
281
+ if (answer.length <= limit)
282
+ return answer;
283
+ const cut = answer.slice(0, limit);
284
+ const boundary = cut.lastIndexOf("\n\nQ: ");
285
+ const kept = boundary > 0 ? cut.slice(0, boundary) : cut;
286
+ return kept + "\n\n[answer truncated]";
287
+ }
167
288
  /**
168
289
  * The answer text, read back as one value per question.
169
290
  *
@@ -180,21 +301,95 @@ export function answerWithoutPreamble(answer) {
180
301
  * With more than one there is, and nothing is filed.
181
302
  */
182
303
  export function questionAnswersByQuestion(message, questions) {
183
- const answers = {};
184
- const known = new Map(questions.map((entry) => [entry.question, entry.question]));
185
- for (const block of message.split("\n\n")) {
186
- const match = /^Q: ([\s\S]+?)\nA: ([\s\S]+)$/.exec(block.trim());
187
- if (!match)
304
+ return questionAnswersById(message, questions.map(question => ({ id: question.question, question })));
305
+ }
306
+ /** Longest protocol id accepted. Ids are harness-internal keys, never shown
307
+ * on the card, but they ride the reply — so they are capped like the rest. */
308
+ const MAX_QUESTION_ID = 200;
309
+ /** Parse a protocol questions array whose entries carry their own string
310
+ * ids (codex's item/tool/requestUserInput). The same caps and skip rules
311
+ * as parseAskQuestions; an entry without both a usable id and a usable
312
+ * question is skipped rather than fatal, and nothing answerable parses to
313
+ * null. */
314
+ export function parseProtocolAskQuestions(entries) {
315
+ if (!Array.isArray(entries))
316
+ return null;
317
+ const parsed = [];
318
+ const seenIds = new Set();
319
+ for (const raw of entries) {
320
+ if (!isRecord(raw))
321
+ continue;
322
+ const question = parseQuestion(raw);
323
+ // The id is opaque protocol data echoed back verbatim in the answer, so
324
+ // it is preserved exactly as sent — trimmed only to reject an
325
+ // all-whitespace id — and an overlong id is rejected whole rather than
326
+ // truncated. A duplicate id fails the whole request: two questions
327
+ // cannot share one answer slot, and silently dropping one would ask
328
+ // the person a question whose answer could never be delivered.
329
+ const id = raw.id;
330
+ if (!question || typeof id !== "string" || !id.trim() || id.length > MAX_QUESTION_ID)
331
+ continue;
332
+ if (seenIds.has(id))
333
+ return null;
334
+ seenIds.add(id);
335
+ parsed.push({ id, question });
336
+ if (parsed.length === MAX_QUESTIONS)
337
+ break;
338
+ }
339
+ return parsed.length ? parsed : null;
340
+ }
341
+ /**
342
+ * The id-keyed counterpart of questionAnswersByQuestion, for a protocol
343
+ * that answers by id (codex's requestUserInput) rather than by question
344
+ * text.
345
+ *
346
+ * Each protocol entry's id is paired with the AskQuestion parsed from that
347
+ * same entry, so a Q:/A: block the card wrote is matched against the text
348
+ * that produced it and filed under the id that asked. Only ids with a real
349
+ * block are answered — a question left unanswered gets no entry rather
350
+ * than a guess. When the reply carries no blocks at all, the flat path
351
+ * applies with exactly one question: the whole message is that question's
352
+ * answer, as it is for a phone or an older client. With more than one,
353
+ * nothing is filed.
354
+ */
355
+ export function questionAnswersById(message, questions) {
356
+ const answers = Object.create(null);
357
+ const idByText = new Map();
358
+ const ambiguous = new Set();
359
+ for (const { id, question } of questions) {
360
+ if (idByText.has(question.question)) {
361
+ // Two questions with identical text cannot be told apart in a block;
362
+ // answering either id would be a guess, so neither is answered.
363
+ ambiguous.add(question.question);
364
+ continue;
365
+ }
366
+ idByText.set(question.question, id);
367
+ }
368
+ // A block runs from one Q: header to the next (blank lines inside an
369
+ // answer are the answer's, not block boundaries), so the reply is scanned
370
+ // for header-to-header spans instead of split on every blank line.
371
+ const blocks = message.matchAll(/(?:^|\n\n)Q: ([\s\S]+?)\nA: ([\s\S]*?)(?=\n\nQ: |$)/g);
372
+ let sawBlock = false;
373
+ for (const match of blocks) {
374
+ sawBlock = true;
375
+ const asked = match[1].trim();
376
+ if (ambiguous.has(asked))
188
377
  continue;
189
- // Only a question this ask actually posed. An unrecognized block is
190
- // dropped rather than filed under a key the tool never asked about.
191
- const question = known.get(match[1].trim());
192
- if (question)
193
- answers[question] = match[2].trim();
378
+ const id = idByText.get(asked);
379
+ const answer = match[2].trim();
380
+ // A blank A: is no answer: the id stays unanswered rather than filed
381
+ // as an empty string.
382
+ if (id && answer)
383
+ answers[id] = answer;
194
384
  }
195
385
  if (Object.keys(answers).length)
196
386
  return answers;
387
+ // A structured reply that matched a block is authoritative even when it
388
+ // answered nothing: falling through to the flat fallback would file the
389
+ // literal Q:/A: text as the person's answer.
390
+ if (sawBlock)
391
+ return {};
197
392
  const only = questions.length === 1 ? questions[0] : undefined;
198
393
  const flat = message.trim();
199
- return only && flat ? { [only.question]: flat } : {};
394
+ return only && flat ? { [only.id]: flat } : {};
200
395
  }
@@ -9,3 +9,18 @@ export const BOT_PROFILE_LIMITS = {
9
9
  voice: 200,
10
10
  soul: 24_000,
11
11
  };
12
+ /** A name or title is quoted inside prompts and cards as one line — a
13
+ * roster entry, a "Name: …" speaker line, the bracketed provenance note.
14
+ * Text that can break out of that line (a newline, a control byte, the
15
+ * Unicode separators) is refused at the door rather than flattened later,
16
+ * so what the person sees in the sidebar is what every prompt sees too.
17
+ * Written as a scan because a control-character class is the kind of
18
+ * literal the linter (rightly) refuses. */
19
+ export function fitsOnOneLine(value) {
20
+ for (let i = 0; i < value.length; i += 1) {
21
+ const code = value.charCodeAt(i);
22
+ if (code < 0x20 || (code >= 0x7f && code <= 0x9f) || code === 0x2028 || code === 0x2029)
23
+ return false;
24
+ }
25
+ return true;
26
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -14,7 +14,7 @@ export const CREDENTIAL_TARGETS = {
14
14
  label: "Box API key",
15
15
  description: "Gives bots an isolated cloud computer when Box is selected.",
16
16
  placeholder: "Paste your Box API key",
17
- helpUrl: "https://docs.ascii.dev/box/api-keys",
17
+ helpUrl: "https://docs.boat.dev/api-keys",
18
18
  },
19
19
  opencodeGoApiKey: {
20
20
  label: "OpenCode API key",
@@ -76,11 +76,15 @@ export function credentialIsConfigured(config, id) {
76
76
  return Boolean(config.imageGen?.key);
77
77
  }
78
78
  }
79
- export function isReusableCredentialRequest(message, target, requestingBotId, roomThread) {
79
+ /** A still-pending credential card for this target — the set a newer
80
+ * request supersedes. Provided, dismissed, and already-superseded cards
81
+ * keep their settled state untouched. */
82
+ export function isPendingCredentialRequest(message, target, requestingBotId, roomThread) {
80
83
  return (message.kind === "secret" &&
81
84
  message.secret?.target === target &&
82
85
  message.secret.provided !== true &&
83
86
  message.secret.dismissed !== true &&
87
+ message.secret.superseded !== true &&
84
88
  (!roomThread || message.from?.botId === requestingBotId));
85
89
  }
86
90
  export function credentialResumeOutcome(state) {
@@ -40,3 +40,26 @@ export function lineDiff(before, after) {
40
40
  out.push(`+${b[j++]}`);
41
41
  return out;
42
42
  }
43
+ /** Cap on diff lines rendered inside an approval card. Past this the card
44
+ * shows the complete proposed document instead of a truncated diff, because
45
+ * truncation can hide the very instructions being approved. */
46
+ export const MAX_DIFF_LINES = 400;
47
+ /** The SOUL.md review block shared by the profile and team-setup cards:
48
+ * byte counts, a line diff for reviewable changes, and the complete
49
+ * replacement when the diff would outrun the card. The full replacement is
50
+ * already byte-bounded by profile validation and is readable in the same
51
+ * generic card on desktop and phones. */
52
+ export function soulDiffLines(before, after) {
53
+ const encoder = new TextEncoder();
54
+ const bytesBefore = encoder.encode(before).length;
55
+ const bytesAfter = encoder.encode(after).length;
56
+ const diff = lineDiff(before, after);
57
+ if (diff.length > MAX_DIFF_LINES) {
58
+ return [
59
+ `SOUL.md (${bytesBefore} → ${bytesAfter} bytes):`,
60
+ "Large change — complete proposed instructions (replaces the current SOUL.md):",
61
+ after || "(empty)",
62
+ ];
63
+ }
64
+ return [`SOUL.md (${bytesBefore} → ${bytesAfter} bytes):`, ...diff];
65
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ /** The one bot allowed to create passive choice cards for the Watcher flow. */
2
+ export const WATCHER_OPTIONS_CARD_BOT_ID = "f08dd8e3-f942-4eb4-8783-df32b26b88a4";
3
+ /** Keep agent-authored cards small enough to remain useful on desktop and mobile. */
4
+ export const OPTIONS_CARD_LIMITS = {
5
+ title: 120,
6
+ subtitle: 1_000,
7
+ option: 120,
8
+ minOptions: 2,
9
+ maxOptions: 6,
10
+ };
11
+ function record(value) {
12
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
13
+ }
14
+ function boundedText(value, field, maximum) {
15
+ if (typeof value !== "string")
16
+ return { error: `${field} must be a string.` };
17
+ const text = value.trim();
18
+ if (!text)
19
+ return { error: `${field} must not be blank.` };
20
+ if (text.length > maximum)
21
+ return { error: `${field} must be at most ${maximum} characters.` };
22
+ return text;
23
+ }
24
+ /** Normalize untrusted model input before it is persisted into a native card. */
25
+ export function parseOptionsCardInput(value) {
26
+ if (!record(value))
27
+ return { ok: false, error: "create_options_card needs an object." };
28
+ const title = boundedText(value.title, "title", OPTIONS_CARD_LIMITS.title);
29
+ if (typeof title !== "string")
30
+ return { ok: false, error: title.error };
31
+ const subtitle = boundedText(value.subtitle, "subtitle", OPTIONS_CARD_LIMITS.subtitle);
32
+ if (typeof subtitle !== "string")
33
+ return { ok: false, error: subtitle.error };
34
+ if (!Array.isArray(value.options))
35
+ return { ok: false, error: "options must be an array of strings." };
36
+ if (value.options.length < OPTIONS_CARD_LIMITS.minOptions || value.options.length > OPTIONS_CARD_LIMITS.maxOptions) {
37
+ return { ok: false, error: `options must contain ${OPTIONS_CARD_LIMITS.minOptions}-${OPTIONS_CARD_LIMITS.maxOptions} items.` };
38
+ }
39
+ const options = [];
40
+ const seen = new Set();
41
+ for (let index = 0; index < value.options.length; index += 1) {
42
+ const option = boundedText(value.options[index], `options[${index}]`, OPTIONS_CARD_LIMITS.option);
43
+ if (typeof option !== "string")
44
+ return { ok: false, error: option.error };
45
+ if (seen.has(option))
46
+ return { ok: false, error: `options must be unique; "${option}" is repeated.` };
47
+ seen.add(option);
48
+ options.push(option);
49
+ }
50
+ return { ok: true, value: { title, subtitle, options } };
51
+ }