@sayknow-cli/coding-agent 0.3.0 → 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 (106) hide show
  1. package/dist/types/cli/notify-cli.d.ts +2 -0
  2. package/dist/types/config/settings-schema.d.ts +98 -3
  3. package/dist/types/defaults/skc-defaults.d.ts +2 -13
  4. package/dist/types/extensibility/shared-events.d.ts +1 -0
  5. package/dist/types/i18n/messages/en.d.ts +1 -0
  6. package/dist/types/lsp/types.d.ts +2 -0
  7. package/dist/types/modes/components/settings-defs.d.ts +1 -0
  8. package/dist/types/modes/theme/theme.d.ts +1 -1
  9. package/dist/types/notifications/attachment-registry.d.ts +17 -0
  10. package/dist/types/notifications/chat-adapters.d.ts +9 -0
  11. package/dist/types/notifications/config.d.ts +9 -1
  12. package/dist/types/notifications/engine.d.ts +59 -0
  13. package/dist/types/notifications/managed-daemon.d.ts +48 -0
  14. package/dist/types/notifications/telegram-daemon.d.ts +19 -0
  15. package/dist/types/notifications/threaded-inbound.d.ts +19 -0
  16. package/dist/types/notifications/threaded-render.d.ts +6 -1
  17. package/dist/types/session/agent-session.d.ts +2 -0
  18. package/dist/types/skc-runtime/ralplan-runtime.d.ts +1 -1
  19. package/dist/types/tools/fetch.d.ts +23 -0
  20. package/dist/types/tools/index.d.ts +1 -0
  21. package/dist/types/tools/telegram-send.d.ts +32 -0
  22. package/dist/types/web/insane/bridge.d.ts +103 -0
  23. package/dist/types/web/insane/url-guard.d.ts +22 -0
  24. package/dist/types/web/search/provider.d.ts +18 -1
  25. package/dist/types/web/search/providers/insane.d.ts +53 -0
  26. package/dist/types/web/search/providers/text-citations.d.ts +23 -0
  27. package/dist/types/web/search/types.d.ts +12 -4
  28. package/package.json +10 -8
  29. package/scripts/verify-insane-vendor.ts +132 -0
  30. package/src/cli/args.ts +1 -1
  31. package/src/cli/fast-help.ts +1 -1
  32. package/src/cli/notify-cli.ts +152 -5
  33. package/src/commands/team.ts +1 -1
  34. package/src/config/settings-schema.ts +119 -5
  35. package/src/defaults/skc/skills/ralplan/SKILL.md +11 -4
  36. package/src/defaults/skc-defaults.ts +2 -27
  37. package/src/extensibility/shared-events.ts +1 -0
  38. package/src/i18n/messages/de.ts +1 -0
  39. package/src/i18n/messages/en.ts +1 -0
  40. package/src/i18n/messages/es.ts +1 -0
  41. package/src/i18n/messages/fr.ts +1 -0
  42. package/src/i18n/messages/ja.ts +1 -0
  43. package/src/i18n/messages/ko.ts +1 -0
  44. package/src/i18n/messages/zh.ts +1 -0
  45. package/src/internal-urls/docs-index.generated.ts +9 -9
  46. package/src/lsp/config.ts +16 -3
  47. package/src/lsp/defaults.json +7 -0
  48. package/src/lsp/types.ts +2 -0
  49. package/src/modes/components/settings-defs.ts +2 -1
  50. package/src/modes/components/settings-selector.ts +7 -2
  51. package/src/modes/controllers/event-controller.ts +15 -0
  52. package/src/modes/interactive-mode.ts +46 -2
  53. package/src/modes/theme/theme.ts +4 -0
  54. package/src/modes/utils/context-usage.ts +2 -2
  55. package/src/notifications/attachment-registry.ts +23 -0
  56. package/src/notifications/chat-adapters.ts +147 -0
  57. package/src/notifications/config.ts +23 -2
  58. package/src/notifications/engine.ts +100 -0
  59. package/src/notifications/index.ts +180 -38
  60. package/src/notifications/managed-daemon.ts +163 -0
  61. package/src/notifications/telegram-daemon.ts +235 -14
  62. package/src/notifications/threaded-inbound.ts +60 -4
  63. package/src/notifications/threaded-render.ts +20 -2
  64. package/src/session/agent-session.ts +82 -51
  65. package/src/skc-runtime/launch-tmux.ts +17 -3
  66. package/src/skc-runtime/ledger-event-renderer.ts +1 -0
  67. package/src/skc-runtime/ralplan-runtime.ts +2 -2
  68. package/src/skc-runtime/workflow-manifest.generated.json +29 -0
  69. package/src/skc-runtime/workflow-manifest.ts +7 -2
  70. package/src/tools/fetch.ts +78 -1
  71. package/src/tools/index.ts +3 -0
  72. package/src/tools/telegram-send.ts +137 -0
  73. package/src/web/insane/bridge.ts +350 -0
  74. package/src/web/insane/url-guard.ts +155 -0
  75. package/src/web/search/provider.ts +77 -18
  76. package/src/web/search/providers/anthropic.ts +70 -3
  77. package/src/web/search/providers/codex.ts +1 -119
  78. package/src/web/search/providers/gemini.ts +99 -0
  79. package/src/web/search/providers/insane.ts +551 -0
  80. package/src/web/search/providers/openai-compatible.ts +66 -32
  81. package/src/web/search/providers/text-citations.ts +111 -0
  82. package/src/web/search/types.ts +13 -2
  83. package/vendor/insane-search/LICENSE +21 -0
  84. package/vendor/insane-search/MANIFEST.json +24 -0
  85. package/vendor/insane-search/engine/__init__.py +23 -0
  86. package/vendor/insane-search/engine/__main__.py +128 -0
  87. package/vendor/insane-search/engine/bias_check.py +183 -0
  88. package/vendor/insane-search/engine/executor.py +254 -0
  89. package/vendor/insane-search/engine/fetch_chain.py +725 -0
  90. package/vendor/insane-search/engine/learning.py +175 -0
  91. package/vendor/insane-search/engine/phase0.py +214 -0
  92. package/vendor/insane-search/engine/safety.py +91 -0
  93. package/vendor/insane-search/engine/templates/package.json +11 -0
  94. package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
  95. package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
  96. package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
  97. package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
  98. package/vendor/insane-search/engine/tests/test_u1.py +200 -0
  99. package/vendor/insane-search/engine/tests/test_u4.py +131 -0
  100. package/vendor/insane-search/engine/tests/test_u5.py +163 -0
  101. package/vendor/insane-search/engine/tests/test_u7.py +124 -0
  102. package/vendor/insane-search/engine/transport.py +211 -0
  103. package/vendor/insane-search/engine/url_transforms.py +98 -0
  104. package/vendor/insane-search/engine/validators.py +331 -0
  105. package/vendor/insane-search/engine/waf_detector.py +214 -0
  106. package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
