@sayknow-cli/coding-agent 0.2.7 → 0.3.1

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/CHANGELOG.md +20 -0
  2. package/dist/types/async/job-manager.d.ts +3 -1
  3. package/dist/types/cli/daemon-cli.d.ts +25 -0
  4. package/dist/types/cli/notify-cli.d.ts +25 -0
  5. package/dist/types/cli/setup-cli.d.ts +20 -1
  6. package/dist/types/commands/daemon.d.ts +41 -0
  7. package/dist/types/commands/notify.d.ts +41 -0
  8. package/dist/types/config/model-profile-activation.d.ts +12 -0
  9. package/dist/types/config/model-profiles.d.ts +2 -1
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +136 -3
  13. package/dist/types/coordinator/contract.d.ts +1 -1
  14. package/dist/types/daemon/builtin.d.ts +20 -0
  15. package/dist/types/daemon/control-types.d.ts +57 -0
  16. package/dist/types/daemon/runtime.d.ts +25 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  18. package/dist/types/extensibility/shared-events.d.ts +1 -0
  19. package/dist/types/i18n/messages/en.d.ts +1 -0
  20. package/dist/types/lsp/types.d.ts +2 -0
  21. package/dist/types/modes/components/oauth-selector.d.ts +2 -0
  22. package/dist/types/modes/components/settings-defs.d.ts +1 -0
  23. package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
  24. package/dist/types/modes/interactive-mode.d.ts +1 -1
  25. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  26. package/dist/types/modes/theme/theme.d.ts +1 -1
  27. package/dist/types/modes/types.d.ts +7 -1
  28. package/dist/types/notifications/attachment-registry.d.ts +17 -0
  29. package/dist/types/notifications/chat-adapters.d.ts +9 -0
  30. package/dist/types/notifications/config-commands.d.ts +26 -0
  31. package/dist/types/notifications/config.d.ts +69 -0
  32. package/dist/types/notifications/engine.d.ts +59 -0
  33. package/dist/types/notifications/helpers.d.ts +55 -0
  34. package/dist/types/notifications/html-format.d.ts +62 -0
  35. package/dist/types/notifications/index.d.ts +28 -0
  36. package/dist/types/notifications/managed-daemon.d.ts +48 -0
  37. package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
  38. package/dist/types/notifications/telegram-cli.d.ts +19 -0
  39. package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
  40. package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
  41. package/dist/types/notifications/telegram-daemon.d.ts +295 -0
  42. package/dist/types/notifications/telegram-reference.d.ts +111 -0
  43. package/dist/types/notifications/threaded-inbound.d.ts +77 -0
  44. package/dist/types/notifications/threaded-render.d.ts +71 -0
  45. package/dist/types/notifications/topic-registry.d.ts +67 -0
  46. package/dist/types/rlm/index.d.ts +12 -0
  47. package/dist/types/session/agent-session.d.ts +41 -2
  48. package/dist/types/session/auth-storage.d.ts +1 -1
  49. package/dist/types/setup/credential-auto-import.d.ts +63 -0
  50. package/dist/types/setup/credential-import.d.ts +3 -0
  51. package/dist/types/setup/host-plugin-setup.d.ts +39 -0
  52. package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
  53. package/dist/types/skc-runtime/ralplan-runtime.d.ts +1 -1
  54. package/dist/types/skc-runtime/state-writer.d.ts +2 -0
  55. package/dist/types/skc-runtime/tmux-common.d.ts +3 -0
  56. package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
  57. package/dist/types/skc-runtime/ultragoal-guard.d.ts +15 -0
  58. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +14 -0
  59. package/dist/types/tools/ask-answer-registry.d.ts +13 -0
  60. package/dist/types/tools/fetch.d.ts +23 -0
  61. package/dist/types/tools/index.d.ts +19 -0
  62. package/dist/types/tools/subagent.d.ts +3 -0
  63. package/dist/types/tools/telegram-send.d.ts +32 -0
  64. package/dist/types/web/insane/bridge.d.ts +103 -0
  65. package/dist/types/web/insane/url-guard.d.ts +22 -0
  66. package/dist/types/web/search/provider.d.ts +18 -1
  67. package/dist/types/web/search/providers/insane.d.ts +53 -0
  68. package/dist/types/web/search/providers/text-citations.d.ts +23 -0
  69. package/dist/types/web/search/types.d.ts +12 -4
  70. package/package.json +10 -8
  71. package/scripts/build-binary.ts +3 -0
  72. package/scripts/verify-insane-vendor.ts +132 -0
  73. package/src/async/job-manager.ts +5 -1
  74. package/src/cli/args.ts +1 -1
  75. package/src/cli/daemon-cli.ts +122 -0
  76. package/src/cli/fast-help.ts +1 -1
  77. package/src/cli/notify-cli.ts +421 -0
  78. package/src/cli/setup-cli.ts +173 -84
  79. package/src/cli.ts +3 -3
  80. package/src/commands/daemon.ts +47 -0
  81. package/src/commands/notify.ts +61 -0
  82. package/src/commands/setup.ts +11 -1
  83. package/src/commands/team.ts +1 -1
  84. package/src/config/model-profile-activation.ts +74 -5
  85. package/src/config/model-profiles.ts +7 -4
  86. package/src/config/model-registry.ts +6 -3
  87. package/src/config/models-config-schema.ts +1 -1
  88. package/src/config/settings-schema.ts +143 -0
  89. package/src/coordinator/contract.ts +3 -0
  90. package/src/coordinator-mcp/server.ts +270 -1
  91. package/src/daemon/builtin.ts +46 -0
  92. package/src/daemon/control-types.ts +65 -0
  93. package/src/daemon/runtime.ts +51 -0
  94. package/src/defaults/skc/rules/ponytail.md +68 -0
  95. package/src/defaults/skc/skills/ralplan/SKILL.md +11 -4
  96. package/src/defaults/skc/skills/ultragoal/SKILL.md +16 -0
  97. package/src/edit/modes/replace.ts +1 -1
  98. package/src/extensibility/extensions/runner.ts +4 -0
  99. package/src/extensibility/extensions/types.ts +8 -0
  100. package/src/extensibility/shared-events.ts +1 -0
  101. package/src/goals/tools/goal-tool.ts +11 -2
  102. package/src/hashline/hash.ts +1 -1
  103. package/src/i18n/messages/de.ts +1 -0
  104. package/src/i18n/messages/en.ts +1 -0
  105. package/src/i18n/messages/es.ts +1 -0
  106. package/src/i18n/messages/fr.ts +1 -0
  107. package/src/i18n/messages/ja.ts +1 -0
  108. package/src/i18n/messages/ko.ts +1 -0
  109. package/src/i18n/messages/zh.ts +1 -0
  110. package/src/internal-urls/docs-index.generated.ts +12 -10
  111. package/src/lsp/config.ts +16 -3
  112. package/src/lsp/defaults.json +7 -0
  113. package/src/lsp/types.ts +2 -0
  114. package/src/main.ts +30 -0
  115. package/src/modes/acp/acp-event-mapper.ts +1 -0
  116. package/src/modes/components/hook-editor.ts +7 -2
  117. package/src/modes/components/oauth-selector.ts +19 -0
  118. package/src/modes/components/settings-defs.ts +2 -1
  119. package/src/modes/components/settings-selector.ts +7 -2
  120. package/src/modes/controllers/event-controller.ts +35 -0
  121. package/src/modes/controllers/selector-controller.ts +80 -17
  122. package/src/modes/interactive-mode.ts +52 -4
  123. package/src/modes/runtime-init.ts +1 -0
  124. package/src/modes/shared/agent-wire/event-contract.ts +1 -0
  125. package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
  126. package/src/modes/shared/agent-wire/event-observation.ts +16 -0
  127. package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
  128. package/src/modes/theme/theme.ts +4 -0
  129. package/src/modes/types.ts +7 -1
  130. package/src/modes/utils/context-usage.ts +2 -2
  131. package/src/modes/utils/ui-helpers.ts +23 -0
  132. package/src/notifications/attachment-registry.ts +23 -0
  133. package/src/notifications/chat-adapters.ts +147 -0
  134. package/src/notifications/config-commands.ts +50 -0
  135. package/src/notifications/config.ts +128 -0
  136. package/src/notifications/engine.ts +100 -0
  137. package/src/notifications/helpers.ts +135 -0
  138. package/src/notifications/html-format.ts +389 -0
  139. package/src/notifications/index.ts +842 -0
  140. package/src/notifications/managed-daemon.ts +163 -0
  141. package/src/notifications/rate-limit-pool.ts +179 -0
  142. package/src/notifications/telegram-cli.ts +194 -0
  143. package/src/notifications/telegram-daemon-cli.ts +74 -0
  144. package/src/notifications/telegram-daemon-control.ts +370 -0
  145. package/src/notifications/telegram-daemon.ts +1591 -0
  146. package/src/notifications/telegram-reference.ts +335 -0
  147. package/src/notifications/threaded-inbound.ts +136 -0
  148. package/src/notifications/threaded-render.ts +173 -0
  149. package/src/notifications/topic-registry.ts +133 -0
  150. package/src/rlm/index.ts +19 -0
  151. package/src/sdk.ts +16 -0
  152. package/src/session/agent-session.ts +195 -54
  153. package/src/session/auth-storage.ts +3 -0
  154. package/src/session/session-dump-format.ts +43 -2
  155. package/src/session/session-manager.ts +39 -5
  156. package/src/setup/credential-auto-import.ts +258 -0
  157. package/src/setup/credential-import.ts +17 -0
  158. package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
  159. package/src/setup/host-plugin-setup.ts +142 -0
  160. package/src/skc-runtime/deep-interview-recorder.ts +2 -2
  161. package/src/skc-runtime/launch-tmux.ts +27 -5
  162. package/src/skc-runtime/ledger-event-renderer.ts +1 -0
  163. package/src/skc-runtime/ralplan-runtime.ts +2 -2
  164. package/src/skc-runtime/state-runtime.ts +18 -10
  165. package/src/skc-runtime/state-writer.ts +8 -8
  166. package/src/skc-runtime/tmux-common.ts +8 -0
  167. package/src/skc-runtime/tmux-sessions.ts +8 -1
  168. package/src/skc-runtime/ultragoal-guard.ts +57 -2
  169. package/src/skc-runtime/ultragoal-runtime.ts +105 -19
  170. package/src/skc-runtime/workflow-manifest.generated.json +56 -2
  171. package/src/skc-runtime/workflow-manifest.ts +18 -3
  172. package/src/slash-commands/builtin-registry.ts +4 -1
  173. package/src/task/executor.ts +5 -1
  174. package/src/tools/ask-answer-registry.ts +25 -0
  175. package/src/tools/ask.ts +77 -6
  176. package/src/tools/fetch.ts +78 -1
  177. package/src/tools/image-gen.ts +5 -8
  178. package/src/tools/index.ts +22 -0
  179. package/src/tools/inspect-image.ts +16 -11
  180. package/src/tools/subagent-render.ts +7 -0
  181. package/src/tools/subagent.ts +38 -7
  182. package/src/tools/telegram-send.ts +137 -0
  183. package/src/web/insane/bridge.ts +350 -0
  184. package/src/web/insane/url-guard.ts +155 -0
  185. package/src/web/search/provider.ts +77 -18
  186. package/src/web/search/providers/anthropic.ts +70 -3
  187. package/src/web/search/providers/codex.ts +1 -119
  188. package/src/web/search/providers/gemini.ts +99 -0
  189. package/src/web/search/providers/insane.ts +551 -0
  190. package/src/web/search/providers/openai-compatible.ts +66 -32
  191. package/src/web/search/providers/text-citations.ts +111 -0
  192. package/src/web/search/types.ts +13 -2
  193. package/vendor/insane-search/LICENSE +21 -0
  194. package/vendor/insane-search/MANIFEST.json +24 -0
  195. package/vendor/insane-search/engine/__init__.py +23 -0
  196. package/vendor/insane-search/engine/__main__.py +128 -0
  197. package/vendor/insane-search/engine/bias_check.py +183 -0
  198. package/vendor/insane-search/engine/executor.py +254 -0
  199. package/vendor/insane-search/engine/fetch_chain.py +725 -0
  200. package/vendor/insane-search/engine/learning.py +175 -0
  201. package/vendor/insane-search/engine/phase0.py +214 -0
  202. package/vendor/insane-search/engine/safety.py +91 -0
  203. package/vendor/insane-search/engine/templates/package.json +11 -0
  204. package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
  205. package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
  206. package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
  207. package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
  208. package/vendor/insane-search/engine/tests/test_u1.py +200 -0
  209. package/vendor/insane-search/engine/tests/test_u4.py +131 -0
  210. package/vendor/insane-search/engine/tests/test_u5.py +163 -0
  211. package/vendor/insane-search/engine/tests/test_u7.py +124 -0
  212. package/vendor/insane-search/engine/transport.py +211 -0
  213. package/vendor/insane-search/engine/url_transforms.py +98 -0
  214. package/vendor/insane-search/engine/validators.py +331 -0
  215. package/vendor/insane-search/engine/waf_detector.py +214 -0
  216. package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
