@sayknow-cli/coding-agent 0.3.7 → 0.3.9

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 (272) hide show
  1. package/dist/types/append-only-mode.d.ts +31 -0
  2. package/dist/types/capability/rule.d.ts +8 -0
  3. package/dist/types/cli/completion-cli.d.ts +41 -0
  4. package/dist/types/cli/config-cli.d.ts +1 -0
  5. package/dist/types/cli/fixture-report.d.ts +20 -0
  6. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  7. package/dist/types/cli/nofile-limit.d.ts +11 -0
  8. package/dist/types/cli/notify-cli.d.ts +7 -0
  9. package/dist/types/cli/update-cli.d.ts +22 -0
  10. package/dist/types/cli.d.ts +124 -1
  11. package/dist/types/commands/completion.d.ts +29 -0
  12. package/dist/types/commands/config.d.ts +3 -0
  13. package/dist/types/commands/deep-interview.d.ts +3 -0
  14. package/dist/types/config/file-lock.d.ts +7 -6
  15. package/dist/types/config/keybindings.d.ts +2 -2
  16. package/dist/types/config/model-profile-activation.d.ts +6 -0
  17. package/dist/types/config/model-profiles.d.ts +1 -0
  18. package/dist/types/config/settings-schema.d.ts +119 -1
  19. package/dist/types/config/settings.d.ts +8 -0
  20. package/dist/types/discovery/index.d.ts +1 -0
  21. package/dist/types/export/html/template.generated.d.ts +1 -1
  22. package/dist/types/export/ttsr.d.ts +14 -3
  23. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  24. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  25. package/dist/types/goals/runtime.d.ts +1 -0
  26. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  27. package/dist/types/main.d.ts +4 -1
  28. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  29. package/dist/types/modes/components/model-selector.d.ts +4 -0
  30. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  31. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  32. package/dist/types/modes/components/status-line.d.ts +9 -0
  33. package/dist/types/modes/components/thinking-selector.d.ts +2 -2
  34. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  35. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  36. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  37. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  38. package/dist/types/modes/interactive-mode.d.ts +4 -0
  39. package/dist/types/modes/print-mode.d.ts +13 -1
  40. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  41. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
  42. package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
  43. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  44. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  45. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  46. package/dist/types/modes/types.d.ts +3 -0
  47. package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
  48. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  49. package/dist/types/notifications/config-commands.d.ts +9 -0
  50. package/dist/types/notifications/config.d.ts +30 -1
  51. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  52. package/dist/types/notifications/index.d.ts +8 -3
  53. package/dist/types/notifications/lifecycle-commands.d.ts +12 -3
  54. package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
  55. package/dist/types/notifications/reply-sent-store.d.ts +53 -0
  56. package/dist/types/notifications/rich-draft.d.ts +68 -0
  57. package/dist/types/notifications/rich-render.d.ts +90 -0
  58. package/dist/types/notifications/telegram-daemon.d.ts +45 -3
  59. package/dist/types/notifications/telegram-reference.d.ts +7 -5
  60. package/dist/types/notifications/threaded-render.d.ts +14 -0
  61. package/dist/types/runtime-api-key.d.ts +7 -0
  62. package/dist/types/sdk.d.ts +3 -0
  63. package/dist/types/session/agent-session.d.ts +61 -6
  64. package/dist/types/session/agent-storage.d.ts +1 -0
  65. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  66. package/dist/types/session/messages.d.ts +4 -0
  67. package/dist/types/session/session-manager.d.ts +17 -1
  68. package/dist/types/skc-runtime/deep-interview-ambiguity.d.ts +39 -0
  69. package/dist/types/skc-runtime/deep-interview-recorder.d.ts +10 -1
  70. package/dist/types/skc-runtime/deep-interview-state.d.ts +17 -2
  71. package/dist/types/skc-runtime/session-state-sidecar.d.ts +7 -0
  72. package/dist/types/skc-runtime/state-writer.d.ts +3 -0
  73. package/dist/types/skc-runtime/tmux-common.d.ts +1 -0
  74. package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  75. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +120 -13
  76. package/dist/types/skill-state/active-state.d.ts +18 -1
  77. package/dist/types/system-prompt.d.ts +18 -2
  78. package/dist/types/task/executor.d.ts +3 -0
  79. package/dist/types/task/fork-context-budget.d.ts +7 -0
  80. package/dist/types/task/token-log.d.ts +30 -0
  81. package/dist/types/task/token-log.test.d.ts +1 -0
  82. package/dist/types/tools/bisect.d.ts +81 -0
  83. package/dist/types/tools/computer.d.ts +2 -2
  84. package/dist/types/tools/index.d.ts +1 -0
  85. package/dist/types/tools/search.d.ts +2 -2
  86. package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
  87. package/dist/types/utils/file-mentions.d.ts +14 -0
  88. package/dist/types/utils/git.d.ts +32 -0
  89. package/dist/types/utils/pasted-image-path.d.ts +30 -0
  90. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  91. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  92. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  93. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  94. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  95. package/package.json +8 -8
  96. package/src/append-only-mode.ts +58 -0
  97. package/src/autoresearch/prompt.md +1 -1
  98. package/src/autoresearch/tools/log-experiment.ts +1 -1
  99. package/src/capability/rule.ts +9 -0
  100. package/src/cli/completion-cli.ts +234 -0
  101. package/src/cli/config-cli.ts +61 -9
  102. package/src/cli/fast-help.ts +1 -0
  103. package/src/cli/fixture-report.test.ts +230 -0
  104. package/src/cli/fixture-report.ts +171 -0
  105. package/src/cli/nofile-limit.ts +48 -0
  106. package/src/cli/notify-cli.ts +103 -17
  107. package/src/cli/telegram-cli.ts +1 -1
  108. package/src/cli/update-cli.ts +148 -33
  109. package/src/cli.ts +29 -1
  110. package/src/commands/completion.ts +84 -0
  111. package/src/commands/config.ts +2 -0
  112. package/src/commands/contribution-prep.ts +1 -1
  113. package/src/commands/deep-interview.ts +2 -1
  114. package/src/config/file-lock.ts +73 -34
  115. package/src/config/keybindings.ts +3 -3
  116. package/src/config/model-profile-activation.ts +65 -0
  117. package/src/config/model-profiles.ts +31 -2
  118. package/src/config/settings-schema.ts +76 -1
  119. package/src/config/settings.ts +22 -0
  120. package/src/defaults/skc/skills/team/SKILL.md +17 -14
  121. package/src/defaults/skc/skills/ultragoal/SKILL.md +59 -4
  122. package/src/defaults/skc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  123. package/src/defaults/skc-defaults.ts +9 -0
  124. package/src/discovery/helpers.ts +10 -0
  125. package/src/discovery/index.ts +1 -0
  126. package/src/export/html/template.generated.ts +1 -1
  127. package/src/export/html/template.js +60 -2
  128. package/src/export/ttsr.ts +96 -18
  129. package/src/extensibility/extensions/runner.ts +183 -219
  130. package/src/extensibility/extensions/types.ts +9 -0
  131. package/src/extensibility/skc-plugins/runtime-adapters.ts +156 -44
  132. package/src/goals/runtime.ts +10 -2
  133. package/src/hindsight/backend.ts +1 -3
  134. package/src/hooks/native-skill-hook.ts +124 -4
  135. package/src/internal-urls/docs-index.generated.ts +17 -8
  136. package/src/lsp/client.ts +1 -0
  137. package/src/main.ts +95 -43
  138. package/src/memories/index.ts +12 -1
  139. package/src/modes/components/assistant-message.ts +241 -82
  140. package/src/modes/components/custom-editor.ts +1 -0
  141. package/src/modes/components/footer.ts +7 -2
  142. package/src/modes/components/model-selector.ts +54 -11
  143. package/src/modes/components/queued-message-selector.ts +92 -0
  144. package/src/modes/components/settings-selector.ts +4 -0
  145. package/src/modes/components/status-line.ts +167 -54
  146. package/src/modes/components/thinking-selector.ts +10 -8
  147. package/src/modes/components/tool-execution.ts +44 -19
  148. package/src/modes/components/welcome.ts +181 -44
  149. package/src/modes/controllers/command-controller.ts +40 -4
  150. package/src/modes/controllers/event-controller.ts +77 -49
  151. package/src/modes/controllers/extension-ui-controller.ts +12 -11
  152. package/src/modes/controllers/input-controller.ts +192 -35
  153. package/src/modes/controllers/selector-controller.ts +168 -24
  154. package/src/modes/interactive-mode.ts +56 -16
  155. package/src/modes/print-mode.ts +43 -4
  156. package/src/modes/prompt-action-autocomplete.ts +21 -20
  157. package/src/modes/rpc/rpc-mode.ts +32 -12
  158. package/src/modes/rpc/rpc-socket-security.ts +13 -1
  159. package/src/modes/rpc/rpc-types.ts +9 -0
  160. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  161. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  162. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  163. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  164. package/src/modes/types.ts +3 -0
  165. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  166. package/src/modes/utils/injected-user-submission.ts +94 -0
  167. package/src/modes/utils/ui-helpers.ts +20 -7
  168. package/src/notifications/config-commands.ts +20 -0
  169. package/src/notifications/config.ts +52 -5
  170. package/src/notifications/daemon-paths.ts +2 -0
  171. package/src/notifications/index.ts +144 -18
  172. package/src/notifications/lifecycle-commands.ts +74 -23
  173. package/src/notifications/lifecycle-control-runtime.ts +8 -3
  174. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  175. package/src/notifications/reply-sent-store.ts +134 -0
  176. package/src/notifications/rich-draft.ts +107 -0
  177. package/src/notifications/rich-render.ts +142 -0
  178. package/src/notifications/telegram-daemon-control.ts +5 -3
  179. package/src/notifications/telegram-daemon.ts +606 -98
  180. package/src/notifications/telegram-reference.ts +17 -16
  181. package/src/notifications/threaded-render.ts +45 -2
  182. package/src/prompts/agents/architect.md +19 -10
  183. package/src/prompts/agents/critic.md +16 -18
  184. package/src/prompts/agents/executor.md +14 -11
  185. package/src/prompts/agents/planner.md +18 -13
  186. package/src/prompts/ci-green-request.md +3 -8
  187. package/src/prompts/goals/goal-continuation.md +0 -5
  188. package/src/prompts/goals/goal-mode-active.md +0 -5
  189. package/src/prompts/system/agent-creation-architect.md +2 -2
  190. package/src/prompts/system/plan-mode-active.md +1 -1
  191. package/src/prompts/system/plan-mode-subagent.md +3 -3
  192. package/src/prompts/system/project-prompt.md +0 -13
  193. package/src/prompts/system/subagent-system-prompt.md +3 -2
  194. package/src/prompts/system/system-prompt.md +29 -17
  195. package/src/prompts/system/volatile-project-context.md +10 -0
  196. package/src/prompts/system/web-search.md +4 -7
  197. package/src/prompts/tools/apply-patch.md +6 -0
  198. package/src/prompts/tools/ast-edit.md +1 -0
  199. package/src/prompts/tools/bash.md +1 -1
  200. package/src/prompts/tools/bisect.md +22 -0
  201. package/src/prompts/tools/browser.md +1 -1
  202. package/src/prompts/tools/computer.md +2 -2
  203. package/src/prompts/tools/cron.md +0 -2
  204. package/src/prompts/tools/eval.md +2 -2
  205. package/src/prompts/tools/find.md +1 -1
  206. package/src/prompts/tools/github.md +6 -6
  207. package/src/prompts/tools/goal.md +1 -3
  208. package/src/prompts/tools/image-gen.md +12 -5
  209. package/src/prompts/tools/irc.md +5 -4
  210. package/src/prompts/tools/job.md +2 -2
  211. package/src/prompts/tools/lsp.md +1 -1
  212. package/src/prompts/tools/monitor.md +2 -2
  213. package/src/prompts/tools/patch.md +2 -2
  214. package/src/prompts/tools/replace.md +2 -21
  215. package/src/prompts/tools/rewind.md +1 -1
  216. package/src/prompts/tools/search-tool-bm25.md +6 -11
  217. package/src/prompts/tools/search.md +1 -1
  218. package/src/prompts/tools/skill.md +3 -2
  219. package/src/prompts/tools/ssh.md +2 -1
  220. package/src/prompts/tools/subagent.md +2 -2
  221. package/src/prompts/tools/task.md +4 -5
  222. package/src/prompts/tools/todo-write.md +1 -1
  223. package/src/prompts/tools/vim.md +1 -0
  224. package/src/prompts/tools/web-search.md +2 -0
  225. package/src/prompts/tools/write.md +8 -2
  226. package/src/runtime-api-key.ts +16 -0
  227. package/src/sdk.ts +88 -36
  228. package/src/session/agent-session.ts +1013 -141
  229. package/src/session/agent-storage.ts +4 -0
  230. package/src/session/file-mention-pruning.ts +64 -0
  231. package/src/session/messages.ts +4 -0
  232. package/src/session/session-manager.ts +103 -4
  233. package/src/skc-runtime/deep-interview-ambiguity.ts +206 -0
  234. package/src/skc-runtime/deep-interview-recorder.ts +62 -8
  235. package/src/skc-runtime/deep-interview-state.ts +44 -2
  236. package/src/skc-runtime/session-state-sidecar.ts +263 -20
  237. package/src/skc-runtime/state-runtime.ts +12 -2
  238. package/src/skc-runtime/state-writer.ts +15 -0
  239. package/src/skc-runtime/tmux-common.ts +1 -0
  240. package/src/skc-runtime/ultragoal-guard.ts +55 -66
  241. package/src/skc-runtime/ultragoal-receipt-freshness.ts +315 -0
  242. package/src/skc-runtime/ultragoal-runtime.ts +1672 -159
  243. package/src/skc-runtime/workflow-manifest.generated.json +113 -2
  244. package/src/skc-runtime/workflow-manifest.ts +74 -1
  245. package/src/skill-state/active-state.ts +141 -5
  246. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  247. package/src/slash-commands/builtin-registry.ts +296 -72
  248. package/src/system-prompt.ts +57 -16
  249. package/src/task/executor.ts +132 -24
  250. package/src/task/fork-context-advisory.ts +2 -7
  251. package/src/task/fork-context-budget.ts +9 -0
  252. package/src/task/index.ts +13 -6
  253. package/src/task/output-manager.ts +20 -8
  254. package/src/task/receipt.ts +26 -7
  255. package/src/task/token-log.test.ts +188 -0
  256. package/src/task/token-log.ts +179 -0
  257. package/src/tools/bisect.ts +367 -0
  258. package/src/tools/computer.ts +246 -43
  259. package/src/tools/index.ts +3 -0
  260. package/src/tools/output-meta.ts +116 -4
  261. package/src/tools/search.ts +8 -2
  262. package/src/tools/skill.ts +15 -0
  263. package/src/tools/telegram-send.ts +6 -0
  264. package/src/tools/ultragoal-ask-guard.ts +39 -5
  265. package/src/utils/file-mentions.ts +39 -7
  266. package/src/utils/git.ts +73 -0
  267. package/src/utils/pasted-image-path.ts +170 -0
  268. package/src/web/scrapers/youtube.ts +24 -12
  269. package/src/workflow/workflow-intent-diff.ts +29 -6
  270. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  271. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  272. package/src/workflow/workflow-intent-report.ts +135 -0
