codemini-cli 0.1.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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/OPERATIONS.md +202 -0
  3. package/README.md +138 -0
  4. package/bin/coder.js +7 -0
  5. package/deployment.md +205 -0
  6. package/package.json +54 -0
  7. package/skills/brainstorming-lite/SKILL.md +37 -0
  8. package/skills/executing-plan-lite/SKILL.md +41 -0
  9. package/skills/superpowers-lite/SKILL.md +44 -0
  10. package/souls/anime.md +3 -0
  11. package/souls/default.md +3 -0
  12. package/souls/playful.md +3 -0
  13. package/souls/professional.md +3 -0
  14. package/src/cli.js +62 -0
  15. package/src/commands/chat.js +106 -0
  16. package/src/commands/config.js +61 -0
  17. package/src/commands/doctor.js +87 -0
  18. package/src/commands/run.js +64 -0
  19. package/src/commands/skill.js +264 -0
  20. package/src/core/agent-loop.js +281 -0
  21. package/src/core/chat-runtime.js +2075 -0
  22. package/src/core/checkpoint-store.js +66 -0
  23. package/src/core/command-loader.js +201 -0
  24. package/src/core/command-policy.js +71 -0
  25. package/src/core/config-store.js +196 -0
  26. package/src/core/context-compact.js +90 -0
  27. package/src/core/default-system-prompt.js +5 -0
  28. package/src/core/fs-utils.js +16 -0
  29. package/src/core/input-history-store.js +48 -0
  30. package/src/core/input-parser.js +15 -0
  31. package/src/core/paths.js +109 -0
  32. package/src/core/provider/openai-compatible.js +228 -0
  33. package/src/core/session-store.js +178 -0
  34. package/src/core/shell-profile.js +122 -0
  35. package/src/core/shell.js +71 -0
  36. package/src/core/skill-registry.js +55 -0
  37. package/src/core/soul.js +55 -0
  38. package/src/core/task-store.js +116 -0
  39. package/src/core/tools.js +237 -0
  40. package/src/tui/chat-app.js +2007 -0
  41. package/src/tui/input-escape.js +21 -0