@@ -369,6 +369,9 @@
369
369
  {
370
370
  "id": "revision"
371
371
  },
372
+ {
373
+ "id": "post-interview"
374
+ },
372
375
  {
373
376
  "id": "adr"
374
377
  },
@@ -401,6 +404,26 @@
401
404
  "to": "revision",
402
405
  "verb": "write-artifact"
403
406
  },
407
+ {
408
+ "from": "revision",
409
+ "to": "post-interview",
410
+ "verb": "write-artifact"
411
+ },
412
+ {
413
+ "from": "critic",
414
+ "to": "post-interview",
415
+ "verb": "write-artifact"
416
+ },
417
+ {
418
+ "from": "post-interview",
419
+ "to": "revision",
420
+ "verb": "write-artifact"
421
+ },
422
+ {
423
+ "from": "post-interview",
424
+ "to": "adr",
425
+ "verb": "write-artifact"
426
+ },
404
427
  {
405
428
  "from": "revision",
406
429
  "to": "adr",
@@ -435,6 +458,11 @@
435
458
  "from": "adr",
436
459
  "to": "handoff",
437
460
  "verb": "handoff"
461
+ },
462
+ {
463
+ "from": "post-interview",
464
+ "to": "handoff",
465
+ "verb": "handoff"
438
466
  }
