@sayknow-cli/coding-agent 0.3.5 → 0.3.7

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 (238) hide show
  1. package/README.md +76 -0
  2. package/dist/types/capability/mcp.d.ts +2 -0
  3. package/dist/types/cli/local-provider-smoke.d.ts +59 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -1
  5. package/dist/types/cli/telegram-cli.d.ts +13 -0
  6. package/dist/types/cli/update-cli.d.ts +24 -1
  7. package/dist/types/commands/local-provider.d.ts +34 -0
  8. package/dist/types/commands/telegram.d.ts +22 -0
  9. package/dist/types/config/keybindings.d.ts +12 -6
  10. package/dist/types/config/model-registry.d.ts +5 -0
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +284 -4
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/config/telegram-autostart.d.ts +14 -0
  15. package/dist/types/config/telegram-env-bridge.d.ts +18 -0
  16. package/dist/types/coordinator-mcp/server.d.ts +8 -0
  17. package/dist/types/defaults/skc-defaults.d.ts +7 -0
  18. package/dist/types/discovery/cursor.d.ts +4 -1
  19. package/dist/types/discovery/index.d.ts +0 -1
  20. package/dist/types/discovery/windsurf.d.ts +3 -1
  21. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  22. package/dist/types/i18n/messages/en.d.ts +1 -0
  23. package/dist/types/modes/components/custom-editor.d.ts +6 -1
  24. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
  25. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  26. package/dist/types/modes/components/model-selector.d.ts +2 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +5 -1
  28. package/dist/types/modes/components/skill-message.d.ts +1 -0
  29. package/dist/types/modes/components/status-line/types.d.ts +5 -7
  30. package/dist/types/modes/components/status-line.d.ts +15 -3
  31. package/dist/types/modes/components/two-column-body.d.ts +9 -0
  32. package/dist/types/modes/components/welcome.d.ts +9 -1
  33. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  34. package/dist/types/modes/controllers/input-controller.d.ts +6 -1
  35. package/dist/types/modes/controllers/selector-controller.d.ts +10 -1
  36. package/dist/types/modes/interactive-mode.d.ts +2 -0
  37. package/dist/types/modes/prompt-action-autocomplete.d.ts +3 -0
  38. package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
  39. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
  40. package/dist/types/modes/slash-command-visibility.d.ts +1 -0
  41. package/dist/types/modes/theme/theme.d.ts +1 -1
  42. package/dist/types/modes/tmux-scroll.d.ts +18 -0
  43. package/dist/types/modes/types.d.ts +5 -1
  44. package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
  45. package/dist/types/notifications/daemon-paths.d.ts +9 -0
  46. package/dist/types/notifications/html-format.d.ts +2 -0
  47. package/dist/types/notifications/recent-activity.d.ts +5 -1
  48. package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
  49. package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
  50. package/dist/types/notifications/telegram-daemon.d.ts +4 -10
  51. package/dist/types/notifications/telegram-reference.d.ts +4 -0
  52. package/dist/types/notifications/threaded-render.d.ts +1 -0
  53. package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
  54. package/dist/types/runtime-mcp/config.d.ts +2 -0
  55. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  56. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  57. package/dist/types/runtime-mcp/types.d.ts +6 -0
  58. package/dist/types/sdk.d.ts +1 -1
  59. package/dist/types/skc-runtime/launch-tmux.d.ts +2 -2
  60. package/dist/types/skc-runtime/ralplan-runtime.d.ts +3 -2
  61. package/dist/types/skc-runtime/restricted-role-agent-bash.d.ts +1 -0
  62. package/dist/types/skc-runtime/session-state-sidecar.d.ts +5 -0
  63. package/dist/types/skc-runtime/state-writer.d.ts +1 -0
  64. package/dist/types/skc-runtime/team-runtime.d.ts +1 -1
  65. package/dist/types/skc-runtime/tmux-common.d.ts +12 -0
  66. package/dist/types/skc-runtime/ultragoal-guard.d.ts +7 -1
  67. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +0 -4
  68. package/dist/types/slash-commands/types.d.ts +2 -0
  69. package/dist/types/system-prompt.d.ts +7 -0
  70. package/dist/types/tools/skill.d.ts +1 -1
  71. package/dist/types/utils/cmux-workspace.d.ts +52 -0
  72. package/dist/types/web/search/index.d.ts +4 -2
  73. package/dist/types/web/search/provider.d.ts +14 -0
  74. package/dist/types/web/search/providers/utils.d.ts +42 -13
  75. package/dist/types/web/search/render.d.ts +1 -0
  76. package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
  77. package/package.json +8 -8
  78. package/scripts/build-binary.ts +3 -0
  79. package/scripts/verify-insane-vendor.ts +0 -5
  80. package/src/capability/mcp.ts +2 -0
  81. package/src/cli/local-provider-smoke.ts +632 -0
  82. package/src/cli/notify-cli.ts +4 -2
  83. package/src/cli/telegram-cli.ts +156 -0
  84. package/src/cli/update-cli.ts +179 -13
  85. package/src/cli/web-search-cli.ts +5 -7
  86. package/src/cli.ts +20 -1
  87. package/src/commands/launch.ts +47 -2
  88. package/src/commands/local-provider.ts +62 -0
  89. package/src/commands/notify.ts +1 -1
  90. package/src/commands/ralplan.ts +1 -0
  91. package/src/commands/telegram.ts +47 -0
  92. package/src/config/keybindings.ts +15 -6
  93. package/src/config/mcp-schema.json +4 -0
  94. package/src/config/model-registry.ts +55 -1
  95. package/src/config/models-config-schema.ts +9 -0
  96. package/src/config/settings-schema.ts +256 -4
  97. package/src/config/settings.ts +11 -0
  98. package/src/config/telegram-autostart.ts +107 -0
  99. package/src/config/telegram-env-bridge.ts +92 -0
  100. package/src/coordinator-mcp/server.ts +276 -34
  101. package/src/defaults/skc/skills/ralplan/SKILL.md +29 -20
  102. package/src/defaults/skc/skills/ultragoal/SKILL.md +37 -112
  103. package/src/defaults/skc-defaults.ts +16 -0
  104. package/src/discovery/builtin.ts +12 -0
  105. package/src/discovery/cursor.ts +5 -84
  106. package/src/discovery/gemini.ts +3 -89
  107. package/src/discovery/index.ts +4 -1
  108. package/src/discovery/mcp-json.ts +11 -0
  109. package/src/discovery/opencode.ts +5 -122
  110. package/src/discovery/windsurf.ts +4 -82
  111. package/src/edit/read-file.ts +9 -1
  112. package/src/extensibility/extensions/runner.ts +2 -1
  113. package/src/extensibility/extensions/types.ts +11 -0
  114. package/src/extensibility/skills.ts +30 -2
  115. package/src/hooks/skill-state.ts +33 -73
  116. package/src/i18n/messages/de.ts +1 -0
  117. package/src/i18n/messages/en.ts +1 -0
  118. package/src/i18n/messages/es.ts +1 -0
  119. package/src/i18n/messages/fr.ts +1 -0
  120. package/src/i18n/messages/ja.settings.ts +28 -0
  121. package/src/i18n/messages/ja.ts +1 -0
  122. package/src/i18n/messages/ko.settings.ts +28 -0
  123. package/src/i18n/messages/ko.ts +1 -0
  124. package/src/i18n/messages/zh.settings.ts +28 -0
  125. package/src/i18n/messages/zh.ts +1 -0
  126. package/src/internal-urls/docs-index.generated.ts +12 -7
  127. package/src/main.ts +5 -2
  128. package/src/migrate/executor.ts +23 -6
  129. package/src/modes/acp/acp-agent.ts +7 -6
  130. package/src/modes/components/agent-dashboard.ts +3 -35
  131. package/src/modes/components/custom-editor.ts +26 -0
  132. package/src/modes/components/custom-model-preset-wizard.ts +30 -189
  133. package/src/modes/components/extensions/extension-dashboard.ts +2 -48
  134. package/src/modes/components/hook-selector.ts +21 -3
  135. package/src/modes/components/model-selector.ts +246 -11
  136. package/src/modes/components/session-observer-overlay.ts +3 -3
  137. package/src/modes/components/session-selector.ts +19 -9
  138. package/src/modes/components/settings-selector.ts +551 -11
  139. package/src/modes/components/skill-message.ts +18 -10
  140. package/src/modes/components/status-line/presets.ts +11 -0
  141. package/src/modes/components/status-line/segments.ts +16 -19
  142. package/src/modes/components/status-line/types.ts +6 -2
  143. package/src/modes/components/status-line.ts +281 -117
  144. package/src/modes/components/tree-selector.ts +28 -8
  145. package/src/modes/components/two-column-body.ts +36 -0
  146. package/src/modes/components/welcome.ts +165 -30
  147. package/src/modes/controllers/command-controller.ts +26 -2
  148. package/src/modes/controllers/event-controller.ts +102 -4
  149. package/src/modes/controllers/extension-ui-controller.ts +21 -2
  150. package/src/modes/controllers/input-controller.ts +214 -47
  151. package/src/modes/controllers/selector-controller.ts +35 -33
  152. package/src/modes/interactive-mode.ts +36 -30
  153. package/src/modes/prompt-action-autocomplete.ts +48 -8
  154. package/src/modes/rpc/rpc-types.ts +1 -0
  155. package/src/modes/shared/agent-wire/deep-interview-gate.ts +72 -15
  156. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
  157. package/src/modes/slash-command-visibility.ts +9 -0
  158. package/src/modes/theme/theme.ts +5 -1
  159. package/src/modes/tmux-scroll.ts +92 -0
  160. package/src/modes/types.ts +8 -1
  161. package/src/modes/utils/hotkeys-markdown.ts +5 -2
  162. package/src/modes/utils/terminal-bell.ts +48 -0
  163. package/src/modes/utils/ui-helpers.ts +76 -3
  164. package/src/notifications/daemon-paths.ts +22 -0
  165. package/src/notifications/html-format.ts +75 -1
  166. package/src/notifications/index.ts +32 -2
  167. package/src/notifications/lifecycle-control-runtime.ts +12 -3
  168. package/src/notifications/recent-activity.ts +32 -8
  169. package/src/notifications/telegram-daemon-cli.ts +125 -13
  170. package/src/notifications/telegram-daemon-control.ts +9 -0
  171. package/src/notifications/telegram-daemon.ts +88 -46
  172. package/src/notifications/telegram-reference.ts +36 -14
  173. package/src/notifications/threaded-render.ts +14 -6
  174. package/src/prompts/agents/architect.md +7 -4
  175. package/src/prompts/agents/critic.md +15 -6
  176. package/src/prompts/agents/planner.md +8 -3
  177. package/src/prompts/system/system-prompt.md +20 -2
  178. package/src/prompts/tools/bash.md +1 -1
  179. package/src/runtime-mcp/config-writer.ts +28 -0
  180. package/src/runtime-mcp/config.ts +7 -0
  181. package/src/runtime-mcp/loader.ts +3 -0
  182. package/src/runtime-mcp/manager.ts +3 -0
  183. package/src/runtime-mcp/types.ts +6 -0
  184. package/src/sdk.ts +10 -11
  185. package/src/session/agent-session.ts +103 -13
  186. package/src/session/session-manager.ts +58 -18
  187. package/src/skc-runtime/launch-tmux.ts +34 -8
  188. package/src/skc-runtime/launch-worktree.ts +16 -2
  189. package/src/skc-runtime/ralplan-runtime.ts +22 -6
  190. package/src/skc-runtime/restricted-role-agent-bash.ts +1 -0
  191. package/src/skc-runtime/session-state-sidecar.ts +167 -24
  192. package/src/skc-runtime/state-runtime.ts +8 -5
  193. package/src/skc-runtime/state-writer.ts +5 -5
  194. package/src/skc-runtime/team-runtime.ts +21 -6
  195. package/src/skc-runtime/tmux-common.ts +16 -0
  196. package/src/skc-runtime/tmux-sessions.ts +13 -9
  197. package/src/skc-runtime/ultragoal-guard.ts +150 -24
  198. package/src/skc-runtime/ultragoal-runtime.ts +16 -124
  199. package/src/skc-runtime/workflow-manifest.generated.json +0 -8
  200. package/src/skc-runtime/workflow-manifest.ts +0 -1
  201. package/src/slash-commands/builtin-registry.ts +136 -23
  202. package/src/slash-commands/types.ts +2 -0
  203. package/src/system-prompt.ts +8 -0
  204. package/src/task/executor.ts +5 -0
  205. package/src/tools/ast-grep.ts +10 -7
  206. package/src/tools/bash.ts +39 -14
  207. package/src/tools/index.ts +8 -1
  208. package/src/tools/skill.ts +1 -1
  209. package/src/utils/cmux-workspace.ts +233 -0
  210. package/src/utils/title-generator.ts +2 -0
  211. package/src/web/kagi.ts +1 -1
  212. package/src/web/parallel.ts +2 -2
  213. package/src/web/search/index.ts +137 -52
  214. package/src/web/search/provider.ts +80 -0
  215. package/src/web/search/providers/anthropic.ts +1 -1
  216. package/src/web/search/providers/brave.ts +1 -1
  217. package/src/web/search/providers/codex.ts +1 -1
  218. package/src/web/search/providers/duckduckgo.ts +1 -1
  219. package/src/web/search/providers/exa.ts +1 -1
  220. package/src/web/search/providers/gemini.ts +7 -3
  221. package/src/web/search/providers/jina.ts +1 -1
  222. package/src/web/search/providers/kimi.ts +6 -1
  223. package/src/web/search/providers/openai-compatible.ts +1 -1
  224. package/src/web/search/providers/parallel.ts +1 -1
  225. package/src/web/search/providers/perplexity.ts +2 -2
  226. package/src/web/search/providers/searxng.ts +1 -1
  227. package/src/web/search/providers/synthetic.ts +1 -1
  228. package/src/web/search/providers/tavily.ts +1 -1
  229. package/src/web/search/providers/utils.ts +67 -17
  230. package/src/web/search/providers/xai.ts +1 -1
  231. package/src/web/search/providers/zai.ts +1 -1
  232. package/src/web/search/render.ts +3 -0
  233. package/src/workflow/workflow-intent-diff.ts +199 -0
  234. package/vendor/insane-search/MANIFEST.json +1 -3
  235. package/vendor/insane-search/engine/tests/test_u1.py +0 -28
  236. package/vendor/insane-search/engine/transport.py +2 -3
  237. package/vendor/insane-search/engine/validators.py +0 -14
  238. package/src/defaults/skc/rules/ponytail.md +0 -68