@@ -286,6 +286,8 @@ import { ToolChoiceQueue } from "./tool-choice-queue";
286
286
  import { YieldQueue } from "./yield-queue";
287
287
 
288
288
  /** Session-specific events that extend the core AgentEvent */
289
+ export type AutoCompactionContinuationSkipReason = "auto_continue_disabled_non_resumable_tail";
290
+
289
291
  export type AgentSessionEvent =
290
292
  | AgentEvent
291
293
  | { type: "auto_compaction_start"; reason: "threshold" | "overflow" | "idle"; action: "context-full" | "handoff" }
@@ -298,6 +300,7 @@ export type AgentSessionEvent =
298
300
  errorMessage?: string;
299
301
  /** True when compaction was skipped for a benign reason (no model, no candidates, nothing to compact). */
300
302
  skipped?: boolean;
303
+ continuationSkipReason?: AutoCompactionContinuationSkipReason;
301
304
  }
302
305
  | {
303
306
  type: "auto_retry_start";
@@ -1869,7 +1872,6 @@ export class AgentSession {
1869
1872
  this.#resolveTtsrResume();
1870
1873
  return;
1871
1874
  }
1872
- this.#ttsrAbortPending = false;
1873
1875
  this.#perToolTtsrInjections.clear();
1874
1876
  const ttsrSettings = this.#ttsrManager?.getSettings();
1875
1877
  if (ttsrSettings?.contextMode === "discard" && targetAssistantIndex !== -1) {
@@ -1898,11 +1900,22 @@ export class AgentSession {
1898
1900
  );
1899
1901
  this.#markTtsrInjected(details.rules);
1900
1902
  }
1901
- try {
1902
- await this.agent.continue();
1903
- } catch {
1904
- this.#resolveTtsrResume();
1905
- }
1903
+ await this.#scheduleAgentContinue({
1904
+ delayMs: 0,
1905
+ generation,
1906
+ shouldContinue: () => {
1907
+ this.#ttsrAbortPending = false;
1908
+ return true;
1909
+ },
1910
+ onSkip: () => {
1911
+ this.#ttsrAbortPending = false;
1912
+ this.#resolveTtsrResume();
1913
+ },
1914
+ onError: () => {
1915
+ this.#ttsrAbortPending = false;
1916
+ this.#resolveTtsrResume();
1917
+ },
1918
+ });
1906
1919
  },