439
467
  ],
440
468
  "typedArgs": [
@@ -592,6 +620,7 @@
592
620
  "architect",
593
621
  "critic",
594
622
  "revision",
623
+ "post-interview",
595
624
  "adr",
596
625
  "final"
597
626
  ],
@@ -1443,7 +1472,8 @@
1443
1472
  "appliesToVerbs": [
1444
1473
  "checkpoint",
1445
1474
  "record-review-blockers",
1446
- "steer"
1475
+ "steer",
1476
+ "classify-blocker"
1447
1477
  ],
1448
1478
  "name": "evidence",
1449
1479
  "required": true,
@@ -1471,6 +1501,25 @@
1471
1501
  "name": "goal-id",
1472
1502
  "type": "string"
1473
1503
  },
1504
+ {
1505
+ "appliesToVerbs": [
1506
+ "classify-blocker"
1507
+ ],
1508
+ "name": "goal-id",
1509
+ "type": "string"
1510
+ },
1511
+ {
1512
+ "appliesToVerbs": [
1513
+ "classify-blocker"
1514
+ ],
1515
+ "enumValues": [
1516
+ "human_blocked",
1517
+ "resolvable"
1518
+ ],
1519
+ "name": "classification",
1520
+ "required": true,
1521
+ "type": "enum"
1522
+ },
1474
1523
  {
1475
1524
  "appliesToVerbs": [
1476
1525
  "steer"
@@ -1570,7 +1619,8 @@
1570
1619
  "review",
1571
1620
  "checkpoint",
1572
1621
  "record-review-blockers",
1573
- "steer"
1622
+ "steer",
1623
+ "classify-blocker"
1574
1624
  ],
1575
1625
  "name": "json",
1576
1626
  "type": "boolean"
@@ -1651,6 +1701,10 @@
1651
1701
  "name": "steer",
1652
1702
  "surface": "command-positional"
1653
1703
  },
