icopilot 2.2.0

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 (203) hide show
  1. package/CHANGELOG.md +250 -0
  2. package/LICENSE +21 -0
  3. package/README.md +214 -0
  4. package/bin/icopilot.js +6 -0
  5. package/dist/acp/router.js +123 -0
  6. package/dist/acp/schema.js +53 -0
  7. package/dist/agents/aggregator.js +187 -0
  8. package/dist/agents/custom-agents.js +97 -0
  9. package/dist/agents/goal-driven.js +411 -0
  10. package/dist/agents/multi-repo.js +350 -0
  11. package/dist/agents/parallel-runner.js +181 -0
  12. package/dist/agents/router.js +144 -0
  13. package/dist/agents/self-heal.js +481 -0
  14. package/dist/agents/tdd-agent.js +278 -0
  15. package/dist/api/github-models.js +158 -0
  16. package/dist/bridge/ide-bridge.js +479 -0
  17. package/dist/cloud/routine-executor.js +34 -0
  18. package/dist/cloud/routine-scheduler.js +67 -0
  19. package/dist/cloud/routine-storage.js +297 -0
  20. package/dist/commands/acp-cmd.js +143 -0
  21. package/dist/commands/actions-cmd.js +624 -0
  22. package/dist/commands/agent-cmd.js +144 -0
  23. package/dist/commands/alias-cmd.js +132 -0
  24. package/dist/commands/bookmark-cmd.js +77 -0
  25. package/dist/commands/changelog-cmd.js +99 -0
  26. package/dist/commands/changes-cmd.js +120 -0
  27. package/dist/commands/clipboard-cmd.js +217 -0
  28. package/dist/commands/cloud-routine-cmd.js +265 -0
  29. package/dist/commands/codegen-cmd.js +544 -0
  30. package/dist/commands/compare-cmd.js +116 -0
  31. package/dist/commands/context-cmd.js +247 -0
  32. package/dist/commands/context-viz-cmd.js +43 -0
  33. package/dist/commands/conventions-cmd.js +116 -0
  34. package/dist/commands/cost-cmd.js +51 -0
  35. package/dist/commands/deps-cmd.js +294 -0
  36. package/dist/commands/diagram-cmd.js +658 -0
  37. package/dist/commands/diff-review-cmd.js +92 -0
  38. package/dist/commands/doc-cmd.js +412 -0
  39. package/dist/commands/doctor-cmd.js +152 -0
  40. package/dist/commands/editor-cmd.js +49 -0
  41. package/dist/commands/env-cmd.js +86 -0
  42. package/dist/commands/explain-cmd.js +78 -0
  43. package/dist/commands/explain-shell-cmd.js +22 -0
  44. package/dist/commands/explore-cmd.js +231 -0
  45. package/dist/commands/feedback-cmd.js +98 -0
  46. package/dist/commands/fix-cmd.js +17 -0
  47. package/dist/commands/generate-cmd.js +38 -0
  48. package/dist/commands/git-extra.js +197 -0
  49. package/dist/commands/git-log-cmd.js +98 -0
  50. package/dist/commands/git-undo-cmd.js +137 -0
  51. package/dist/commands/git.js +155 -0
  52. package/dist/commands/history-cmd.js +122 -0
  53. package/dist/commands/index-cmd.js +65 -0
  54. package/dist/commands/init-cmd.js +73 -0
  55. package/dist/commands/lint-cmd.js +133 -0
  56. package/dist/commands/memory-cmd.js +98 -0
  57. package/dist/commands/metrics-cmd.js +97 -0
  58. package/dist/commands/mode-prefix.js +30 -0
  59. package/dist/commands/multi-cmd.js +44 -0
  60. package/dist/commands/notify-cmd.js +204 -0
  61. package/dist/commands/profile-cmd.js +101 -0
  62. package/dist/commands/prompts.js +17 -0
  63. package/dist/commands/rag-cmd.js +60 -0
  64. package/dist/commands/readme-cmd.js +564 -0
  65. package/dist/commands/reasoning-cmd.js +34 -0
  66. package/dist/commands/refactor-cmd.js +96 -0
  67. package/dist/commands/release-cmd.js +450 -0
  68. package/dist/commands/repo-cmd.js +195 -0
  69. package/dist/commands/route-cmd.js +21 -0
  70. package/dist/commands/schedule-cmd.js +109 -0
  71. package/dist/commands/search-cmd.js +47 -0
  72. package/dist/commands/security-cmd.js +156 -0
  73. package/dist/commands/settings-cmd.js +238 -0
  74. package/dist/commands/skill-cmd.js +338 -0
  75. package/dist/commands/slash.js +2721 -0
  76. package/dist/commands/snippets-cmd.js +83 -0
  77. package/dist/commands/space-cmd.js +92 -0
  78. package/dist/commands/stash-cmd.js +156 -0
  79. package/dist/commands/stats-cmd.js +36 -0
  80. package/dist/commands/style-cmd.js +85 -0
  81. package/dist/commands/suggest-cmd.js +40 -0
  82. package/dist/commands/summary-cmd.js +138 -0
  83. package/dist/commands/task-cmd.js +58 -0
  84. package/dist/commands/team-memory-cmd.js +97 -0
  85. package/dist/commands/template-cmd.js +475 -0
  86. package/dist/commands/test-cmd.js +146 -0
  87. package/dist/commands/todo-cmd.js +172 -0
  88. package/dist/commands/tokens-cmd.js +277 -0
  89. package/dist/commands/trigger-cmd.js +147 -0
  90. package/dist/commands/undo-cmd.js +18 -0
  91. package/dist/commands/voice-cmd.js +89 -0
  92. package/dist/commands/watch-cmd.js +110 -0
  93. package/dist/commands/web-cmd.js +183 -0
  94. package/dist/commands/worktree-cmd.js +119 -0
  95. package/dist/config-profile.js +66 -0
  96. package/dist/config.js +288 -0
  97. package/dist/context/compactor.js +53 -0
  98. package/dist/context/dep-context.js +329 -0
  99. package/dist/context/file-refs.js +54 -0
  100. package/dist/context/git-context.js +229 -0
  101. package/dist/context/image-input.js +66 -0
  102. package/dist/context/memory.js +55 -0
  103. package/dist/context/persistent-memory.js +104 -0
  104. package/dist/context/pinned.js +96 -0
  105. package/dist/context/priority.js +150 -0
  106. package/dist/context/read-only.js +48 -0
  107. package/dist/context/smart-files.js +286 -0
  108. package/dist/context/team-memory.js +156 -0
  109. package/dist/extensions/loader.js +149 -0
  110. package/dist/extensions/marketplace.js +49 -0
  111. package/dist/extensions/slack-provider.js +181 -0
  112. package/dist/extensions/team.js +56 -0
  113. package/dist/extensions/teams-provider.js +222 -0
  114. package/dist/extensions/voice.js +18 -0
  115. package/dist/hooks/lifecycle.js +215 -0
  116. package/dist/hooks/precommit.js +463 -0
  117. package/dist/index/embeddings.js +23 -0
  118. package/dist/index/indexer.js +86 -0
  119. package/dist/index/retrieve.js +20 -0
  120. package/dist/index/store.js +95 -0
  121. package/dist/index.js +286 -0
  122. package/dist/intelligence/dead-code.js +457 -0
  123. package/dist/intelligence/error-watch.js +263 -0
  124. package/dist/intelligence/navigation.js +141 -0
  125. package/dist/intelligence/stack-trace.js +210 -0
  126. package/dist/intelligence/symbol-index.js +410 -0
  127. package/dist/knowledge/auto-memory.js +412 -0
  128. package/dist/knowledge/conventions.js +475 -0
  129. package/dist/knowledge/corrections.js +213 -0
  130. package/dist/knowledge/rag.js +450 -0
  131. package/dist/knowledge/style-learner.js +324 -0
  132. package/dist/logger.js +35 -0
  133. package/dist/mcp/client.js +144 -0
  134. package/dist/mcp/config.js +24 -0
  135. package/dist/mcp/index.js +89 -0
  136. package/dist/modes/auto-compact.js +20 -0
  137. package/dist/modes/autopilot.js +157 -0
  138. package/dist/modes/background.js +82 -0
  139. package/dist/modes/interactive.js +187 -0
  140. package/dist/modes/oneshot.js +36 -0
  141. package/dist/modes/tui.js +265 -0
  142. package/dist/modes/turn.js +342 -0
  143. package/dist/notifications/manager.js +107 -0
  144. package/dist/plugins/marketplace.js +244 -0
  145. package/dist/providers/custom-provider.js +298 -0
  146. package/dist/providers/local-model.js +121 -0
  147. package/dist/routing/profiles.js +44 -0
  148. package/dist/routing/router.js +18 -0
  149. package/dist/sandbox/container.js +151 -0
  150. package/dist/security/audit.js +237 -0
  151. package/dist/security/content-filter.js +449 -0
  152. package/dist/security/proxy.js +301 -0
  153. package/dist/security/retention.js +281 -0
  154. package/dist/security/roles.js +252 -0
  155. package/dist/server/api-server.js +679 -0
  156. package/dist/session/bookmarks.js +72 -0
  157. package/dist/session/cloud-session.js +291 -0
  158. package/dist/session/handoff.js +405 -0
  159. package/dist/session/manager.js +35 -0
  160. package/dist/session/session.js +296 -0
  161. package/dist/session/share.js +313 -0
  162. package/dist/session/undo-journal.js +91 -0
  163. package/dist/snippets/store.js +60 -0
  164. package/dist/spaces/space-config.js +156 -0
  165. package/dist/spaces/space.js +220 -0
  166. package/dist/stats/store.js +101 -0
  167. package/dist/tools/apply-patch.js +134 -0
  168. package/dist/tools/auto-check.js +218 -0
  169. package/dist/tools/diff-edit.js +150 -0
  170. package/dist/tools/diff-prompt.js +36 -0
  171. package/dist/tools/edit-file.js +66 -0
  172. package/dist/tools/file-ops.js +205 -0
  173. package/dist/tools/glob.js +17 -0
  174. package/dist/tools/grep.js +56 -0
  175. package/dist/tools/image.js +194 -0
  176. package/dist/tools/list-directory.js +228 -0
  177. package/dist/tools/memory.js +17 -0
  178. package/dist/tools/multi-edit.js +299 -0
  179. package/dist/tools/policy.js +95 -0
  180. package/dist/tools/registry.js +484 -0
  181. package/dist/tools/retry.js +74 -0
  182. package/dist/tools/run-in-terminal.js +162 -0
  183. package/dist/tools/safety.js +64 -0
  184. package/dist/tools/sandbox.js +15 -0
  185. package/dist/tools/search-symbols.js +212 -0
  186. package/dist/tools/shell.js +118 -0
  187. package/dist/tools/web.js +167 -0
  188. package/dist/ui/prompt.js +37 -0
  189. package/dist/ui/render.js +96 -0
  190. package/dist/ui/screen.js +13 -0
  191. package/dist/ui/theme.js +56 -0
  192. package/dist/util/browser.js +34 -0
  193. package/dist/util/completion.js +350 -0
  194. package/dist/util/cost.js +28 -0
  195. package/dist/util/keybindings.js +113 -0
  196. package/dist/util/lazy.js +26 -0
  197. package/dist/util/perf.js +25 -0
  198. package/dist/util/token-worker.js +11 -0
  199. package/dist/util/tokens.js +50 -0
  200. package/dist/workflows/builtins.js +128 -0
  201. package/dist/workflows/engine.js +496 -0
  202. package/dist/workflows/file-trigger.js +197 -0
  203. package/package.json +79 -0