@@ -0,0 +1,2075 @@
1
+ import { parseInput } from './input-parser.js';
2
+ import { loadCommandsAndSkills, renderCommandPrompt } from './command-loader.js';
3
+ import { runAgentLoop } from './agent-loop.js';
4
+ import fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import {
7
+ createChatCompletion,
8
+ createChatCompletionStream
9
+ } from './provider/openai-compatible.js';
10
+ import { isDangerousCommand, runShellCommand } from './shell.js';
11
+ import { getBuiltinTools } from './tools.js';
12
+ import { listSessions, loadSession, pruneSessions, saveSession } from './session-store.js';
13
+ import { getConfigValue, loadConfig, resetConfig, setConfigValue } from './config-store.js';
14
+ import { evaluateCommandPolicy } from './command-policy.js';
15
+ import { appendInputHistory, loadInputHistory } from './input-history-store.js';
16
+ import {
17
+ clearTasks,
18
+ createTasks,
19
+ deleteTasks,
20
+ loadTasks,
21
+ updateTask
22
+ } from './task-store.js';
23
+ import { createCheckpoint, listCheckpoints, loadCheckpoint } from './checkpoint-store.js';
24
+ import {
25
+ compactMessagesLocally,
26
+ estimateMessagesTokens,
27
+ parseCompactArgs
28
+ } from './context-compact.js';
29
+ import { buildSystemPromptWithSoul } from './soul.js';
30
+
31
+ function toOpenAIMessages(sessionMessages) {
32
+ const mapped = [];
33
+ for (const msg of sessionMessages) {
34
+ if (msg.role === 'tool') {
35
+ mapped.push({
36
+ role: 'tool',
37
+ content: msg.content,
38
+ tool_call_id: msg.tool_call_id
39
+ });
40
+ continue;
41
+ }
42
+ mapped.push({
43
+ role: msg.role,
44
+ content: msg.content,
45
+ ...(msg.tool_calls ? { tool_calls: msg.tool_calls } : {})
46
+ });
47
+ }
48
+ return mapped;
49
+ }
50
+
51
+ function slugify(input) {
52
+ const base = String(input || '')
53
+ .toLowerCase()
54
+ .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
55
+ .replace(/^-+|-+$/g, '');
56
+ return base || 'untitled';
57
+ }
58
+
59
+ function nowStamp() {
60
+ return new Date().toISOString().replace(/[:.]/g, '-');
61
+ }
62
+
63
+ const SUB_AGENT_ROLES = ['planner', 'coder', 'reviewer', 'tester'];
64
+ const SUB_AGENT_CONTEXT_MAX_MESSAGES = 4;
65
+ const SUB_AGENT_CONTEXT_MAX_CHARS = 1200;
66
+ const SUB_AGENT_EVIDENCE_MAX_ITEMS = 3;
67
+ const SUB_AGENT_HANDOFF_MAX_ITEMS = 6;
68
+ const AUTO_SKILL_NAMES = ['superpowers-lite', 'brainstorming-lite', 'executing-plan-lite'];
69
+
70
+ function getSubAgentRolePrompt(role) {
71
+ if (role === 'planner') {
72
+ return 'You are a planning sub-agent. Produce a concrete implementation plan with risks and verification.';
73
+ }
74
+ if (role === 'reviewer') {
75
+ return [
76
+ 'You are a review sub-agent. Focus on bugs, regressions, edge cases, and missing tests.',
77
+ 'Start with the focused files or directories handed to you. Do not roam unrelated parts of the repo unless the handed-off evidence is insufficient.',
78
+ 'Use this exact output structure:',
79
+ 'Findings:',
80
+ '- <bug, regression, risk, or "none">',
81
+ 'Verified:',
82
+ '- <what you checked>',
83
+ 'Not Verified:',
84
+ '- <what remains uncertain>',
85
+ 'Next Action:',
86
+ '- <single best next step>'
87
+ ].join('\n');
88
+ }
89
+ if (role === 'tester') {
90
+ return [
91
+ 'You are a testing sub-agent. Focus on verification strategy, real test execution evidence, missing coverage, and whether the work was actually validated.',
92
+ 'Prefer running concrete verification commands over only suggesting them.',
93
+ 'Start with the focused files or directories handed to you. Verify those artifacts first before scanning the wider repo.',
94
+ 'Use this exact output structure:',
95
+ 'Verified:',
96
+ '- <commands run and evidence>',
97
+ 'Not Verified:',
98
+ '- <what could not be validated>',
99
+ 'Failures:',
100
+ '- <failed command or "none">',
101
+ 'Next Action:',
102
+ '- <single best next step>'
103
+ ].join('\n');
104
+ }
105
+ return 'You are an execution sub-agent. Produce practical implementation guidance with code-level detail.';
106
+ }
107
+
108
+ function trimInlineText(value, maxLen = 220) {
109
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
110
+ if (!text) return '';
111
+ if (text.length <= maxLen) return text;
112
+ return `${text.slice(0, maxLen - 3)}...`;
113
+ }
114
+
115
+ function buildSubAgentContextPacket(session) {
116
+ const source = Array.isArray(session?.messages) ? session.messages : [];
117
+ const recent = source
118
+ .filter((msg) => msg && (msg.role === 'user' || msg.role === 'assistant'))
119
+ .slice(-SUB_AGENT_CONTEXT_MAX_MESSAGES);
120
+ if (recent.length === 0) return '';
121
+
122
+ const lines = [];
123
+ let usedChars = 0;
124
+ for (const msg of recent) {
125
+ const role = msg.role === 'assistant' ? 'assistant' : 'user';
126
+ const text = trimInlineText(msg.content, 260);
127
+ if (!text) continue;
128
+ const line = `- ${role}: ${text}`;
129
+ if (usedChars + line.length > SUB_AGENT_CONTEXT_MAX_CHARS) break;
130
+ lines.push(line);
131
+ usedChars += line.length;
132
+ }
133
+ if (lines.length === 0) return '';
134
+ return [
135
+ 'Scoped parent context (recent only, not full history):',
136
+ ...lines,
137
+ 'Use this context only if it helps the current task.'
138
+ ].join('\n');
139
+ }
140
+
141
+ function maybePushEvidence(out, seen, filePath, summary) {
142
+ const pathText = trimInlineText(filePath, 160);
143
+ const summaryText = trimInlineText(summary, 200);
144
+ if (!pathText || seen.has(pathText)) return;
145
+ seen.add(pathText);
146
+ out.push(`- ${pathText}${summaryText ? ` :: ${summaryText}` : ''}`);
147
+ }
148
+
149
+ function extractEvidenceFromToolMessage(rawContent, out, seen) {
150
+ if (!rawContent) return;
151
+ let parsed = null;
152
+ try {
153
+ parsed = JSON.parse(String(rawContent));
154
+ } catch {}
155
+
156
+ if (parsed && typeof parsed === 'object') {
157
+ if (parsed.path) {
158
+ const summary = parsed.content || parsed.diff_preview || parsed.stdout || parsed.next || '';
159
+ maybePushEvidence(out, seen, parsed.path, summary);
160
+ }
161
+ const stdout = typeof parsed.stdout === 'string' ? parsed.stdout : '';
162
+ const stderr = typeof parsed.stderr === 'string' ? parsed.stderr : '';
163
+ const merged = `${stdout}\n${stderr}`.trim();
164
+ const matches = merged.matchAll(/(?:^|\s)([A-Za-z0-9_./\\-]+\.[A-Za-z0-9_]+):\d+(?::\d+)?/g);
165
+ for (const match of matches) {
166
+ if (out.length >= SUB_AGENT_EVIDENCE_MAX_ITEMS) break;
167
+ maybePushEvidence(out, seen, match[1], merged);
168
+ }
169
+ return;
170
+ }
171
+
172
+ const text = String(rawContent || '');
173
+ const matches = text.matchAll(/(?:^|\s)([A-Za-z0-9_./\\-]+\.[A-Za-z0-9_]+):\d+(?::\d+)?/g);
174
+ for (const match of matches) {
175
+ if (out.length >= SUB_AGENT_EVIDENCE_MAX_ITEMS) break;
176
+ maybePushEvidence(out, seen, match[1], text);
177
+ }
178
+ }
179
+
180
+ function buildSubAgentEvidencePacket(session) {
181
+ const source = Array.isArray(session?.messages) ? session.messages : [];
182
+ const toolMessages = source.filter((msg) => msg && msg.role === 'tool').slice(-6).reverse();
183
+ const lines = [];
184
+ const seen = new Set();
185
+ for (const msg of toolMessages) {
186
+ extractEvidenceFromToolMessage(msg.content, lines, seen);
187
+ if (lines.length >= SUB_AGENT_EVIDENCE_MAX_ITEMS) break;
188
+ }
189
+ if (lines.length === 0) return '';
190
+ return ['Scoped file evidence (recent tool outputs only):', ...lines].join('\n');
191
+ }
192
+
193
+ function extractLikelyPathsFromText(rawText, out, seen) {
194
+ const text = String(rawText || '');
195
+ if (!text) return;
196
+ const matches = text.matchAll(
197
+ /(?:^|[\s("'`])([A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+|[A-Za-z0-9_.-]+\.[A-Za-z0-9_]+)(?=$|[\s)"'`:,`])/g
198
+ );
199
+ for (const match of matches) {
200
+ const value = String(match[1] || '').replace(/\/+$/, '');
201
+ if (!value || seen.has(value)) continue;
202
+ seen.add(value);
203
+ out.push(value);
204
+ if (out.length >= SUB_AGENT_HANDOFF_MAX_ITEMS) break;
205
+ }
206
+ }
207
+
208
+ function summarizeStepOutput(step) {
209
+ const text = trimInlineText(step?.output || step?.task || '', 220);
210
+ return text || 'No concise output captured.';
211
+ }
212
+
213
+ function collectStepArtifacts(runItems, role) {
214
+ if (!Array.isArray(runItems) || runItems.length === 0) return '';
215
+
216
+ const relevantSteps =
217
+ role === 'reviewer' || role === 'tester'
218
+ ? runItems.filter((step) => step && !step.failed && step.role !== 'reviewer' && step.role !== 'tester')
219
+ : runItems.filter((step) => step && !step.failed);
220
+ if (relevantSteps.length === 0) return '';
221
+
222
+ const focusPaths = [];
223
+ const seenPaths = new Set();
224
+ const summaries = [];
225
+
226
+ for (const step of relevantSteps.slice(-4)) {
227
+ if (Array.isArray(step.artifactPaths)) {
228
+ for (const artifactPath of step.artifactPaths) {
229
+ if (!artifactPath || seenPaths.has(artifactPath)) continue;
230
+ seenPaths.add(artifactPath);
231
+ focusPaths.push(artifactPath);
232
+ if (focusPaths.length >= SUB_AGENT_HANDOFF_MAX_ITEMS) break;
233
+ }
234
+ }
235
+ extractLikelyPathsFromText(step.output, focusPaths, seenPaths);
236
+ const summary = summarizeStepOutput(step);
237
+ summaries.push(`- [${step.role}] ${step.title}: ${summary}`);
238
+ if (focusPaths.length >= SUB_AGENT_HANDOFF_MAX_ITEMS && summaries.length >= 3) break;
239
+ }
240
+
241
+ return { focusPaths, summaries };
242
+ }
243
+
244
+ function buildStepArtifactPacket(runItems, role) {
245
+ const collected = collectStepArtifacts(runItems, role);
246
+ if (!collected) return '';
247
+ const { focusPaths, summaries } = collected;
248
+
249
+ if (focusPaths.length === 0 && summaries.length === 0) return '';
250
+
251
+ const lines = ['Implementation handoff from earlier plan steps:'];
252
+ if (focusPaths.length > 0) {
253
+ lines.push('Focus paths first:');
254
+ for (const value of focusPaths.slice(0, SUB_AGENT_HANDOFF_MAX_ITEMS)) {
255
+ lines.push(`- ${value}`);
256
+ }
257
+ if (role === 'reviewer' || role === 'tester') {
258
+ lines.push('Start with these files/directories before exploring unrelated repo areas.');
259
+ }
260
+ }
261
+ if (summaries.length > 0) {
262
+ lines.push('Prior step summaries:');
263
+ lines.push(...summaries.slice(-3));
264
+ }
265
+ return lines.join('\n');
266
+ }
267
+
268
+ function buildFocusedTaskNote(role, focusPaths) {
269
+ if (!Array.isArray(focusPaths) || focusPaths.length === 0) return '';
270
+ const head = focusPaths.slice(0, 4).join(', ');
271
+ if (role === 'reviewer') {
272
+ return `Focus review on these artifacts first: ${head}. Only inspect unrelated repo areas if these artifacts do not provide enough evidence.`;
273
+ }
274
+ if (role === 'tester') {
275
+ return `Focus verification on these artifacts first: ${head}. Prefer commands and reads that directly validate these paths before wider repo exploration.`;
276
+ }
277
+ return '';
278
+ }
279
+
280
+ async function pathExists(targetPath) {
281
+ try {
282
+ await fs.access(targetPath);
283
+ return true;
284
+ } catch {
285
+ return false;
286
+ }
287
+ }
288
+
289
+ async function readJsonSafe(targetPath) {
290
+ try {
291
+ return JSON.parse(await fs.readFile(targetPath, 'utf8'));
292
+ } catch {
293
+ return null;
294
+ }
295
+ }
296
+
297
+ async function buildTesterVerificationPacket(focusPaths = []) {
298
+ const cwd = process.cwd();
299
+ const primary = [];
300
+ const secondary = [];
301
+ const fallback = [];
302
+ const packageJsonPath = path.join(cwd, 'package.json');
303
+ const pyprojectPath = path.join(cwd, 'pyproject.toml');
304
+ const cargoPath = path.join(cwd, 'Cargo.toml');
305
+ const goModPath = path.join(cwd, 'go.mod');
306
+ const focusTargets = Array.isArray(focusPaths) ? focusPaths.filter(Boolean).slice(0, 4) : [];
307
+
308
+ if (await pathExists(packageJsonPath)) {
309
+ const pkg = await readJsonSafe(packageJsonPath);
310
+ const scripts = pkg?.scripts || {};
311
+ if (typeof scripts.test === 'string' && scripts.test.trim()) {
312
+ primary.push(`- npm test :: package.json script = ${trimInlineText(scripts.test, 140)}`);
313
+ }
314
+ if (typeof scripts.build === 'string' && scripts.build.trim()) {
315
+ secondary.push(`- npm run build :: package.json script = ${trimInlineText(scripts.build, 140)}`);
316
+ }
317
+ if (typeof scripts.lint === 'string' && scripts.lint.trim()) {
318
+ secondary.push(`- npm run lint :: package.json script = ${trimInlineText(scripts.lint, 140)}`);
319
+ }
320
+ fallback.push('- If test/build scripts are not usable, inspect package.json scripts and run the narrowest relevant check.');
321
+ }
322
+
323
+ if (await pathExists(pyprojectPath)) {
324
+ primary.push('- pytest');
325
+ }
326
+ if (await pathExists(cargoPath)) {
327
+ primary.push('- cargo test');
328
+ }
329
+ if (await pathExists(goModPath)) {
330
+ primary.push('- go test ./...');
331
+ }
332
+
333
+ if (primary.length === 0 && secondary.length === 0) {
334
+ return [
335
+ 'Verification guidance:',
336
+ '- No obvious project-level test command was detected automatically.',
337
+ '- Prefer running at least one concrete verification command when possible.',
338
+ '- Fall back to the lightest real check you can justify for the files involved.',
339
+ '- If no runnable checks exist, explicitly say what you tried and what remains unverified.'
340
+ ].join('\n');
341
+ }
342
+
343
+ const lines = [
344
+ 'Verification guidance:',
345
+ 'Prefer executing real verification commands before concluding the work is done.',
346
+ 'Use the strongest available evidence first, then fall back in order.',
347
+ 'Start with artifact-scoped checks for the handed-off files/directories before broad repo discovery.',
348
+ 'Read package.json scripts before inventing commands. If a test or build script exists, prefer that exact script name first.',
349
+ 'Priority order:'
350
+ ];
351
+
352
+ if (focusTargets.length > 0) {
353
+ lines.push('Artifact focus:');
354
+ for (const target of focusTargets) {
355
+ lines.push(`- ${target}`);
356
+ }
357
+ }
358
+
359
+ if (primary.length > 0) {
360
+ lines.push('1. Primary verification commands:');
361
+ lines.push(...primary);
362
+ }
363
+ if (secondary.length > 0) {
364
+ lines.push(`${primary.length > 0 ? '2' : '1'}. Secondary verification commands:`);
365
+ lines.push(...secondary);
366
+ }
367
+ lines.push(`${primary.length > 0 || secondary.length > 0 ? '3' : '2'}. Fallback rules:`);
368
+ lines.push('- If the top command fails because the repo is not set up for it, report that clearly and try the next best command.');
369
+ lines.push('- Prefer narrow checks that mention the handed-off path (for example the target directory or file) before scanning the full repository.');
370
+ lines.push('- Do not use unrelated directories as a starting point if focused artifacts were handed to you.');
371
+ lines.push('- Do not treat ls/find/grep directory discovery as verification evidence by itself.');
372
+ lines.push('- Prefer concrete execution evidence over narrative claims.');
373
+ lines.push('- End with two explicit sections: "Verified" and "Not Verified".');
374
+ lines.push(...fallback);
375
+
376
+ return lines.join('\n');
377
+ }
378
+
379
+ function isSkillEnabled(config, name) {
380
+ return config.skills?.enabled?.[name] !== false;
381
+ }
382
+
383
+ function selectAutoSkillNames(text = '') {
384
+ const input = String(text || '').toLowerCase();
385
+ const selected = ['superpowers-lite'];
386
+ if (
387
+ /(brainstorm|头脑风暴|方案|思路|设计一下|设计方案|怎么做|如何做|approach|options?)/i.test(input)
388
+ ) {
389
+ selected.push('brainstorming-lite');
390
+ }
391
+ if (
392
+ /(按计划|执行计划|继续执行|下一步|implement|execute|carry out|完成验证|verify|plan)/i.test(input)
393
+ ) {
394
+ selected.push('executing-plan-lite');
395
+ }
396
+ return selected;
397
+ }
398
+
399
+ function buildAutoSkillSystemPrompt(baseSystemPrompt, commands, config, text) {
400
+ const selected = selectAutoSkillNames(text).filter((name) => isSkillEnabled(config, name));
401
+ if (selected.length === 0) return baseSystemPrompt;
402
+
403
+ const blocks = [];
404
+ for (const name of selected) {
405
+ const skill = commands.get(name);
406
+ if (!skill || skill.metadata?.type !== 'skill') continue;
407
+ blocks.push(`[Auto skill: ${name}]\n${skill.content}`);
408
+ }
409
+ if (blocks.length === 0) return baseSystemPrompt;
410
+ return `${baseSystemPrompt}\n\n${blocks.join('\n\n')}`;
411
+ }
412
+
413
+ function extractJsonBlock(text) {
414
+ const raw = String(text || '').trim();
415
+ if (!raw) return null;
416
+ try {
417
+ return JSON.parse(raw);
418
+ } catch {}
419
+
420
+ const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
421
+ if (fenced?.[1]) {
422
+ try {
423
+ return JSON.parse(fenced[1]);
424
+ } catch {}
425
+ }
426
+
427
+ const first = raw.indexOf('{');
428
+ const last = raw.lastIndexOf('}');
429
+ if (first !== -1 && last !== -1 && last > first) {
430
+ try {
431
+ return JSON.parse(raw.slice(first, last + 1));
432
+ } catch {}
433
+ }
434
+ return null;
435
+ }
436
+
437
+ function normalizeAutoPlan(parsed, goal) {
438
+ const steps = Array.isArray(parsed?.steps) ? parsed.steps : [];
439
+ const cleaned = steps
440
+ .map((s) => ({
441
+ title: String(s?.title || '').trim(),
442
+ role: String(s?.role || '').trim().toLowerCase(),
443
+ task: String(s?.task || '').trim()
444
+ }))
445
+ .filter((s) => s.title && s.task && SUB_AGENT_ROLES.includes(s.role));
446
+
447
+ const basePlan =
448
+ cleaned.length === 0
449
+ ? {
450
+ summary: `Auto plan for: ${goal}`,
451
+ steps: [
452
+ {
453
+ title: 'Initial analysis',
454
+ role: 'planner',
455
+ task: `Break down and propose implementation steps for: ${goal}`
456
+ }
457
+ ]
458
+ }
459
+ : {
460
+ summary: String(parsed?.summary || `Auto plan for: ${goal}`).trim(),
461
+ steps: cleaned
462
+ };
463
+
464
+ return enforceAutoPlanGuardrailSteps(basePlan, goal);
465
+ }
466
+
467
+ function enforceAutoPlanGuardrailSteps(plan, goal) {
468
+ const source = Array.isArray(plan?.steps) ? plan.steps : [];
469
+ const implementationSteps = source.filter((step) => step.role !== 'reviewer' && step.role !== 'tester');
470
+ const reviewerStep = source.find((step) => step.role === 'reviewer') || {
471
+ title: 'Review implementation',
472
+ role: 'reviewer',
473
+ task: `Review the completed work for: ${goal}. Start with the files and directories produced by earlier implementation steps, then check bugs, regressions, risky assumptions, edge cases, and missing tests.`
474
+ };
475
+ const testerStep = source.find((step) => step.role === 'tester') || {
476
+ title: 'Test and verify',
477
+ role: 'tester',
478
+ task: `Test and verify the completed work for: ${goal}. Start with the artifacts produced by earlier implementation steps, run the most relevant checks available, report concrete evidence, and call out anything still unverified.`
479
+ };
480
+
481
+ return {
482
+ summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
483
+ steps: [...implementationSteps.slice(0, 6), reviewerStep, testerStep]
484
+ };
485
+ }
486
+
487
+ function looksLikeSuccessfulStepOutput(text = '') {
488
+ const value = String(text || '').trim();
489
+ if (!value) return false;
490
+ if (/(^|\n)\s*(error|failures?)\s*:\s*(?!none\b)/i.test(value)) return false;
491
+ if (/(^|\n)\s*next action\s*:\s*-\s*retry\b/i.test(value)) return false;
492
+ return true;
493
+ }
494
+
495
+ function buildAutoPlanSystemSummary(auto) {
496
+ const statusTitle =
497
+ auto.failedCount > 0 ? 'Auto plan finished with failures' : auto.warningCount > 0 ? 'Auto plan finished with warnings' : 'Auto plan finished';
498
+ const lines = [
499
+ statusTitle,
500
+ `File: ${auto.filePath}`,
501
+ `Summary: ${auto.summary || '-'}`,
502
+ `Steps: ${auto.steps.length} total`,
503
+ `Completed: ${auto.completedCount}`,
504
+ `Warnings: ${auto.warningCount}`,
505
+ `Failed: ${auto.failedCount}`
506
+ ];
507
+ if (auto.warningTitles?.length) {
508
+ lines.push(`Warning steps: ${auto.warningTitles.slice(0, 5).join(', ')}`);
509
+ }
510
+ if (auto.failedTitles?.length) {
511
+ lines.push(`Failed steps: ${auto.failedTitles.slice(0, 5).join(', ')}`);
512
+ }
513
+ return lines.join('\n');
514
+ }
515
+
516
+ async function writeMarkdownInCoderDir(subDir, title, body, fallbackName, sessionId) {
517
+ const parts = [process.cwd(), '.coder', subDir];
518
+ if (sessionId) parts.push(String(sessionId));
519
+ const dir = path.join(...parts);
520
+ await fs.mkdir(dir, { recursive: true });
521
+ const slug = slugify(title).slice(0, 64);
522
+ const fileName = `${nowStamp()}-${slug || fallbackName}.md`;
523
+ const filePath = path.join(dir, fileName);
524
+ await fs.writeFile(filePath, `${body.trim()}\n`, 'utf8');
525
+ return filePath;
526
+ }
527
+
528
+ function buildSpecTemplate(topic) {
529
+ return `
530
+ # Spec: ${topic}
531
+
532
+ ## 1. Background
533
+ - Why this work is needed
534
+ - Existing pain points
535
+
536
+ ## 2. Goals
537
+ - Primary goal
538
+ - Non-goals
539
+
540
+ ## 3. Scope
541
+ - In scope
542
+ - Out of scope
543
+
544
+ ## 4. Requirements
545
+ - Functional requirements
546
+ - Non-functional requirements
547
+ - Win10 compatibility requirements
548
+
549
+ ## 5. Design
550
+ - Architecture sketch
551
+ - Data flow
552
+ - Key interfaces/commands
553
+
554
+ ## 6. Risks and Mitigations
555
+ - Risk
556
+ - Mitigation
557
+
558
+ ## 7. Validation
559
+ - Test strategy
560
+ - Acceptance checklist
561
+ `;
562
+ }
563
+
564
+ function extractSpecTitle(specText, fallback = 'spec') {
565
+ const raw = String(specText || '');
566
+ const heading = raw.match(/^#\s+Spec:\s+(.+)$/m) || raw.match(/^#\s+(.+)$/m);
567
+ return String(heading?.[1] || fallback).trim();
568
+ }
569
+
570
+ async function buildSpecWithModel({
571
+ topic,
572
+ config,
573
+ model,
574
+ systemPrompt
575
+ }) {
576
+ const prompt = [
577
+ 'Write a practical engineering spec in markdown.',
578
+ 'Use these sections exactly:',
579
+ '# Spec: <title>',
580
+ '## 1. Background',
581
+ '## 2. Goals',
582
+ '## 3. Scope',
583
+ '## 4. Requirements',
584
+ '## 5. Design',
585
+ '## 6. Risks and Mitigations',
586
+ '## 7. Validation',
587
+ 'Make it implementation-oriented and suitable for a Win10-first internal coding CLI.'
588
+ ].join('\n');
589
+
590
+ const result = await createChatCompletion({
591
+ baseUrl: config.gateway.base_url,
592
+ apiKey: config.gateway.api_key,
593
+ model: model || config.model.name,
594
+ messages: [
595
+ { role: 'system', content: `${systemPrompt}\n${prompt}` },
596
+ { role: 'user', content: `Topic: ${topic}` }
597
+ ],
598
+ timeoutMs: config.gateway.timeout_ms || 90000,
599
+ maxRetries: config.gateway.max_retries ?? 2
600
+ });
601
+ return String(result.text || '').trim();
602
+ }
603
+
604
+ function buildPlanTemplate(goal) {
605
+ return `
606
+ # Plan: ${goal}
607
+
608
+ ## Phase 1: Discovery
609
+ 1. Confirm constraints and environment assumptions
610
+ 2. Inspect related modules and dependencies
611
+ 3. Define verification approach
612
+
613
+ ## Phase 2: Implementation
614
+ 1. Implement core flow
615
+ 2. Integrate with existing command/runtime paths
616
+ 3. Add guards for Win10-specific behavior
617
+
618
+ ## Phase 3: Verification
619
+ 1. Run automated tests
620
+ 2. Run manual TUI validation
621
+ 3. Document usage and rollback steps
622
+
623
+ ## Task Breakdown
624
+ - [ ] Task A
625
+ - [ ] Task B
626
+ - [ ] Task C
627
+ `;
628
+ }
629
+
630
+ async function buildPlanFromSpecWithModel({
631
+ specText,
632
+ specPath,
633
+ config,
634
+ model,
635
+ systemPrompt
636
+ }) {
637
+ const prompt = [
638
+ 'Convert the provided engineering spec into an implementation plan in markdown.',
639
+ 'Use this structure exactly:',
640
+ '# Plan: <title>',
641
+ '## Phase 1: Discovery',
642
+ '## Phase 2: Implementation',
643
+ '## Phase 3: Verification',
644
+ '## Task Breakdown',
645
+ 'Make the plan concrete and ordered for a coding agent.'
646
+ ].join('\n');
647
+
648
+ const result = await createChatCompletion({
649
+ baseUrl: config.gateway.base_url,
650
+ apiKey: config.gateway.api_key,
651
+ model: model || config.model.name,
652
+ messages: [
653
+ { role: 'system', content: `${systemPrompt}\n${prompt}` },
654
+ {
655
+ role: 'user',
656
+ content: `Spec path: ${specPath || '(inline)'}\n\n${specText}`
657
+ }
658
+ ],
659
+ timeoutMs: config.gateway.timeout_ms || 90000,
660
+ maxRetries: config.gateway.max_retries ?? 2
661
+ });
662
+ return String(result.text || '').trim();
663
+ }
664
+
665
+ function clampRange(start, end, max) {
666
+ const s = Math.max(1, Math.min(start, max));
667
+ const e = Math.max(s, Math.min(end, max));
668
+ return { s, e };
669
+ }
670
+
671
+ function effectiveMaxContextTokens(config) {
672
+ const modelCap = Number(config.model?.max_context_tokens);
673
+ if (Number.isFinite(modelCap) && modelCap > 0) return modelCap;
674
+ const legacy = Number(config.context?.max_tokens);
675
+ if (Number.isFinite(legacy) && legacy > 0) return legacy;
676
+ return 32000;
677
+ }
678
+
679
+ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMode }) {
680
+ return {
681
+ sessionId: currentSession?.id || '',
682
+ mode: executionMode || config.execution?.mode || 'auto',
683
+ model: model || config.model?.name || '',
684
+ maxContextTokens: effectiveMaxContextTokens(config)
685
+ };
686
+ }
687
+
688
+ function estimatePromptTokensForRequest(sessionMessages, userText = '') {
689
+ const tokenMsgs = [
690
+ ...(Array.isArray(sessionMessages) ? sessionMessages : []),
691
+ { role: 'user', content: String(userText || '') }
692
+ ];
693
+ return estimateMessagesTokens(tokenMsgs);
694
+ }
695
+
696
+ function stampedMessage(role, content, extra = {}) {
697
+ return {
698
+ role,
699
+ content,
700
+ at: new Date().toISOString(),
701
+ ...extra
702
+ };
703
+ }
704
+
705
+ async function resolveSpecPath(rawArg = '', sessionId = '') {
706
+ const input = String(rawArg || '').trim();
707
+ const roots = [
708
+ path.join(process.cwd(), '.coder', 'specs', String(sessionId || '')),
709
+ path.join(process.cwd(), '.coder', 'specs')
710
+ ];
711
+
712
+ if (input) {
713
+ const direct = path.resolve(process.cwd(), input);
714
+ try {
715
+ await fs.access(direct);
716
+ return direct;
717
+ } catch {}
718
+
719
+ for (const root of roots) {
720
+ try {
721
+ const entries = await fs.readdir(root);
722
+ const match = entries.find((name) => name.endsWith('.md') && name.includes(input));
723
+ if (match) return path.join(root, match);
724
+ } catch {
725
+ continue;
726
+ }
727
+ }
728
+ }
729
+
730
+ for (const root of roots) {
731
+ try {
732
+ const latest = (await fs.readdir(root))
733
+ .filter((name) => name.endsWith('.md'))
734
+ .sort()
735
+ .reverse()[0];
736
+ if (latest) return path.join(root, latest);
737
+ } catch {
738
+ continue;
739
+ }
740
+ }
741
+ return '';
742
+ }
743
+
744
+ async function expandFileMentions(rawText, workspaceRoot = process.cwd()) {
745
+ const text = String(rawText || '');
746
+ const mentionRegex = /@([A-Za-z0-9_./\\-]+)(?::(\d+)-(\d+))?/g;
747
+ const matches = Array.from(text.matchAll(mentionRegex));
748
+ if (matches.length === 0) return text;
749
+
750
+ let out = text;
751
+ for (const m of matches) {
752
+ const full = m[0];
753
+ const relPath = m[1];
754
+ const a = m[2] ? Number(m[2]) : null;
755
+ const b = m[3] ? Number(m[3]) : null;
756
+ const abs = path.resolve(workspaceRoot, relPath);
757
+ if (!abs.startsWith(path.resolve(workspaceRoot))) continue;
758
+ try {
759
+ const content = await fs.readFile(abs, 'utf8');
760
+ let snippet = content;
761
+ if (a && b) {
762
+ const lines = content.split('\n');
763
+ const { s, e } = clampRange(a, b, lines.length);
764
+ snippet = lines.slice(s - 1, e).join('\n');
765
+ }
766
+ const replacement = `\n[FILE:${relPath}${a && b ? `:${a}-${b}` : ''}]\n${snippet}\n[/FILE]\n`;
767
+ out = out.replace(full, replacement);
768
+ } catch {
769
+ continue;
770
+ }
771
+ }
772
+ return out;
773
+ }
774
+
775
+ async function askModel({
776
+ text,
777
+ session,
778
+ config,
779
+ model,
780
+ systemPrompt,
781
+ onAgentEvent,
782
+ persistSession = true,
783
+ executionMode,
784
+ alwaysAllowTools
785
+ }) {
786
+ const maxContextTokens = effectiveMaxContextTokens(config);
787
+ const triggerPct = Number(config.context?.preflight_trigger_pct || 92);
788
+ const hardPct = Number(config.context?.hard_limit_pct || 98);
789
+ const preflightTokens = estimatePromptTokensForRequest(session.messages, text);
790
+ const preflightPct = (preflightTokens / maxContextTokens) * 100;
791
+
792
+ if (preflightPct >= triggerPct) {
793
+ const auto = compactMessagesLocally(session.messages, {
794
+ mode: preflightPct >= hardPct ? 'aggressive' : 'conservative'
795
+ });
796
+ if (auto.changed) {
797
+ session.messages = auto.compacted.map((m) => ({ ...m, at: new Date().toISOString() }));
798
+ await saveSession(session);
799
+ if (onAgentEvent) {
800
+ onAgentEvent({
801
+ type: 'compact:auto',
802
+ mode: preflightPct >= hardPct ? 'aggressive' : 'conservative',
803
+ threshold: Math.round(preflightPct)
804
+ });
805
+ }
806
+ }
807
+ }
808
+
809
+ let saveTimer = null;
810
+ let saveResolver = null;
811
+ let savePromise = null;
812
+ const scheduleSessionSave = () => {
813
+ if (!persistSession) return;
814
+ if (saveTimer) return;
815
+ savePromise = new Promise((resolve) => {
816
+ saveResolver = resolve;
817
+ });
818
+ saveTimer = setTimeout(async () => {
819
+ const done = saveResolver;
820
+ saveTimer = null;
821
+ saveResolver = null;
822
+ try {
823
+ await saveSession(session);
824
+ } finally {
825
+ if (done) done();
826
+ savePromise = null;
827
+ }
828
+ }, 400);
829
+ };
830
+ const flushScheduledSave = async () => {
831
+ if (!persistSession) return;
832
+ if (saveTimer) {
833
+ clearTimeout(saveTimer);
834
+ const done = saveResolver;
835
+ saveTimer = null;
836
+ saveResolver = null;
837
+ savePromise = null;
838
+ await saveSession(session);
839
+ if (done) done();
840
+ return;
841
+ }
842
+ if (savePromise) await savePromise;
843
+ };
844
+
845
+ if (persistSession && text) {
846
+ session.messages.push(stampedMessage('user', text));
847
+ await saveSession(session);
848
+ }
849
+
850
+ const { definitions, handlers } = getBuiltinTools({
851
+ workspaceRoot: process.cwd(),
852
+ config,
853
+ sessionId: session.id
854
+ });
855
+
856
+ let activeAssistantIndex = -1;
857
+ const wrappedAgentEvent = (event) => {
858
+ if (!persistSession) {
859
+ if (onAgentEvent) onAgentEvent(event);
860
+ return;
861
+ }
862
+
863
+ if (event?.type === 'assistant:start') {
864
+ session.messages.push(stampedMessage('assistant', ''));
865
+ activeAssistantIndex = session.messages.length - 1;
866
+ scheduleSessionSave();
867
+ } else if (event?.type === 'assistant:delta') {
868
+ if (activeAssistantIndex >= 0 && session.messages[activeAssistantIndex]) {
869
+ const current = session.messages[activeAssistantIndex];
870
+ current.content = `${current.content || ''}${event.text || ''}`;
871
+ current.at = new Date().toISOString();
872
+ scheduleSessionSave();
873
+ }
874
+ } else if (event?.type === 'assistant:response') {
875
+ if (activeAssistantIndex >= 0 && session.messages[activeAssistantIndex]) {
876
+ const current = session.messages[activeAssistantIndex];
877
+ current.content = event.assistantMessage?.content ?? event.text ?? current.content;
878
+ if (Array.isArray(event.assistantMessage?.tool_calls) && event.assistantMessage.tool_calls.length > 0) {
879
+ current.tool_calls = event.assistantMessage.tool_calls;
880
+ }
881
+ current.at = new Date().toISOString();
882
+ scheduleSessionSave();
883
+ }
884
+ activeAssistantIndex = -1;
885
+ } else if (event?.type === 'tool:result') {
886
+ session.messages.push(
887
+ stampedMessage('tool', event.content || '', {
888
+ tool_call_id: event.id || ''
889
+ })
890
+ );
891
+ scheduleSessionSave();
892
+ }
893
+
894
+ if (onAgentEvent) onAgentEvent(event);
895
+ };
896
+
897
+ const loopUserPrompt = persistSession ? '' : text;
898
+ const loopResult = await runAgentLoop({
899
+ systemPrompt,
900
+ userPrompt: loopUserPrompt,
901
+ model: model || config.model.name,
902
+ maxSteps: Number(config.execution?.max_steps || 16),
903
+ toolDefinitions: definitions,
904
+ toolHandlers: handlers,
905
+ initialMessages: toOpenAIMessages(session.messages),
906
+ onEvent: wrappedAgentEvent,
907
+ executionMode: executionMode || config.execution?.mode || 'auto',
908
+ alwaysAllowTools:
909
+ alwaysAllowTools || config.execution?.always_allow_tools || ['run_command', 'read_file', 'write_file'],
910
+ toolResultMaxChars: config.context?.tool_result_max_chars || 12000,
911
+ requestCompletion: async ({ messages, tools, model: selectedModel }) => {
912
+ if (onAgentEvent) onAgentEvent({ type: 'assistant:start' });
913
+ return createChatCompletionStream({
914
+ baseUrl: config.gateway.base_url,
915
+ apiKey: config.gateway.api_key,
916
+ model: selectedModel,
917
+ messages,
918
+ tools,
919
+ timeoutMs: config.gateway.timeout_ms || 90000,
920
+ maxRetries: config.gateway.max_retries ?? 2,
921
+ onTextDelta: (delta) => {
922
+ if (onAgentEvent) onAgentEvent({ type: 'assistant:delta', text: delta });
923
+ }
924
+ });
925
+ }
926
+ });
927
+
928
+ if (persistSession) {
929
+ session.messages = loopResult.messages
930
+ .filter((m) => m.role !== 'system')
931
+ .map((m) => ({ ...m, at: new Date().toISOString() }));
932
+ await flushScheduledSave();
933
+ await saveSession(session);
934
+ try {
935
+ await pruneSessions(config.sessions || {});
936
+ } catch {
937
+ // keep chat usable even if pruning fails
938
+ }
939
+ }
940
+ return { text: loopResult.text };
941
+ }
942
+
943
+ async function runSubAgentTask({
944
+ role,
945
+ task,
946
+ priorSteps = [],
947
+ parentSession,
948
+ config,
949
+ model,
950
+ systemPrompt,
951
+ onAgentEvent
952
+ }) {
953
+ const subSession = { id: `sub-${Date.now()}`, messages: [] };
954
+ const rolePrompt = getSubAgentRolePrompt(role);
955
+ const contextPacket = buildSubAgentContextPacket(parentSession);
956
+ const evidencePacket = buildSubAgentEvidencePacket(parentSession);
957
+ const handoffPacket = buildStepArtifactPacket(priorSteps, role);
958
+ const handoffFocusPaths = collectStepArtifacts(priorSteps, role)?.focusPaths || [];
959
+ const focusedTaskNote = buildFocusedTaskNote(role, handoffFocusPaths);
960
+ const verificationPacket = role === 'tester' ? await buildTesterVerificationPacket(handoffFocusPaths) : '';
961
+ const scopedTask = [contextPacket, evidencePacket, handoffPacket, verificationPacket, focusedTaskNote, 'Task:', task]
962
+ .filter(Boolean)
963
+ .join('\n\n');
964
+ let blockedCount = 0;
965
+ let toolErrorCount = 0;
966
+ const artifactPaths = [];
967
+ const seenArtifactPaths = new Set();
968
+ const wrappedOnAgentEvent = (evt) => {
969
+ if (evt?.type === 'tool:blocked') blockedCount += 1;
970
+ if (evt?.type === 'tool:error') toolErrorCount += 1;
971
+ if (evt?.type === 'tool:result' && evt.content) {
972
+ try {
973
+ const parsed = JSON.parse(String(evt.content));
974
+ if (parsed?.path) {
975
+ const artifactPath = String(parsed.path);
976
+ if (!seenArtifactPaths.has(artifactPath)) {
977
+ seenArtifactPaths.add(artifactPath);
978
+ artifactPaths.push(artifactPath);
979
+ }
980
+ }
981
+ if (typeof parsed?.stdout === 'string') {
982
+ extractLikelyPathsFromText(parsed.stdout, artifactPaths, seenArtifactPaths);
983
+ }
984
+ } catch {}
985
+ }
986
+ if (onAgentEvent) onAgentEvent(evt);
987
+ };
988
+ const subResult = await askModel({
989
+ text: scopedTask,
990
+ session: subSession,
991
+ config,
992
+ model,
993
+ systemPrompt: `${systemPrompt}\n${rolePrompt}`,
994
+ onAgentEvent: wrappedOnAgentEvent,
995
+ persistSession: false,
996
+ executionMode: 'auto'
997
+ });
998
+ const text = subResult.text || '';
999
+ const hasErrorLine = /(^|\n)\s*error\s*:/i.test(text);
1000
+ return {
1001
+ text,
1002
+ blockedCount,
1003
+ toolErrorCount,
1004
+ hasErrorLine,
1005
+ artifactPaths: artifactPaths.slice(0, SUB_AGENT_HANDOFF_MAX_ITEMS)
1006
+ };
1007
+ }
1008
+
1009
+ async function buildAutoPlanAndRun({
1010
+ goal,
1011
+ session,
1012
+ config,
1013
+ model,
1014
+ systemPrompt,
1015
+ onAgentEvent,
1016
+ sessionId
1017
+ }) {
1018
+ const plannerPrompt =
1019
+ 'Return strict JSON only with shape {"summary":"...","steps":[{"title":"...","role":"planner|coder|reviewer|tester","task":"..."}]}. No markdown. Always include final reviewer and tester steps.';
1020
+ let autoPlan = {
1021
+ summary: `Auto plan for: ${goal}`,
1022
+ steps: [
1023
+ {
1024
+ title: 'Initial analysis',
1025
+ role: 'planner',
1026
+ task: `Break down and propose implementation steps for: ${goal}`
1027
+ }
1028
+ ]
1029
+ };
1030
+ let planningError = '';
1031
+ try {
1032
+ const planning = await createChatCompletion({
1033
+ baseUrl: config.gateway.base_url,
1034
+ apiKey: config.gateway.api_key,
1035
+ model: model || config.model.name,
1036
+ messages: [
1037
+ { role: 'system', content: `${systemPrompt}\n${plannerPrompt}` },
1038
+ {
1039
+ role: 'user',
1040
+ content: `Create an execution plan and assign best sub-agent role for each step.\nGoal: ${goal}\nThe final steps must include review and testing/verification.`
1041
+ }
1042
+ ],
1043
+ timeoutMs: config.gateway.timeout_ms || 90000,
1044
+ maxRetries: config.gateway.max_retries ?? 2
1045
+ });
1046
+ const parsed = extractJsonBlock(planning.text || '');
1047
+ autoPlan = normalizeAutoPlan(parsed, goal);
1048
+ } catch (err) {
1049
+ planningError = String(err?.message || err || 'planning failed');
1050
+ }
1051
+
1052
+ const runItems = [];
1053
+ for (let i = 0; i < autoPlan.steps.length; i += 1) {
1054
+ const step = autoPlan.steps[i];
1055
+ if (onAgentEvent) {
1056
+ onAgentEvent({
1057
+ type: 'assistant:delta',
1058
+ text: `\n[plan] Step ${i + 1}/${autoPlan.steps.length} -> ${step.role}: ${step.title}\n`
1059
+ });
1060
+ }
1061
+ try {
1062
+ const stepResult = await runSubAgentTask({
1063
+ role: step.role,
1064
+ task: step.task,
1065
+ priorSteps: runItems,
1066
+ parentSession: session,
1067
+ config,
1068
+ model,
1069
+ systemPrompt,
1070
+ onAgentEvent
1071
+ });
1072
+ const outputLooksSuccessful = looksLikeSuccessfulStepOutput(stepResult.text);
1073
+ const warningParts = [];
1074
+ if (stepResult.blockedCount > 0) warningParts.push(`${stepResult.blockedCount} blocked tool call(s)`);
1075
+ if (stepResult.toolErrorCount > 0) warningParts.push(`${stepResult.toolErrorCount} tool error(s)`);
1076
+ const warning = warningParts.length > 0 ? `sub-agent recovered after ${warningParts.join(', ')}` : '';
1077
+ const failed = stepResult.hasErrorLine || (!outputLooksSuccessful && (stepResult.blockedCount > 0 || stepResult.toolErrorCount > 0));
1078
+ let error = '';
1079
+ if (stepResult.hasErrorLine) {
1080
+ error = 'sub-agent output contains error line(s)';
1081
+ } else if (failed && stepResult.blockedCount > 0) {
1082
+ error = `sub-agent ended with ${stepResult.blockedCount} blocked tool call(s)`;
1083
+ } else if (failed && stepResult.toolErrorCount > 0) {
1084
+ error = `sub-agent ended with ${stepResult.toolErrorCount} tool error(s)`;
1085
+ }
1086
+ runItems.push({
1087
+ ...step,
1088
+ output: stepResult.text,
1089
+ error,
1090
+ warning,
1091
+ failed,
1092
+ artifactPaths: stepResult.artifactPaths || []
1093
+ });
1094
+ } catch (err) {
1095
+ runItems.push({
1096
+ ...step,
1097
+ output: '',
1098
+ error: String(err?.message || err || 'sub-agent step failed'),
1099
+ warning: '',
1100
+ failed: true
1101
+ });
1102
+ }
1103
+ }
1104
+
1105
+ const failedItems = runItems.filter((s) => s.failed || s.error);
1106
+ const warningItems = runItems.filter((s) => !s.failed && s.warning);
1107
+ const completedItems = runItems.filter((s) => !s.failed);
1108
+
1109
+ const lines = [];
1110
+ lines.push(`# Auto Plan: ${goal}`);
1111
+ lines.push('');
1112
+ lines.push(`## Summary`);
1113
+ lines.push(autoPlan.summary || `Auto plan for: ${goal}`);
1114
+ if (planningError) {
1115
+ lines.push('');
1116
+ lines.push(`Planning Error: ${planningError}`);
1117
+ }
1118
+ lines.push('');
1119
+ lines.push('## Steps');
1120
+ autoPlan.steps.forEach((s, idx) => {
1121
+ lines.push(`${idx + 1}. [${s.role}] ${s.title}`);
1122
+ lines.push(` - task: ${s.task}`);
1123
+ });
1124
+ lines.push('');
1125
+ lines.push('## Sub-Agent Outputs');
1126
+ runItems.forEach((s, idx) => {
1127
+ lines.push(`### ${idx + 1}. [${s.role}] ${s.title}`);
1128
+ if (s.error) {
1129
+ lines.push(`Error: ${s.error}`);
1130
+ if (s.output) {
1131
+ lines.push('');
1132
+ lines.push(s.output);
1133
+ }
1134
+ lines.push('');
1135
+ return;
1136
+ }
1137
+ if (s.warning) {
1138
+ lines.push(`Note: ${s.warning}`);
1139
+ lines.push('');
1140
+ }
1141
+ lines.push(s.output || '(empty)');
1142
+ lines.push('');
1143
+ });
1144
+
1145
+ const filePath = await writeMarkdownInCoderDir(
1146
+ 'plans',
1147
+ `${goal}-auto`,
1148
+ lines.join('\n'),
1149
+ 'plan-auto',
1150
+ sessionId
1151
+ );
1152
+ return {
1153
+ filePath,
1154
+ summary: autoPlan.summary,
1155
+ steps: autoPlan.steps,
1156
+ completedCount: completedItems.length,
1157
+ warningCount: warningItems.length,
1158
+ failedCount: failedItems.length,
1159
+ warningTitles: warningItems.map((s) => `${s.role}:${s.title}`),
1160
+ failedTitles: failedItems.map((s) => `${s.role}:${s.title}`)
1161
+ };
1162
+ }
1163
+
1164
+ async function handleShellInput(shellText, config) {
1165
+ if (!shellText) return { text: '' };
1166
+ if (
1167
+ !config.policy.allow_dangerous_commands &&
1168
+ isDangerousCommand(shellText, config.policy.blocked_command_patterns)
1169
+ ) {
1170
+ return { text: 'Blocked by policy: dangerous command pattern detected' };
1171
+ }
1172
+ const check = evaluateCommandPolicy(shellText, config, process.cwd());
1173
+ if (!check.allowed) {
1174
+ return { text: `Blocked by safe mode: ${check.reason}${check.suggestion ? ` | ${check.suggestion}` : ''}` };
1175
+ }
1176
+ const result = await runShellCommand({
1177
+ command: shellText,
1178
+ shell: config.shell.default,
1179
+ timeoutMs: config.shell.timeout_ms
1180
+ });
1181
+ const chunks = [];
1182
+ if (result.stdout.trim()) chunks.push(result.stdout.trimEnd());
1183
+ if (result.stderr.trim()) chunks.push(result.stderr.trimEnd());
1184
+ if (result.code !== 0) chunks.push(`exit code: ${result.code}`);
1185
+ return { text: chunks.join('\n') };
1186
+ }
1187
+
1188
+ export async function createChatRuntime({
1189
+ session,
1190
+ config: initialConfig,
1191
+ model,
1192
+ systemPrompt
1193
+ }) {
1194
+ let currentSession = session;
1195
+ let config = initialConfig;
1196
+ const baseSystemPrompt = systemPrompt;
1197
+ let executionMode = config.execution?.mode || 'auto';
1198
+ const commands = await loadCommandsAndSkills();
1199
+ const compactState = {
1200
+ backupMessages: null,
1201
+ autoEnabled: true,
1202
+ threshold: 60,
1203
+ mode: 'conservative'
1204
+ };
1205
+ let historyIdCache = [currentSession.id];
1206
+ let historySessionCache = [
1207
+ {
1208
+ id: currentSession.id,
1209
+ messageCount: Array.isArray(currentSession.messages) ? currentSession.messages.length : 0
1210
+ }
1211
+ ];
1212
+
1213
+ const configKeyHints = [
1214
+ 'gateway.base_url',
1215
+ 'gateway.api_key',
1216
+ 'gateway.timeout_ms',
1217
+ 'gateway.max_retries',
1218
+ 'model.name',
1219
+ 'model.max_context_tokens',
1220
+ 'execution.mode',
1221
+ 'execution.always_allow_tools',
1222
+ 'execution.max_steps',
1223
+ 'context.preflight_trigger_pct',
1224
+ 'context.hard_limit_pct',
1225
+ 'context.tool_result_max_chars',
1226
+ 'context.read_file_default_lines',
1227
+ 'context.read_file_max_chars',
1228
+ 'sessions.max_sessions',
1229
+ 'sessions.retention_days',
1230
+ 'shell.default',
1231
+ 'shell.timeout_ms',
1232
+ 'context.max_tokens',
1233
+ 'policy.safe_mode',
1234
+ 'policy.allow_dangerous_commands'
1235
+ ];
1236
+
1237
+ const listCommandNames = () => {
1238
+ const builtins = [
1239
+ { name: 'help', description: 'show chat help' },
1240
+ { name: 'exit', description: 'exit chat' },
1241
+ { name: 'commands', description: 'list slash/custom commands' },
1242
+ { name: 'status', description: 'show runtime status (mode/model/session)' },
1243
+ { name: 'mode', description: 'set execution mode: normal|auto|plan' },
1244
+ { name: 'compact', description: 'compress message context' },
1245
+ { name: 'tasks', description: 'task board management' },
1246
+ { name: 'checkpoint', description: 'create/list/load conversation checkpoints' },
1247
+ { name: 'spec', description: 'create a spec markdown file in .coder/specs' },
1248
+ { name: 'plan', description: 'create an implementation plan markdown file in .coder/plans' },
1249
+ { name: 'agents', description: 'run/list sub-agent roles' },
1250
+ { name: 'config', description: 'set/get/list/reset config values' },
1251
+ { name: 'history', description: 'list/resume sessions' },
1252
+ { name: 'debug', description: 'runtime debug switches' },
1253
+ { name: 'retry', description: 'retry the last user request' }
1254
+ ];
1255
+ const out = [];
1256
+ for (const cmd of commands.values()) {
1257
+ if (cmd.metadata.type === 'skill' && config.skills?.enabled?.[cmd.name] === false) {
1258
+ continue;
1259
+ }
1260
+ out.push({
1261
+ name: cmd.name,
1262
+ description: cmd.metadata.description || ''
1263
+ });
1264
+ }
1265
+ return [...builtins, ...out].sort((a, b) => a.name.localeCompare(b.name));
1266
+ };
1267
+
1268
+ const compactOptions = [
1269
+ '--preview',
1270
+ '--restore',
1271
+ '--aggressive',
1272
+ '--conservative',
1273
+ '--default',
1274
+ '--auto-on',
1275
+ '--auto-off',
1276
+ '--threshold 60'
1277
+ ];
1278
+
1279
+ const configTemplates = [
1280
+ '/config list',
1281
+ '/config get <key>',
1282
+ '/config set <key> <value>',
1283
+ '/config reset'
1284
+ ];
1285
+
1286
+ const historyTemplates = ['/history list', '/history current', '/history resume <session_id>'];
1287
+ const modeTemplates = ['/mode normal', '/mode auto', '/mode plan'];
1288
+ const taskTemplates = ['/tasks', '/tasks add <title>', '/tasks start <id>', '/tasks done <id>', '/tasks remove <id>', '/tasks clear'];
1289
+ const checkpointTemplates = [
1290
+ '/checkpoint create <name>',
1291
+ '/checkpoint list',
1292
+ '/checkpoint list --all',
1293
+ '/checkpoint load <id>'
1294
+ ];
1295
+ const specTemplates = ['/spec <topic>'];
1296
+ const planTemplates = ['/plan <goal>', '/plan auto <goal>', '/plan from-spec <spec-path?>'];
1297
+ const agentTemplates = ['/agents list', '/agents run planner <task>', '/agents run coder <task>', '/agents run reviewer <task>', '/agents run tester <task>'];
1298
+ const debugTemplates = ['/debug keys on', '/debug keys off', '/debug keys status'];
1299
+ const compactTemplates = compactOptions.map((opt) => `/compact ${opt}`);
1300
+ const slashTemplates = [
1301
+ ...configTemplates,
1302
+ ...historyTemplates,
1303
+ ...modeTemplates,
1304
+ ...taskTemplates,
1305
+ ...checkpointTemplates,
1306
+ ...specTemplates,
1307
+ ...planTemplates,
1308
+ ...agentTemplates,
1309
+ ...debugTemplates,
1310
+ ...compactTemplates,
1311
+ '/retry',
1312
+ '/status'
1313
+ ];
1314
+ const compactKey = (value) => String(value || '').toLowerCase().replace(/[\/\s<>?]/g, '');
1315
+ const matchCompactTemplates = (value) => {
1316
+ const needle = compactKey(value);
1317
+ if (!needle) return [];
1318
+ return slashTemplates.filter((template) => compactKey(template).startsWith(needle));
1319
+ };
1320
+
1321
+ const getCompletionOptions = (rawInput) => {
1322
+ const input = String(rawInput || '');
1323
+ if (!input.startsWith('/')) return [];
1324
+
1325
+ const hasTrailingSpace = /\s$/.test(input);
1326
+ const body = input.slice(1);
1327
+ const tokens = body.trim().split(/\s+/).filter(Boolean);
1328
+ const commandPart = tokens[0] || '';
1329
+
1330
+ const allCommands = listCommandNames().map((c) => c.name);
1331
+
1332
+ if (!commandPart) {
1333
+ return allCommands.map((name) => `/${name}`);
1334
+ }
1335
+
1336
+ if (tokens.length === 1 && !hasTrailingSpace) {
1337
+ const direct = allCommands
1338
+ .filter((name) => name.startsWith(commandPart))
1339
+ .map((name) => `/${name}`);
1340
+ if (direct.length > 0) return direct;
1341
+ return matchCompactTemplates(input);
1342
+ }
1343
+
1344
+ if (commandPart === 'config') {
1345
+ if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
1346
+ const sub = tokens[1] || '';
1347
+ return ['list', 'get', 'set', 'reset']
1348
+ .filter((s) => s.startsWith(sub))
1349
+ .map((s) => `/config ${s}`);
1350
+ }
1351
+
1352
+ const sub = tokens[1] || '';
1353
+ if (sub === 'get') {
1354
+ const keyPrefix = tokens[2] || '';
1355
+ return configKeyHints
1356
+ .filter((k) => k.startsWith(keyPrefix))
1357
+ .map((k) => `/config get ${k}`);
1358
+ }
1359
+ if (sub === 'set') {
1360
+ const keyPrefix = tokens[2] || '';
1361
+ return configKeyHints
1362
+ .filter((k) => k.startsWith(keyPrefix))
1363
+ .map((k) => `/config set ${k} `);
1364
+ }
1365
+
1366
+ return configTemplates;
1367
+ }
1368
+
1369
+ if (commandPart === 'compact') {
1370
+ const joined = tokens.slice(1).join(' ');
1371
+ return compactOptions
1372
+ .filter((opt) => opt.includes(joined) || joined === '')
1373
+ .map((opt) => `/compact ${opt}`);
1374
+ }
1375
+
1376
+ if (commandPart === 'retry') {
1377
+ return ['/retry'];
1378
+ }
1379
+ if (commandPart === 'status') {
1380
+ return ['/status'];
1381
+ }
1382
+ if (commandPart === 'mode') {
1383
+ if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
1384
+ const sub = tokens[1] || '';
1385
+ return ['normal', 'auto', 'plan']
1386
+ .filter((m) => m.startsWith(sub))
1387
+ .map((m) => `/mode ${m}`);
1388
+ }
1389
+ return modeTemplates;
1390
+ }
1391
+ if (commandPart === 'tasks') {
1392
+ if (tokens.length <= 2 && !hasTrailingSpace) {
1393
+ const sub = tokens[1] || '';
1394
+ return ['add', 'start', 'done', 'remove', 'rm', 'clear']
1395
+ .filter((s) => s.startsWith(sub))
1396
+ .map((s) => `/tasks ${s}`);
1397
+ }
1398
+ return taskTemplates;
1399
+ }
1400
+ if (commandPart === 'checkpoint') {
1401
+ if (tokens.length <= 2 && !hasTrailingSpace) {
1402
+ const sub = tokens[1] || '';
1403
+ return ['create', 'list', 'load']
1404
+ .filter((s) => s.startsWith(sub))
1405
+ .map((s) => `/checkpoint ${s}`);
1406
+ }
1407
+ if (tokens[1] === 'list') {
1408
+ const hint = tokens[2] || '';
1409
+ return ['--all'].filter((v) => v.startsWith(hint)).map((v) => `/checkpoint list ${v}`);
1410
+ }
1411
+ if (tokens[1] === 'load') {
1412
+ if (tokens.length >= 3) {
1413
+ const hint = tokens[3] || '';
1414
+ return ['--all']
1415
+ .filter((v) => v.startsWith(hint))
1416
+ .map((v) => `/checkpoint load ${tokens[2]} ${v}`);
1417
+ }
1418
+ }
1419
+ return checkpointTemplates;
1420
+ }
1421
+ if (commandPart === 'spec') {
1422
+ return specTemplates;
1423
+ }
1424
+ if (commandPart === 'plan') {
1425
+ if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
1426
+ const sub = tokens[1] || '';
1427
+ return ['auto', 'from-spec']
1428
+ .filter((s) => s.startsWith(sub))
1429
+ .map((s) => `/plan ${s}`);
1430
+ }
1431
+ return planTemplates;
1432
+ }
1433
+ if (commandPart === 'agents') {
1434
+ if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
1435
+ const sub = tokens[1] || '';
1436
+ return ['list', 'run']
1437
+ .filter((s) => s.startsWith(sub))
1438
+ .map((s) => `/agents ${s}`);
1439
+ }
1440
+ if (tokens[1] === 'run') {
1441
+ const rolePrefix = tokens[2] || '';
1442
+ return ['planner', 'coder', 'reviewer', 'tester']
1443
+ .filter((r) => r.startsWith(rolePrefix))
1444
+ .map((r) => `/agents run ${r} `);
1445
+ }
1446
+ return agentTemplates;
1447
+ }
1448
+
1449
+ if (commandPart === 'history') {
1450
+ if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
1451
+ const sub = tokens[1] || '';
1452
+ return ['list', 'current', 'resume']
1453
+ .filter((s) => s.startsWith(sub))
1454
+ .map((s) => `/history ${s}`);
1455
+ }
1456
+ const sub = tokens[1] || '';
1457
+ if (sub === 'resume') {
1458
+ const idPrefix = tokens[2] || '';
1459
+ const dynamic = historySessionCache
1460
+ .filter((session) => String(session.id || '').startsWith(idPrefix))
1461
+ .map((session) => ({
1462
+ value: `/history resume ${session.id}`,
1463
+ display: `/history resume ${session.id} · ${Number(session.messageCount || 0)} msgs`
1464
+ }));
1465
+ if (dynamic.length > 0) return dynamic;
1466
+ return historyTemplates;
1467
+ }
1468
+ return historyTemplates;
1469
+ }
1470
+
1471
+ if (commandPart === 'debug') {
1472
+ const sub = tokens[1] || '';
1473
+ if (!sub) return debugTemplates;
1474
+ if (sub === 'keys') {
1475
+ const action = tokens[2] || '';
1476
+ return ['on', 'off', 'status']
1477
+ .filter((v) => v.startsWith(action))
1478
+ .map((v) => `/debug keys ${v}`);
1479
+ }
1480
+ return debugTemplates;
1481
+ }
1482
+
1483
+ return [];
1484
+ };
1485
+
1486
+ const persistLocalExchange = async (userText, systemText, { includeUser = true } = {}) => {
1487
+ if (includeUser && userText) {
1488
+ currentSession.messages.push(stampedMessage('user', userText));
1489
+ }
1490
+ if (systemText) {
1491
+ currentSession.messages.push(stampedMessage('system', systemText));
1492
+ }
1493
+ await saveSession(currentSession);
1494
+ };
1495
+
1496
+ const isImmediateLocalInput = (line) => {
1497
+ const parsedInput = parseInput(line);
1498
+ if (parsedInput.type !== 'slash') return false;
1499
+ const command = String(parsedInput.command || '').trim().toLowerCase();
1500
+ if (!command) return false;
1501
+ if (command === 'agents') {
1502
+ const sub = String(parsedInput.args?.[0] || 'list').trim().toLowerCase();
1503
+ return sub === 'list';
1504
+ }
1505
+ const localCommands = new Set([
1506
+ 'exit',
1507
+ 'help',
1508
+ 'commands',
1509
+ 'status',
1510
+ 'mode',
1511
+ 'tasks',
1512
+ 'checkpoint',
1513
+ 'history',
1514
+ 'config',
1515
+ 'compact',
1516
+ 'debug'
1517
+ ]);
1518
+ return localCommands.has(command);
1519
+ };
1520
+
1521
+ const submit = async (line, onAgentEvent) => {
1522
+ const activeBaseSystemPrompt = baseSystemPrompt;
1523
+ const activeReplySystemPrompt = await buildSystemPromptWithSoul(baseSystemPrompt, config);
1524
+ try {
1525
+ await appendInputHistory(line);
1526
+ } catch {
1527
+ // Non-fatal: history persistence should not block chat flow.
1528
+ }
1529
+ const parsedInput = parseInput(line);
1530
+ if (parsedInput.type === 'empty') {
1531
+ return { type: 'noop' };
1532
+ }
1533
+ if (parsedInput.type === 'shell') {
1534
+ const shell = await handleShellInput(parsedInput.command, config);
1535
+ return { type: 'shell', text: shell.text };
1536
+ }
1537
+ if (parsedInput.type === 'slash') {
1538
+ if (parsedInput.command === 'exit') return { type: 'exit' };
1539
+ if (parsedInput.command === 'help') {
1540
+ return {
1541
+ type: 'system',
1542
+ text: 'Commands: /help /exit /commands /status /mode /compact /tasks /checkpoint /spec /plan /agents /config /history /debug /retry /<custom> !<shell>'
1543
+ };
1544
+ }
1545
+ if (parsedInput.command === 'status') {
1546
+ const taskCount = (await loadTasks(process.cwd(), currentSession.id)).length;
1547
+ return {
1548
+ type: 'system',
1549
+ text: `mode=${executionMode} | model=${model || config.model.name} | max_ctx=${effectiveMaxContextTokens(config)} | session=${currentSession.id} | tasks=${taskCount}`
1550
+ };
1551
+ }
1552
+ if (parsedInput.command === 'mode') {
1553
+ const next = (parsedInput.args[0] || '').trim().toLowerCase();
1554
+ if (!next) {
1555
+ return { type: 'system', text: `Current mode: ${executionMode} (available: normal|auto|plan)` };
1556
+ }
1557
+ if (!['normal', 'auto', 'plan'].includes(next)) {
1558
+ return { type: 'system', text: 'Usage: /mode <normal|auto|plan>' };
1559
+ }
1560
+ executionMode = next;
1561
+ await setConfigValue('execution.mode', next);
1562
+ config = await loadConfig();
1563
+ const text = `Execution mode set to: ${next}`;
1564
+ await persistLocalExchange(line, text);
1565
+ return { type: 'system', text };
1566
+ }
1567
+ if (parsedInput.command === 'tasks') {
1568
+ const sub = (parsedInput.args[0] || '').trim().toLowerCase();
1569
+ if (!sub) {
1570
+ const tasks = await loadTasks(process.cwd(), currentSession.id);
1571
+ if (tasks.length === 0) return { type: 'system', text: 'No tasks' };
1572
+ const rows = tasks.map((t, idx) => `${idx + 1}. ${t.id} | ${t.status} | ${t.title}`);
1573
+ return { type: 'system', text: rows.join('\n') };
1574
+ }
1575
+ if (sub === 'add') {
1576
+ const title = parsedInput.args.slice(1).join(' ').trim();
1577
+ if (!title) return { type: 'system', text: 'Usage: /tasks add <title>' };
1578
+ const created = await createTasks([{ title }], process.cwd(), currentSession.id);
1579
+ const text = `Created task: ${created[0]?.id || '-'} | ${title}`;
1580
+ await persistLocalExchange(line, text);
1581
+ return { type: 'system', text };
1582
+ }
1583
+ if (sub === 'start') {
1584
+ const id = parsedInput.args[1];
1585
+ if (!id) return { type: 'system', text: 'Usage: /tasks start <id>' };
1586
+ const updated = await updateTask(id, { status: 'in_progress' }, process.cwd(), currentSession.id);
1587
+ if (!updated) return { type: 'system', text: `Task not found: ${id}` };
1588
+ const text = `Task in progress: ${id}`;
1589
+ await persistLocalExchange(line, text);
1590
+ return { type: 'system', text };
1591
+ }
1592
+ if (sub === 'done') {
1593
+ const id = parsedInput.args[1];
1594
+ if (!id) return { type: 'system', text: 'Usage: /tasks done <id>' };
1595
+ const updated = await updateTask(id, { status: 'completed' }, process.cwd(), currentSession.id);
1596
+ if (!updated) return { type: 'system', text: `Task not found: ${id}` };
1597
+ const text = `Task completed: ${id}`;
1598
+ await persistLocalExchange(line, text);
1599
+ return { type: 'system', text };
1600
+ }
1601
+ if (sub === 'remove' || sub === 'rm') {
1602
+ const id = parsedInput.args[1];
1603
+ if (!id) return { type: 'system', text: 'Usage: /tasks remove <id>' };
1604
+ const result = await deleteTasks([id], process.cwd(), currentSession.id);
1605
+ const text = `Removed=${result.removed}, Remaining=${result.remaining}`;
1606
+ await persistLocalExchange(line, text);
1607
+ return { type: 'system', text };
1608
+ }
1609
+ if (sub === 'clear') {
1610
+ await clearTasks(process.cwd(), currentSession.id);
1611
+ const text = 'All tasks cleared';
1612
+ await persistLocalExchange(line, text);
1613
+ return { type: 'system', text };
1614
+ }
1615
+ // shorthand: /tasks implement x
1616
+ const title = parsedInput.args.join(' ').trim();
1617
+ if (title) {
1618
+ const created = await createTasks([{ title }], process.cwd(), currentSession.id);
1619
+ const text = `Created task: ${created[0]?.id || '-'} | ${title}`;
1620
+ await persistLocalExchange(line, text);
1621
+ return { type: 'system', text };
1622
+ }
1623
+ }
1624
+ if (parsedInput.command === 'checkpoint') {
1625
+ const sub = (parsedInput.args[0] || 'list').trim().toLowerCase();
1626
+ if (sub === 'create') {
1627
+ const name = parsedInput.args.slice(1).join(' ').trim();
1628
+ const tasks = await loadTasks(process.cwd(), currentSession.id);
1629
+ const cp = await createCheckpoint(
1630
+ {
1631
+ name,
1632
+ session: currentSession,
1633
+ config,
1634
+ tasks
1635
+ },
1636
+ process.cwd()
1637
+ );
1638
+ const text = `Checkpoint created: ${cp.id}`;
1639
+ await persistLocalExchange(line, text);
1640
+ return { type: 'system', text };
1641
+ }
1642
+ if (sub === 'list') {
1643
+ const showAll = parsedInput.args.includes('--all');
1644
+ const checkpoints = (await listCheckpoints(process.cwd())).filter((c) =>
1645
+ showAll ? true : c.sessionId === currentSession.id
1646
+ );
1647
+ if (checkpoints.length === 0) return { type: 'system', text: 'No checkpoints found' };
1648
+ const rows = checkpoints.map(
1649
+ (c, idx) =>
1650
+ `${idx + 1}. ${c.id} | session:${c.sessionId || '-'} | ${c.createdAt} | ${c.name || '-'}`
1651
+ );
1652
+ return { type: 'system', text: rows.join('\n') };
1653
+ }
1654
+ if (sub === 'load') {
1655
+ const id = parsedInput.args[1];
1656
+ if (!id) return { type: 'system', text: 'Usage: /checkpoint load <id>' };
1657
+ const cp = await loadCheckpoint(id, process.cwd());
1658
+ if (cp?.session?.id && cp.session.id !== currentSession.id && !parsedInput.args.includes('--all')) {
1659
+ return {
1660
+ type: 'system',
1661
+ text: `Checkpoint belongs to session ${cp.session.id}. Use /checkpoint load ${id} --all to force load.`
1662
+ };
1663
+ }
1664
+ if (cp?.session?.id) currentSession = cp.session;
1665
+ if (cp?.config) {
1666
+ config = cp.config;
1667
+ executionMode = config.execution?.mode || executionMode;
1668
+ }
1669
+ if (Array.isArray(cp?.tasks)) {
1670
+ await clearTasks(process.cwd(), currentSession.id);
1671
+ if (cp.tasks.length > 0) {
1672
+ // restore with new ids to avoid stale references
1673
+ await createTasks(
1674
+ cp.tasks.map((t) => ({ title: t.title, description: t.description })),
1675
+ process.cwd(),
1676
+ currentSession.id
1677
+ );
1678
+ }
1679
+ }
1680
+ const text = `Checkpoint loaded: ${id}`;
1681
+ await persistLocalExchange(line, text, { includeUser: false });
1682
+ return { type: 'system', text };
1683
+ }
1684
+ return { type: 'system', text: 'Usage: /checkpoint create <name> | /checkpoint list | /checkpoint load <id>' };
1685
+ }
1686
+ if (parsedInput.command === 'spec') {
1687
+ const topic = parsedInput.args.join(' ').trim();
1688
+ if (!topic) return { type: 'system', text: 'Usage: /spec <topic>' };
1689
+ let content = '';
1690
+ let buildNote = '';
1691
+ try {
1692
+ content = await buildSpecWithModel({
1693
+ topic,
1694
+ config,
1695
+ model,
1696
+ systemPrompt: activeBaseSystemPrompt
1697
+ });
1698
+ } catch (err) {
1699
+ content = buildSpecTemplate(topic);
1700
+ buildNote = `\nGenerated with fallback template because model spec generation failed: ${String(err?.message || err)}`;
1701
+ }
1702
+ const filePath = await writeMarkdownInCoderDir(
1703
+ 'specs',
1704
+ topic,
1705
+ content,
1706
+ 'spec',
1707
+ currentSession.id
1708
+ );
1709
+ const text = `Spec created: ${filePath}${buildNote}`;
1710
+ await persistLocalExchange(line, text);
1711
+ return { type: 'system', text };
1712
+ }
1713
+ if (parsedInput.command === 'plan') {
1714
+ const sub = (parsedInput.args[0] || '').trim().toLowerCase();
1715
+ if (sub === 'auto') {
1716
+ const goal = parsedInput.args.slice(1).join(' ').trim();
1717
+ if (!goal) return { type: 'system', text: 'Usage: /plan auto <goal>' };
1718
+ const auto = await buildAutoPlanAndRun({
1719
+ goal,
1720
+ session: currentSession,
1721
+ config,
1722
+ model,
1723
+ systemPrompt: activeBaseSystemPrompt,
1724
+ onAgentEvent,
1725
+ sessionId: currentSession.id
1726
+ });
1727
+ const text = buildAutoPlanSystemSummary(auto);
1728
+ await persistLocalExchange(line, text);
1729
+ return {
1730
+ type: 'system',
1731
+ text
1732
+ };
1733
+ }
1734
+ if (sub === 'from-spec') {
1735
+ const specArg = parsedInput.args.slice(1).join(' ').trim();
1736
+ const specPath = await resolveSpecPath(specArg, currentSession.id);
1737
+ if (!specPath) {
1738
+ return { type: 'system', text: 'Usage: /plan from-spec <spec-path-or-fragment>\nNo spec file found.' };
1739
+ }
1740
+ const specText = await fs.readFile(specPath, 'utf8');
1741
+ const specTitle = extractSpecTitle(specText, path.basename(specPath, '.md'));
1742
+ let planContent = '';
1743
+ let buildNote = '';
1744
+ try {
1745
+ planContent = await buildPlanFromSpecWithModel({
1746
+ specText,
1747
+ specPath,
1748
+ config,
1749
+ model,
1750
+ systemPrompt: activeBaseSystemPrompt
1751
+ });
1752
+ } catch (err) {
1753
+ planContent = buildPlanTemplate(specTitle);
1754
+ buildNote = `\nGenerated with fallback template because model plan generation failed: ${String(err?.message || err)}`;
1755
+ }
1756
+ const filePath = await writeMarkdownInCoderDir(
1757
+ 'plans',
1758
+ `${specTitle}-from-spec`,
1759
+ planContent,
1760
+ 'plan-from-spec',
1761
+ currentSession.id
1762
+ );
1763
+ const text = `Plan created from spec: ${filePath}\nSpec: ${specPath}${buildNote}`;
1764
+ await persistLocalExchange(line, text);
1765
+ return { type: 'system', text };
1766
+ }
1767
+
1768
+ const goal = parsedInput.args.join(' ').trim();
1769
+ if (!goal) return { type: 'system', text: 'Usage: /plan <goal> | /plan auto <goal> | /plan from-spec <spec-path?>' };
1770
+ const content = buildPlanTemplate(goal);
1771
+ const filePath = await writeMarkdownInCoderDir(
1772
+ 'plans',
1773
+ goal,
1774
+ content,
1775
+ 'plan',
1776
+ currentSession.id
1777
+ );
1778
+ const text = `Plan created: ${filePath}`;
1779
+ await persistLocalExchange(line, text);
1780
+ return { type: 'system', text };
1781
+ }
1782
+ if (parsedInput.command === 'agents') {
1783
+ const sub = parsedInput.args[0] || 'list';
1784
+ if (sub === 'list') {
1785
+ return {
1786
+ type: 'system',
1787
+ text: 'Sub-agent roles: planner, coder, reviewer, tester\nUse: /agents run <role> <task>'
1788
+ };
1789
+ }
1790
+ if (sub === 'run') {
1791
+ const role = (parsedInput.args[1] || '').trim().toLowerCase();
1792
+ const task = parsedInput.args.slice(2).join(' ').trim();
1793
+ if (!role || !task) return { type: 'system', text: 'Usage: /agents run <role> <task>' };
1794
+ if (!SUB_AGENT_ROLES.includes(role)) {
1795
+ return { type: 'system', text: 'Unknown role. Allowed: planner|coder|reviewer|tester' };
1796
+ }
1797
+ const output = await runSubAgentTask({
1798
+ role,
1799
+ task,
1800
+ parentSession: currentSession,
1801
+ config,
1802
+ model,
1803
+ systemPrompt: activeBaseSystemPrompt,
1804
+ onAgentEvent
1805
+ });
1806
+ const text = `[sub-agent:${role}]\n${output.text || output}`;
1807
+ await persistLocalExchange(line, text);
1808
+ return { type: 'assistant', text };
1809
+ }
1810
+ return { type: 'system', text: `Unknown /agents subcommand: ${sub}` };
1811
+ }
1812
+ if (parsedInput.command === 'debug') {
1813
+ const sub = parsedInput.args[0] || '';
1814
+ const action = parsedInput.args[1] || '';
1815
+ if (sub === 'keys') {
1816
+ if (action === 'on') return { type: 'system', text: '[debug:keys:on]' };
1817
+ if (action === 'off') return { type: 'system', text: '[debug:keys:off]' };
1818
+ if (action === 'status') return { type: 'system', text: '[debug:keys:status]' };
1819
+ return { type: 'system', text: 'Usage: /debug keys on|off|status' };
1820
+ }
1821
+ return { type: 'system', text: 'Usage: /debug keys on|off|status' };
1822
+ }
1823
+ if (parsedInput.command === 'history') {
1824
+ const sub = parsedInput.args[0] || 'list';
1825
+ if (sub === 'list') {
1826
+ const sessions = await listSessions(20);
1827
+ historyIdCache = sessions.map((s) => s.id);
1828
+ historySessionCache = sessions.map((s) => ({
1829
+ id: s.id,
1830
+ messageCount: Number(s.messageCount || 0)
1831
+ }));
1832
+ if (sessions.length === 0) return { type: 'system', text: 'No sessions found' };
1833
+ const rows = sessions.map(
1834
+ (s, idx) =>
1835
+ `${idx + 1}. ${s.id} | msgs:${s.messageCount} | updated:${s.updatedAt || '-'}${s.preview ? ` | ${s.preview}` : ''}`
1836
+ );
1837
+ return {
1838
+ type: 'system',
1839
+ text: `Current: ${currentSession.id}\n${rows.join('\n')}\nUse /history resume <session_id>`
1840
+ };
1841
+ }
1842
+ if (sub === 'current') {
1843
+ return {
1844
+ type: 'system',
1845
+ text: `Current session: ${currentSession.id} (${currentSession.messages.length} messages)`
1846
+ };
1847
+ }
1848
+ if (sub === 'resume') {
1849
+ const targetId = parsedInput.args[1];
1850
+ if (!targetId) return { type: 'system', text: 'Usage: /history resume <session_id>' };
1851
+ const loaded = await loadSession(targetId);
1852
+ currentSession = loaded;
1853
+ if (!historyIdCache.includes(targetId)) historyIdCache.unshift(targetId);
1854
+ historySessionCache = [
1855
+ { id: targetId, messageCount: Array.isArray(loaded.messages) ? loaded.messages.length : 0 },
1856
+ ...historySessionCache.filter((s) => s.id !== targetId)
1857
+ ];
1858
+ return {
1859
+ type: 'system',
1860
+ text: `Switched to session: ${targetId} (${loaded.messages.length} messages)`
1861
+ };
1862
+ }
1863
+ return { type: 'system', text: `Unknown /history subcommand: ${sub}` };
1864
+ }
1865
+ if (parsedInput.command === 'retry') {
1866
+ const lastUser = [...currentSession.messages].reverse().find((m) => m.role === 'user');
1867
+ if (!lastUser?.content) {
1868
+ return { type: 'system', text: 'No previous user message to retry' };
1869
+ }
1870
+ const result = await askModel({
1871
+ text: String(lastUser.content),
1872
+ session: currentSession,
1873
+ config,
1874
+ model,
1875
+ systemPrompt: activeReplySystemPrompt,
1876
+ onAgentEvent,
1877
+ executionMode
1878
+ });
1879
+ return { type: 'assistant', text: result.text };
1880
+ }
1881
+ if (parsedInput.command === 'config') {
1882
+ const sub = parsedInput.args[0];
1883
+ if (!sub || sub === 'help') {
1884
+ return {
1885
+ type: 'system',
1886
+ text: 'Usage:\n/config list\n/config get <key>\n/config set <key> <value>\n/config reset'
1887
+ };
1888
+ }
1889
+
1890
+ if (sub === 'list') {
1891
+ config = await loadConfig();
1892
+ return { type: 'system', text: JSON.stringify(config, null, 2) };
1893
+ }
1894
+
1895
+ if (sub === 'get') {
1896
+ const key = parsedInput.args[1];
1897
+ if (!key) return { type: 'system', text: 'Usage: /config get <key>' };
1898
+ const value = await getConfigValue(key);
1899
+ if (value === undefined) return { type: 'system', text: 'undefined' };
1900
+ return {
1901
+ type: 'system',
1902
+ text: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)
1903
+ };
1904
+ }
1905
+
1906
+ if (sub === 'set') {
1907
+ const key = parsedInput.args[1];
1908
+ const value = parsedInput.args.slice(2).join(' ');
1909
+ if (!key || !value) return { type: 'system', text: 'Usage: /config set <key> <value>' };
1910
+ await setConfigValue(key, value);
1911
+ config = await loadConfig();
1912
+ const text = `Set ${key}=${value}`;
1913
+ await persistLocalExchange(line, text);
1914
+ return { type: 'system', text };
1915
+ }
1916
+
1917
+ if (sub === 'reset') {
1918
+ await resetConfig();
1919
+ config = await loadConfig();
1920
+ compactState.threshold = 60;
1921
+ compactState.mode = 'conservative';
1922
+ compactState.autoEnabled = true;
1923
+ const text = 'Config reset complete';
1924
+ await persistLocalExchange(line, text);
1925
+ return { type: 'system', text };
1926
+ }
1927
+
1928
+ return { type: 'system', text: `Unknown /config subcommand: ${sub}` };
1929
+ }
1930
+ if (parsedInput.command === 'compact') {
1931
+ const cargs = parseCompactArgs(parsedInput.args);
1932
+
1933
+ if (cargs.auto === 'on') compactState.autoEnabled = true;
1934
+ if (cargs.auto === 'off') compactState.autoEnabled = false;
1935
+ if (typeof cargs.threshold === 'number' && cargs.threshold >= 50 && cargs.threshold <= 95) {
1936
+ compactState.threshold = cargs.threshold;
1937
+ }
1938
+ if (cargs.mode) compactState.mode = cargs.mode;
1939
+
1940
+ if (cargs.restore) {
1941
+ if (!compactState.backupMessages) {
1942
+ return { type: 'system', text: 'No backup available to restore' };
1943
+ }
1944
+ currentSession.messages = structuredClone(compactState.backupMessages);
1945
+ await saveSession(currentSession);
1946
+ const text = 'Context restored from backup';
1947
+ await persistLocalExchange(line, text, { includeUser: false });
1948
+ return { type: 'system', text };
1949
+ }
1950
+
1951
+ const beforeTokens = estimateMessagesTokens(currentSession.messages);
1952
+ const result = compactMessagesLocally(currentSession.messages, { mode: compactState.mode });
1953
+ if (!result.changed) {
1954
+ return { type: 'system', text: 'Nothing to compact yet' };
1955
+ }
1956
+ const afterTokens = estimateMessagesTokens(result.compacted);
1957
+ const report = `Compact ${cargs.preview ? 'preview' : 'applied'} (${compactState.mode}): ${beforeTokens} -> ${afterTokens} tokens`;
1958
+
1959
+ if (cargs.preview) {
1960
+ return { type: 'system', text: `${report}\n\n${result.summary}` };
1961
+ }
1962
+
1963
+ compactState.backupMessages = structuredClone(currentSession.messages);
1964
+ currentSession.messages = result.compacted.map((m) => ({ ...m, at: new Date().toISOString() }));
1965
+ await saveSession(currentSession);
1966
+ await persistLocalExchange(line, report, { includeUser: false });
1967
+ return { type: 'system', text: report };
1968
+ }
1969
+ if (parsedInput.command === 'commands') {
1970
+ const all = listCommandNames();
1971
+ if (all.length === 0) {
1972
+ return { type: 'system', text: 'No commands/skills available' };
1973
+ }
1974
+ const rows = all.map((c) => `/${c.name}${c.description ? ` - ${c.description}` : ''}`);
1975
+ return { type: 'system', text: rows.join('\n') };
1976
+ }
1977
+
1978
+ const custom = commands.get(parsedInput.command);
1979
+ if (!custom) {
1980
+ return { type: 'system', text: `Unknown slash command: /${parsedInput.command}` };
1981
+ }
1982
+ if (custom.metadata.type === 'skill' && config.skills?.enabled?.[custom.name] === false) {
1983
+ return { type: 'system', text: `Skill is disabled: ${custom.name}` };
1984
+ }
1985
+
1986
+ const rendered = await expandFileMentions(
1987
+ renderCommandPrompt(custom, parsedInput.args),
1988
+ process.cwd()
1989
+ );
1990
+ if (custom.metadata.type === 'skill' && onAgentEvent) {
1991
+ onAgentEvent({ type: 'skill:start', name: custom.name });
1992
+ }
1993
+ let result;
1994
+ try {
1995
+ result = await askModel({
1996
+ text: rendered,
1997
+ session: currentSession,
1998
+ config,
1999
+ model,
2000
+ systemPrompt: activeBaseSystemPrompt,
2001
+ onAgentEvent,
2002
+ executionMode
2003
+ });
2004
+ } catch (error) {
2005
+ if (custom.metadata.type === 'skill' && onAgentEvent) {
2006
+ onAgentEvent({
2007
+ type: 'skill:error',
2008
+ name: custom.name,
2009
+ summary: error instanceof Error ? error.message : String(error)
2010
+ });
2011
+ }
2012
+ throw error;
2013
+ }
2014
+ if (custom.metadata.type === 'skill' && onAgentEvent) {
2015
+ onAgentEvent({ type: 'skill:end', name: custom.name });
2016
+ }
2017
+ return { type: 'assistant', text: result.text };
2018
+ }
2019
+
2020
+ if (compactState.autoEnabled) {
2021
+ const currentTokens = estimateMessagesTokens(currentSession.messages);
2022
+ const maxTokens = effectiveMaxContextTokens(config);
2023
+ const usagePct = (currentTokens / maxTokens) * 100;
2024
+ if (usagePct >= compactState.threshold) {
2025
+ const autoResult = compactMessagesLocally(currentSession.messages, {
2026
+ mode: compactState.mode
2027
+ });
2028
+ if (autoResult.changed) {
2029
+ compactState.backupMessages = structuredClone(currentSession.messages);
2030
+ currentSession.messages = autoResult.compacted.map((m) => ({
2031
+ ...m,
2032
+ at: new Date().toISOString()
2033
+ }));
2034
+ await saveSession(currentSession);
2035
+ if (onAgentEvent) {
2036
+ onAgentEvent({
2037
+ type: 'compact:auto',
2038
+ mode: compactState.mode,
2039
+ threshold: compactState.threshold
2040
+ });
2041
+ }
2042
+ }
2043
+ }
2044
+ }
2045
+
2046
+ const expandedText = await expandFileMentions(parsedInput.text, process.cwd());
2047
+ const routedSystemPrompt = buildAutoSkillSystemPrompt(activeReplySystemPrompt, commands, config, expandedText);
2048
+ const result = await askModel({
2049
+ text: expandedText,
2050
+ session: currentSession,
2051
+ config,
2052
+ model,
2053
+ systemPrompt: routedSystemPrompt,
2054
+ onAgentEvent,
2055
+ executionMode
2056
+ });
2057
+ return { type: 'assistant', text: result.text };
2058
+ };
2059
+
2060
+ return {
2061
+ listCommandNames,
2062
+ getCompletionOptions,
2063
+ isImmediateLocalInput,
2064
+ submit,
2065
+ getInputHistory: () => loadInputHistory(),
2066
+ getCurrentSessionId: () => currentSession.id,
2067
+ getRuntimeState: () =>
2068
+ buildRuntimeStateSnapshot({
2069
+ currentSession,
2070
+ config,
2071
+ model,
2072
+ executionMode
2073
+ })
2074
+ };
2075
+ }