1704
+ {
1705
+ "name": "classify-blocker",
1706
+ "surface": "command-positional"
1707
+ },
1654
1708
  {
1655
1709
  "name": "graph",
1656
1710
  "planned": true,
@@ -172,12 +172,16 @@ export const WORKFLOW_MANIFEST: Record<CanonicalSkcWorkflowSkill, SkillManifest>
172
172
  }),
173
173
  ralplan: manifest({
174
174
  skill: "ralplan",
175
- states: ["planner", "architect", "critic", "revision", "adr", "final", "handoff"],
175
+ states: ["planner", "architect", "critic", "revision", "post-interview", "adr", "final", "handoff"],
176
176
  terminalStates: ["final", "handoff"],
177
177
  transitions: [
178
178
  { from: "planner", to: "architect", verb: "write-artifact" },
179
179
  { from: "architect", to: "critic", verb: "write-artifact" },
180
180
  { from: "critic", to: "revision", verb: "write-artifact" },
181
+ { from: "revision", to: "post-interview", verb: "write-artifact" },
182
+ { from: "critic", to: "post-interview", verb: "write-artifact" },
183
+ { from: "post-interview", to: "revision", verb: "write-artifact" },
184
+ { from: "post-interview", to: "adr", verb: "write-artifact" },
181
185
  { from: "revision", to: "adr", verb: "write-artifact" },
182
186
  { from: "adr", to: "final", verb: "write-artifact" },
183
187
  { from: "planner", to: "handoff", verb: "handoff" },
@@ -185,6 +189,7 @@ export const WORKFLOW_MANIFEST: Record<CanonicalSkcWorkflowSkill, SkillManifest>
185
189
  { from: "critic", to: "handoff", verb: "handoff" },
186
190
  { from: "revision", to: "handoff", verb: "handoff" },
187
191
  { from: "adr", to: "handoff", verb: "handoff" },
192
+ { from: "post-interview", to: "handoff", verb: "handoff" },
188
193
  ],
189
194
  verbs: [...stateVerbs(), ...flagVerbs(["kickoff", "write-artifact"]), ...plannedVerbs(PLANNED_ADMIN_VERBS)],
190
195
  typedArgs: [
@@ -196,7 +201,7 @@ export const WORKFLOW_MANIFEST: Record<CanonicalSkcWorkflowSkill, SkillManifest>
196
201
  {
197
202
  name: "stage",
198
203
  type: "enum",
199
- enumValues: ["planner", "architect", "critic", "revision", "adr", "final"],
204
+ enumValues: ["planner", "architect", "critic", "revision", "post-interview", "adr", "final"],
200
205
  appliesToVerbs: ["write-artifact"],
201
206
  },
202
207
  { name: "stage_n", type: "number", appliesToVerbs: ["write-artifact"] },
@@ -237,6 +242,7 @@ export const WORKFLOW_MANIFEST: Record<CanonicalSkcWorkflowSkill, SkillManifest>
237
242
  "review",
238
243
  "record-review-blockers",
239
244
  "steer",
245
+ "classify-blocker",
240
246
  ]),
241
247
  ...plannedVerbs(PLANNED_ADMIN_VERBS),
242
248
  ],
@@ -263,11 +269,19 @@ export const WORKFLOW_MANIFEST: Record<CanonicalSkcWorkflowSkill, SkillManifest>
263
269
  name: "evidence",
264
270
  type: "string",
265
271
  required: true,
266
- appliesToVerbs: ["checkpoint", "record-review-blockers", "steer"],
272
+ appliesToVerbs: ["checkpoint", "record-review-blockers", "steer", "classify-blocker"],
267
273
  },
268
274
  { name: "skc-goal-json", type: "string", appliesToVerbs: ["checkpoint", "record-review-blockers"] },
269
275
  { name: "quality-gate-json", type: "string", appliesToVerbs: ["checkpoint"] },
270
276
  { name: "goal-id", type: "string", appliesToVerbs: ["steer"] },
277
+ { name: "goal-id", type: "string", appliesToVerbs: ["classify-blocker"] },
278
+ {
279
+ name: "classification",
280
+ type: "enum",
281
+ enumValues: ["human_blocked", "resolvable"],
282
+ required: true,
283
+ appliesToVerbs: ["classify-blocker"],
284
+ },
271
285
  {
272
286
  name: "kind",
273
287
  type: "enum",
@@ -307,6 +321,7 @@ export const WORKFLOW_MANIFEST: Record<CanonicalSkcWorkflowSkill, SkillManifest>
307
321
  "checkpoint",
308
322
  "record-review-blockers",
309
323
  "steer",
324
+ "classify-blocker",
310
325
  ],
311
326
  },
312
327
  { name: "directive-json", type: "string", appliesToVerbs: ["steer"], planned: true },
@@ -798,7 +798,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
798
798
  return;
799
799
  }
800
800
 
801
- runtime.ctx.showProviderOnboarding();
801
+ runtime.ctx.showOAuthSelector("login", undefined, {
802
+ allowExternalCredentialDiscovery: true,
803
+ trigger: "bare-login",
804
+ });
802
805
  runtime.ctx.editor.setText("");
