@sayknow-cli/coding-agent 0.3.0 → 0.3.2

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 (233) hide show
  1. package/bin/skc.js +4 -0
  2. package/dist/types/cli/mcp-cli.d.ts +25 -0
  3. package/dist/types/cli/notify-cli.d.ts +2 -0
  4. package/dist/types/cli/plugin-cli.d.ts +2 -0
  5. package/dist/types/cli.d.ts +6 -0
  6. package/dist/types/commands/mcp.d.ts +70 -0
  7. package/dist/types/commands/plugin.d.ts +6 -0
  8. package/dist/types/commands/session.d.ts +6 -0
  9. package/dist/types/config/keybindings.d.ts +2 -2
  10. package/dist/types/config/model-profile-activation.d.ts +8 -1
  11. package/dist/types/config/settings-schema.d.ts +39 -2
  12. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  13. package/dist/types/defaults/skc-defaults.d.ts +2 -13
  14. package/dist/types/extensibility/shared-events.d.ts +1 -0
  15. package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
  16. package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
  17. package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
  18. package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
  19. package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
  20. package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
  21. package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
  22. package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
  23. package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
  24. package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
  25. package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
  26. package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
  27. package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
  28. package/dist/types/lsp/types.d.ts +2 -0
  29. package/dist/types/main.d.ts +2 -0
  30. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  31. package/dist/types/modes/components/model-selector.d.ts +8 -0
  32. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  33. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  34. package/dist/types/notifications/attachment-registry.d.ts +17 -0
  35. package/dist/types/notifications/chat-adapters.d.ts +9 -0
  36. package/dist/types/notifications/config.d.ts +9 -1
  37. package/dist/types/notifications/engine.d.ts +59 -0
  38. package/dist/types/notifications/html-format.d.ts +11 -0
  39. package/dist/types/notifications/index.d.ts +149 -1
  40. package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
  41. package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
  42. package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
  43. package/dist/types/notifications/managed-daemon.d.ts +48 -0
  44. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  45. package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
  46. package/dist/types/notifications/recent-activity.d.ts +35 -0
  47. package/dist/types/notifications/telegram-daemon.d.ts +133 -16
  48. package/dist/types/notifications/telegram-reference.d.ts +3 -1
  49. package/dist/types/notifications/threaded-inbound.d.ts +19 -0
  50. package/dist/types/notifications/threaded-render.d.ts +6 -1
  51. package/dist/types/notifications/topic-registry.d.ts +12 -9
  52. package/dist/types/runtime-mcp/types.d.ts +7 -0
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +16 -4
  55. package/dist/types/session/blob-store.d.ts +25 -0
  56. package/dist/types/session/session-manager.d.ts +57 -0
  57. package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
  58. package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
  59. package/dist/types/skc-runtime/ralplan-runtime.d.ts +1 -1
  60. package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
  61. package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
  62. package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
  63. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
  64. package/dist/types/system-prompt.d.ts +2 -0
  65. package/dist/types/task/executor.d.ts +9 -1
  66. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  67. package/dist/types/tools/fetch.d.ts +23 -0
  68. package/dist/types/tools/index.d.ts +4 -1
  69. package/dist/types/tools/telegram-send.d.ts +32 -0
  70. package/dist/types/utils/changelog.d.ts +1 -0
  71. package/dist/types/web/insane/bridge.d.ts +103 -0
  72. package/dist/types/web/insane/url-guard.d.ts +25 -0
  73. package/dist/types/web/scrapers/types.d.ts +5 -0
  74. package/dist/types/web/scrapers/utils.d.ts +7 -1
  75. package/dist/types/web/search/provider.d.ts +18 -1
  76. package/dist/types/web/search/providers/insane.d.ts +53 -0
  77. package/dist/types/web/search/providers/text-citations.d.ts +23 -0
  78. package/dist/types/web/search/types.d.ts +12 -4
  79. package/package.json +14 -10
  80. package/scripts/g004-tmux-smoke.ts +100 -0
  81. package/scripts/g005-daemon-smoke.ts +180 -0
  82. package/scripts/g011-daemon-path-smoke.ts +153 -0
  83. package/scripts/verify-insane-vendor.ts +132 -0
  84. package/src/cli/args.ts +1 -1
  85. package/src/cli/fast-help.ts +30 -2
  86. package/src/cli/mcp-cli.ts +272 -0
  87. package/src/cli/notify-cli.ts +152 -5
  88. package/src/cli/plugin-cli.ts +66 -3
  89. package/src/cli.ts +30 -12
  90. package/src/commands/mcp.ts +117 -0
  91. package/src/commands/plugin.ts +4 -0
  92. package/src/commands/session.ts +18 -0
  93. package/src/commands/team.ts +1 -1
  94. package/src/config/keybindings.ts +2 -2
  95. package/src/config/model-profile-activation.ts +55 -7
  96. package/src/config/settings-schema.ts +30 -1
  97. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  98. package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
  99. package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
  100. package/src/defaults/skc/skills/ralplan/SKILL.md +11 -4
  101. package/src/defaults/skc/skills/team/SKILL.md +5 -3
  102. package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
  103. package/src/defaults/skc-defaults.ts +2 -27
  104. package/src/export/html/index.ts +2 -2
  105. package/src/extensibility/extensions/runner.ts +1 -0
  106. package/src/extensibility/shared-events.ts +1 -0
  107. package/src/extensibility/skc-plugins/compiler.ts +351 -0
  108. package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
  109. package/src/extensibility/skc-plugins/index.ts +9 -0
  110. package/src/extensibility/skc-plugins/injection.ts +109 -0
  111. package/src/extensibility/skc-plugins/installer.ts +434 -0
  112. package/src/extensibility/skc-plugins/loader.ts +3 -1
  113. package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
  114. package/src/extensibility/skc-plugins/observability.ts +84 -0
  115. package/src/extensibility/skc-plugins/paths.ts +1 -1
  116. package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
  117. package/src/extensibility/skc-plugins/registry.ts +180 -0
  118. package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
  119. package/src/extensibility/skc-plugins/schema.ts +250 -20
  120. package/src/extensibility/skc-plugins/session-validation.ts +147 -0
  121. package/src/extensibility/skc-plugins/types.ts +199 -3
  122. package/src/extensibility/skc-plugins/validation.ts +80 -0
  123. package/src/extensibility/skills.ts +15 -0
  124. package/src/hooks/skill-state.ts +57 -0
  125. package/src/internal-urls/docs-index.generated.ts +17 -13
  126. package/src/lsp/config.ts +16 -3
  127. package/src/lsp/defaults.json +7 -0
  128. package/src/lsp/types.ts +2 -0
  129. package/src/main.ts +14 -3
  130. package/src/modes/bridge/bridge-mode.ts +11 -0
  131. package/src/modes/components/assistant-message.ts +49 -1
  132. package/src/modes/components/custom-editor.ts +2 -0
  133. package/src/modes/components/footer.ts +2 -3
  134. package/src/modes/components/hook-editor.ts +1 -1
  135. package/src/modes/components/hook-selector.ts +67 -43
  136. package/src/modes/components/model-selector.ts +56 -11
  137. package/src/modes/components/status-line/git-utils.ts +25 -0
  138. package/src/modes/components/status-line.ts +10 -11
  139. package/src/modes/components/welcome.ts +2 -3
  140. package/src/modes/controllers/event-controller.ts +15 -0
  141. package/src/modes/controllers/extension-ui-controller.ts +0 -27
  142. package/src/modes/controllers/selector-controller.ts +53 -11
  143. package/src/modes/interactive-mode.ts +50 -3
  144. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  145. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  146. package/src/modes/theme/defaults/index.ts +2 -0
  147. package/src/modes/utils/context-usage.ts +2 -2
  148. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  149. package/src/notifications/attachment-registry.ts +23 -0
  150. package/src/notifications/chat-adapters.ts +147 -0
  151. package/src/notifications/config.ts +23 -2
  152. package/src/notifications/engine.ts +100 -0
  153. package/src/notifications/html-format.ts +38 -0
  154. package/src/notifications/index.ts +417 -45
  155. package/src/notifications/lifecycle-commands.ts +238 -0
  156. package/src/notifications/lifecycle-control-runtime.ts +405 -0
  157. package/src/notifications/lifecycle-orchestrator.ts +358 -0
  158. package/src/notifications/managed-daemon.ts +163 -0
  159. package/src/notifications/operator-runtime.ts +171 -0
  160. package/src/notifications/rate-limit-pool.ts +19 -0
  161. package/src/notifications/recent-activity.ts +132 -0
  162. package/src/notifications/telegram-daemon.ts +984 -242
  163. package/src/notifications/telegram-reference.ts +25 -7
  164. package/src/notifications/threaded-inbound.ts +60 -4
  165. package/src/notifications/threaded-render.ts +20 -2
  166. package/src/notifications/topic-registry.ts +23 -9
  167. package/src/prompts/agents/executor.md +2 -2
  168. package/src/runtime-mcp/transports/stdio.ts +38 -4
  169. package/src/runtime-mcp/types.ts +7 -0
  170. package/src/sdk.ts +157 -10
  171. package/src/session/agent-session.ts +248 -125
  172. package/src/session/blob-store.ts +196 -8
  173. package/src/session/session-manager.ts +762 -12
  174. package/src/skc-runtime/launch-tmux.ts +85 -22
  175. package/src/skc-runtime/ledger-event-renderer.ts +1 -0
  176. package/src/skc-runtime/psmux-detect.ts +239 -0
  177. package/src/skc-runtime/ralplan-runtime.ts +2 -2
  178. package/src/skc-runtime/team-runtime.ts +56 -23
  179. package/src/skc-runtime/tmux-common.ts +88 -4
  180. package/src/skc-runtime/tmux-sessions.ts +111 -9
  181. package/src/skc-runtime/ultragoal-guard.ts +25 -8
  182. package/src/skc-runtime/ultragoal-runtime.ts +75 -15
  183. package/src/skc-runtime/workflow-manifest.generated.json +29 -0
  184. package/src/skc-runtime/workflow-manifest.ts +7 -2
  185. package/src/slash-commands/builtin-registry.ts +23 -3
  186. package/src/slash-commands/helpers/fast-status-report.ts +13 -3
  187. package/src/slash-commands/helpers/parse.ts +2 -1
  188. package/src/system-prompt.ts +9 -0
  189. package/src/task/executor.ts +31 -7
  190. package/src/task/index.ts +2 -0
  191. package/src/tools/ask.ts +5 -1
  192. package/src/tools/bash.ts +9 -0
  193. package/src/tools/composer-bash-policy.ts +96 -0
  194. package/src/tools/fetch.ts +94 -1
  195. package/src/tools/index.ts +6 -1
  196. package/src/tools/telegram-send.ts +137 -0
  197. package/src/utils/changelog.ts +8 -0
  198. package/src/web/insane/bridge.ts +350 -0
  199. package/src/web/insane/url-guard.ts +159 -0
  200. package/src/web/scrapers/types.ts +143 -45
  201. package/src/web/scrapers/utils.ts +70 -19
  202. package/src/web/search/provider.ts +77 -18
  203. package/src/web/search/providers/anthropic.ts +70 -3
  204. package/src/web/search/providers/codex.ts +1 -119
  205. package/src/web/search/providers/gemini.ts +99 -0
  206. package/src/web/search/providers/insane.ts +551 -0
  207. package/src/web/search/providers/openai-compatible.ts +66 -32
  208. package/src/web/search/providers/text-citations.ts +111 -0
  209. package/src/web/search/types.ts +13 -2
  210. package/vendor/insane-search/LICENSE +21 -0
  211. package/vendor/insane-search/MANIFEST.json +24 -0
  212. package/vendor/insane-search/engine/__init__.py +23 -0
  213. package/vendor/insane-search/engine/__main__.py +128 -0
  214. package/vendor/insane-search/engine/bias_check.py +183 -0
  215. package/vendor/insane-search/engine/executor.py +254 -0
  216. package/vendor/insane-search/engine/fetch_chain.py +725 -0
  217. package/vendor/insane-search/engine/learning.py +175 -0
  218. package/vendor/insane-search/engine/phase0.py +214 -0
  219. package/vendor/insane-search/engine/safety.py +91 -0
  220. package/vendor/insane-search/engine/templates/package.json +11 -0
  221. package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
  222. package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
  223. package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
  224. package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
  225. package/vendor/insane-search/engine/tests/test_u1.py +200 -0
  226. package/vendor/insane-search/engine/tests/test_u4.py +131 -0
  227. package/vendor/insane-search/engine/tests/test_u5.py +163 -0
  228. package/vendor/insane-search/engine/tests/test_u7.py +124 -0
  229. package/vendor/insane-search/engine/transport.py +211 -0
  230. package/vendor/insane-search/engine/url_transforms.py +98 -0
  231. package/vendor/insane-search/engine/validators.py +331 -0
  232. package/vendor/insane-search/engine/waf_detector.py +214 -0
  233. package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