@@ -10,6 +10,29 @@ const ULTRAGOAL_ASK_GUARD = Symbol.for("sayknow-cli.ultragoalAskGuard");
10
10
 
11
11
  type GuardedTool = AgentTool & { [ULTRAGOAL_ASK_GUARD]?: true };
12
12
 
13
+ export interface UltragoalAskGuardContext {
14
+ activeSkillState?: { skill?: string; session_id?: string } | null;
15
+ sessionId?: string | null;
16
+ }
17
+
18
+ const UPSTREAM_PLANNING_ASK_SKILLS = new Set(["deep-interview", "ralplan"]);
19
+
20
+ function normalizedActiveSkill(context?: UltragoalAskGuardContext): string | undefined {
21
+ const skill = context?.activeSkillState?.skill?.trim();
22
+ return skill || undefined;
23
+ }
24
+
25
+ function sessionScopedAskGuardId(
26
+ context: UltragoalAskGuardContext,
27
+ activeSkill: string | undefined,
28
+ ): string | undefined {
29
+ if (activeSkill !== "ultragoal" && !UPSTREAM_PLANNING_ASK_SKILLS.has(activeSkill ?? "")) return undefined;
30
+ const activeSessionId = context.activeSkillState?.session_id?.trim();
31
+ if (activeSessionId) return activeSessionId;
32
+ const sessionId = context.sessionId?.trim();
33
+ return sessionId || undefined;
34
+ }
35
+
13
36
  export function formatUltragoalAskBlockMessage(diagnostic: UltragoalAskBlockDiagnostic): string {
14
37
  return [
15
38
  diagnostic.message,
@@ -18,15 +41,26 @@ export function formatUltragoalAskBlockMessage(diagnostic: UltragoalAskBlockDiag
18
41
  ].join("\n");
19
42
  }
20
43
 
21
- export async function assertUltragoalAskAllowed(cwd: string): Promise<void> {
22
- const diagnostic = await isUltragoalAskBlocked(cwd);
44
+ export async function assertUltragoalAskAllowed(cwd: string, context: UltragoalAskGuardContext = {}): Promise<void> {
45
+ const activeSkill = normalizedActiveSkill(context);
46
+ // Deep-interview and ralplan are upstream planning workflows whose core gates
47
+ // are `ask` calls. Scope their Ultragoal check to the current session so stale
48
+ // or ambiguous Ultragoal durable state from another session cannot hijack those
49
+ // prompts; same-session active Ultragoal state still falls through to the
50
+ // blocker/nudge checks below.
51
+ const sessionId = sessionScopedAskGuardId(context, activeSkill);
52
+ const diagnostic = await isUltragoalAskBlocked(cwd, { sessionId });
23
53
  if (!diagnostic.active) return;
24
- const nudge = await consumeUltragoalAskNudge(cwd);
54
+ const nudge = await consumeUltragoalAskNudge(cwd, sessionId);
25
55
  if (nudge.nudged) throw new ToolError(nudge.message);
26
56
  throw new ToolError(formatUltragoalAskBlockMessage(diagnostic));
27
57
  }
28
58
 
29
- export function guardToolForUltragoalAsk<T extends AgentTool>(tool: T, getCwd: () => string): T {
59
+ export function guardToolForUltragoalAsk<T extends AgentTool>(
60
+ tool: T,
61
+ getCwd: () => string,
62
+ getContext: () => UltragoalAskGuardContext = () => ({}),
63
+ ): T {
30
64
  if (tool.name !== "ask") return tool;
31
65
  const candidate = tool as GuardedTool;
32
66
  if (candidate[ULTRAGOAL_ASK_GUARD]) return tool;
@@ -35,7 +69,7 @@ export function guardToolForUltragoalAsk<T extends AgentTool>(tool: T, getCwd: (
35
69
  if (prop === ULTRAGOAL_ASK_GUARD) return true;
36
70
  if (prop !== "execute") return Reflect.get(target, prop, receiver);
37
71
  return async (...args: unknown[]): Promise<unknown> => {
38
- await assertUltragoalAskAllowed(getCwd());
72
+ await assertUltragoalAskAllowed(getCwd(), getContext());
39
73
  return Reflect.apply(target.execute, target, args);
40
74
  };
41
75
  },
@@ -159,24 +159,32 @@ async function resolveMentionPath(
159
159
  return best?.candidate.path ?? null;
160
160
  }
161
161
 
162
- function buildTextOutput(textContent: string): { output: string; lineCount: number } {
162
+ /**
163
+ * Default inline cap for auto-read file mentions, deliberately below the
164
+ * read-tool cap (`DEFAULT_MAX_BYTES`, 50KB) so an incidental `@path` mention
165
+ * injects a smaller snippet than an explicit `read`. Overridable per call.
166
+ */
167
+ export const DEFAULT_FILE_MENTION_INLINE_BYTES = 20 * 1024;
168
+
169
+ function buildTextOutput(textContent: string, maxInlineBytes?: number): { output: string; lineCount: number } {
170
+ const cap = maxInlineBytes && maxInlineBytes > 0 ? maxInlineBytes : DEFAULT_FILE_MENTION_INLINE_BYTES;
163
171
  const allLines = textContent.split("\n");
164
172
  const totalFileLines = allLines.length;
165
- const truncation = truncateHead(textContent);
173
+ const truncation = truncateHead(textContent, { maxBytes: cap });
166
174
 
167
175
  if (truncation.firstLineExceedsLimit) {
168
176
  const firstLine = allLines[0] ?? "";
169
177
  const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
170
- const snippet = truncateHeadBytes(firstLine, DEFAULT_MAX_BYTES);
178
+ const snippet = truncateHeadBytes(firstLine, cap);
171
179
  let outputText = snippet.text;
172
180
 
173
181
  if (outputText.length > 0) {
174
182
  outputText += `\n\n[Line 1 is ${formatBytes(firstLineBytes)}, exceeds ${formatBytes(
175
- DEFAULT_MAX_BYTES,
183
+ cap,
176
184
  )} limit. Showing first ${formatBytes(snippet.bytes)} of the line.]`;
177
185
  } else {
178
186
  outputText = `[Line 1 is ${formatBytes(firstLineBytes)}, exceeds ${formatBytes(
179
- DEFAULT_MAX_BYTES,
187
+ cap,
180
188
  )} limit. Unable to display a valid UTF-8 snippet.]`;
181
189
  }
182
190
 
@@ -277,7 +285,18 @@ export function extractFileMentions(text: string): string[] {
277
285
  export async function generateFileMentionMessages(
278
286
  filePaths: string[],
279
287
  cwd: string,
280
- options?: { autoResizeImages?: boolean; useHashLines?: boolean },
288
+ options?: {
289
+ autoResizeImages?: boolean;
290
+ useHashLines?: boolean;
291
+ /** Inline byte cap for auto-read text. Default: DEFAULT_FILE_MENTION_INLINE_BYTES. */
292
+ maxInlineBytes?: number;
293
+ /**
294
+ * Resolved paths already shown (read or mentioned) recently in the
295
+ * conversation. A mention of one of these emits a compact duplicate note
296
+ * instead of re-injecting the full body.
297
+ */
298
+ recentlyShownPaths?: ReadonlySet<string>;
299
+ },
281
300
  ): Promise<AgentMessage[]> {
282
301
  if (filePaths.length === 0) return [];
283
302
 
@@ -290,12 +309,25 @@ export async function generateFileMentionMessages(
290
309
  return mentionCandidatesPromise;
291
310
  };
292
311
 
312
+ const shownInBatch = new Set<string>();
293
313
  for (const filePath of filePaths) {
294
314
  const resolvedPath = await resolveMentionPath(filePath, cwd, getMentionCandidates);
295
315
  if (!resolvedPath) {
296
316
  continue;
297
317
  }
298
318
  const absolutePath = resolveReadPath(resolvedPath, cwd);
319
+ // Duplicate suppression, keyed by absolute path: same path shown earlier this
320
+ // batch or recently in the conversation → compact note, no re-read (the path
321
+ // stays referenced so the model can re-fetch via the read tool).
322
+ if (shownInBatch.has(absolutePath) || options?.recentlyShownPaths?.has(absolutePath)) {
323
+ files.push({
324
+ path: resolvedPath,
325
+ content: `(already shown in this conversation; not re-read — use the read tool to fetch \`${resolvedPath}\` again)`,
326
+ duplicate: true,
327
+ });
328
+ continue;
329
+ }
330
+ shownInBatch.add(absolutePath);
299
331
  try {
300
332
  const stat = await Bun.file(absolutePath).stat();
301
333
  if (stat.isDirectory()) {
@@ -354,7 +386,7 @@ export async function generateFileMentionMessages(
354
386
  }
355
387
 
356
388
  const content = await Bun.file(absolutePath).text();
357
- let { output, lineCount } = buildTextOutput(content);
389
+ let { output, lineCount } = buildTextOutput(content, options?.maxInlineBytes);
358
390
  if (options?.useHashLines) {
359
391
  output = formatHashLines(output);
360
392
  }
package/src/utils/git.ts CHANGED
@@ -1317,6 +1317,79 @@ export async function clean(
1317
1317
  await runEffect(cwd, args, { signal: options.signal });
1318
1318
  }
1319
1319
 
1320
+ // ════════════════════════════════════════════════════════════════════════════
1321
+ // API: bisect
1322
+ // ════════════════════════════════════════════════════════════════════════════
1323
+
1324
+ export interface BisectCommitInfo {
1325
+ sha: string;
1326
+ author: string;
1327
+ date: string;
1328
+ subject: string;
1329
+ stat: string;
1330
+ }
1331
+
1332
+ /**
1333
+ * Thin wrappers over `git bisect`. The marking commands (`good`/`bad`/`skip`)
1334
+ * return the raw {@link GitCommandResult} without throwing so callers can both
1335
+ * detect a genuine failure (non-zero exit) and scan the output for the
1336
+ * "<sha> is the first bad commit" conclusion. `reset` is best-effort and never
1337
+ * throws so it is safe to call from a `finally` block to restore the worktree.
1338
+ */
1339
+ export const bisect = {
1340
+ /** Begin a bisect session (does not seed good/bad endpoints). */
1341
+ async start(cwd: string, signal?: AbortSignal): Promise<void> {
1342
+ await runEffect(cwd, ["bisect", "start"], { signal });
1343
+ },
1344
+
1345
+ /** Mark a revision (or current HEAD when omitted) good. Returns raw output. */
1346
+ async good(cwd: string, ref?: string, signal?: AbortSignal): Promise<GitCommandResult> {
1347
+ return runCommand(cwd, ref ? ["bisect", "good", ref] : ["bisect", "good"], { signal });
1348
+ },
1349
+
1350
+ /** Mark a revision (or current HEAD when omitted) bad. Returns raw output. */
1351
+ async bad(cwd: string, ref?: string, signal?: AbortSignal): Promise<GitCommandResult> {
1352
+ return runCommand(cwd, ref ? ["bisect", "bad", ref] : ["bisect", "bad"], { signal });
1353
+ },
1354
+
1355
+ /** Skip the current (untestable) revision. Returns raw output. */
1356
+ async skip(cwd: string, signal?: AbortSignal): Promise<GitCommandResult> {
1357
+ return runCommand(cwd, ["bisect", "skip"], { signal });
1358
+ },
1359
+
1360
+ /** Read the replayable bisect log for the current session. */
1361
+ async log(cwd: string, signal?: AbortSignal): Promise<string> {
1362
+ return runText(cwd, ["bisect", "log"], { signal });
1363
+ },
1364
+
1365
+ /** End the bisect session, restoring the original HEAD. Never throws. */
1366
+ async reset(cwd: string, signal?: AbortSignal): Promise<void> {
1367
+ await runCommand(cwd, ["bisect", "reset"], { signal });
1368
+ },
1369
+
1370
+ /** True when `ancestor` is a first-parent-or-merge ancestor of `descendant`. */
1371
+ async isAncestor(cwd: string, ancestor: string, descendant: string, signal?: AbortSignal): Promise<boolean> {
1372
+ const result = await runCommand(cwd, ["merge-base", "--is-ancestor", ancestor, descendant], {
1373
+ readOnly: true,
1374
+ signal,
1375
+ });
1376
+ return result.exitCode === 0;
1377
+ },
1378
+
1379
+ /** Resolve author/date/subject and a diffstat for a commit (for the receipt). */
1380
+ async describe(cwd: string, sha: string, signal?: AbortSignal): Promise<BisectCommitInfo> {
1381
+ // Newline-delimited so the format string carries no NUL byte (argv cannot
1382
+ // contain NUL); %s is already a single line, so the four fields never wrap.
1383
+ const meta = await runText(cwd, ["show", "-s", "--format=%H%n%an <%ae>%n%aI%n%s", sha], {
1384
+ readOnly: true,
1385
+ signal,
1386
+ });
1387
+ const [resolvedSha = sha, author = "", date = "", subject = ""] = meta.split("\n");
1388
+ const stat = await runText(cwd, ["show", "--stat", "--format=", sha], { readOnly: true, signal });
1389
+ return { sha: resolvedSha, author, date, subject, stat: stat.trim() };
1390
+ },
1391
+ };
1392
+
1320
1393
  // ════════════════════════════════════════════════════════════════════════════
1321
1394
  // API: ls
1322
1395
  // ════════════════════════════════════════════════════════════════════════════
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Resolve pasted editor text to an image file path.
3
+ *
4
+ * Terminals insert a shell-escaped filesystem path when a file is drag-dropped
5
+ * onto them (e.g. macOS `Screenshot\ 2026-07-07\ at\ 11.06.38 PM.png`, where
6
+ * the visible "space" before AM/PM is U+202F and stays unescaped). When the
7
+ * entire paste is a single path to an existing image file, the interactive
8
+ * editor attaches the image and inserts an `[image N]` placeholder instead of
9
+ * leaving the raw path in the prompt.
10
+ */
11
+ import * as fs from "node:fs";
12
+ import * as os from "node:os";
13
+ import * as path from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const IMAGE_FILE_EXTENSION_PATTERN = /\.(?:png|jpe?g|gif|webp)$/i;
17
+
18
+ export interface DecodePastedPathOptions {
19
+ /**
20
+ * Platform whose path semantics apply when decoding (shell unescaping,
21
+ * `file://` drive letters / UNC hosts). Defaults to `process.platform`;
22
+ * injectable so tests can pin the win32 contract from any host.
23
+ */
24
+ platform?: NodeJS.Platform;
25
+ /** Home directory for `~/` expansion. Defaults to `os.homedir()`. */
26
+ homedir?: string;
27
+ }
28
+
29
+ export interface ResolvePastedImagePathOptions extends DecodePastedPathOptions {
30
+ /** Base directory for relative paths. Defaults to `process.cwd()`. */
31
+ cwd?: string;
32
+ }
33
+
34
+ /**
35
+ * Convert a `file://` URI to a filesystem path for the given platform, or
36
+ * `undefined` when the URI is invalid.
37
+ *
38
+ * The production path (injected platform === host platform, always true
39
+ * outside tests) delegates to Node's `fileURLToPath`. When tests inject a
40
+ * foreign platform, a mirror of `fileURLToPath`'s rules is applied instead —
41
+ * Bun (1.3) accepts but ignores `fileURLToPath`'s `windows` option, so the
42
+ * native call cannot emulate a foreign platform.
43
+ */
44
+ function decodeFileUrl(candidate: string, platform: NodeJS.Platform): string | undefined {
45
+ let url: URL;
46
+ try {
47
+ url = new URL(candidate);
48
+ } catch {
49
+ return undefined;
50
+ }
51
+ if (url.protocol !== "file:") return undefined;
52
+
53
+ if (platform === process.platform) {
54
+ try {
55
+ return fileURLToPath(url);
56
+ } catch {
57
+ return undefined;
58
+ }
59
+ }
60
+
61
+ // Foreign-platform mirror of fileURLToPath (tests only).
62
+ if (/%2f/i.test(url.pathname) || (platform === "win32" && /%5c/i.test(url.pathname))) return undefined;
63
+ let pathname: string;
64
+ try {
65
+ pathname = decodeURIComponent(url.pathname);
66
+ } catch {
67
+ return undefined;
68
+ }
69
+ if (platform === "win32") {
70
+ if (url.hostname && url.hostname !== "localhost") {
71
+ // UNC: file://server/share/img.png -> \\server\share\img.png
72
+ return `\\\\${url.hostname}${pathname.replaceAll("/", "\\")}`;
73
+ }
74
+ // Drive letter required: file:///C:/x -> C:\x
75
+ if (!/^\/[A-Za-z]:/.test(pathname)) return undefined;
76
+ return pathname.slice(1).replaceAll("/", "\\");
77
+ }
78
+ if (url.hostname && url.hostname !== "localhost") return undefined;
79
+ return pathname;
80
+ }
81
+
82
+ /**
83
+ * Decode pasted text into a filesystem path candidate. No filesystem access.
84
+ *
85
+ * Handles terminal drag-drop shell escaping (`\ `, `\(`, ...; skipped on
86
+ * win32 where `\` is the path separator), quoted paths, `file://` URIs
87
+ * (drive-letter, `file://localhost`, and UNC forms on win32), and `~/`
88
+ * expansion. Returns `undefined` when the text is empty, spans multiple
89
+ * lines, or is an invalid `file://` URI.
90
+ */
91
+ export function decodePastedPathCandidate(text: string, options?: DecodePastedPathOptions): string | undefined {
92
+ const platform = options?.platform ?? process.platform;
93
+ let candidate = text.trim();
94
+ if (!candidate || /[\r\n]/.test(candidate)) return undefined;
95
+
96
+ // Quoted paths (some terminals quote instead of escaping).
97
+ if (
98
+ candidate.length >= 2 &&
99
+ (candidate.startsWith('"') || candidate.startsWith("'")) &&
100
+ candidate.endsWith(candidate[0])
101
+ ) {
102
+ candidate = candidate.slice(1, -1);
103
+ }
104
+
105
+ if (candidate.startsWith("file://")) {
106
+ const decoded = decodeFileUrl(candidate, platform);
107
+ if (decoded === undefined) return undefined;
108
+ candidate = decoded;
109
+ } else if (platform !== "win32") {
110
+ // Terminal drag-drop escapes shell-special characters (`\ `, `\(`, ...).
111
+ // Skipped on Windows where `\` is the path separator.
112
+ candidate = candidate.replace(/\\(.)/g, "$1");
113
+ }
114
+
115
+ if (candidate.startsWith("~/")) {
116
+ candidate = path.join(options?.homedir ?? os.homedir(), candidate.slice(2));
117
+ }
118
+
119
+ return candidate;
120
+ }
121
+
122
+ /**
123
+ * Sniff the file header for a supported image signature (PNG, JPEG, GIF,
124
+ * WEBP). Prevents existing non-image files with image-looking extensions
125
+ * (e.g. a text file named `not-image.png`) from being treated as image
126
+ * candidates — consuming such a paste would lose the raw path when the
127
+ * image loader later rejects the content.
128
+ */
129
+ function hasSupportedImageMagic(filePath: string): boolean {
130
+ let fd: number;
131
+ try {
132
+ fd = fs.openSync(filePath, "r");
133
+ } catch {
134
+ return false;
135
+ }
136
+ try {
137
+ // Zero-filled; short reads leave trailing zeros so comparisons fail safely.
138
+ const header = Buffer.alloc(12);
139
+ fs.readSync(fd, header, 0, 12, 0);
140
+ if (header[0] === 0x89 && header[1] === 0x50 && header[2] === 0x4e && header[3] === 0x47) return true; // PNG
141
+ if (header[0] === 0xff && header[1] === 0xd8 && header[2] === 0xff) return true; // JPEG
142
+ const ascii6 = header.toString("latin1", 0, 6);
143
+ if (ascii6 === "GIF87a" || ascii6 === "GIF89a") return true; // GIF
144
+ if (header.toString("latin1", 0, 4) === "RIFF" && header.toString("latin1", 8, 12) === "WEBP") return true; // WEBP
145
+ return false;
146
+ } catch {
147
+ return false;
148
+ } finally {
149
+ fs.closeSync(fd);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Returns the resolved path when the whole pasted text is a single path to an
155
+ * existing image file (verified by extension AND content signature),
156
+ * otherwise `undefined` (the paste is inserted as text).
157
+ */
158
+ export function resolvePastedImagePath(text: string, options?: ResolvePastedImagePathOptions): string | undefined {
159
+ const candidate = decodePastedPathCandidate(text, options);
160
+ if (!candidate || !IMAGE_FILE_EXTENSION_PATTERN.test(candidate)) return undefined;
161
+
162
+ const resolved = path.resolve(options?.cwd ?? process.cwd(), candidate);
163
+ try {
164
+ if (!fs.statSync(resolved).isFile()) return undefined;
165
+ } catch {
166
+ return undefined;
167
+ }
168
+ if (!hasSupportedImageMagic(resolved)) return undefined;
169
+ return resolved;
170
+ }
@@ -10,44 +10,56 @@ import { extractWithParallel, findParallelApiKey, getParallelExtractContent } fr
10
10
  import type { RenderResult, SpecialHandler } from "./types";
11
11
  import { buildResult, formatMediaDuration, formatNumber } from "./types";
12
12
 
13
- interface YouTubeUrl {
13
+ export interface YouTubeUrl {
14
14
  videoId: string;
15
15
  playlistId?: string;
16
16
  }
17
17
 
18
+ const YOUTUBE_VIDEO_ID_PATTERN = /^[a-zA-Z0-9_-]{11}$/;
19
+
20
+ function isYouTubeHost(hostname: string): boolean {
21
+ return hostname === "youtube.com" || hostname === "m.youtube.com" || hostname === "music.youtube.com";
22
+ }
23
+
24
+ function isYouTubeEmbedHost(hostname: string): boolean {
25
+ return isYouTubeHost(hostname) || hostname === "youtube-nocookie.com";
26
+ }
27
+
18
28
  /**
19
29
  * Parse YouTube URL into components
20
30
  */
21
- function parseYouTubeUrl(url: string): YouTubeUrl | null {
31
+ export function parseYouTubeUrl(url: string): YouTubeUrl | null {
22
32
  try {
23
33
  const parsed = new URL(url);
24
34
  const hostname = parsed.hostname.replace(/^www\./, "");
25
35
 
26
- // youtube.com/watch?v=VIDEO_ID
27
- if ((hostname === "youtube.com" || hostname === "m.youtube.com") && parsed.pathname === "/watch") {
36
+ // youtube.com/watch?v=VIDEO_ID and music.youtube.com/watch?v=VIDEO_ID
37
+ if (isYouTubeHost(hostname) && parsed.pathname === "/watch") {
28
38
  const videoId = parsed.searchParams.get("v");
29
39
  const playlistId = parsed.searchParams.get("list") || undefined;
30
- if (videoId) return { videoId, playlistId };
40
+ if (videoId && YOUTUBE_VIDEO_ID_PATTERN.test(videoId)) return { videoId, playlistId };
31
41
  }
32
42
 
33
- // youtube.com/v/VIDEO_ID or youtube.com/embed/VIDEO_ID
34
- if (hostname === "youtube.com" || hostname === "m.youtube.com") {
35
- const match = parsed.pathname.match(/^\/(v|embed)\/([a-zA-Z0-9_-]{11})/);
43
+ // youtube.com/v/VIDEO_ID, youtube.com/embed/VIDEO_ID, youtube.com/live/VIDEO_ID,
44
+ // and youtube-nocookie.com/embed/VIDEO_ID. Require the id segment to end
45
+ // before matching so malformed overlong ids are not silently truncated.
46
+ if (isYouTubeEmbedHost(hostname)) {
47
+ const match = parsed.pathname.match(/^\/(v|embed|live)\/([a-zA-Z0-9_-]{11})(?:$|\/)/);
36
48
  if (match) return { videoId: match[2] };
37
49
  }
38
50
 
39
51
  // youtu.be/VIDEO_ID
40
52
  if (hostname === "youtu.be") {
41
53
  const videoId = parsed.pathname.slice(1).split("/")[0];
42
- if (videoId && /^[a-zA-Z0-9_-]{11}$/.test(videoId)) {
54
+ if (videoId && YOUTUBE_VIDEO_ID_PATTERN.test(videoId)) {
43
55
  return { videoId };
44
56
  }
45
57
  }
46
58
 
47
- // youtube.com/shorts/VIDEO_ID
48
- if (hostname === "youtube.com" && parsed.pathname.startsWith("/shorts/")) {
59
+ // youtube.com/shorts/VIDEO_ID and m.youtube.com/shorts/VIDEO_ID
60
+ if (isYouTubeHost(hostname) && parsed.pathname.startsWith("/shorts/")) {
49
61
  const videoId = parsed.pathname.replace("/shorts/", "").split("/")[0];
50
- if (videoId && /^[a-zA-Z0-9_-]{11}$/.test(videoId)) {
62
+ if (videoId && YOUTUBE_VIDEO_ID_PATTERN.test(videoId)) {
51
63
  return { videoId };
52
64
  }
53
65
  }
@@ -1,6 +1,15 @@
1
+ import {
2
+ buildWorkflowIntentReport,
3
+ type WorkflowClaimsLedger,
4
+ type WorkflowConsensusReport,
5
+ } from "./workflow-intent-report";
6
+
1
7
  export const WORKFLOW_INTENT_DIFF_CUSTOM_TYPE = "workflow-intent-diff";
8
+ export const WORKFLOW_INTENT_ROUTES = ["direct", "deep-interview", "ralplan", "ultragoal", "team"] as const;
9
+ export const WORKFLOW_ESCALATION_ROUTES = ["deep-interview", "ralplan", "ultragoal", "team"] as const;
2
10
 
3
- export type WorkflowIntentRoute = "direct" | "deep-interview" | "ralplan" | "ultragoal" | "team";
11
+ export type WorkflowIntentRoute = (typeof WORKFLOW_INTENT_ROUTES)[number];
12
+ export type WorkflowEscalationRoute = (typeof WORKFLOW_ESCALATION_ROUTES)[number];
4
13
  export type DirectTrackingMode = "custom-entry-only" | "not-direct";
5
14
  export type RootCausePhaseStatus = "active" | "inactive";
6
15
 
@@ -9,13 +18,15 @@ export interface WorkflowIntentDiff {
9
18
  readonly route: WorkflowIntentRoute;
10
19
  readonly reason: string;
11
20
  readonly directTracking: DirectTrackingMode;
12
- readonly recommendedSkill?: Exclude<WorkflowIntentRoute, "direct">;
21
+ readonly recommendedSkill?: WorkflowEscalationRoute;
13
22
  readonly recommendedInvocation?: string;
14
23
  readonly triggers: readonly string[];
15
24
  readonly rootCausePhase: {
16
25
  readonly status: RootCausePhaseStatus;
17
26
  readonly triggers: readonly string[];
18
27
  };
28
+ readonly claimsLedger: WorkflowClaimsLedger;
29
+ readonly consensusReport: WorkflowConsensusReport;
19
30
  readonly promptPreview: string;
20
31
  }
21
32
 
@@ -182,6 +193,19 @@ export function buildWorkflowIntentDiff(text: string): WorkflowIntentDiff | null
182
193
  const rootCauseTriggers = collectRootCauseTriggers(text);
183
194
  const rootCauseActive = rootCauseTriggers.length > 0;
184
195
  const direct = route.route === "direct";
196
+ const rootCauseStatus: RootCausePhaseStatus = rootCauseActive ? "active" : "inactive";
197
+ const rootCausePhase = {
198
+ status: rootCauseStatus,
199
+ triggers: rootCauseTriggers,
200
+ };
201
+ const report = buildWorkflowIntentReport({
202
+ route: route.route,
203
+ reason: route.reason,
204
+ direct,
205
+ recommendedInvocation: route.recommendedInvocation,
206
+ triggers: route.triggers,
207
+ rootCausePhase,
208
+ });
185
209
 
186
210
  return {
187
211
  version: 1,
@@ -190,10 +214,9 @@ export function buildWorkflowIntentDiff(text: string): WorkflowIntentDiff | null
190
214
  directTracking: direct ? "custom-entry-only" : "not-direct",
191
215
  ...(direct ? {} : { recommendedSkill: route.route, recommendedInvocation: route.recommendedInvocation }),
192
216
  triggers: route.triggers,
193
- rootCausePhase: {
194
- status: rootCauseActive ? "active" : "inactive",
195
- triggers: rootCauseTriggers,
196
- },
217
+ rootCausePhase,
218
+ claimsLedger: report.claimsLedger,
219
+ consensusReport: report.consensusReport,
197
220
  promptPreview,
198
221
  };
199
222
  }