803
806
  },
804
807
  },
@@ -1298,11 +1298,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1298
1298
  requestPause: () => {
1299
1299
  pauseRequested = true;
1300
1300
  },
1301
- injectMessage: async (content, deliverAs) => {
1301
+ injectMessage: async (content, deliverAs, opts) => {
1302
1302
  if (deliverAs === "nextTurn") {
1303
1303
  await session.prompt(content, { attribution: "agent" });
1304
1304
  return;
1305
1305
  }
1306
+ if (deliverAs === "steer") {
1307
+ const from = opts?.fromAgentId ?? manager.getSubagentRecord(liveSubagentId)?.ownerId ?? "?";
1308
+ session.emitSubagentSteerObservation({ from, to: liveSubagentId, body: content });
1309
+ }
1306
1310
  await session.sendUserMessage(content, { deliverAs });
1307
1311
  },
1308
1312
  });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Process-wide registry mapping a session id to its active {@link AskAnswerSource}.
3
+ *
4
+ * Decouples the `ask` tool (which reads the source via `AgentSession`) from the
5
+ * notifications extension (which registers one), without threading a new method
6
+ * through the extension/runner/controller wiring. A session has at most one
7
+ * source; registering returns a disposer.
8
+ */
9
+
10
+ import type { AskAnswerSource } from "./index";
11
+
12
+ const sources = new Map<string, AskAnswerSource>();
13
+
14
+ /** Register `source` for `sessionId`. Returns a disposer that clears it. */
15
+ export function registerAskAnswerSource(sessionId: string, source: AskAnswerSource): () => void {
16
+ sources.set(sessionId, source);
17
+ return () => {
18
+ if (sources.get(sessionId) === source) sources.delete(sessionId);
19
+ };
20
+ }
21
+
22
+ /** The answer source for `sessionId`, if one is registered. */
23
+ export function getAskAnswerSource(sessionId: string): AskAnswerSource | undefined {
24
+ return sources.get(sessionId);
25
+ }
package/src/tools/ask.ts CHANGED
@@ -430,8 +430,18 @@ async function askSingleQuestion(
430
430
  // If input was dismissed (undefined), keep prior selectedOptions/customInput intact
431
431
  }
432
432
  } else {
433
- selectedOptions = [stripRecommendedSuffix(choice)];
434
- customInput = undefined;
433
+ const stripped = stripRecommendedSuffix(choice);
434
+ if (optionLabels.includes(stripped)) {
435
+ selectedOptions = [stripped];
436
+ customInput = undefined;
437
+ } else {
438
+ // A remote answer (e.g. a typed Telegram reply) that is not one of the
439
+ // listed options is the "provide my own" custom input — recorded the same
440
+ // as picking Other and typing it. The local selector can only ever return
441
+ // a listed entry, so this branch is reached only for free-text answers.
442
+ customInput = choice;
443
+ selectedOptions = [];
444
+ }
435
445
  }
436
446
  if (navigation?.allowForward) {
437
447
  return { selectedOptions, customInput, timedOut, navigation: "forward" };
@@ -551,11 +561,71 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
551
561
  const ui: UIContext = {
552
562
  select: (prompt, options, dialogOptions) => {
553
563
  if (!extensionUi) throw new ToolAbortError("Ask tool requires interactive mode");
554
- return extensionUi.select(prompt, options, dialogOptions);
564
+ const source = this.session.getAskAnswerSource?.();
565
+ if (!source) return extensionUi.select(prompt, options, dialogOptions);
566
+ // Race the local UI against a remote answer (e.g. a Telegram reply via the
567
+ // notifications SDK) so asks can be answered without RPC mode. When the
568
+ // local UI wins, abort the remote source so it stops waiting and marks the
569
+ // action resolved-locally. First valid answer wins.
570
+ // Race the local UI against a remote answer (e.g. a Telegram reply via the
571
+ // notifications SDK) so asks can be answered without RPC mode. First valid
572
+ // answer wins; the loser is aborted so neither side is left hanging:
573
+ // - local wins -> abort the remote source (marks the action resolved-locally)
574
+ // - remote wins -> abort the local selector so the TUI dialog actually closes
575
+ const remoteController = new AbortController();
576
+ const localController = new AbortController();
577
+ // Propagate an external cancel (the tool's signal) to the local selector too.
578
+ const toolSignal = dialogOptions?.signal;
579
+ if (toolSignal) {
580
+ if (toolSignal.aborted) localController.abort();
581
+ else toolSignal.addEventListener("abort", () => localController.abort(), { once: true });
582
+ }
583
+ const remote = source.awaitAnswer(prompt, options, remoteController.signal).then(answer => {
584
+ // undefined is not a valid remote answer (registration failed, or the local
585
+ // UI already won and aborted us): never settle the race, let the local
586
+ // selector decide instead of cancelling the ask.
587
+ if (answer === undefined) return new Promise<string | undefined>(() => {});
588
+ localController.abort();
589
+ return answer;
590
+ });
591
+ const local = extensionUi
592
+ .select(prompt, options, { ...dialogOptions, signal: localController.signal })
593
+ .then(answer => {
594
+ remoteController.abort();
595
+ return answer;
596
+ });
597
+ // The losing selector may reject when aborted after the race already settled;
598
+ // swallow that so it is not an unhandled rejection (the race result is unaffected).
599
+ void local.catch(() => undefined);
600
+ return Promise.race([local, remote]);
555
601
  },
556
602
  editor: (title, prefill, dialogOptions, editorOptions) => {
557
603
  if (!extensionUi) throw new ToolAbortError("Ask tool requires interactive mode");
558
- return extensionUi.editor(title, prefill, dialogOptions, editorOptions);
604
+ const source = this.session.getAskAnswerSource?.();
605
+ if (!source) return extensionUi.editor(title, prefill, dialogOptions, editorOptions);
606
+ // Race the local editor against a remote free-text answer so "Other / type
607
+ // your own" custom input can be provided remotely (e.g. a typed Telegram
608
+ // reply) instead of blocking on the local-only editor. Mirrors `select`.
609
+ const remoteController = new AbortController();
610
+ const localController = new AbortController();
611
+ const toolSignal = dialogOptions?.signal;
612
+ if (toolSignal) {
613
+ if (toolSignal.aborted) localController.abort();
614
+ else toolSignal.addEventListener("abort", () => localController.abort(), { once: true });
615
+ }
616
+ const remote = source.awaitAnswer(title, [], remoteController.signal).then(answer => {
617
+ if (answer === undefined) return new Promise<string | undefined>(() => {});
618
+ localController.abort();
619
+ return answer;
620
+ });
621
+ const local = extensionUi
622
+ .editor(title, prefill, { ...(dialogOptions ?? {}), signal: localController.signal }, editorOptions)
623
+ .then(answer => {
624
+ remoteController.abort();
625
+ return answer;
626
+ });
627
+ void local.catch(() => undefined);
628
+ return Promise.race([local, remote]);
559
629
  },
560
630
  };
561
631
 
@@ -605,8 +675,9 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
605
675
  }