@@ -6,6 +6,7 @@ import { getOAuthProviders } from "@sayknow-cli/ai/utils/oauth";
6
6
  import { Spacer, Text } from "@sayknow-cli/tui";
7
7
  import { setProjectDir } from "@sayknow-cli/utils";
8
8
  import { jobElapsedMs } from "../async";
9
+ import { materializeActiveModelProfileAssignment } from "../config/model-profile-activation";
9
10
  import {
10
11
  SKC_MODEL_ASSIGNMENT_TARGET_IDS,
11
12
  SKC_MODEL_ASSIGNMENT_TARGETS,
@@ -299,6 +300,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
299
300
  selector: selection.selector,
300
301
  thinkingLevel: selection.thinkingLevel,
301
302
  });
303
+ materializeActiveModelProfileAssignment({
304
+ session: runtime.session,
305
+ settings: runtime.settings,
306
+ role: parsedArgs.targetId,
307
+ selector: persistedSelector,
308
+ });
302
309
  if (selection.thinkingLevel) {
303
310
  runtime.session.setThinkingLevel(selection.thinkingLevel);
304
311
  }
@@ -317,10 +324,23 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
317
324
  selection.thinkingLevel ??
318
325
  extractExplicitThinkingSelector(overrides[parsedArgs.targetId], runtime.settings);