@@ -10,7 +10,14 @@ import type { DaemonRuntimeInfo } from "../daemon/control-types";
10
10
  import { resolveSkcRuntimeSpawnInfo } from "../daemon/runtime";
11
11
  import { getNotificationConfig, isGloballyConfigured, tokenFingerprint } from "./config";
12
12
  import { parseInThreadConfigCommand } from "./config-commands";
13
- import { buildCompactChoiceGrid, TELEGRAM_PARSE_MODE } from "./html-format";
13
+ import { daemonPaths } from "./daemon-paths";
14
+ import {
15
+ buildCompactChoiceGrid,
16
+ code,
17
+ splitTelegramHtml,
18
+ TELEGRAM_MESSAGE_LIMIT,
19
+ TELEGRAM_PARSE_MODE,
20
+ } from "./html-format";
14
21
  import type {
15
22
  SessionCloseTarget,
16
23
  SessionCreateTarget,
@@ -62,16 +69,6 @@ export interface DaemonState {
62
69
  version: 1;
63
70
  stoppedAt?: number;
64
71
  }
65
-
66
- export interface DaemonPaths {
67
- dir: string;
68
- lock: string;
69
- state: string;
70
- roots: string;
71
- steal: string;
72
- aliases: string;
73
- }
74
-
75
72
  export interface TelegramDaemonFs {
76
73
  mkdir(path: string, opts?: fs.MakeDirectoryOptions): Promise<void>;
77
74
  readFile(path: string, encoding: BufferEncoding): Promise<string>;
@@ -131,6 +128,24 @@ const QUEUED_REACTION = "👀";
131
128
  const PENDING_TOPIC_FRAME_LIMIT = 20;
132
129
  const CONSUMED_REACTION = "✅";
133
130
 
131
+ function splitTelegramPlainText(text: string, max = TELEGRAM_MESSAGE_LIMIT): string[] {
132
+ if (text.length <= max) return [text];
133
+ const chunks: string[] = [];
134
+ let out = "";
135
+ for (const ch of text) {
136
+ if (out.length + ch.length > max) {
137
+ chunks.push(out);
138
+ out = "";
139
+ }
140
+ out += ch;
141
+ }
142
+ if (out) chunks.push(out);
143
+ return chunks;
144
+ }
145
+ function endpointGenerationKey(url: string, token: string): string {
146
+ return `${url}\0${token}`;
147
+ }
148
+
134
149
  /**
135
150
  * Whether `err` is a transient network failure worth retrying. Telegram API
136
151
  * calls over HTTP/2 occasionally surface mid-stream `ECONNRESET` (and similar)
@@ -184,17 +199,7 @@ async function fetchWithRetry(
184
199
  throw lastErr;
185
200
  }
186
201
 
187
- export function daemonPaths(agentDir: string): DaemonPaths {
188
- const dir = path.join(agentDir, "notifications");
189
- return {
190
- dir,
191
- lock: path.join(dir, "telegram-daemon.lock"),
192
- state: path.join(dir, "telegram-daemon.state.json"),
193
- roots: path.join(dir, "telegram-daemon.roots.json"),
194
- steal: path.join(dir, "telegram-daemon.steal"),
195
- aliases: path.join(dir, "telegram-callback-aliases.json"),
196
- };
197
- }
202
+ export { type DaemonPaths, daemonPaths } from "./daemon-paths";
198
203
 
199
204
  /**
200
205
  * Attach session-lifecycle control (create/close/resume) to the running daemon.
@@ -770,6 +775,7 @@ export interface TelegramDaemonOptions {
770
775
  interface SessionSocket {
771
776
  sessionId: string;
772
777
  token: string;
778
+ endpointKey: string;
773
779
  ws: WebSocket;
774
780
  pending: Map<string, { sessionId: string; actionId: string }>;
775
781
  /** True once the server advertised the `client_ping_pong` capability. */
@@ -806,6 +812,8 @@ export class TelegramNotificationDaemon {
806
812
  private readonly topicOwnerByIdentity = new Map<string, string>();
807
813
  /** Non-identity frames held until identity creates the correct thread. */
808
814
  private readonly pendingThreadedFrames = new Map<string, PendingThreadedFrame[]>();
815
+ /** Endpoint generation tombstones for sessions that already sent session_closed. */
816
+ private readonly closedEndpointKeys = new Map<string, string>();
809
817
  /** True once the daemon has nudged the user to enable Threaded Mode. */
810
818
  private threadedFallbackNoticeSent = false;
811
819
  /** Sessions whose identity header was already sent flat (Threaded Mode off). */
@@ -1072,14 +1080,29 @@ export class TelegramNotificationDaemon {
1072
1080
  threadId: number | undefined,
1073
1081
  ): Promise<boolean> {
1074
1082
  if (!isLifecycleCommandText(text)) return false;
1075
- const reply = (body: string) =>
1076
- this.botApi
1077
- .call("sendMessage", {
1078
- chat_id: this.opts.chatId,
1079
- ...(threadId !== undefined ? { message_thread_id: threadId } : {}),
1080
- text: body,
1081
- })
1082
- .catch(() => undefined);
1083
+ const reply = async (body: string): Promise<void> => {
1084
+ for (const text of splitTelegramPlainText(body)) {
1085
+ await this.botApi
1086
+ .call("sendMessage", {
1087
+ chat_id: this.opts.chatId,
1088
+ ...(threadId !== undefined ? { message_thread_id: threadId } : {}),
1089
+ text,
1090
+ })
1091
+ .catch(() => undefined);
1092
+ }
1093
+ };
1094
+ const replyHtml = async (body: string): Promise<void> => {
1095
+ for (const text of splitTelegramHtml(body)) {
1096
+ await this.botApi
1097
+ .call("sendMessage", {
1098
+ chat_id: this.opts.chatId,
1099
+ ...(threadId !== undefined ? { message_thread_id: threadId } : {}),
1100
+ text,
1101
+ parse_mode: TELEGRAM_PARSE_MODE,
1102
+ })
1103
+ .catch(() => undefined);
1104
+ }
1105
+ };
1083
1106
 
1084
1107
  if (!this.lifecycleControlActive) {
1085
1108
  await reply("Session lifecycle control is not available right now.");
@@ -1098,11 +1121,12 @@ export class TelegramNotificationDaemon {
1098
1121
  const recent = listRecentSessions({
1099
1122
  sessionsRoot: path.join(this.opts.settings.getAgentDir(), "sessions"),
1100
1123
  limit: 10,
1124
+ includeInternal: false,
1101
1125
  });
1102
- const lines = recent.length
1103
- ? recent.map(e => `\u2022 ${e.sessionId}${e.path ? ` (${e.path})` : ""}`).join("\n")
1126
+ const body = recent.length
1127
+ ? recent.map(e => `• ${code(e.sessionId)}${e.path ? ` (${code(e.path)})` : ""}`).join("\n")
1104
1128
  : "No recent sessions.";
1105
- await reply(lines);
1129
+ await replyHtml(body);
1106
1130
  return true;
1107
1131
  }
1108
1132
 
@@ -1205,7 +1229,9 @@ export class TelegramNotificationDaemon {
1205
1229
  matches: msg => msg.type === "session_closed",
1206
1230
  handle: async session => {
1207
1231
  this.busy.delete(session.sessionId);
1232
+ this.closedEndpointKeys.set(session.sessionId, session.endpointKey);
1208
1233
  await this.deleteTopic(session.sessionId);
1234
+ this.dropSession(session, "session_closed");
1209
1235
  },
1210
1236
  });
1211
1237
  }
@@ -1242,6 +1268,9 @@ export class TelegramNotificationDaemon {
1242
1268
  // would chase a dead, token-bearing record forever.
1243
1269
  const pidAlive = this.opts.pidAlive ?? defaultPidAlive;
1244
1270
  if (endpoint.stale || (endpoint.pid !== undefined && !pidAlive(endpoint.pid))) continue;
1271
+ const endpointKey = endpointGenerationKey(endpoint.url, endpoint.token);
1272
+ if (this.closedEndpointKeys.get(sessionId) === endpointKey) continue;
1273
+ this.closedEndpointKeys.delete(sessionId);
1245
1274
  this.connectSession(sessionId, endpoint.url, endpoint.token);
1246
1275
  } catch {}
1247
1276
  }
@@ -1251,9 +1280,12 @@ export class TelegramNotificationDaemon {
1251
1280
  connectSession(sessionId: string, url: string, token: string): void {
1252
1281
  const WS = this.opts.WebSocketImpl ?? WebSocket;
1253
1282
  const ws = new WS(`${url}/?token=${encodeURIComponent(token)}`);
1283
+ const endpointKey = endpointGenerationKey(url, token);
1284
+ this.closedEndpointKeys.delete(sessionId);
1254
1285
  const session: SessionSocket = {
1255
1286
  sessionId,
1256
1287
  token,
1288
+ endpointKey,
1257
1289
  ws,
1258
1290
  pending: new Map(),
1259
1291
  capable: false,
@@ -1631,12 +1663,14 @@ export class TelegramNotificationDaemon {
1631
1663
  parse_mode: TELEGRAM_PARSE_MODE,
1632
1664
  });
1633
1665
  } else if (send.text) {
1634
- await this.botApi.call("sendMessage", {
1635
- chat_id: this.opts.chatId,
1636
- ...threadField,
1637
- text: send.text,
1638
- parse_mode: TELEGRAM_PARSE_MODE,
1639
- });
1666
+ for (const text of splitTelegramHtml(send.text)) {
1667
+ await this.botApi.call("sendMessage", {
1668
+ chat_id: this.opts.chatId,
1669
+ ...threadField,
1670
+ text,
1671
+ parse_mode: TELEGRAM_PARSE_MODE,
1672
+ });
1673
+ }
1640
1674
  }
1641
1675
  } catch {
1642
1676
  // Best-effort: a failed send must never stop the daemon.
@@ -1839,13 +1873,17 @@ export class TelegramNotificationDaemon {
1839
1873
  const inline_keyboard = buildCompactChoiceGrid(options, (i: number) =>
1840
1874
  this.aliasTable.put({ sessionId: session.sessionId, actionId: msg.id, answer: i }),
1841
1875
  );
1842
- const result = (await this.botApi.call("sendMessage", {
1843
- chat_id: this.opts.chatId,
1844
- ...threadField,
1845
- text: rendered.text,
1846
- parse_mode: TELEGRAM_PARSE_MODE,
1847
- ...(inline_keyboard.length ? { reply_markup: { inline_keyboard } } : {}),
1848
- })) as { result?: { message_id?: number } };
1876
+ const chunks = splitTelegramHtml(rendered.text);
1877
+ let result: { result?: { message_id?: number } } = {};
1878
+ for (let i = 0; i < chunks.length; i++) {
1879
+ result = (await this.botApi.call("sendMessage", {
1880
+ chat_id: this.opts.chatId,
1881
+ ...threadField,
1882
+ text: chunks[i]!,
1883
+ parse_mode: TELEGRAM_PARSE_MODE,
1884
+ ...(i === chunks.length - 1 && inline_keyboard.length ? { reply_markup: { inline_keyboard } } : {}),
1885
+ })) as { result?: { message_id?: number } };
1886
+ }
1849
1887
  const messageId = result.result?.message_id;
1850
1888
  if (messageId !== undefined)
1851
1889
  this.messageRoutes.set(String(messageId), { sessionId: session.sessionId, actionId: msg.id });
@@ -2005,6 +2043,10 @@ export class TelegramNotificationDaemon {
2005
2043
  { command: "verbose", description: "Mirror full tool output + reasoning in this thread" },
2006
2044
  { command: "lean", description: "Mirror assistant text + tool names only (default)" },
2007
2045
  { command: "redact", description: "Toggle redaction of streamed content: /redact <on|off>" },
2046
+ { command: "session_create", description: "Create a SKC session: path, worktree, or dir" },
2047
+ { command: "session_recent", description: "List recent SKC sessions" },
2048
+ { command: "session_close", description: "Close a SKC-managed session" },
2049
+ { command: "session_resume", description: "Resume or reattach a session" },
2008
2050
  ],
2009
2051
  });
2010
2052
  } catch {
@@ -20,8 +20,8 @@ import {
20
20
  buildCompactChoiceGrid,
21
21
  escapeHtml,
22
22
  numberedOptionList,
23
+ splitTelegramHtml,
23
24
  TELEGRAM_PARSE_MODE,
24
- truncateTelegramHtml,
25
25
  } from "./html-format";
26
26
  import { renderThreadedFrame } from "./threaded-render";
27
27
 
@@ -37,6 +37,8 @@ export interface RenderedMessage {
37
37
  inline_keyboard?: InlineButton[][];
38
38
  }
39
39
 
40
+ type TelegramSend = (method: string, body: unknown) => Promise<Response>;
41
+
40
42
  /** Encode `actionId` + option `index` into Telegram callback_data (<=64 bytes). */
41
43
  export function encodeCallbackData(actionId: string, index: number): string {
42
44
  return `r:${index}:${actionId}`.slice(0, 64);
@@ -131,14 +133,32 @@ export function buildActionMessage(action: {
131
133
  }): RenderedMessage {
132
134
  if (action.kind === "idle") {
133
135
  const text = action.summary ? `🟢 Agent idle\n${escapeHtml(action.summary)}` : "🟢 Agent idle";
134
- return { text: truncateTelegramHtml(text) };
136
+ return { text };
135
137
  }
136
138
  const text = `❓ ${bold(action.question ?? "Question")}`;
137
139
  const options = action.options ?? [];
138
- if (options.length === 0) return { text: truncateTelegramHtml(`${text}\n\n(reply with text)`) };
140
+ if (options.length === 0) return { text: `${text}\n\n(reply with text)` };
139
141
  const body = `${text}\n\n${numberedOptionList(options)}`;
140
142
  const inline_keyboard = buildCompactChoiceGrid(options, i => encodeCallbackData(action.id, i));
141
- return { text: truncateTelegramHtml(body), inline_keyboard };
143
+ return { text: body, inline_keyboard };
144
+ }
145
+
146
+ /** Send Telegram HTML text chunks sequentially so long messages preserve order. */
147
+ export async function sendTelegramHtmlChunks(
148
+ send: TelegramSend,
149
+ chatId: string,
150
+ text: string,
151
+ inlineKeyboard?: InlineButton[][],
152
+ ): Promise<void> {
153
+ const chunks = splitTelegramHtml(text);
154
+ for (let i = 0; i < chunks.length; i++) {
155
+ await send("sendMessage", {
156
+ chat_id: chatId,
157
+ text: chunks[i]!,
158
+ parse_mode: TELEGRAM_PARSE_MODE,
159
+ ...(i === chunks.length - 1 && inlineKeyboard ? { reply_markup: { inline_keyboard: inlineKeyboard } } : {}),
160
+ });
161
+ }
142
162
  }
143
163
 
144
164
  /** A protocol `reply` frame the client should send to the server. */
@@ -285,15 +305,15 @@ export async function runTelegramReferenceClient(opts: TelegramReferenceOptions)
285
305
  const ws = new WebSocket(`${url}/?token=${encodeURIComponent(token)}`);
286
306
  let latestPendingAskId: string | undefined;
287
307
 
288
- const send = (method: string, body: unknown): Promise<Response> =>
308
+ const send: TelegramSend = (method, body) =>
289
309
  fetchImpl(`${api}/${method}`, {
290
310
  method: "POST",
291
311
  headers: { "content-type": "application/json" },
292
312
  body: JSON.stringify(body),
293
313
  });
294
314
 
295
- ws.addEventListener("message", (ev: MessageEvent) => {
296
- const msg = JSON.parse(String(ev.data)) as {
315
+ const handleServerMessage = async (data: string): Promise<void> => {
316
+ const msg = JSON.parse(data) as {
297
317
  type: string;
298
318
  kind?: "ask" | "idle";
299
319
  id?: string;
@@ -311,12 +331,7 @@ export async function runTelegramReferenceClient(opts: TelegramReferenceOptions)
311
331
  options: msg.options,
312
332
  summary: msg.summary,
313
333
  });
314
- void send("sendMessage", {
315
- chat_id: opts.chatId,
316
- text: rendered.text,
317
- parse_mode: TELEGRAM_PARSE_MODE,
318
- ...(rendered.inline_keyboard ? { reply_markup: { inline_keyboard: rendered.inline_keyboard } } : {}),
319
- });
334
+ await sendTelegramHtmlChunks(send, opts.chatId, rendered.text, rendered.inline_keyboard);
320
335
  } else if (msg.type === "action_resolved" && msg.id === latestPendingAskId) {
321
336
  latestPendingAskId = undefined;
322
337
  } else {
@@ -325,9 +340,16 @@ export async function runTelegramReferenceClient(opts: TelegramReferenceOptions)
325
340
  // session's forum topic; this reference shows the minimal handling.
326
341
  const threaded = renderThreadedFrame(msg as never);
327
342
  if (threaded?.text) {
328
- void send("sendMessage", { chat_id: opts.chatId, text: threaded.text, parse_mode: TELEGRAM_PARSE_MODE });
343
+ await sendTelegramHtmlChunks(send, opts.chatId, threaded.text);
329
344
  }
330
345
  }
346
+ };
347
+
348
+ let messageQueue = Promise.resolve();
349
+ ws.addEventListener("message", ev => {
350
+ const data = String(ev.data);
351
+ messageQueue = messageQueue.catch(() => undefined).then(() => handleServerMessage(data));
352
+ void messageQueue.catch(() => undefined);
331
353
  });
332
354
 
333
355
  // Telegram long-poll loop.
@@ -49,6 +49,7 @@ interface ThreadedFrame {
49
49
  tokenUsage?: unknown;
50
50
  model?: unknown;
51
51
  diff?: unknown;
52
+ cwd?: unknown;
52
53
  // turn_stream
53
54
  phase?: unknown;
54
55
  text?: unknown;
@@ -89,18 +90,25 @@ export function formatIdentityHeader(frame: {
89
90
  /** Format a streamed context update into a compact block (omitting empty fields). */
90
91
  export function formatContextUpdate(frame: ThreadedFrame): string | undefined {
91
92
  const lines: string[] = [];
93
+ const session = str(frame.sessionId);
94
+ const cwd = str(frame.cwd);
92
95
  const last = str(frame.lastMessage);
93
- if (last) lines.push(escapeHtml(truncate(last, 600)));
94
96
  const task = str(frame.task);
95
- if (task) lines.push(`${italic("task:")} ${escapeHtml(task)}`);
96
97
  const goal = str(frame.goal);
97
- if (goal) lines.push(`${italic("goal:")} ${escapeHtml(goal)}`);
98
98
  const usage = str(frame.tokenUsage);
99
99
  const model = str(frame.model);
100
- if (usage || model) lines.push(`ctx: ${code([usage, model].filter(Boolean).join(" · "))}`);
101
100
  const diff = str(frame.diff);
101
+ if (!cwd && !last && !task && !goal && !usage && !model && !diff) return undefined;
102
+ const meta = [session ? `session: ${code(session)}` : undefined, cwd ? `cwd: ${code(cwd)}` : undefined].filter(
103
+ Boolean,
104
+ );
105
+ if (meta.length > 0) lines.push(meta.join(" · "));
106
+ if (last) lines.push(escapeHtml(truncate(last, 600)));
107
+ if (task) lines.push(`${italic("task:")} ${escapeHtml(task)}`);
108
+ if (goal) lines.push(`${italic("goal:")} ${escapeHtml(goal)}`);
109
+ if (usage || model) lines.push(`ctx: ${code([usage, model].filter(Boolean).join(" · "))}`);
102
110
  if (diff) lines.push(`diff:\n${pre(truncate(diff, 1200))}`);
103
- return lines.length ? lines.join("\n") : undefined;
111
+ return lines.join("\n");
104
112
  }
105
113
 
106
114
  /**
@@ -125,7 +133,7 @@ export function renderThreadedFrame(frame: ThreadedFrame): ThreadedSend | undefi
125
133
  case "turn_stream": {
126
134
  const raw = str(frame.text);
127
135
  if (!raw) return undefined;
128
- const text = finalizeTelegramHtml(markdownToTelegramHtml(raw));
136
+ const text = markdownToTelegramHtml(raw);
129
137
  const finalized = frame.phase === "finalized";
130
138
  return {
131
139
  method: "sendMessage",
@@ -25,7 +25,7 @@ You may receive a forked parent-conversation snapshot as background. Your read-o
25
25
 
26
26
  <constraints>
27
27
  - Read-only: never write, edit, format, commit, push, or mutate files.
28
- - Exception: you may use the restricted `bash` tool only for sanctioned SKC workflow CLI persistence (`skc ralplan --write ...`) and SKC workflow state read/write/contract commands (`skc state ...`). For `skc ralplan --write`, pass the verdict markdown inline in `--artifact`, not as a file path. Do not use bash for product-source writes, direct handoffs, state clears, or general shell work.
28
+ - Exception: you may use the restricted `bash` tool only for sanctioned SKC workflow CLI persistence (`skc ralplan --write ...`) and SKC workflow state read/write/contract commands (`skc state ...`). For `skc ralplan --write`, pass the verdict markdown through the `SKC_RALPLAN_ARTIFACT` env override and `--artifact-env SKC_RALPLAN_ARTIFACT`, not as a file path. Do not use bash for product-source writes, direct handoffs, state clears, or general shell work.
29
29
  - Never approve code or plans you have not grounded in inspected files.
30
30
  - Never give generic advice detached from this codebase.
31
31
  - Never approve CRITICAL or HIGH severity issues.
@@ -63,6 +63,9 @@ A narrow compatibility fallback can be acceptable only when it is scoped to a kn
63
63
  ## Summary
64
64
  2-3 sentences with result and main recommendation.
65
65
 
66
+ ## Claims
67
+ Evidence-backed claims being reviewed or introduced.
68
+
66
69
  ## Analysis
67
70
  Evidence-backed findings.
68
71
 
@@ -81,12 +84,12 @@ Prioritized concrete actions.
81
84
  ## Code Review Recommendation
82
85
  `APPROVE` / `COMMENT` / `REQUEST CHANGES`
83
86
 
84
- ## Trade-offs
87
+ ## Tradeoffs
85
88
  Table or bullets comparing viable options when relevant.
86
89
 
87
- Persist this full review as the durable artifact via the restricted bash CLI, passing the markdown inline (never a file path, never `/tmp`):
90
+ Persist this full review as the durable artifact via the restricted bash CLI, passing the markdown through the `SKC_RALPLAN_ARTIFACT` env override (never a file path, never `/tmp`):
88
91
 
89
- skc ralplan --write --stage architect --stage_n <N> --artifact "<full review markdown>" --json
92
+ skc ralplan --write --stage architect --stage_n <N> --artifact-env SKC_RALPLAN_ARTIFACT --json
90
93
 
91
94
  Then return to the caller ONLY the write receipt (`run_id`, `path`, `sha256`, `stage`, `stage_n`) plus the compact verdict (Architectural Status + Code Review Recommendation). Never paste the full review body back into your response — the caller reads the persisted artifact when it needs the full text.
92
95
  </output_contract>
@@ -17,7 +17,7 @@ Review plan clarity, completeness, verification, big-picture fit, referenced fil
17
17
 
18
18
  <constraints>
19
19
  - Read-only: do not write, edit, format, commit, push, or mutate files.
20
- - Exception: you may use the restricted `bash` tool only for sanctioned SKC workflow CLI persistence (`skc ralplan --write ...`) and SKC workflow state read/write/contract commands (`skc state ...`). For `skc ralplan --write`, pass the evaluation markdown inline in `--artifact`, not as a file path. Do not use bash for product-source writes, direct handoffs, state clears, or general shell work.
20
+ - Exception: you may use the restricted `bash` tool only for sanctioned SKC workflow CLI persistence (`skc ralplan --write ...`) and SKC workflow state read/write/contract commands (`skc state ...`). For `skc ralplan --write`, pass the evaluation markdown through the `SKC_RALPLAN_ARTIFACT` env override and `--artifact-env SKC_RALPLAN_ARTIFACT`, not as a file path. Do not use bash for product-source writes, direct handoffs, state clears, or general shell work.
21
21
  - A lone file path is valid input; read and evaluate it.
22
22
  - Reject YAML-only plans as invalid plan format when a human-readable plan is required.
23
23
  - Do not invent problems; report no issues found when the plan passes.
@@ -42,11 +42,19 @@ Review plan clarity, completeness, verification, big-picture fit, referenced fil
42
42
  </success_criteria>
43
43
 
44
44
  <output_contract>
45
+ ## Verdict
45
46
  **[OKAY / ITERATE / REJECT]**
46
47
 
47
- **Justification**: concise evidence-backed explanation.
48
+ ## Claim Checks
49
+ Concise evidence-backed explanation of verified claims.
48
50
 
49
- **Summary**:
51
+ ## Missing Evidence
52
+ Definitely missing or unverified evidence, or `None`.
53
+
54
+ ## Approval Boundary
55
+ What execution may proceed with, and what remains outside approval.
56
+
57
+ ## Summary
50
58
  - Clarity
51
59
  - Verifiability
52
60
  - Completeness
@@ -55,11 +63,12 @@ Review plan clarity, completeness, verification, big-picture fit, referenced fil
55
63
  - Alternatives Depth
56
64
  - Risk/Verification Rigor
57
65
 
58
- If not OKAY, list concrete required fixes.
66
+ ## Required Changes
67
+ If not OKAY, list concrete required fixes; otherwise write `None`.
59
68
 
60
- Persist this full evaluation as the durable artifact via the restricted bash CLI, passing the markdown inline (never a file path, never `/tmp`):
69
+ Persist this full evaluation as the durable artifact via the restricted bash CLI, passing the markdown through the `SKC_RALPLAN_ARTIFACT` env override (never a file path, never `/tmp`):
61
70
 
62
- skc ralplan --write --stage critic --stage_n <N> --artifact "<full evaluation markdown>" --json
71
+ skc ralplan --write --stage critic --stage_n <N> --artifact-env SKC_RALPLAN_ARTIFACT --json
63
72
 
64
73
  Then return to the caller ONLY the write receipt (`run_id`, `path`, `sha256`, `stage`, `stage_n`) plus the compact verdict (OKAY / ITERATE / REJECT). Never paste the full evaluation body back into your response — the caller reads the persisted artifact when it needs the full text.
65
74
  </output_contract>
@@ -17,7 +17,7 @@ Leave execution with a right-sized, evidence-grounded plan: scope, steps, accept
17
17
 
18
18
  <constraints>
19
19
  - Read-only: never write, edit, format, commit, push, or mutate files.
20
- - Exception: you may use the restricted `bash` tool only for sanctioned SKC workflow CLI persistence (`skc ralplan --write ...`) and SKC workflow state read/write/contract commands (`skc state ...`). For `skc ralplan --write`, pass the plan markdown inline in `--artifact`, not as a file path. Do not use bash for product-source writes, direct handoffs, state clears, or general shell work.
20
+ - Exception: you may use the restricted `bash` tool only for sanctioned SKC workflow CLI persistence (`skc ralplan --write ...`) and SKC workflow state read/write/contract commands (`skc state ...`). For `skc ralplan --write`, pass the plan markdown through the `SKC_RALPLAN_ARTIFACT` env override and `--artifact-env SKC_RALPLAN_ARTIFACT`, not as a file path. Do not use bash for product-source writes, direct handoffs, state clears, or general shell work.
21
21
  - Persist durable plans only through `skc ralplan --write`. Never write plan files to `/tmp`, the repository, or any other path, and never rely on a file the caller must read back. The CLI is your only persistence channel.
22
22
  - Inspect the repository before asking about code facts.
23
23
  - Ask only about priorities, tradeoffs, scope decisions, timelines, or preferences that repository inspection cannot resolve.
@@ -45,16 +45,21 @@ Leave execution with a right-sized, evidence-grounded plan: scope, steps, accept
45
45
  <output_contract>
46
46
  Build the full plan as a single markdown document containing:
47
47
  - Summary
48
+ - Intent Diff
49
+ - Decision Drivers
50
+ - Options
48
51
  - In scope / out of scope
49
52
  - File-level changes
50
53
  - Sequencing and dependencies
51
54
  - Acceptance criteria
52
55
  - Verification
56
+ - Escalation/Risk Gate
57
+ - Verification Plan
53
58
  - Risks and mitigations
54
59
 
55
- Persist that markdown as the durable artifact via the restricted bash CLI, passing the plan inline (never a file path, never `/tmp`):
60
+ Persist that markdown as the durable artifact via the restricted bash CLI, passing the plan through the `SKC_RALPLAN_ARTIFACT` env override (never a file path, never `/tmp`):
56
61
 
57
- skc ralplan --write --stage planner --stage_n <N> --artifact "<full plan markdown>" --json
62
+ skc ralplan --write --stage planner --stage_n <N> --artifact-env SKC_RALPLAN_ARTIFACT --json
58
63
 
59
64
  Then return to the caller ONLY the write receipt (`run_id`, `path`, `sha256`, `stage`, `stage_n`) plus a compact plan summary (<=10 lines). Never paste the full plan body back into your response — the caller reads the persisted artifact when it needs the full text.
60
65
  </output_contract>
@@ -60,8 +60,13 @@ Use for read-only plan critique. It approves only when execution can proceed wit
60
60
 
61
61
  <routing>
62
62
  - Clear, low-risk implementation request → implement directly with focused verification.
63
+ - Do not invoke `deep-interview`, `ralplan`, `ultragoal`, `team`, or role agents for simple clear implementation requests; direct tools and the default launch path are enough.
64
+ - The runtime records a `workflow-intent-diff` CustomEntry for direct-path traceability; it does not participate in LLM context and is not a reason to slow down direct execution.
65
+ - When a task is clear, bounded, and low-risk, the default action is to make the smallest correct change and verify it, not to interview, plan, open a durable ledger, or delegate.
66
+ - Small verification needs do not make a task a planning workflow. Escalate only for real ambiguity, non-trivial architecture/sequence risk, durable multi-goal tracking, or useful coordinated workers.
67
+ - Root-cause phase schema is active only for contradiction, regression, or high-risk transition work; otherwise keep ordinary verification and do not add root-cause ceremony.
63
68
  - Vague requirements → use `deep-interview` before planning or execution.
64
- - Clear requirements but non-trivial architecture/sequence risk → use `ralplan` and stop at pending approval.
69
+ - Clear requirements but non-trivial architecture/sequence risk → use `ralplan --deliberate` and stop at pending approval.
65
70
  - Durable goal ledger needed → use `ultragoal`; if no approved plan exists, run `ralplan` first.
66
71
  - Approved work benefits from coordinated persistent workers → use `team`.
67
72
  - Large enough implementation work → delegate bounded slices to `executor` through the task/sub-agent tool when it improves quality or throughput.
@@ -131,6 +136,18 @@ Use tools whenever they materially improve correctness, completeness, or groundi
131
136
  </inventory>
132
137
  {{/if}}
133
138
 
139
+ {{#if toolDiscoveryActive}}
140
+ <tool-discovery>
141
+ Only essential tools are loaded up front; other tools are hidden to save context and are available on demand. Call `{{toolRefs.search_tool_bm25}}` with a short natural-language query to find and activate the tool you need, then call it. Prefer discovering a purpose-built tool over forcing the task onto a resident one.
142
+ {{#if discoverableTools.length}}
143
+ Discoverable tools:
144
+ {{#each discoverableTools}}
145
+ - `{{name}}`{{#if summary}} — {{summary}}{{/if}}
146
+ {{/each}}
147
+ {{/if}}
148
+ </tool-discovery>
149
+ {{/if}}
150
+
134
151
  <inputs>
135
152
  - Keep tool inputs concise where possible.
136
153
  - For `path` or path-like fields, prefer relative paths.
@@ -233,9 +250,10 @@ For image understanding, call `{{toolRefs.read}}` on the image path; the image i
233
250
  </before-editing>
234
251
 
235
252
  <decomposition>
236
- - Use todo tracking for tasks with three or more distinct steps.
253
+ - Use todo tracking for tasks with three or more distinct steps; skip it for one-step or obvious two-step fixes where the next action is already clear.
237
254
  - Mark completed tasks immediately and continue to the next task without yielding.
238
255
  - Delegate rather than silently shrinking scope. Prefer `executor` for bounded implementation slices, `planner` for sequencing, `architect` for architecture/code-review lanes, and `critic` for plan critique.
256
+ - Do not delegate for single-line typos, obvious syntax errors, single-file known-location fixes, or direct answers.
239
257
  </decomposition>
240
258
 
241
259
  <verification>
@@ -33,7 +33,7 @@ This session's bash tool is restricted. It only accepts commands beginning with:
33
33
  {{#each restrictedAllowedPrefixes}}
34
34
  - `{{this}}`
35
35
  {{/each}}
36
- Use it only for sanctioned SKC workflow CLI persistence or state read/write/contract operations; per-command env overrides and all other shell command shapes are blocked before execution.
36
+ Use it only for sanctioned SKC workflow CLI persistence or state read/write/contract operations; the only per-command env override allowed is `SKC_RALPLAN_ARTIFACT` when paired with `skc ralplan --write ... --artifact-env SKC_RALPLAN_ARTIFACT`, and all other shell command shapes are blocked before execution.
37
37
  {{/when}}
38
38
  </restricted-bash-mode>
39
39
  {{/if}}
@@ -237,6 +237,34 @@ export async function listMCPServers(filePath: string): Promise<string[]> {
237
237
  return Object.keys(config.mcpServers ?? {});
238
238
  }
239
239
 
240
+ /**
241
+ * Set a server's autoload flag (connect automatically at session startup).
242
+ *
243
+ * Autoload defaults to true, so turning it on removes the key to keep the
244
+ * config file free of redundant fields; turning it off writes `autoload: false`.
245
+ *
246
+ * @throws Error if the server doesn't exist
247
+ */
248
+ export async function setServerAutoload(filePath: string, name: string, autoload: boolean): Promise<void> {
249
+ const existing = await readMCPConfigFile(filePath);
250
+ const server = existing.mcpServers?.[name];
251
+ if (!server) {
252
+ throw new Error(`Server "${name}" not found in ${filePath}`);
253
+ }
254
+
255
+ const { autoload: _removed, ...rest } = server;
256
+ const updatedServer = (autoload ? rest : { ...rest, autoload: false }) as MCPServerConfig;
257
+ const updated: MCPConfigFile = {
258
+ ...existing,
259
+ mcpServers: {
260
+ ...existing.mcpServers,
261
+ [name]: updatedServer,
262
+ },
263
+ };
264
+
265
+ await writeMCPConfigFile(filePath, updated);
266
+ }
267
+
240
268
  /**
241
269
  * Read the disabled servers list from a config file.
242
270
  */
@@ -20,6 +20,8 @@ export interface LoadMCPConfigsOptions {
20
20
  filterExa?: boolean;
21
21
  /** Whether to filter out browser MCP servers when builtin browser tool is enabled (default: false) */
22
22
  filterBrowser?: boolean;
23
+ /** Only include servers eligible for startup connection, i.e. autoload !== false (default: false) */
24
+ autoloadOnly?: boolean;
23
25
  }
24
26
 
25
27
  /** Result of loading MCP configs */
@@ -40,6 +42,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
40
42
  const transport = server.transport ?? (server.command ? "stdio" : server.url ? "http" : "stdio");
41
43
  const shared = {
42
44
  enabled: server.enabled,
45
+ autoload: server.autoload,
43
46
  timeout: server.timeout,
44
47
  auth: server.auth,
45
48
  oauth: server.oauth,
@@ -96,6 +99,7 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
96
99
  const enableProjectConfig = options?.enableProjectConfig ?? true;
97
100
  const filterExa = options?.filterExa ?? true;
98
101
  const filterBrowser = options?.filterBrowser ?? false;
102
+ const autoloadOnly = options?.autoloadOnly ?? false;
99
103
 
100
104
  // Load MCP servers via capability system
101
105
  const result = await loadCapability<MCPServer>(mcpCapability.id, { cwd });
@@ -115,6 +119,9 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
115
119
  if (config.enabled === false || disabledServers.has(server.name)) {
116
120
  continue;
117
121
  }
122
+ if (autoloadOnly && config.autoload === false) {
123
+ continue;
124
+ }
118
125
  configs[server.name] = config;
119
126
  sources[server.name] = server._source;
120
127
  }
@@ -34,6 +34,8 @@ export interface MCPToolsLoadOptions {
34
34
  filterExa?: boolean;
35
35
  /** Whether to filter out browser MCP servers when builtin browser tool is enabled (default: false) */
36
36
  filterBrowser?: boolean;
37
+ /** Only connect servers with autoload !== false (default: false) */
38
+ autoloadOnly?: boolean;
37
39
  /** SQLite storage for MCP tool cache (null disables cache) */
38
40
  cacheStorage?: AgentStorage | null;
39
41
  /** Auth storage used to resolve OAuth credentials before initial MCP connect */
@@ -72,6 +74,7 @@ export async function discoverAndLoadMCPTools(cwd: string, options?: MCPToolsLoa
72
74
  enableProjectConfig: options?.enableProjectConfig,
73
75
  filterExa: options?.filterExa,
74
76
  filterBrowser: options?.filterBrowser,
77
+ autoloadOnly: options?.autoloadOnly,
75
78
  });
76
79
  } catch (error) {
77
80
  // If discovery fails entirely, return empty result