1907
1920
  { delayMs: 50 },
1908
1921
  );
@@ -2214,7 +2227,7 @@ export class AgentSession {
2214
2227
  #schedulePostPromptTask(
2215
2228
  task: (signal: AbortSignal) => Promise<void>,
2216
2229
  options?: { delayMs?: number; generation?: number; onSkip?: () => void },
2217
- ): void {
2230
+ ): Promise<void> {
2218
2231
  const delayMs = options?.delayMs ?? 0;
2219
2232
  const signal = this.#postPromptTasksAbortController.signal;
2220
2233
  const scheduled = (async () => {
@@ -2236,18 +2249,20 @@ export class AgentSession {
2236
2249
  await task(signal);
2237
2250
  })();
2238
2251
  this.#trackPostPromptTask(scheduled);
2252
+ return scheduled;
2239
2253
  }
2240
2254
 
2241
2255
  #scheduleAgentContinue(options?: {
2242
2256
  delayMs?: number;
2243
2257
  generation?: number;
2258
+ skipCompactionCheck?: boolean;
2244
2259
  shouldContinue?: () => boolean;
2245
2260
  onSkip?: (reason: "generation_changed" | "aborted_signal" | "queue_drained") => void;
2246
2261
  onError?: (error: unknown) => void;
2247
- }): void {
2262
+ }): Promise<void> {
2248
2263
  const scheduledGeneration = options?.generation;
2249
2264
  const signal = this.#postPromptTasksAbortController.signal;
2250
- this.#schedulePostPromptTask(
2265
+ return this.#schedulePostPromptTask(
2251
2266
  async () => {
2252
2267
  if (signal.aborted) {
2253
2268
  options?.onSkip?.("aborted_signal");
@@ -2263,6 +2278,9 @@ export class AgentSession {
2263
2278
  }
2264
2279
  try {
2265
2280
  await this.#maybeRestoreRetryFallbackPrimary();
2281
+ if (!options?.skipCompactionCheck) {
2282
+ await this.#checkEstimatedContextBeforePrompt();
2283
+ }
2266
2284
  await this.agent.continue();
2267
2285
  } catch (error) {
2268
2286
  logger.warn("agent.continue failed after scheduling", {
@@ -2307,6 +2325,34 @@ export class AgentSession {
2307
2325
  }
2308
2326
  }
2309
2327
 
2328
+ #detectOverflowRetryContinuationSkip(): AutoCompactionContinuationSkipReason | undefined {
2329
+ this.#stripOverflowFailedTurnForRetry();
2330
+ if (this.#isResumableAgentTail()) return undefined;
2331
+ const compactionSettings = this.settings.getGroup("compaction");
2332
+ return compactionSettings.autoContinue === false ? "auto_continue_disabled_non_resumable_tail" : undefined;
2333
+ }
2334
+
2335
+ #scheduleOverflowRetryContinuation(generation: number): void {
2336
+ this.#stripOverflowFailedTurnForRetry();
2337
+ if (this.#isResumableAgentTail()) {
2338
+ this.#scheduleAgentContinue({
2339
+ delayMs: 100,
2340
+ generation,
2341
+ onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
2342
+ onError: error => this.#logCompactionContinuationError("overflow_retry", error),
2343
+ });
2344
+ return;
2345
+ }
2346
+
2347
+ const compactionSettings = this.settings.getGroup("compaction");
2348
+ if (compactionSettings.autoContinue !== false) {
2349
+ this.#scheduleAutoContinuePrompt(generation);
2350
+ return;
2351
+ }
2352
+
2353
+ this.#logCompactionContinuationSkipped("overflow_retry", "auto_continue_disabled_non_resumable_tail");
2354
+ }
2355
+
2310
2356
  #scheduleAutoContinuePrompt(generation: number): void {
2311
2357
  const continuePrompt = async () => {
2312
2358
  await this.#promptWithMessage(
@@ -3052,6 +3098,7 @@ export class AgentSession {
3052
3098
  willRetry: event.willRetry,
3053
3099
  errorMessage: event.errorMessage,
3054
3100
  skipped: event.skipped,
3101
+ continuationSkipReason: event.continuationSkipReason,
3055
3102
  });
3056
3103
  } else if (event.type === "auto_retry_start") {
3057
3104
  await this.#extensionRunner.emit({
@@ -6722,6 +6769,8 @@ export class AgentSession {
6722
6769
  const compactionSettings = this.settings.getGroup("compaction");
6723
6770
  if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
6724
6771
  await this.#runAutoCompaction("overflow", true);
6772
+ } else {
6773
+ this.#scheduleOverflowRetryContinuation(generation);
6725
6774
  }
6726
6775
  return;
6727
6776
  }
@@ -6732,17 +6781,19 @@ export class AgentSession {
6732
6781
  // Skip if this was an error (non-overflow errors don't have usage data)
6733
6782
  if (assistantMessage.stopReason === "error") return;
6734
6783
  let contextTokens = calculateContextTokens(assistantMessage.usage);
6735
- const maxOutputTokens = this.model?.maxTokens ?? 0;
6784
+ // Model maxTokens is a capability ceiling, not a per-turn reservation.
6785
+ // Auto maintenance should track actual context fullness.
6786
+ const autoCompactionOutputReserveTokens = 0;
6736
6787
  // Cache-epoch invariant: pruning rewrites already-sent toolResult history,
6737
6788
  // which breaks the provider prompt-cache prefix mid-epoch. Only prune at a
6738
6789
  // sanctioned maintenance boundary, i.e. when the un-pruned context already
6739
6790
  // crosses the compaction threshold. Pruning may then avert full compaction.
6740
- if (!shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) return;
6791
+ if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) return;
6741
6792
  const pruneResult = await this.#pruneToolOutputs();
6742
6793
  if (pruneResult) {
6743
6794
  contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
6744
6795
  }
6745
- if (shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) {
6796
+ if (shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
6746
6797
  // Try promotion first — if a larger model is available, switch instead of compacting
6747
6798
  const promoted = await this.#tryContextPromotion(assistantMessage);
6748
6799
  if (!promoted) {
@@ -6820,14 +6871,16 @@ export class AgentSession {
6820
6871
  if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
6821
6872
 
6822
6873
  let contextTokens = this.#estimateContextTokensForCompaction(pendingMessages).tokens;
6823
- const maxOutputTokens = model.maxTokens ?? 0;
6824
- if (!shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) return;
6874
+ // Model maxTokens is a capability ceiling, not a per-turn reservation.
6875
+ // Auto maintenance should track actual context fullness.
6876
+ const autoCompactionOutputReserveTokens = 0;
6877
+ if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) return;
6825
6878
 
6826
6879
  const pruneResult = await this.#pruneToolOutputs();
6827
6880
  if (pruneResult) {
6828
6881
  contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
6829
6882
  }
6830
- if (shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) {
6883
+ if (shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
6831
6884
  await this.#runAutoCompaction("threshold", false, false, {
6832
6885
  continueAfterMaintenance: false,
6833
6886
  deferHandoffMaintenance: false,
@@ -7730,32 +7783,18 @@ export class AgentSession {
7730
7783
 
7731
7784
  const preparation = prepareCompaction(pathEntries, compactionSettings);
7732
7785
  if (!preparation) {
7786
+ const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
7733
7787
  await this.#emitSessionEvent({
7734
7788
  type: "auto_compaction_end",
7735
7789
  action,
7736
7790
  result: undefined,
7737
7791
  aborted: false,
7738
- willRetry: false,
7792
+ willRetry: willRetry && !continuationSkipReason,
7739
7793
  skipped: true,
7794
+ continuationSkipReason,
7740
7795
  });
7741
7796
  if (willRetry) {
7742
- this.#stripOverflowFailedTurnForRetry();
7743
- if (this.#isResumableAgentTail()) {
7744
- this.#scheduleAgentContinue({
7745
- delayMs: 100,
7746
- generation,
7747
- onSkip: skipReason => this.#logCompactionContinuationSkipped("overflow_retry", skipReason),
7748
- onError: error => this.#logCompactionContinuationError("overflow_retry", error),
7749
- });
7750
- } else {
7751
- const tail = this.agent.state.messages.at(-1) as AssistantMessage | undefined;
7752
- logger.warn("Auto-compaction continuation skipped", {
7753
- source: "overflow_retry",
7754
- reason: "not_resumable_tail",
7755
- role: tail?.role,
7756
- stopReason: tail?.stopReason,
7757
- });
7758
- }
7797
+ this.#scheduleOverflowRetryContinuation(generation);
7759
7798
  } else if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
7760
7799
  this.#scheduleAgentContinue({
7761
7800
  delayMs: 100,
@@ -7966,26 +8005,18 @@ export class AgentSession {
7966
8005
  details,
7967
8006
  preserveData,
7968
8007
  };
7969
- await this.#emitSessionEvent({ type: "auto_compaction_end", action, result, aborted: false, willRetry });
8008
+ const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
8009
+ await this.#emitSessionEvent({
8010
+ type: "auto_compaction_end",
8011
+ action,
8012
+ result,
8013
+ aborted: false,
8014
+ willRetry: willRetry && !continuationSkipReason,
8015
+ continuationSkipReason,
8016
+ });
7970
8017
 
7971
8018
  if (willRetry) {
7972
- this.#stripOverflowFailedTurnForRetry();
7973
- if (!this.#isResumableAgentTail()) {
7974
- const tail = this.agent.state.messages.at(-1) as AssistantMessage | undefined;
7975
- logger.warn("Auto-compaction continuation skipped", {
7976
- source: "overflow_retry",
7977
- reason: "not_resumable_tail",
7978
- role: tail?.role,
7979
- stopReason: tail?.stopReason,
7980
- });
7981
- } else {
7982
- this.#scheduleAgentContinue({
7983
- delayMs: 100,
7984
- generation,
7985
- onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
7986
- onError: error => this.#logCompactionContinuationError("overflow_retry", error),
7987
- });
7988
- }
8019
+ this.#scheduleOverflowRetryContinuation(generation);
7989
8020
  } else if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