606
676
  try {
607
677
  const deepInterviewPrompt = formatDeepInterviewSelectorPrompt(q.question);
678
+ const isDeepInterviewQuestion = deepInterviewPrompt !== null || q.deepInterview !== undefined;
608
679
  const displayQuestion = deepInterviewPrompt ?? q.question;
609
- const shouldNumberOptions = isDeepInterviewAskQuestion(q.question);
680
+ const shouldNumberOptions = isDeepInterviewQuestion || isDeepInterviewAskQuestion(q.question);
610
681
  const optionLabels = shouldNumberOptions ? numberOptionLabels(rawOptionLabels) : rawOptionLabels;
611
682
  const initialSelection =
612
683
  shouldNumberOptions && options?.previous
@@ -630,7 +701,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
630
701
  signal,
631
702
  initialSelection,
632
703
  navigation: options?.navigation,
633
- scrollTitleRows: deepInterviewPrompt === null ? undefined : DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS,
704
+ scrollTitleRows: isDeepInterviewQuestion ? DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS : undefined,
634
705
  otherOptionLabel: shouldNumberOptions
635
706
  ? formatNumberedOptionLabel(OTHER_OPTION, optionLabels.length)
636
707
  : undefined,
@@ -16,6 +16,8 @@ import { renderStatusLine } from "../tui";
16
16
  import { CachedOutputBlock } from "../tui/output-block";
17
17
  import { formatDimensionNote, resizeImage } from "../utils/image-resize";
18
18
  import { ensureTool } from "../utils/tools-manager";
19
+ import { INSANE_NOTES, tryInsaneFetch } from "../web/insane/bridge";
20
+ import { validatePublicHttpUrlForInsane } from "../web/insane/url-guard";
19
21
  import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../web/parallel";
20
22
  import { specialHandlers } from "../web/scrapers";
21
23
  import type { RenderResult } from "../web/scrapers/types";
@@ -705,6 +707,55 @@ async function handleSpecialUrls(
705
707
  // Main Render Function
706
708
  // =============================================================================
707
709
 
710
+ /**
711
+ * Opt-in insane-search fallback for blocked / degraded public URL reads.
712
+ *
713
+ * Returns a finalized `method: "insane"` result on success, or null (so the
714
+ * caller continues with its normal degraded behavior). Fail-closed: no note,
715
+ * guard DNS, dependency probe, or subprocess when raw mode or the opt-in
716
+ * setting is off. The public-URL guard runs BEFORE any probe/spawn.
717
+ */
718
+ export async function tryInsaneFallback(args: {
719
+ url: string;
720
+ finalUrl: string;
721
+ timeout: number;
722
+ raw: boolean;
723
+ settings: Settings;
724
+ signal: AbortSignal | undefined;
725
+ fetchedAt: string;
726
+ notes: string[];
727
+ }): Promise<FetchRenderResult | null> {
728
+ if (args.raw) return null;
729
+ if (args.settings.get("web.insaneFallback") !== true) return null;
730
+
731
+ const target = args.finalUrl || args.url;
732
+ const guard = await validatePublicHttpUrlForInsane(target);
733
+ if (!guard.ok) {
734
+ args.notes.push(INSANE_NOTES.guardBlocked(guard.reason));
735
+ return null;
736
+ }
737
+
738
+ const result = await tryInsaneFetch(guard.url.toString(), {
739
+ timeoutMs: args.timeout * 1000,
740
+ signal: args.signal,
741
+ });
742
+ if (result.ok) {
743
+ const output = finalizeOutput(result.content);
744
+ return {
745
+ url: args.url,
746
+ finalUrl: target,
747
+ contentType: "text/markdown",
748
+ method: "insane",
749
+ content: output.content,
750
+ fetchedAt: args.fetchedAt,
751
+ truncated: output.truncated,
752
+ notes: [...args.notes, ...result.notes],
753
+ };
754
+ }
755
+ for (const note of result.notes) args.notes.push(note);
756
+ return null;
757
+ }
758
+
708
759
  /**
709
760
  * Main render function implementing the full pipeline
710
761
  */
@@ -751,6 +802,19 @@ async function renderUrl(
751
802
  throw new ToolAbortError();
752
803
  }
753
804
  if (!response.ok) {
805
+ const failureNote = response.status ? `Failed to fetch URL (HTTP ${response.status})` : "Failed to fetch URL";
806
+ notes.push(failureNote);
807
+ const insane = await tryInsaneFallback({
808
+ url,
809
+ finalUrl: response.finalUrl || url,
810
+ timeout,
811
+ raw,
812
+ settings,
813
+ signal,
814
+ fetchedAt,
815
+ notes,
816
+ });
817
+ if (insane) return insane;
754
818
  return {
755
819
  url,
756
820
  finalUrl: response.finalUrl || url,
@@ -759,7 +823,7 @@ async function renderUrl(
759
823
  content: "",
760
824
  fetchedAt,
761
825
  truncated: false,
762
- notes: [response.status ? `Failed to fetch URL (HTTP ${response.status})` : "Failed to fetch URL"],
826
+ notes,
763
827
  };
764
828
  }
765
829
 
@@ -1062,6 +1126,8 @@ async function renderUrl(
1062
1126
  const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, settings, signal, storage);
1063
1127
  if (!htmlResult.ok) {
1064
1128
  notes.push("html rendering failed (lynx/html2text unavailable)");
1129
+ const insane = await tryInsaneFallback({ url, finalUrl, timeout, raw, settings, signal, fetchedAt, notes });
1130
+ if (insane) return insane;
1065
1131
  const output = finalizeOutput(rawContent);
1066
1132
  return {
1067
1133
  url,
@@ -1122,6 +1188,17 @@ async function renderUrl(
1122
1188
  };
1123
1189
  }
1124
1190
 
1191
+ const insaneLowQuality = await tryInsaneFallback({
1192
+ url,
1193
+ finalUrl,
1194
+ timeout,
1195
+ raw,
1196
+ settings,
1197
+ signal,
1198
+ fetchedAt,
1199
+ notes,
1200
+ });
1201
+ if (insaneLowQuality) return insaneLowQuality;
1125
1202
  notes.push("Page appears to require JavaScript or is mostly navigation");
1126
1203
  }
1127
1204
 
@@ -472,20 +472,17 @@ async function findImageApiKey(
472
472
  const openAI = await findOpenAIHostedImageCredentials(modelRegistry, activeModel, sessionId);
473
473
  if (openAI) return openAI;
474
474
  // Fall through to auto-detect if preferred provider key not found.
475
- } else if (preferredImageProvider === "antigravity" && modelRegistry) {
476
- const antigravity = await findAntigravityCredentials(modelRegistry, sessionId);
477
- if (antigravity) return antigravity;
478
- // Fall through to auto-detect if preferred provider key not found.
475
+ } else if (preferredImageProvider === "antigravity") {
476
+ if (!modelRegistry) return null;
477
+ return await findAntigravityCredentials(modelRegistry, sessionId);
479
478
  } else if (preferredImageProvider === "gemini") {
480
479
  const geminiKey = getEnvApiKey("google");
481
480
  if (geminiKey) return { provider: "gemini", apiKey: geminiKey };
482
481
  const googleKey = $env.GOOGLE_API_KEY;
483
- if (googleKey) return { provider: "gemini", apiKey: googleKey };
484
- // Fall through to auto-detect if preferred provider key not found.
482
+ return googleKey ? { provider: "gemini", apiKey: googleKey } : null;
485
483
  } else if (preferredImageProvider === "openrouter") {
486
484
  const openRouterKey = getEnvApiKey("openrouter");
487
- if (openRouterKey) return { provider: "openrouter", apiKey: openRouterKey };
488
- // Fall through to auto-detect if preferred provider key not found.
485
+ return openRouterKey ? { provider: "openrouter", apiKey: openRouterKey } : null;
489
486
  }
490
487
 
491
488
  // Auto-detect: GPT hosted image generation, then Antigravity, OpenRouter, Gemini.
@@ -58,6 +58,7 @@ import { SearchToolBm25Tool } from "./search-tool-bm25";
58
58
  import { SkillTool } from "./skill";
59
59
  import { loadSshTool } from "./ssh";
60
60
  import { SubagentTool } from "./subagent";
61
+ import { TelegramSendTool } from "./telegram-send";
61
62
  import { type TodoPhase, TodoWriteTool } from "./todo-write";
62
63
  import { WriteTool } from "./write";
63
64
  import { YieldTool } from "./yield";
@@ -96,6 +97,7 @@ export * from "./search-tool-bm25";
96
97
  export * from "./skill";
97
98
  export * from "./ssh";
98
99
  export * from "./subagent";
100
+ export * from "./telegram-send";
99
101
  export * from "./todo-write";
100
102
  export * from "./vim";
101
103
  export * from "./write";
@@ -118,6 +120,18 @@ export type {
118
120
  DiscoverableToolSource,
119
121
  } from "../tool-discovery/tool-index";
120
122
 
123
+ /**
124
+ * Source of remote answers for interactive asks (e.g. a Telegram reply routed
125
+ * through the notifications SDK). Lets a pending ask resolve without RPC mode.
126
+ */
127
+ export interface AskAnswerSource {
128
+ /**
129
+ * Race a remote answer against the local UI for one question. Resolves with the
130
+ * chosen option label or free-text answer, or `undefined` to defer to local UI.
131
+ */
132
+ awaitAnswer(question: string, options: string[], signal?: AbortSignal): Promise<string | undefined>;
133
+ }
134
+
121
135
  /** Session context for tool factories */
122
136
  export interface ToolSession {
123
137
  /** Current working directory */
@@ -214,6 +228,13 @@ export interface ToolSession {
214
228
  getGoalModeState?: () => GoalModeState | undefined;
215
229
  /** Unattended workflow-gate emitter (present only when unattended mode is negotiated). */
216
230
  getWorkflowGateEmitter?: () => WorkflowGateEmitter | undefined;
231
+ /**
232
+ * Optional remote answer source for interactive asks. When present, the ask
233
+ * tool races the local UI selection against a remote answer (e.g. a Telegram
234
+ * reply via the notifications SDK) so asks can be answered without RPC mode.
235
+ * No-op when undefined: the ask path behaves exactly as before.
236
+ */
237
+ getAskAnswerSource?: () => AskAnswerSource | undefined;
217
238
  /** Optional per-session restriction for goal tool operations. */
218
239
  goalToolAllowedOps?: readonly ("create" | "get" | "complete" | "resume" | "drop" | "pause")[];
219
240
  /** Goal runtime for the active agent session. */
@@ -383,6 +404,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
383
404
  todo_write: s => new TodoWriteTool(s),
384
405
  web_search: s => new WebSearchTool(s),
385
406
  search_tool_bm25: SearchToolBm25Tool.createIf,
407
+ telegram_send: TelegramSendTool.createIf,
386
408
  write: s => new WriteTool(s),
387
409
  skill: SkillTool.createIf,
388
410
  goal: s => new GoalTool(s),
@@ -78,21 +78,26 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
78
78
  };
79
79
 
80
80
  const activeModelPattern = this.session.getActiveModelString?.() ?? this.session.getModelString?.();
81
- let model = resolvePattern("pi/default") ?? resolvePattern(activeModelPattern) ?? availableModels[0];
81
+ const configuredVisionPattern = this.session.settings.getModelRole("vision")?.trim();
82
+ const configuredVisionModel = configuredVisionPattern ? resolvePattern("pi/vision") : undefined;
83
+ if (configuredVisionPattern && !configuredVisionModel) {
84
+ throw new ToolError(
85
+ `Configured modelRoles.vision (${configuredVisionPattern}) did not resolve to an available model. Configure modelRoles.vision with a vision-capable model.`,
86
+ );
87
+ }
88
+ const model = configuredVisionModel ?? resolvePattern("pi/default") ?? resolvePattern(activeModelPattern);
82
89
  if (!model) {
83
- throw new ToolError("Unable to resolve a model for inspect_image.");
90
+ throw new ToolError(
91
+ "Unable to resolve a model for inspect_image. Configure modelRoles.vision with a vision-capable model or select a vision-capable active/default model.",
92
+ );
84
93
  }
85
94
 
86
- // inspect_image requires image input; if the resolved model is text-only,
87
- // fall back to any available vision-capable model before failing.
95
+ // inspect_image requires image input. A text-only selected model must be
96
+ // paired with an explicit vision role so the model/cost boundary is visible.
88
97
  if (!model.input.includes("image")) {
89
- const visionModel = availableModels.find(candidate => candidate.input.includes("image"));
90
- if (!visionModel) {
91
- throw new ToolError(
92
- `Resolved model ${model.provider}/${model.id} does not support image input, and no vision-capable model is available. Configure a vision-capable model.`,
93
- );
94
- }
95
- model = visionModel;
98
+ throw new ToolError(
99
+ `Resolved model ${model.provider}/${model.id} does not support image input. Configure modelRoles.vision with a vision-capable model.`,
100
+ );
96
101
  }
97
102
 
98
103
  const apiKey = await modelRegistry.getApiKey(model);
@@ -160,6 +160,13 @@ function renderSubagentSnapshotBody(snapshot: SubagentSnapshot, expanded: boolea
160
160
  lines.push(` ${theme.fg("dim", "Assignment:")}`);
161
161
  for (const al of snapshot.assignment.split("\n")) lines.push(` ${theme.fg("toolOutput", replaceTabs(al))}`);
162
162
  }
163
+ if (snapshot.steerMessage) {
164
+ lines.push(` ${theme.fg("accent", `Steer (${snapshot.steerState ?? "queued"})`)}`);
165
+ const maxLines = expanded ? PREVIEW_LINES_EXPANDED : PREVIEW_LINES_COLLAPSED;
166
+ for (const pl of getPreviewLines(snapshot.steerMessage, maxLines, PREVIEW_LINE_WIDTH, Ellipsis.Unicode)) {
167
+ lines.push(` ${theme.fg("toolOutput", replaceTabs(pl))}`);
168
+ }
169
+ }
163
170
 
164
171
  // Defense in depth: the producer only attaches `progress` when a live producer
165
172
  // exists (subagent.ts #liveProgressFields), but the renderer also honors an