319
326
  const roleSelector = formatModelSelectorValue(selection.selector, thinkingLevel);
320
- runtime.settings.set("task.agentModelOverrides", {
321
- ...overrides,
322
- [parsedArgs.targetId]: roleSelector,
327
+ const materializedProfile = materializeActiveModelProfileAssignment({
328
+ session: runtime.session,
329
+ settings: runtime.settings,
330
+ role: parsedArgs.targetId,
331
+ selector: roleSelector,
323
332
  });
333
+ if (!materializedProfile) {
334
+ const target = SKC_MODEL_ASSIGNMENT_TARGETS[parsedArgs.targetId];
335
+ if (target.settingsPath === "modelRoles") {
336
+ runtime.settings.setModelRole(parsedArgs.targetId, roleSelector);
337
+ } else {
338
+ runtime.settings.set("task.agentModelOverrides", {
339
+ ...overrides,
340
+ [parsedArgs.targetId]: roleSelector,
341
+ });
342
+ }
343
+ }
324
344
  runtime.settings.getStorage()?.recordModelUsage(`${selection.model.provider}/${selection.model.id}`);
325
345
  await runtime.output(`${parsedArgs.targetId} agent model set to ${roleSelector}.`);
326
346
  }
@@ -56,6 +56,12 @@ export interface FastStatusSessionLike {
56
56
  readonly model?: Model;
57
57
  /** Fast predicate against the main session tier (current model + `modelRoles`). */
58
58
  isFastForProvider(provider?: string): boolean;
59
+ /**
60
+ * Current-model EFFECTIVE fast state (intent minus any provider auto-disable).
61
+ * Used for the current-model row so it matches what the next request does.
62
+ * Optional so lightweight fakes can omit it; falls back to `isFastForProvider`.
63
+ */
64
+ isFastModeActive?(): boolean;
59
65
  /** Fast predicate against the effective subagent tier (`task.agentModelOverrides` roles). */
60
66
  isFastForSubagentProvider(provider?: string): boolean;
61
67
  resolveRoleModelWithThinking(role: string): { model?: Model };
@@ -95,9 +101,13 @@ export interface BuildFastStatusReportArgs {
95
101
  */
96
102
  export function buildFastStatusReport(args: BuildFastStatusReportArgs): string {
97
103
  const { session, roleTargets, iconFast, formatInactive } = args;
98
- const rows: FastStatusRow[] = [
99
- { label: "현재 모델", model: session.model, fast: session.isFastForProvider(session.model?.provider) },
100
- ];
104
+ // Current-model row uses the EFFECTIVE predicate (intent minus any provider
105
+ // auto-disable) so it matches the next request; `modelRoles` rows below stay
106
+ // on pure intent. Fall back to intent when a fake omits `isFastModeActive`.
107
+ const currentFast = session.isFastModeActive
108
+ ? session.isFastModeActive()
109
+ : session.isFastForProvider(session.model?.provider);
110
+ const rows: FastStatusRow[] = [{ label: "현재 모델", model: session.model, fast: currentFast }];
101
111
  for (const target of roleTargets) {
102
112
  const resolved = session.resolveRoleModelWithThinking(target.id);
103
113
  if (resolved.model) {
@@ -1,3 +1,4 @@
1
+ import { parseCommandArgs } from "../../utils/command-args";
1
2
  import type { ParsedSlashCommand, SlashCommandResult, SlashCommandRuntime } from "../types";
2
3
 
3
4
  export interface ParsedSubcommand {
@@ -65,7 +66,7 @@ export function errorMessage(error: unknown): string {
65
66
  * "name required" diagnostics with their own messaging.
66
67
  */
67
68
  export function parseNamedScopeArgs(rest: string, invalidScopeMessage: string): NamedScopeArgs {
68
- const tokens = rest.split(/\s+/).filter(Boolean);
69
+ const tokens = parseCommandArgs(rest);
69
70
  let name: string | undefined;
70
71
  let scope: ConfigScope = "project";
71
72
  let i = 0;
@@ -336,6 +336,8 @@ export interface BuildSystemPromptOptions {
336
336
  toolNames?: string[];
337
337
  /** Text to append to system prompt. */
338
338
  appendSystemPrompt?: string;
339
+ /** Rendered SKC plugin system-appendix blocks (lower-authority, appended last). */
340
+ pluginAppendices?: string;
339
341
  /** Repeat full tool descriptions in system prompt. Default: false */
340
342
  repeatToolDescriptions?: boolean;
341
343
  /** Skills settings for discovery. */
@@ -380,6 +382,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
380
382
  customPrompt,
381
383
  tools,
382
384
  appendSystemPrompt,
385
+ pluginAppendices,
383
386
  repeatToolDescriptions = false,
384
387
  skillsSettings,
385
388
  toolNames: providedToolNames,
@@ -579,5 +582,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
579
582
  systemPrompt.push(projectPrompt);
580
583
  }
581
584
 
585
+ // Plugin system appendices are appended last as a lower-authority block; they
586
+ // can never override base/project/developer instructions above them.
587
+ if (pluginAppendices?.trim()) {
588
+ systemPrompt.push(pluginAppendices.trim());
589
+ }
590
+
582
591
  return { systemPrompt };
583
592
  }
@@ -9,6 +9,7 @@ import * as fs from "node:fs/promises";
9
9
  import path from "node:path";
10
10
  import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@sayknow-cli/agent-core";
11
11
  import { recordHandoff, resolveTelemetry } from "@sayknow-cli/agent-core";
12
+ import type { ServiceTier } from "@sayknow-cli/ai";
12
13
  import { type JsonSchemaValidationIssue, validateJsonSchemaValue } from "@sayknow-cli/ai/utils/schema";
13
14
  import { logger, prompt, untilAborted } from "@sayknow-cli/utils";
14
15
  import { AsyncJobManager } from "../async";
@@ -19,7 +20,7 @@ import { Settings } from "../config/settings";
19
20
  import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
20
21
  import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
21
22
  import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
22
- import { buildAgentSubskillInjection } from "../extensibility/skc-plugins";
23
+ import { buildAgentSubskillInjection, renderAgentPromptAdditions } from "../extensibility/skc-plugins";
23
24
  import { buildSkillPromptMessage, type Skill } from "../extensibility/skills";
24
25
  import type { HindsightSessionState } from "../hindsight/state";
25
26
  import type { LocalProtocolOptions } from "../internal-urls";
@@ -146,6 +147,13 @@ export interface ExecutorOptions {
146
147
  authStorage?: AuthStorage;
147
148
  modelRegistry?: ModelRegistry;
148
149
  settings?: Settings;
150
+ /**
151
+ * Live service-tier intent of the parent session (`AgentSession.serviceTier`),
152
+ * used as the inherited tier when `task.serviceTier === "inherit"`. Passing the
153
+ * live value (not the stale settings snapshot) lets a runtime `/fast on` reach
154
+ * subagents, and a main-model fast-mode auto-disable does not clobber it.
155
+ */
156
+ inheritedServiceTier?: ServiceTier;
149
157
  /** Override local:// protocol options so subagent shares parent's local:// root */
150
158
  localProtocolOptions?: LocalProtocolOptions;
151
159
  /**
@@ -482,15 +490,19 @@ function getUsageTokens(usage: unknown): number {
482
490
  return firstNumberField(record, ["totalTokens", "total_tokens"]) ?? 0;
483
491
  }
484
492
 
485
- export function createSubagentSettings(baseSettings: Settings): Settings {
493
+ export function createSubagentSettings(baseSettings: Settings, inheritedServiceTier?: ServiceTier): Settings {
486
494
  const snapshot: Partial<Record<SettingPath, unknown>> = {};
487
495
  for (const key of Object.keys(SETTINGS_SCHEMA) as SettingPath[]) {
488
496
  snapshot[key] = baseSettings.get(key);
489
497
  }
490
- // Subagent-scoped service-tier override: "inherit" keeps the snapshotted main
491
- // session tier; any explicit value applies only to subagent sessions.
498
+ // Subagent-scoped service-tier override: "inherit" uses the parent session's
499
+ // LIVE intent (so a runtime `/fast on` reaches subagents and a main-model
500
+ // fast-mode auto-disable never clobbers it); any explicit value applies only
501
+ // to subagent sessions and wins over inherited intent.
492
502
  const taskServiceTier = baseSettings.get("task.serviceTier");
493
- if (taskServiceTier !== "inherit") {
503
+ if (taskServiceTier === "inherit") {
504
+ snapshot.serviceTier = inheritedServiceTier ?? "none";
505
+ } else {
494
506
  snapshot.serviceTier = taskServiceTier;
495
507
  }
496
508
  return Settings.isolated({
@@ -571,7 +583,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
571
583
  }
572
584
 
573
585
  const settings = options.settings ?? Settings.isolated();
574
- const subagentSettings = createSubagentSettings(settings);
586
+ const subagentSettings = createSubagentSettings(settings, options.inheritedServiceTier);
575
587
  const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
576
588
  const maxRuntimeMs = Math.max(0, Math.trunc(Number(settings.get("task.maxRuntimeMs") ?? 0) || 0));
577
589
  const parentDepth = options.taskDepth ?? 0;
@@ -1229,6 +1241,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1229
1241
  agentName: agent.name,
1230
1242
  });
1231
1243
 
1244
+ let agentPromptAdditions = { appendix: "", advertisement: "" };
1245
+ try {
1246
+ agentPromptAdditions = await renderAgentPromptAdditions({ cwd, agentName: agent.name });
1247
+ } catch (error) {
1248
+ logger.warn("Failed to render SKC plugin agent prompt additions", { error });
1249
+ }
1250
+
1232
1251
  const { session } = await awaitAbortable(
1233
1252
  createAgentSession({
1234
1253
  cwd: worktree ?? cwd,
@@ -1259,7 +1278,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1259
1278
  ircSelfId: ircEnabled ? id : "",
1260
1279
  forkContext: forkContextNotice,
1261
1280
  });
1262
- const promptWithSubskills = `${subagentPrompt}${agentSubskillBlock}`;
1281
+ // Order: base agent prompt -> agent appendix -> Tier-1 advertisement -> Tier-2 body.
1282
+ const appendixPart = agentPromptAdditions.appendix ? `\n\n${agentPromptAdditions.appendix}` : "";
1283
+ const advertPart = agentPromptAdditions.advertisement
1284
+ ? `\n\n${agentPromptAdditions.advertisement}`
1285
+ : "";
1286
+ const promptWithSubskills = `${subagentPrompt}${appendixPart}${advertPart}${agentSubskillBlock}`;
1263
1287
  return defaultPrompt.length === 0
1264
1288
  ? [promptWithSubskills]
1265
1289
  : [...defaultPrompt.slice(0, -1), promptWithSubskills, defaultPrompt[defaultPrompt.length - 1]];
package/src/task/index.ts CHANGED
@@ -1311,6 +1311,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1311
1311
  authStorage: this.session.authStorage,
1312
1312
  modelRegistry: this.session.modelRegistry,
1313
1313
  settings: this.session.settings,
1314
+ inheritedServiceTier: this.session.serviceTier,
1314
1315
  contextFiles,
1315
1316
  skills: availableSkills,
1316
1317
  autoloadSkills: resolvedAutoloadSkills,
@@ -1372,6 +1373,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1372
1373
  authStorage: this.session.authStorage,
1373
1374
  modelRegistry: this.session.modelRegistry,
1374
1375
  settings: this.session.settings,
1376
+ inheritedServiceTier: this.session.serviceTier,
1375
1377
  contextFiles,
1376
1378
  skills: availableSkills,
1377
1379
  autoloadSkills: resolvedAutoloadSkills,
package/src/tools/ask.ts CHANGED
@@ -260,7 +260,11 @@ async function askSingleQuestion(
260
260
  ? "up/down navigate enter select ←/→ question esc cancel"
261
261
  : "up/down navigate enter select esc cancel";
262
262
  const helpText =
263
- scrollTitleRows === undefined ? baseHelpText : `${baseHelpText} wheel/PgUp/PgDn scroll question`;
263
+ scrollTitleRows === undefined
264
+ ? baseHelpText
265
+ : navigation
266
+ ? "↑/↓ select enter ←/→ question esc PgUp/PgDn/Ctrl+u/d: question · Wheel: transcript"
267
+ : "↑/↓ select enter esc PgUp/PgDn/Ctrl+u/d: question · Wheel: transcript";
264
268
  const dialogOptions = {
265
269
  initialIndex,
266
270
  timeout,
package/src/tools/bash.ts CHANGED
@@ -25,6 +25,7 @@ import { type BashInteractiveResult, runInteractiveBashPty } from "./bash-intera
25
25
  import { checkBashInterception } from "./bash-interceptor";
26
26
  import { canUseInteractiveBashPty } from "./bash-pty-selection";
27
27
  import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
28
+ import { checkComposerBashPolicy } from "./composer-bash-policy";
28
29
  import { formatStyledTruncationWarning, type OutputMeta, stripOutputNotice } from "./output-meta";
29
30
  import { resolveToCwd } from "./path-utils";
30
31
  import { formatToolWorkingDirectory, replaceTabs } from "./render-utils";
@@ -570,6 +571,14 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
570
571
  }
571
572
  }
572
573
 
574
+ const composerPolicy = checkComposerBashPolicy({
575
+ modelId: this.session.getActiveModelString?.() ?? this.session.getModelString?.() ?? this.session.model?.id,
576
+ commands: rawCommand === command ? [command] : [rawCommand, command],
577
+ });
578
+ if (!composerPolicy.allowed) {
579
+ throw new ToolError(composerPolicy.message);
580
+ }
581
+
573
582
  const internalUrlOptions: InternalUrlExpansionOptions = {
574
583
  skills: this.session.skills ?? [],
575
584
  internalRouter: InternalUrlRouter.instance(),
@@ -0,0 +1,96 @@
1
+ import { isComposerHarnessModel } from "@sayknow-cli/ai/providers/composer-discipline";
2
+
3
+ export const COMPOSER_BASH_POLICY_ERROR =
4
+ "Composer bash policy blocked repository file I/O. Use find, search, read, and edit tools for file discovery, file inspection, and file mutation.";
5
+
6
+ type ComposerBashPolicyResult =
7
+ | { allowed: true }
8
+ | {
9
+ allowed: false;
10
+ reason: string;
11
+ message: string;
12
+ };
13
+
14
+ const BLOCK_PATTERNS: Array<{ id: string; pattern: RegExp }> = [
15
+ { id: "pipe", pattern: /\|/ },
16
+ { id: "process-substitution", pattern: /<[>(]/ },
17
+ { id: "heredoc", pattern: /<<[-~]?/ },
18
+ { id: "command-substitution", pattern: /\$\(|`/ },
19
+ { id: "redirection", pattern: /(^|[^<>])(?:>>?|<)(?!=)/ },
20
+ { id: "tee", pattern: /(?:^|[;&|\s])tee(?:\s|$)/ },
21
+ {
22
+ id: "shell-file-read-discovery",
23
+ pattern: /(?:^|[;&|()\s])(?:\S*\/)?(?:cat|head|tail|less|more|grep|rg|find|fd|tree|ls)\b/,
24
+ },
25
+ {
26
+ id: "shell-file-mutation",
27
+ pattern: /(?:^|[;&|()\s])(?:\S*\/)?(?:cp|mv|rm|touch|mkdir|chmod|chown|ln)\b/,
28
+ },
29
+ { id: "sed-print", pattern: /(?:^|[;&|()\s])sed\s+(?:-[^\s]*n\b|.*\bp\b)/ },
30
+ { id: "awk-print", pattern: /(?:^|[;&|()\s])awk\b/ },
31
+ { id: "git-ls-files", pattern: /(?:^|[;&|()\s])git(?:\s+-C\s+\S+)?\s+ls-files\b/ },
32
+ { id: "git-grep", pattern: /(?:^|[;&|()\s])git(?:\s+-C\s+\S+)?\s+grep\b/ },
33
+ { id: "git-show-path", pattern: /(?:^|[;&|()\s])git(?:\s+-C\s+\S+)?\s+show\s+\S+:\S+/ },
34
+ { id: "git-diff", pattern: /(?:^|[;&|()\s])git(?:\s+-C\s+\S+)?\s+diff(?:\s|$)/ },
35
+ { id: "git-cat-file", pattern: /(?:^|[;&|()\s])git(?:\s+-C\s+\S+)?\s+cat-file\b/ },
36
+ {
37
+ id: "git-show-discovery",
38
+ pattern: /(?:^|[;&|()\s])git(?:\s+-C\s+\S+)?\s+show\b.*(?:--name-only|--name-status|--stat)/,
39
+ },
40
+ {
41
+ id: "git-log-path-discovery",
42
+ pattern: /(?:^|[;&|()\s])git(?:\s+-C\s+\S+)?\s+log\b.*(?:--name-only|--name-status|--stat)/,
43
+ },
44
+ { id: "sed-in-place", pattern: /(?:^|[;&|()\s])sed\s+-[^\s]*i\b/ },
45
+ { id: "perl-in-place", pattern: /(?:^|[;&|()\s])perl\s+-[^\s]*p[^\s]*i\b/ },
46
+ {
47
+ id: "script-file-io",
48
+ pattern:
49
+ /(?:^|[;&|()\s])(?:python3?|node|bun)\s+(?:-\s*<<|-c\b|-e\b|--eval\b).*?(?:read_text|read_bytes|write_text|iterdir|listdir|glob\.glob|readFile|readFileSync|writeFile|writeFileSync|readdir|readdirSync|stat|statSync|cpSync|rmSync|mkdirSync|createReadStream|createWriteStream|Bun\.file|Bun\.write|fs\.readFile|fs\.writeFile|fs\.readdir|fs\.stat|fs\.cp|fs\.rm|fs\.mkdir|open\s*\()/s,
50
+ },
51
+ {
52
+ id: "contaminated-command",
53
+ pattern: /```|^\s*(?:I\s+(?:will|need|am going)|We\s+(?:need|will)|First[, ]|Now[, ]|Let's)\b/im,
54
+ },
55
+ ];
56
+
57
+ const ALLOWED_TERMINAL_PATTERNS: RegExp[] = [
58
+ /^bun\s+test(?:\s+[\w./:@=-]+)*$/,
59
+ /^bun\s+run\s+(?:check(?::[\w-]+)?|test(?::[\w-]+)?|build(?::[\w-]+)?)(?:\s+[\w./:@=-]+)*$/,
60
+ /^bun\s+--version$/,
61
+ /^mise\s+x\s+bun@\d+\.\d+\.\d+\s+--\s+bun\s+test(?:\s+[\w./:@=-]+)*$/,
62
+ /^mise\s+x\s+bun@\d+\.\d+\.\d+\s+--\s+bun\s+run\s+(?:check(?::[\w-]+)?|test(?::[\w-]+)?|build(?::[\w-]+)?)(?:\s+[\w./:@=-]+)*$/,
63
+ /^cargo\s+(?:test|check|build)(?:\s+[\w./:@=-]+)*$/,
64
+ /^git\s+status(?:\s+--short)?(?:\s+--branch)?$/,
65
+ /^git\s+rev-parse\s+HEAD$/,
66
+ /^npm\s+--version$/,
67
+ /^pnpm\s+--version$/,
68
+ /^yarn\s+--version$/,
69
+ ];
70
+
71
+ function isAllowedComposerTerminalCommand(command: string): boolean {
72
+ const normalized = command.trim().replace(/\s+/g, " ");
73
+ return ALLOWED_TERMINAL_PATTERNS.some(pattern => pattern.test(normalized));
74
+ }
75
+
76
+ export function isComposerBashPolicyModel(modelId: string | undefined): boolean {
77
+ return Boolean(modelId && isComposerHarnessModel(modelId));
78
+ }
79
+
80
+ export function checkComposerBashPolicy(input: {
81
+ modelId?: string;
82
+ commands: readonly string[];
83
+ }): ComposerBashPolicyResult {
84
+ if (!isComposerBashPolicyModel(input.modelId)) return { allowed: true };
85
+ for (const command of input.commands) {
86
+ for (const block of BLOCK_PATTERNS) {
87
+ if (block.pattern.test(command)) {
88
+ return { allowed: false, reason: block.id, message: COMPOSER_BASH_POLICY_ERROR };
89
+ }
90
+ }
91
+ if (!isAllowedComposerTerminalCommand(command)) {
92
+ return { allowed: false, reason: "not-allowlisted", message: COMPOSER_BASH_POLICY_ERROR };
93
+ }
94
+ }
95
+ return { allowed: true };
96
+ }
@@ -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 { validatePublicHttpUrl, 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
  */
@@ -738,6 +789,21 @@ async function renderUrl(
738
789
 
739
790
  // Step 0: Normalize URL (ensure scheme for special handlers)
740
791
  url = normalizeUrl(url);
792
+ const publicUrl = await validatePublicHttpUrl(url);
793
+ if (!publicUrl.ok) {
794
+ notes.push(`Blocked URL fetch: target URL is not public HTTP(S): ${publicUrl.reason}`);
795
+ return {
796
+ url,
797
+ finalUrl: url,
798
+ contentType: "unknown",
799
+ method: "failed",
800
+ content: "",
801
+ fetchedAt,
802
+ truncated: false,
803
+ notes,
804
+ };
805
+ }
806
+ url = publicUrl.url.toString();
741
807
 
742
808
  // Step 1: Try special handlers for known sites (unless raw mode)
743
809
  if (!raw) {
@@ -751,6 +817,20 @@ async function renderUrl(
751
817
  throw new ToolAbortError();
752
818
  }
753
819
  if (!response.ok) {
820
+ const failureNote =
821
+ response.error ?? (response.status ? `Failed to fetch URL (HTTP ${response.status})` : "Failed to fetch URL");
822
+ notes.push(failureNote);
823
+ const insane = await tryInsaneFallback({
824
+ url,
825
+ finalUrl: response.finalUrl || url,
826
+ timeout,
827
+ raw,
828
+ settings,
829
+ signal,
830
+ fetchedAt,
831
+ notes,
832
+ });
833
+ if (insane) return insane;
754
834
  return {
755
835
  url,
756
836
  finalUrl: response.finalUrl || url,
@@ -759,7 +839,7 @@ async function renderUrl(
759
839
  content: "",
760
840
  fetchedAt,
761
841
  truncated: false,
762
- notes: [response.status ? `Failed to fetch URL (HTTP ${response.status})` : "Failed to fetch URL"],
842
+ notes,
763
843
  };
764
844
  }
765
845
 
@@ -1062,6 +1142,8 @@ async function renderUrl(
1062
1142
  const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, settings, signal, storage);
1063
1143
  if (!htmlResult.ok) {
1064
1144
  notes.push("html rendering failed (lynx/html2text unavailable)");
1145
+ const insane = await tryInsaneFallback({ url, finalUrl, timeout, raw, settings, signal, fetchedAt, notes });
1146
+ if (insane) return insane;
1065
1147
  const output = finalizeOutput(rawContent);
1066
1148
  return {
1067
1149
  url,
@@ -1122,6 +1204,17 @@ async function renderUrl(
1122
1204
  };
1123
1205
  }
1124
1206
 
1207
+ const insaneLowQuality = await tryInsaneFallback({
1208
+ url,
1209
+ finalUrl,
1210
+ timeout,
1211
+ raw,
1212
+ settings,
1213
+ signal,
1214
+ fetchedAt,
1215
+ notes,
1216
+ });
1217
+ if (insaneLowQuality) return insaneLowQuality;
1125
1218
  notes.push("Page appears to require JavaScript or is mostly navigation");
1126
1219
  }
1127
1220
 
@@ -1,5 +1,5 @@
1
1
  import type { AgentTelemetryConfig, AgentTool } from "@sayknow-cli/agent-core";
2
- import type { Model, ToolChoice } from "@sayknow-cli/ai";
2
+ import type { Model, ServiceTier, ToolChoice } from "@sayknow-cli/ai";
3
3
  import { $env, $flag, logger } from "@sayknow-cli/utils";
4
4
  import type { PromptTemplate } from "../config/prompt-templates";
5
5
  import type { Settings } from "../config/settings";
@@ -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";
@@ -220,6 +222,8 @@ export interface ToolSession {
220
222
  agentOutputManager?: AgentOutputManager;
221
223
  /** Settings instance for passing to subagents */
222
224
  settings: Settings;
225
+ /** Live service-tier intent of the parent session, inherited by `inherit` subagents. */
226
+ serviceTier?: ServiceTier;
223
227
  /** Plan mode state (if active) */
224
228
  getPlanModeState?: () => PlanModeState | undefined;
225
229
  /** Goal mode state (if active or paused) */
@@ -402,6 +406,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
402
406
  todo_write: s => new TodoWriteTool(s),
403
407
  web_search: s => new WebSearchTool(s),
404
408
  search_tool_bm25: SearchToolBm25Tool.createIf,
409
+ telegram_send: TelegramSendTool.createIf,
405
410
  write: s => new WriteTool(s),
406
411
  skill: SkillTool.createIf,
407
412
  goal: s => new GoalTool(s),