7990
8021
  // Auto-compaction can complete while follow-up/steering/custom messages are waiting.
7991
8022
  // Kick the loop so queued messages are actually delivered.
@@ -93,6 +93,10 @@ function hasCurrentSkcVersion(session: SkcTmuxSessionStatus | undefined): boolea
93
93
  return session?.version === VERSION;
94
94
  }
95
95
 
96
+ function allowsExistingTmuxAttach(parsed: Args, env: NodeJS.ProcessEnv): boolean {
97
+ return Boolean(parsed.continue || parsed.resume || explicitTmuxSessionName(env));
98
+ }
99
+
96
100
  function findExistingSessionForLaunch(context: {
97
101
  env: NodeJS.ProcessEnv;
98
102
  project: string;
@@ -352,6 +356,11 @@ function readCurrentBranch(cwd: string): string | null {
352
356
  function cleanupCreatedTmuxSession(plan: TmuxLaunchPlan, spawnSync: TmuxSpawnSync, options: TmuxSpawnOptions): void {
353
357
  spawnSync(plan.tmuxCommand, ["kill-session", "-t", `=${plan.sessionName}`], options);
354
358
  }
359
+ function isTmuxAttachDisconnectError(result: TmuxSpawnResult): boolean {
360
+ if (result.signalCode === "SIGHUP") return true;
361
+ const stderr = result.stderr?.toLowerCase() ?? "";
362
+ return stderr.includes("eio") || stderr.includes("input/output error");
363
+ }
355
364
 
356
365
  export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaunchPlan | undefined {
357
366
  const env = context.env ?? process.env;
@@ -377,14 +386,15 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
377
386
  tmuxRuntimeSessionPath(cwd, skcSessionId, buildSkcTmuxSessionSlug(sessionName));
378
387
  const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
379
388
  if (!tmuxAvailable) return undefined;
380
- const existingSessionName =
381
- "existingBranchSessionName" in context
389
+ const existingSessionName = allowsExistingTmuxAttach(context.parsed, env)
390
+ ? "existingBranchSessionName" in context
382
391
  ? (context.existingBranchSessionName ?? undefined)
383
392
  : findExistingSessionForLaunch({
384
393
  env,
385
394
  project,
386
395
  branch,
387
- });
396
+ })
397
+ : undefined;
388
398
  const innerCommand = buildInnerCommand(
389
399
  {
390
400
  cwd,
@@ -478,6 +488,10 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
478
488
  if (created.exitCode !== 0) return false;
479
489
  const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.sessionName}`], options);
480
490
  if (attached.exitCode === 0) return true;
491
+ if (isTmuxAttachDisconnectError(attached)) {
492
+ (context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr));
493
+ return true;
494
+ }
481
495
  cleanupCreatedTmuxSession(plan, spawnSync, options);
482
496
  (context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
483
497
  return true;
@@ -93,6 +93,7 @@ const RALPLAN_STAGE_CODES: Record<string, string> = {
93
93
  architect: "A",
94
94
  critic: "C",
95
95
  adr: "D",
96
+ "post-interview": "I",
96
97
  final: "F",
97
98
  };
98
99
 
@@ -37,7 +37,7 @@ import {
37
37
  *
38
38
  * 2. **Artifact write**: `skc ralplan --write --stage <type> --stage_n <N> --artifact
39
39
  * <path-or-string> [--run-id <id>] [--session-id <id>] [--json]` persists Planner / Architect
40
- * / Critic / revision / ADR / final markdown under `.skc/plans/ralplan/<run-id>/`, maintains
40
+ * / Critic / revision / post-interview / ADR / final markdown under `.skc/plans/ralplan/<run-id>/`, maintains
41
41
  * an `index.jsonl` audit log, copies `final` stages to `pending-approval.md`, and advances
42
42
  * the HUD chip to reflect the latest persisted stage.
43
43
  */
@@ -48,7 +48,7 @@ export interface RalplanCommandResult {
48
48
  stderr?: string;
49
49
  }
50
50
 
51
- const KNOWN_STAGES = ["planner", "architect", "critic", "revision", "adr", "final"] as const;
51
+ const KNOWN_STAGES = ["planner", "architect", "critic", "revision", "post-interview", "adr", "final"] as const;
52
52
  type RalplanStage = (typeof KNOWN_STAGES)[number];
53
53
 
54
54
  const KNOWN_ARCHITECT_KINDS = new Set(["openai-code"]);
@@ -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
  ],
@@ -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"] },
@@ -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
 
@@ -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";
@@ -402,6 +404,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
402
404
  todo_write: s => new TodoWriteTool(s),
403
405
  web_search: s => new WebSearchTool(s),
404
406
  search_tool_bm25: SearchToolBm25Tool.createIf,
407
+ telegram_send: TelegramSendTool.createIf,
405
408
  write: s => new WriteTool(s),
406
409
  skill: SkillTool.createIf,
407
410
  goal: s => new GoalTool(s),