@@ -0,0 +1,265 @@
1
+ import readline from 'node:readline';
2
+ import { Writable } from 'node:stream';
3
+ import { loadAliases, resolveAlias } from '../commands/alias-cmd.js';
4
+ import { MetricsCollector } from '../commands/metrics-cmd.js';
5
+ import { handleSlash } from '../commands/slash.js';
6
+ import { Session } from '../session/session.js';
7
+ import { altScreenEnter, altScreenExit, clear, hideCursor, showCursor, size, } from '../ui/screen.js';
8
+ import { handlePostTurnContextBudget } from './auto-compact.js';
9
+ import { backgroundTaskManager } from './background.js';
10
+ import { runAutopilot } from './autopilot.js';
11
+ import { runTurn } from './turn.js';
12
+ import { hookManager } from '../hooks/lifecycle.js';
13
+ const VERSION = '1.3.0';
14
+ const FRAME_MS = 33;
15
+ export async function runTui(initialMode = 'ask', opts = {}) {
16
+ const session = new Session({ mode: initialMode });
17
+ await session.initializeGitContext();
18
+ await hookManager.emit('sessionStart', {
19
+ sessionId: session.state.id,
20
+ cwd: session.state.cwd,
21
+ mode: session.state.mode,
22
+ model: session.state.model,
23
+ });
24
+ const metrics = new MetricsCollector();
25
+ const originalWrite = process.stdout.write.bind(process.stdout);
26
+ const writeRaw = (text) => {
27
+ originalWrite(text);
28
+ };
29
+ const silentOutput = new Writable({
30
+ write(_chunk, _encoding, callback) {
31
+ callback();
32
+ },
33
+ });
34
+ silentOutput.columns = size().cols;
35
+ let chat = '';
36
+ let running = true;
37
+ let busy = false;
38
+ let dirty = true;
39
+ let cleaned = false;
40
+ let frame;
41
+ let currentAbort = null;
42
+ const pendingInputs = [];
43
+ const rl = readline.createInterface({
44
+ input: process.stdin,
45
+ output: silentOutput,
46
+ terminal: true,
47
+ });
48
+ const markDirty = () => {
49
+ dirty = true;
50
+ };
51
+ const cleanup = () => {
52
+ if (cleaned)
53
+ return;
54
+ cleaned = true;
55
+ running = false;
56
+ if (currentAbort && !currentAbort.signal.aborted)
57
+ currentAbort.abort();
58
+ process.stdout.write = originalWrite;
59
+ process.stdout.off('resize', onResize);
60
+ process.off('SIGINT', onSigint);
61
+ process.stdin.off('keypress', markDirty);
62
+ if (frame)
63
+ clearInterval(frame);
64
+ rl.close();
65
+ if (process.stdin.isTTY)
66
+ process.stdin.setRawMode(false);
67
+ showCursor();
68
+ clear();
69
+ altScreenExit();
70
+ };
71
+ const onSigint = () => {
72
+ cleanup();
73
+ process.exit(0);
74
+ };
75
+ const onResize = () => {
76
+ silentOutput.columns = size().cols;
77
+ markDirty();
78
+ };
79
+ const render = () => {
80
+ if (!dirty)
81
+ return;
82
+ dirty = false;
83
+ const { rows, cols } = size();
84
+ const chatTop = 2;
85
+ const chatBottom = Math.max(chatTop, rows - 3);
86
+ const chatHeight = Math.max(1, chatBottom - chatTop + 1);
87
+ writeRaw('\x1b[2J\x1b[H');
88
+ writeRaw(statusLine(session, cols));
89
+ const lines = wrapLines(chat.trimEnd(), Math.max(1, cols));
90
+ const visible = lines.slice(-chatHeight);
91
+ for (let i = 0; i < chatHeight; i++) {
92
+ writeRaw(`\x1b[${chatTop + i};1H`);
93
+ writeRaw(pad(visible[i] || '', cols));
94
+ }
95
+ writeRaw(`\x1b[${Math.max(1, rows - 2)};1H`);
96
+ writeRaw('─'.repeat(cols));
97
+ writeRaw(`\x1b[${Math.max(1, rows - 1)};1H`);
98
+ const prompt = `${busy ? '…' : '❯'} ${rl.line || ''}`;
99
+ writeRaw(pad(prompt, cols));
100
+ writeRaw(`\x1b[${rows};1H`);
101
+ writeRaw(pad(busy ? 'Working…' : 'Enter to send • /help for commands', cols));
102
+ };
103
+ const appendCaptured = (chunk) => {
104
+ const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk);
105
+ chat += stripAnsi(text);
106
+ markDirty();
107
+ };
108
+ const captureStdout = () => {
109
+ process.stdout.write = ((chunk, encoding, cb) => {
110
+ appendCaptured(chunk);
111
+ if (typeof encoding === 'function')
112
+ encoding();
113
+ if (typeof cb === 'function')
114
+ cb();
115
+ return true;
116
+ });
117
+ };
118
+ const releaseStdout = () => {
119
+ process.stdout.write = originalWrite;
120
+ };
121
+ const handleLine = async (line, scheduled = false) => {
122
+ if (busy) {
123
+ pendingInputs.push({ line, scheduled });
124
+ chat += scheduled
125
+ ? `\n(system) queued scheduled prompt: ${line}\n`
126
+ : '\n(system) still working; prompt queued.\n';
127
+ markDirty();
128
+ return;
129
+ }
130
+ const trimmed = line.trim();
131
+ if (!trimmed)
132
+ return;
133
+ busy = true;
134
+ currentAbort = new AbortController();
135
+ const resolvedLine = resolveAlias(line, loadAliases()) ?? line;
136
+ chat += `\n${scheduled ? '⏱' : '❯'} ${line}\n`;
137
+ markDirty();
138
+ try {
139
+ captureStdout();
140
+ const slash = await handleSlash(resolvedLine, {
141
+ session,
142
+ abort: currentAbort,
143
+ metrics,
144
+ schedulePrompt: (prompt) => void handleLine(prompt, true),
145
+ exit: () => {
146
+ running = false;
147
+ },
148
+ });
149
+ if (!slash.consumed) {
150
+ const forwardInput = slash.forwardInput ?? resolvedLine;
151
+ const trimmedForwardInput = forwardInput.trim();
152
+ if (trimmedForwardInput.endsWith('&')) {
153
+ const goal = trimmedForwardInput.slice(0, -1).trim();
154
+ if (!goal) {
155
+ process.stdout.write('\nusage: <prompt> &\n');
156
+ }
157
+ else {
158
+ const id = backgroundTaskManager.startTask(goal);
159
+ process.stdout.write(`\n↳ started background task ${id.slice(0, 8)} for: ${goal}\n`);
160
+ }
161
+ return;
162
+ }
163
+ const explicitTurnMode = slash
164
+ .turnMode;
165
+ const effectiveTurnMode = explicitTurnMode ?? opts.defaultTurnMode;
166
+ if (session.state.autopilotEnabled && !effectiveTurnMode) {
167
+ await runAutopilot(forwardInput, {
168
+ session,
169
+ signal: currentAbort.signal,
170
+ });
171
+ }
172
+ else {
173
+ await runTurn({
174
+ session,
175
+ userInput: forwardInput,
176
+ metrics,
177
+ signal: currentAbort.signal,
178
+ turnMode: effectiveTurnMode ?? undefined,
179
+ });
180
+ }
181
+ await handlePostTurnContextBudget(session, currentAbort.signal);
182
+ }
183
+ }
184
+ catch (err) {
185
+ if (err?.name !== 'AbortError' && !currentAbort.signal.aborted) {
186
+ await hookManager.emit('errorOccurred', {
187
+ scope: 'tui',
188
+ sessionId: session.state.id,
189
+ message: err?.message || String(err),
190
+ });
191
+ chat += `\nerror: ${err?.message || err}\n`;
192
+ }
193
+ }
194
+ finally {
195
+ releaseStdout();
196
+ currentAbort = null;
197
+ busy = false;
198
+ markDirty();
199
+ const next = pendingInputs.shift();
200
+ if (next)
201
+ void handleLine(next.line, next.scheduled);
202
+ if (!running)
203
+ cleanup();
204
+ }
205
+ };
206
+ readline.emitKeypressEvents(process.stdin, rl);
207
+ if (process.stdin.isTTY)
208
+ process.stdin.setRawMode(true);
209
+ process.stdout.on('resize', onResize);
210
+ process.on('SIGINT', onSigint);
211
+ rl.on('line', (line) => void handleLine(line));
212
+ rl.on('close', () => {
213
+ if (running)
214
+ cleanup();
215
+ });
216
+ process.stdin.on('keypress', markDirty);
217
+ altScreenEnter();
218
+ hideCursor();
219
+ clear();
220
+ chat = 'Welcome to iCopilot TUI prototype. Type /help for commands.\n';
221
+ frame = setInterval(render, FRAME_MS);
222
+ try {
223
+ await new Promise((resolve) => {
224
+ const wait = setInterval(() => {
225
+ if (!running) {
226
+ clearInterval(wait);
227
+ resolve();
228
+ }
229
+ }, 50);
230
+ });
231
+ }
232
+ finally {
233
+ if (running)
234
+ cleanup();
235
+ await hookManager.emit('sessionEnd', {
236
+ sessionId: session.state.id,
237
+ cwd: session.state.cwd,
238
+ mode: session.state.mode,
239
+ model: session.state.model,
240
+ });
241
+ }
242
+ }
243
+ function statusLine(session, cols) {
244
+ const mode = session.state.mode.toUpperCase();
245
+ const text = ` iCopilot v${VERSION} • model: ${session.state.model} • mode: ${mode} Ctrl+C to exit`;
246
+ return `\x1b[7m${pad(text, cols)}\x1b[0m`;
247
+ }
248
+ function wrapLines(text, width) {
249
+ const out = [];
250
+ for (const rawLine of text.split(/\r?\n/)) {
251
+ const line = rawLine || ' ';
252
+ for (let i = 0; i < line.length; i += width) {
253
+ out.push(line.slice(i, i + width));
254
+ }
255
+ }
256
+ return out.length ? out : [''];
257
+ }
258
+ function pad(text, cols) {
259
+ const clipped = text.length > cols ? text.slice(0, cols) : text;
260
+ return clipped + ' '.repeat(Math.max(0, cols - clipped.length));
261
+ }
262
+ function stripAnsi(text) {
263
+ // eslint-disable-next-line no-control-regex
264
+ return text.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '');
265
+ }
@@ -0,0 +1,342 @@
1
+ import { streamChat } from '../api/github-models.js';
2
+ import { TOOL_SCHEMAS, dispatchTool } from '../tools/registry.js';
3
+ import { StreamSink } from '../ui/render.js';
4
+ import { theme } from '../ui/theme.js';
5
+ import { PLAN_SYSTEM, getAskSystemPrompt } from '../commands/prompts.js';
6
+ import { parseFileRefs, renderFileRefBlock } from '../context/file-refs.js';
7
+ import { renderGitContextBlock } from '../context/git-context.js';
8
+ import { buildImageContent, detectImagePaths, isVisionCapableModel, } from '../context/image-input.js';
9
+ import { loadMemoryBlock } from '../context/memory.js';
10
+ import { PinnedContext } from '../context/pinned.js';
11
+ import { getReadOnlyContext } from '../context/read-only.js';
12
+ import { learnAutoMemories, loadAutoMemoryPromptContext } from '../knowledge/auto-memory.js';
13
+ import { loadCorrectionPromptContext } from '../knowledge/corrections.js';
14
+ import { loadConventionPromptContext } from '../knowledge/conventions.js';
15
+ import { loadStylePromptContext } from '../knowledge/style-learner.js';
16
+ import { config } from '../config.js';
17
+ import { hookManager } from '../hooks/lifecycle.js';
18
+ import { AUTO_FIX_MAX_RETRIES, buildAutoFixPrompt, extractAutoLintResult, extractChangedFilesFromToolResult, formatAutoCheckResult, runAutoTest, } from '../tools/auto-check.js';
19
+ import { loadProjectContentFilter, summarizeFilterResult } from '../security/content-filter.js';
20
+ import { countTokensSync } from '../util/tokens.js';
21
+ import path from 'node:path';
22
+ import os from 'node:os';
23
+ import { recordTurnSnapshot } from '../commands/changes-cmd.js';
24
+ import { pickModel } from '../routing/router.js';
25
+ const MAX_TOOL_HOPS = 6;
26
+ const ASK_ONLY_SYSTEM = `You are iCopilot in question-only mode.
27
+
28
+ Answer directly, explain tradeoffs when helpful, and stay concise.
29
+ Do NOT make tool calls, do NOT edit files, and do NOT claim that you changed anything.`;
30
+ const CODE_MODE_PROMPT = `Code Mode override:
31
+ - Skip planning and start implementing immediately.
32
+ - Use tools when they help you inspect or change code.
33
+ - Prefer direct execution over discussion.`;
34
+ const ARCHITECT_MODE_PROMPT = `Architect Mode override:
35
+ - Execute the supplied architecture plan.
36
+ - Keep implementation aligned to that plan unless a hard blocker appears.
37
+ - Use tools and code edits as needed to complete the task.`;
38
+ const ARCHITECT_PLANNER_PROMPT = `You are the planning half of Architect Mode.
39
+
40
+ Produce a concise implementation plan (3-6 bullets) for the user's request.
41
+ The plan should focus on concrete code changes and verification steps.
42
+ Do not call tools.`;
43
+ /**
44
+ * Run one user→assistant turn. Handles @file injection, tool-call loop,
45
+ * streaming output, and persistent history.
46
+ */
47
+ export async function runTurn(opts) {
48
+ const { session, userInput, metrics, signal, turnMode } = opts;
49
+ const turnStartedAt = Date.now();
50
+ let assistantTokens = 0;
51
+ const hookResult = await hookManager.emit('userPromptSubmit', {
52
+ sessionId: session.state.id,
53
+ cwd: session.state.cwd,
54
+ mode: session.state.mode,
55
+ model: session.state.model,
56
+ prompt: userInput,
57
+ });
58
+ if (hookResult.action === 'deny') {
59
+ throw new Error(hookResult.reason || 'prompt blocked by lifecycle hook');
60
+ }
61
+ const submittedInput = hookResult.action === 'modify' && typeof hookResult.modifications?.prompt === 'string'
62
+ ? String(hookResult.modifications.prompt)
63
+ : userInput;
64
+ const refs = parseFileRefs(submittedInput);
65
+ const refBlock = renderFileRefBlock(refs);
66
+ const promptInput = refBlock ? `${submittedInput}\n\n${refBlock}` : submittedInput;
67
+ const filterResult = loadProjectContentFilter(session.state.cwd).filter(promptInput);
68
+ if (refs.length && !config.quiet && !config.jsonOutput) {
69
+ process.stdout.write(theme.dim(` injected ${refs.length} file ref${refs.length === 1 ? '' : 's'}: ` +
70
+ refs.map((r) => r.rel).join(', ') +
71
+ '\n'));
72
+ }
73
+ if (filterResult.blocked) {
74
+ const blockedRules = [
75
+ ...new Set(filterResult.matches.filter((match) => match.action === 'block').map((match) => match.name)),
76
+ ];
77
+ throw new Error(`prompt blocked by content filter (${summarizeFilterResult(filterResult)}): ${blockedRules.join(', ')}`);
78
+ }
79
+ if ((filterResult.changed || filterResult.warnings > 0) && !config.quiet && !config.jsonOutput) {
80
+ process.stdout.write(theme.warn(` content filter applied: ${summarizeFilterResult(filterResult)}\n`));
81
+ }
82
+ const turnProfile = resolveTurnProfile(session, turnMode);
83
+ const sys = {
84
+ role: 'system',
85
+ content: buildSystemPrompt(session, filterResult.filtered, turnProfile),
86
+ };
87
+ const imagePaths = detectImagePaths(userInput);
88
+ const userContent = buildUserMessageContent(filterResult.filtered, imagePaths, session.state.cwd, session.state.model, (warning) => {
89
+ if (!config.quiet && !config.jsonOutput) {
90
+ process.stdout.write(theme.warn(` ${warning}\n`));
91
+ }
92
+ });
93
+ const userMsg = {
94
+ role: 'user',
95
+ content: userContent,
96
+ };
97
+ session.push(userMsg);
98
+ let modelForTurn = session.state.model;
99
+ if (turnMode === 'architect') {
100
+ const plannerModel = pickModel(session.state.model, 'plan');
101
+ const plannerResult = await streamChat({
102
+ model: plannerModel,
103
+ messages: [
104
+ {
105
+ role: 'system',
106
+ content: `${buildSystemPrompt(session, filterResult.filtered, turnProfile)}\n\n${ARCHITECT_PLANNER_PROMPT}`,
107
+ },
108
+ ...session.state.messages,
109
+ ],
110
+ signal,
111
+ onToken: () => undefined,
112
+ });
113
+ const plan = plannerResult.content?.trim() || '';
114
+ if (plan) {
115
+ if (!config.quiet && !config.jsonOutput) {
116
+ process.stdout.write(`\n${theme.brand('Architect plan')} ${theme.dim(`(${plannerModel})`)}\n${plan}\n\n`);
117
+ }
118
+ sys.content = `${sys.content}\n\nArchitect plan:\n${plan}`;
119
+ }
120
+ modelForTurn = pickModel(session.state.model, 'edit');
121
+ }
122
+ const tools = turnProfile.tools;
123
+ const changedFiles = new Set();
124
+ const failedLintFiles = new Set();
125
+ let lastFailedLint = null;
126
+ let autoFixRetries = 0;
127
+ try {
128
+ await recordTurnSnapshot(session).catch(() => null);
129
+ for (let hop = 0; hop < MAX_TOOL_HOPS; hop++) {
130
+ const sink = new StreamSink();
131
+ if (!config.quiet && !config.jsonOutput) {
132
+ process.stdout.write('\n' + theme.assistant('● ') + '');
133
+ }
134
+ const res = await streamChat({
135
+ model: modelForTurn,
136
+ messages: [sys, ...session.state.messages],
137
+ tools,
138
+ signal,
139
+ onToken: (t) => {
140
+ if (!config.jsonOutput)
141
+ sink.write(t);
142
+ },
143
+ });
144
+ if (!config.jsonOutput) {
145
+ sink.finalize();
146
+ }
147
+ const content = res.content || '';
148
+ const tokenCount = safeTokenCount(content);
149
+ assistantTokens += tokenCount;
150
+ // Persist assistant message
151
+ const assistantMsg = {
152
+ role: 'assistant',
153
+ content,
154
+ ...(res.toolCalls.length
155
+ ? {
156
+ tool_calls: res.toolCalls.map((tc) => ({
157
+ id: tc.id,
158
+ type: 'function',
159
+ function: { name: tc.name, arguments: tc.arguments || '{}' },
160
+ })),
161
+ }
162
+ : {}),
163
+ };
164
+ session.push(assistantMsg);
165
+ if (config.jsonOutput) {
166
+ process.stdout.write(JSON.stringify({
167
+ role: 'assistant',
168
+ content,
169
+ model: session.state.model,
170
+ tokens: tokenCount,
171
+ }) + '\n');
172
+ }
173
+ if (!res.toolCalls.length || res.finishReason === 'stop') {
174
+ if (lastFailedLint &&
175
+ config.autoFix &&
176
+ autoFixRetries < AUTO_FIX_MAX_RETRIES &&
177
+ lastFailedLint.fixable) {
178
+ autoFixRetries += 1;
179
+ session.push({
180
+ role: 'system',
181
+ content: buildAutoFixPrompt('lint', lastFailedLint, autoFixRetries, lastFailedLint.files),
182
+ });
183
+ continue;
184
+ }
185
+ if (changedFiles.size > 0 && config.autoTest) {
186
+ const testResult = await runAutoTest();
187
+ if (!config.quiet && !config.jsonOutput) {
188
+ process.stdout.write(`${theme.dim(formatAutoCheckResult('test', testResult, [...changedFiles]))}\n`);
189
+ }
190
+ if (!testResult.passed &&
191
+ config.autoFix &&
192
+ autoFixRetries < AUTO_FIX_MAX_RETRIES &&
193
+ testResult.fixable) {
194
+ autoFixRetries += 1;
195
+ session.push({
196
+ role: 'system',
197
+ content: buildAutoFixPrompt('test', testResult, autoFixRetries, [...changedFiles]),
198
+ });
199
+ continue;
200
+ }
201
+ }
202
+ learnAutoMemories(userInput, content);
203
+ return;
204
+ }
205
+ // Execute tools and append tool results, then loop
206
+ for (const tc of res.toolCalls) {
207
+ let parsed = {};
208
+ try {
209
+ parsed = tc.arguments ? JSON.parse(tc.arguments) : {};
210
+ }
211
+ catch {
212
+ parsed = { __raw: tc.arguments };
213
+ }
214
+ const toolStartedAt = Date.now();
215
+ const out = await dispatchTool(tc.name, parsed);
216
+ metrics?.recordToolCall(tc.name, Date.now() - toolStartedAt);
217
+ session.push({
218
+ role: 'tool',
219
+ tool_call_id: tc.id,
220
+ content: out,
221
+ });
222
+ const toolChangedFiles = extractChangedFilesFromToolResult(tc.name, parsed, out);
223
+ toolChangedFiles.forEach((file) => changedFiles.add(file));
224
+ const autoLint = extractAutoLintResult(out);
225
+ if (autoLint) {
226
+ if (!config.quiet && !config.jsonOutput) {
227
+ process.stdout.write(`${theme.dim(formatAutoCheckResult('lint', autoLint, toolChangedFiles))}\n`);
228
+ }
229
+ if (autoLint.passed) {
230
+ toolChangedFiles.forEach((file) => failedLintFiles.delete(file));
231
+ if (failedLintFiles.size === 0)
232
+ lastFailedLint = null;
233
+ }
234
+ else {
235
+ toolChangedFiles.forEach((file) => failedLintFiles.add(file));
236
+ lastFailedLint = { ...autoLint, files: toolChangedFiles };
237
+ }
238
+ }
239
+ }
240
+ }
241
+ }
242
+ finally {
243
+ if (!signal.aborted && assistantTokens > 0) {
244
+ const durationMs = Date.now() - turnStartedAt;
245
+ metrics?.recordResponseTime(durationMs);
246
+ if (durationMs > 0) {
247
+ metrics?.recordTokenThroughput(assistantTokens / (durationMs / 1000));
248
+ }
249
+ }
250
+ }
251
+ if (!config.jsonOutput) {
252
+ process.stdout.write(theme.warn(`\n⚠ tool-call hop limit (${MAX_TOOL_HOPS}) reached.\n`));
253
+ }
254
+ }
255
+ export function buildSystemPrompt(session, context = '', profile) {
256
+ const pinnedBlock = PinnedContext.fromJSON(session.state.pinned).render();
257
+ const readOnlyBlock = getReadOnlyContext();
258
+ const gitBlock = renderGitContextBlock(session.state.gitContext ?? []);
259
+ const styleBlock = loadStylePromptContext(session.state.cwd) ?? '';
260
+ const conventionBlock = loadConventionPromptContext(session.state.cwd) ?? '';
261
+ const autoMemoryBlock = loadAutoMemoryPromptContext(context, 12) ?? '';
262
+ const correctionsBlock = loadCorrectionPromptContext(context) ?? '';
263
+ const basePrompt = profile?.systemPrompt ??
264
+ session.state.systemPrompt ??
265
+ ((profile?.baseMode ?? session.state.mode) === 'plan' ? PLAN_SYSTEM : getAskSystemPrompt());
266
+ return [
267
+ loadMemoryBlock(session.state.cwd) ?? '',
268
+ autoMemoryBlock,
269
+ basePrompt,
270
+ correctionsBlock,
271
+ styleBlock,
272
+ conventionBlock,
273
+ pinnedBlock,
274
+ readOnlyBlock,
275
+ gitBlock,
276
+ ]
277
+ .filter((part) => part.trim().length > 0)
278
+ .join('\n\n');
279
+ }
280
+ function resolveTurnProfile(session, turnMode) {
281
+ const askPrompt = session.state.systemPrompt ?? getAskSystemPrompt();
282
+ switch (turnMode) {
283
+ case 'ask':
284
+ return {
285
+ baseMode: 'ask',
286
+ systemPrompt: session.state.systemPrompt && session.state.systemPrompt.trim()
287
+ ? `${session.state.systemPrompt}\n\n${ASK_ONLY_SYSTEM}`
288
+ : ASK_ONLY_SYSTEM,
289
+ tools: undefined,
290
+ };
291
+ case 'code':
292
+ return {
293
+ baseMode: 'ask',
294
+ systemPrompt: `${askPrompt}\n\n${CODE_MODE_PROMPT}`,
295
+ tools: TOOL_SCHEMAS,
296
+ };
297
+ case 'architect':
298
+ return {
299
+ baseMode: 'ask',
300
+ systemPrompt: `${askPrompt}\n\n${ARCHITECT_MODE_PROMPT}`,
301
+ tools: TOOL_SCHEMAS,
302
+ };
303
+ default:
304
+ return {
305
+ baseMode: session.state.mode,
306
+ tools: session.state.mode === 'ask' ? TOOL_SCHEMAS : undefined,
307
+ };
308
+ }
309
+ }
310
+ function safeTokenCount(text) {
311
+ try {
312
+ return countTokensSync(text);
313
+ }
314
+ catch {
315
+ return Math.ceil(text.length / 4);
316
+ }
317
+ }
318
+ function buildUserMessageContent(text, imagePaths, cwd, model, onWarning) {
319
+ if (!imagePaths.length)
320
+ return text;
321
+ if (!isVisionCapableModel(model)) {
322
+ onWarning(`model "${model}" does not support image input; ignoring ${imagePaths.length} image reference${imagePaths.length === 1 ? '' : 's'}.`);
323
+ return text;
324
+ }
325
+ const content = [{ type: 'text', text }];
326
+ const resolvedImagePaths = imagePaths.map((imagePath) => resolveImagePath(imagePath, cwd));
327
+ for (const imagePath of resolvedImagePaths) {
328
+ try {
329
+ content.push(...buildImageContent([imagePath]));
330
+ }
331
+ catch (error) {
332
+ onWarning(`unable to attach image ${imagePath}: ${error?.message || error}`);
333
+ }
334
+ }
335
+ return content.length > 1 ? content : text;
336
+ }
337
+ function resolveImagePath(filePath, cwd) {
338
+ if (filePath.startsWith('~/') || filePath.startsWith('~\\')) {
339
+ return path.join(os.homedir(), filePath.slice(2));
340
+ }
341
+ return path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
342
+ }