codemini-cli 0.3.1 → 0.3.3

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.
@@ -6,20 +6,13 @@ import path from 'node:path';
6
6
  import {
7
7
  createChatCompletion,
8
8
  createChatCompletionStream
9
- } from './provider/openai-compatible.js';
9
+ } from './provider/index.js';
10
10
  import { isDangerousCommand, runShellCommand } from './shell.js';
11
11
  import { getBuiltinTools } from './tools.js';
12
12
  import { listSessions, loadSession, pruneSessions, saveSession } from './session-store.js';
13
13
  import { getConfigValue, loadConfig, resetConfig, setConfigValue } from './config-store.js';
14
14
  import { evaluateCommandPolicy } from './command-policy.js';
15
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
16
  import { createCheckpoint, listCheckpoints, loadCheckpoint } from './checkpoint-store.js';
24
17
  import {
25
18
  compactMessagesLocally,
@@ -30,6 +23,9 @@ import { buildSystemPromptWithReplyLanguage } from './reply-language.js';
30
23
  import { buildSystemPromptWithSoul } from './soul.js';
31
24
  import { getProjectPlansDir, getProjectSpecsDir, getProjectWorkspaceDir, getSessionsDir } from './paths.js';
32
25
  import { buildProjectContextSnippet, initializeProjectIndex } from './project-index.js';
26
+ import { buildMemorySnapshot } from './memory-prompt.js';
27
+ import { forgetMemory, listMemories, searchMemories } from './memory-store.js';
28
+ import { countActiveTodos, normalizeTodos } from './todo-state.js';
33
29
 
34
30
  function toOpenAIMessages(sessionMessages) {
35
31
  const mapped = [];
@@ -74,35 +70,195 @@ function prioritizeByPreferredOrder(items, preferredOrder) {
74
70
  });
75
71
  }
76
72
 
77
- function describeConfigKey(key, mode = 'set') {
78
- const labelMap = {
79
- 'gateway.base_url': 'gateway base URL',
80
- 'gateway.api_key': 'gateway API key',
81
- 'gateway.timeout_ms': 'gateway timeout in milliseconds',
82
- 'gateway.max_retries': 'gateway retry count',
83
- 'model.name': 'active model name',
84
- 'model.max_context_tokens': 'model context token limit',
85
- 'ui.reply_language': 'reply language',
86
- 'execution.mode': 'execution mode',
87
- 'execution.always_allow_tools': 'always-allowed tools',
88
- 'execution.max_steps': 'maximum tool steps',
89
- 'context.preflight_trigger_pct': 'preflight compact threshold',
90
- 'context.hard_limit_pct': 'hard compact threshold',
91
- 'context.tool_result_max_chars': 'tool result character limit',
92
- 'context.read_file_default_lines': 'default read_file line window',
93
- 'context.read_file_max_chars': 'read_file character limit',
94
- 'sessions.max_sessions': 'stored session limit',
95
- 'sessions.retention_days': 'session retention days',
96
- 'shell.default': 'default shell',
97
- 'shell.timeout_ms': 'shell timeout in milliseconds',
98
- 'context.max_tokens': 'context token budget',
99
- 'soul.preset': 'soul preset',
100
- 'soul.custom_path': 'custom soul prompt path',
101
- 'policy.safe_mode': 'safe mode switch',
102
- 'policy.allow_dangerous_commands': 'dangerous command allowance'
103
- };
104
- const label = labelMap[key] || key;
105
- return mode === 'get' ? `show the ${label}` : `set the ${label}`;
73
+ function normalizeUiLocale(value) {
74
+ return String(value || '').toLowerCase().startsWith('en') ? 'en' : 'zh';
75
+ }
76
+
77
+ function getCompletionCopy(language = 'zh') {
78
+ const lang = normalizeUiLocale(language);
79
+ return {
80
+ zh: {
81
+ configLabels: {
82
+ 'gateway.base_url': '网关基础 URL',
83
+ 'gateway.api_key': '网关 API Key',
84
+ 'sdk.provider': 'SDK provider',
85
+ 'gateway.timeout_ms': '网关超时时间(毫秒)',
86
+ 'gateway.max_retries': '网关重试次数',
87
+ 'model.name': '当前模型名称',
88
+ 'model.max_context_tokens': '模型上下文 token 上限',
89
+ 'ui.language': '界面语言',
90
+ 'ui.reply_language': '回复语言',
91
+ 'execution.mode': '执行模式',
92
+ 'execution.always_allow_tools': '始终允许的工具列表',
93
+ 'execution.max_steps': '最大工具步骤数',
94
+ 'context.preflight_trigger_pct': '预压缩阈值',
95
+ 'context.hard_limit_pct': '硬压缩阈值',
96
+ 'context.tool_result_max_chars': '工具结果字符上限',
97
+ 'context.read_file_default_lines': 'read_file 默认行数窗口',
98
+ 'context.read_file_max_chars': 'read_file 字符上限',
99
+ 'sessions.max_sessions': '会话保留上限',
100
+ 'sessions.retention_days': '会话保留天数',
101
+ 'shell.default': '默认 shell',
102
+ 'shell.timeout_ms': 'shell 超时时间(毫秒)',
103
+ 'context.max_tokens': '上下文 token 预算',
104
+ 'soul.preset': 'soul 预设',
105
+ 'soul.custom_path': '自定义 soul 路径',
106
+ 'policy.safe_mode': '安全模式开关',
107
+ 'policy.allow_dangerous_commands': '危险命令开关'
108
+ },
109
+ optionHints: {
110
+ 'sdk.provider': '可选:openai-compatible | anthropic',
111
+ 'ui.language': '可选:zh | en',
112
+ 'ui.reply_language': '可选:zh | en',
113
+ 'execution.mode': '可选:auto | normal | plan',
114
+ 'shell.default': '常用:bash | powershell',
115
+ 'policy.safe_mode': '可选:true | false',
116
+ 'policy.allow_dangerous_commands': '可选:true | false'
117
+ },
118
+ describeSet: (label, hint) => `设置${label}${hint ? `(${hint})` : ''}`,
119
+ describeGet: (label, hint) => `查看${label}${hint ? `(${hint})` : ''}`,
120
+ configSubcommands: {
121
+ '/config set': '设置配置项',
122
+ '/config get': '查看配置项',
123
+ '/config list': '查看完整配置',
124
+ '/config reset': '重置为默认配置'
125
+ },
126
+ planSubcommands: {
127
+ '/plan <goal>': '创建一个人工审阅的实施计划',
128
+ '/plan auto <goal>': '自动生成计划并等待你确认执行',
129
+ '/plan auto run <goal>': '自动生成计划后立即继续执行',
130
+ '/plan approve': '批准当前待确认的计划并开始执行',
131
+ '/plan from-spec <spec-path?>': '从 spec 文件生成实施计划'
132
+ },
133
+ commands: {
134
+ help: '显示聊天帮助',
135
+ exit: '退出聊天',
136
+ commands: '列出 slash/自定义命令',
137
+ status: '查看运行状态(mode/model/session)',
138
+ mode: '设置执行模式:normal|auto|plan',
139
+ compact: '压缩消息上下文',
140
+ checkpoint: '创建/查看/加载检查点',
141
+ spec: '在 .codemini/specs 中创建 spec',
142
+ plan: '在 .codemini/plans 中创建实施计划',
143
+ agents: '列出/运行子代理角色',
144
+ config: '设置/读取/列出/重置配置',
145
+ memory: '查看/搜索/删除持久记忆',
146
+ history: '查看/恢复会话',
147
+ debug: '运行时调试开关',
148
+ retry: '重试上一条用户请求'
149
+ },
150
+ generic: {
151
+ configCommand: '配置命令',
152
+ historyCommand: '历史会话命令',
153
+ modeCommand: '切换执行模式',
154
+ checkpointCommand: '检查点命令',
155
+ specCommand: '创建 spec 文件',
156
+ planCommand: '规划命令',
157
+ agentCommand: '子代理命令',
158
+ memoryCommand: '记忆命令',
159
+ debugCommand: '调试命令',
160
+ keyboardDebugCommand: '键盘调试命令',
161
+ compactCommand: '上下文压缩命令',
162
+ retryCommand: '重试上一条用户请求',
163
+ statusCommand: '查看运行状态',
164
+ resumeSession: '恢复一个已保存的会话'
165
+ }
166
+ },
167
+ en: {
168
+ configLabels: {
169
+ 'gateway.base_url': 'gateway base URL',
170
+ 'gateway.api_key': 'gateway API key',
171
+ 'sdk.provider': 'SDK provider',
172
+ 'gateway.timeout_ms': 'gateway timeout in milliseconds',
173
+ 'gateway.max_retries': 'gateway retry count',
174
+ 'model.name': 'active model name',
175
+ 'model.max_context_tokens': 'model context token limit',
176
+ 'ui.language': 'UI language',
177
+ 'ui.reply_language': 'reply language',
178
+ 'execution.mode': 'execution mode',
179
+ 'execution.always_allow_tools': 'always-allowed tools',
180
+ 'execution.max_steps': 'maximum tool steps',
181
+ 'context.preflight_trigger_pct': 'preflight compact threshold',
182
+ 'context.hard_limit_pct': 'hard compact threshold',
183
+ 'context.tool_result_max_chars': 'tool result character limit',
184
+ 'context.read_file_default_lines': 'default read_file line window',
185
+ 'context.read_file_max_chars': 'read_file character limit',
186
+ 'sessions.max_sessions': 'stored session limit',
187
+ 'sessions.retention_days': 'session retention days',
188
+ 'shell.default': 'default shell',
189
+ 'shell.timeout_ms': 'shell timeout in milliseconds',
190
+ 'context.max_tokens': 'context token budget',
191
+ 'soul.preset': 'soul preset',
192
+ 'soul.custom_path': 'custom soul prompt path',
193
+ 'policy.safe_mode': 'safe mode switch',
194
+ 'policy.allow_dangerous_commands': 'dangerous command allowance'
195
+ },
196
+ optionHints: {
197
+ 'sdk.provider': 'options: openai-compatible | anthropic',
198
+ 'ui.language': 'options: zh | en',
199
+ 'ui.reply_language': 'options: zh | en',
200
+ 'execution.mode': 'options: auto | normal | plan',
201
+ 'shell.default': 'common: bash | powershell',
202
+ 'policy.safe_mode': 'options: true | false',
203
+ 'policy.allow_dangerous_commands': 'options: true | false'
204
+ },
205
+ describeSet: (label, hint) => `set the ${label}${hint ? ` (${hint})` : ''}`,
206
+ describeGet: (label, hint) => `show the ${label}${hint ? ` (${hint})` : ''}`,
207
+ configSubcommands: {
208
+ '/config set': 'update a config value',
209
+ '/config get': 'show a config value',
210
+ '/config list': 'print the full config',
211
+ '/config reset': 'reset config to defaults'
212
+ },
213
+ planSubcommands: {
214
+ '/plan <goal>': 'create an implementation plan for manual review',
215
+ '/plan auto <goal>': 'generate a plan and wait for your approval',
216
+ '/plan auto run <goal>': 'generate a plan and continue execution immediately',
217
+ '/plan approve': 'approve the pending plan and start execution',
218
+ '/plan from-spec <spec-path?>': 'generate an implementation plan from a spec file'
219
+ },
220
+ commands: {
221
+ help: 'show chat help',
222
+ exit: 'exit chat',
223
+ commands: 'list slash/custom commands',
224
+ status: 'show runtime status (mode/model/session)',
225
+ mode: 'set execution mode: normal|auto|plan',
226
+ compact: 'compress message context',
227
+ checkpoint: 'create/list/load conversation checkpoints',
228
+ spec: 'create a spec markdown file in .codemini/specs',
229
+ plan: 'create an implementation plan markdown file in .codemini/plans',
230
+ agents: 'run/list sub-agent roles',
231
+ config: 'set/get/list/reset config values',
232
+ memory: 'list/search/delete persistent memories',
233
+ history: 'list/resume sessions',
234
+ debug: 'runtime debug switches',
235
+ retry: 'retry the last user request'
236
+ },
237
+ generic: {
238
+ configCommand: 'config command',
239
+ historyCommand: 'history command',
240
+ modeCommand: 'switch execution mode',
241
+ checkpointCommand: 'checkpoint command',
242
+ specCommand: 'create a spec file',
243
+ planCommand: 'planning command',
244
+ agentCommand: 'sub-agent command',
245
+ memoryCommand: 'memory command',
246
+ debugCommand: 'debug command',
247
+ keyboardDebugCommand: 'keyboard debug command',
248
+ compactCommand: 'context compaction command',
249
+ retryCommand: 'retry the last user request',
250
+ statusCommand: 'show runtime status',
251
+ resumeSession: 'resume a saved session'
252
+ }
253
+ }
254
+ }[lang];
255
+ }
256
+
257
+ function describeConfigKey(key, mode = 'set', language = 'zh') {
258
+ const copy = getCompletionCopy(language);
259
+ const label = copy.configLabels[key] || key;
260
+ const hint = copy.optionHints[key] || '';
261
+ return mode === 'get' ? copy.describeGet(label, hint) : copy.describeSet(label, hint);
106
262
  }
107
263
 
108
264
  const SUB_AGENT_ROLES = ['planner', 'coder', 'reviewer', 'tester'];
@@ -110,7 +266,7 @@ const SUB_AGENT_CONTEXT_MAX_MESSAGES = 4;
110
266
  const SUB_AGENT_CONTEXT_MAX_CHARS = 1200;
111
267
  const SUB_AGENT_EVIDENCE_MAX_ITEMS = 3;
112
268
  const SUB_AGENT_HANDOFF_MAX_ITEMS = 6;
113
- function getSubAgentRolePrompt(role) {
269
+ export function getSubAgentRolePrompt(role) {
114
270
  if (role === 'planner') {
115
271
  return 'You are a planning sub-agent. Produce a concrete implementation plan with risks and verification.';
116
272
  }
@@ -149,7 +305,11 @@ function getSubAgentRolePrompt(role) {
149
305
  '- <single best next step>'
150
306
  ].join('\n');
151
307
  }
152
- return 'You are an execution sub-agent. Produce practical implementation guidance with code-level detail.';
308
+ return [
309
+ 'You are an execution sub-agent. Produce practical implementation guidance with code-level detail.',
310
+ 'Stop when: you have produced the code change and verified it compiles/passes basic checks.',
311
+ 'If blocked: report what blocked you and what you tried, then stop.'
312
+ ].join('\n');
153
313
  }
154
314
 
155
315
  function trimInlineText(value, maxLen = 220) {
@@ -407,6 +567,24 @@ function isLightweightAutoPlanGoal(goal, requirements = []) {
407
567
  return /\b(add|update|fix|rename|trim|export|create|remove|change|implement)\b/i.test(text);
408
568
  }
409
569
 
570
+ function classifyPlanTaskClass(goal = '') {
571
+ const text = String(goal || '').trim();
572
+ const lowerGoal = text.toLowerCase();
573
+ const advisory =
574
+ /\b(analyze|analysis|review|audit|inspect|assess|recommend|recommendation|optimization|optimize|improve|suggest|brainstorm|plan|feedback)\b/i.test(lowerGoal) ||
575
+ /(分析|审查|审计|检查|评估|建议|优化|优化点|优化建议|改进|改进点|规划|方案|看一下|看看|有哪些问题|有什么问题)/.test(text);
576
+ const implementation =
577
+ /\b(add|build|create|implement|support|introduce|refactor|rewrite|rework|migrate|change|update|fix)\b/i.test(lowerGoal) ||
578
+ /(新增|增加|实现|支持|重构|重写|改造|迁移|修改|更新|修复)/.test(text);
579
+ const verificationHeavy =
580
+ /\b(test|verify|validation|validate|prove|confirm|reproduce|check coverage)\b/i.test(lowerGoal) ||
581
+ /(测试|验证|校验|确认|复现|覆盖率)/.test(text);
582
+
583
+ if (verificationHeavy) return 'verification-heavy';
584
+ if (advisory && !implementation) return 'advisory';
585
+ return 'implementation';
586
+ }
587
+
410
588
  function buildGoalRequirementPacket(goal, role) {
411
589
  const rawGoal = trimInlineText(goal, 800);
412
590
  if (!rawGoal) return '';
@@ -441,7 +619,9 @@ function buildAutoPlanPlannerGuidance() {
441
619
  '- Prefer the smallest local approach that satisfies the goal.',
442
620
  '- Do not output multiple alternative branches in the final plan.',
443
621
  '- Do not assume implementation should begin before the plan is coherent.',
444
- '- Turn the chosen direction into concrete execution steps for planner, coder, reviewer, and tester.',
622
+ '- Available sub-agent roles are planner, coder, reviewer, and tester. Use only the roles the task actually needs.',
623
+ '- For implementation-heavy or risky changes, prefer adding review and/or verification steps.',
624
+ '- For analysis, recommendation, or planning-only goals, you may omit reviewer/tester if they do not add value.',
445
625
  '- Prefer 3-5 steps total unless the task is clearly larger.',
446
626
  '- Keep the plan ordered, implementation-oriented, and easy for small sub-agents to follow.'
447
627
  ].join('\n');
@@ -598,49 +778,56 @@ function selectAutoSkillNames(text = '') {
598
778
  return selected;
599
779
  }
600
780
 
601
- function shouldAutoPlan(text = '') {
781
+ function classifyTaskComplexity(text = '') {
602
782
  const input = String(text || '').trim();
603
- if (!input) return false;
783
+ if (!input) return 'simple';
604
784
 
605
785
  const lower = input.toLowerCase();
606
786
  const explicitPlanning =
607
787
  /(\/plan\b|plan first|make a plan|implementation plan|先做计划|先出方案|先规划|先计划)/i.test(lower);
608
- if (explicitPlanning) return false;
788
+ if (explicitPlanning) return 'complex';
609
789
 
610
790
  const simpleSkip =
611
791
  /(typo|readme|console\.log|log this|rename\s+\w+|one line|small tweak|tiny fix|格式化|拼写|注释|文案|小改|微调)/i.test(
612
792
  lower
613
793
  );
614
- if (simpleSkip) return false;
794
+ if (simpleSkip) return 'simple';
615
795
 
616
796
  const discussionFirst =
617
797
  /(brainstorm|头脑风暴|方案|思路|怎么做|如何做|which (?:approach|option|way)|best way|trade-?off|not sure|unsure|unclear|whether it should|要不要|不确定|先别写|先不要写|先讨论|先想一下)/i.test(
618
798
  lower
619
799
  );
620
- if (discussionFirst) return false;
800
+ if (discussionFirst) return 'simple';
621
801
 
622
802
  const implementationRequest =
623
803
  /\b(add|build|create|implement|support|introduce|design|refactor|rework|migrate|change|update|rewrite|restructure)\b/i.test(
624
804
  lower
625
805
  ) ||
626
806
  /(新增|增加|实现|支持|设计|重构|改造|迁移|调整|重写|重做)/i.test(lower);
627
- if (!implementationRequest) return false;
628
-
629
- const nonTrivialSignals =
630
- /\b(auth|authentication|workflow|flow|system|architecture|api|endpoint|state management|cache|caching|database|migration|service|shared helper|helper module|refactor|multi[- ]file|across files|with tests?|and tests?|with validation|error handling)\b/i.test(
631
- lower
632
- ) ||
633
- /(架构|流程|系统|接口|缓存|数据库|迁移|服务|共享|模块|跨文件|测试|校验|错误处理)/i.test(lower);
807
+ if (!implementationRequest) return 'simple';
634
808
 
809
+ const broadSignalPattern =
810
+ /\b(auth|authentication|workflow|flow|system|architecture|api|endpoint|state management|session state|cache|caching|database|migration|service|integration|error handling|error recovery|shared helper|helper module)\b/gi;
811
+ const broadSignals = lower.match(broadSignalPattern) || [];
635
812
  const multipleActions = /\b(and|plus|also|while|along with)\b/i.test(lower) || /[,、;;].+/.test(input);
636
813
  const singleFileScoped =
637
814
  /\b(?:in|inside|within|only in)\s+[-_/.\w]+\.(?:[cm]?[jt]sx?|py|go|rb|java|rs|php|md)\b/i.test(lower) ||
638
815
  /\b(?:src|app|lib|tests?)\/[-_/.\w]+\.(?:[cm]?[jt]sx?|py|go|rb|java|rs|php|md)\b/i.test(lower);
639
-
640
- if (singleFileScoped && !multipleActions) return false;
641
- if (singleFileScoped && !nonTrivialSignals) return false;
642
-
643
- return nonTrivialSignals || (multipleActions && !singleFileScoped);
816
+ const fileMentions = (lower.match(/[-_/.\w]+\.(?:[cm]?[jt]sx?|py|go|rb|java|rs|php|md)\b/g) || []).length;
817
+ const multiFileScope =
818
+ fileMentions >= 2 ||
819
+ /\b(across|multiple files?|cross-file|cross file)\b/i.test(lower) ||
820
+ /跨文件|多文件/.test(input);
821
+ const verificationHeavy = /\b(with tests?|and tests?|verify|validation|error handling|error recovery)\b/i.test(lower) || /测试|验证|校验|错误处理|错误恢复/.test(input);
822
+ const architectureHeavy =
823
+ broadSignals.length >= 3 ||
824
+ /\b(architecture|workflow|migration|state management|session state|integration)\b/i.test(lower) ||
825
+ /架构|流程|迁移|状态/.test(input);
826
+
827
+ if (singleFileScoped && !multipleActions && !verificationHeavy) return 'simple';
828
+ if (architectureHeavy && (multiFileScope || multipleActions || verificationHeavy)) return 'complex';
829
+ if (multiFileScope || verificationHeavy || multipleActions) return 'medium';
830
+ return 'simple';
644
831
  }
645
832
 
646
833
  function classifyAutoRoute(text = '') {
@@ -650,25 +837,42 @@ function classifyAutoRoute(text = '') {
650
837
  return {
651
838
  mode: 'brainstorm',
652
839
  autoPlan: false,
653
- selectedSkills
840
+ selectedSkills,
841
+ complexity: 'discussion'
654
842
  };
655
843
  }
656
844
 
657
- if (shouldAutoPlan(text)) {
845
+ const complexity = classifyTaskComplexity(text);
846
+ if (complexity === 'complex') {
658
847
  return {
659
848
  mode: 'auto_plan',
660
849
  autoPlan: true,
661
- selectedSkills: ['superpowers-lite']
850
+ selectedSkills: ['superpowers-lite'],
851
+ complexity
662
852
  };
663
853
  }
664
854
 
665
855
  return {
666
- mode: 'direct',
856
+ mode: complexity === 'medium' ? 'direct_medium' : 'direct',
667
857
  autoPlan: false,
668
- selectedSkills
858
+ selectedSkills,
859
+ complexity
669
860
  };
670
861
  }
671
862
 
863
+ function buildMediumTaskSystemPrompt(systemPrompt) {
864
+ const guidance = [
865
+ 'Task Mode: medium',
866
+ 'Execution guidance:',
867
+ '- Give a brief execution outline before coding.',
868
+ '- Keep the outline concise and focused on touched files/behaviors.',
869
+ '- Then implement directly instead of entering pending plan approval.',
870
+ '- Verify the changed behavior before finishing.',
871
+ '- If major ambiguity appears mid-task, say so clearly and ask for a plan instead of guessing.'
872
+ ].join('\n');
873
+ return `${systemPrompt}\n\n${guidance}`;
874
+ }
875
+
672
876
  function buildAutoSkillSystemPrompt(baseSystemPrompt, commands, config, text) {
673
877
  const selected = classifyAutoRoute(text).selectedSkills.filter((name) => isSkillEnabled(config, name));
674
878
  if (selected.length === 0) return baseSystemPrompt;
@@ -809,6 +1013,7 @@ function enforceAutoPlanGuardrailSteps(plan, goal) {
809
1013
  const source = Array.isArray(plan?.steps) ? plan.steps : [];
810
1014
  const requirements = deriveGoalRequirements(goal);
811
1015
  const lightweightGoal = isLightweightAutoPlanGoal(goal, requirements);
1016
+ const taskClass = classifyPlanTaskClass(goal);
812
1017
  const implementationSteps = source.filter((step) => step.role !== 'reviewer' && step.role !== 'tester');
813
1018
  const primaryImplementationStep =
814
1019
  implementationSteps.find((step) => step.role === 'coder') ||
@@ -827,17 +1032,31 @@ function enforceAutoPlanGuardrailSteps(plan, goal) {
827
1032
  role: 'tester',
828
1033
  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.`
829
1034
  };
1035
+ const hasReviewer = source.some((step) => step.role === 'reviewer');
1036
+ const hasTester = source.some((step) => step.role === 'tester');
1037
+
1038
+ if (taskClass === 'advisory') {
1039
+ const advisorySteps = source.filter((step) => step.role === 'planner' || step.role === 'coder');
1040
+ return {
1041
+ summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
1042
+ steps: advisorySteps.length > 0 ? advisorySteps.slice(0, 6) : [primaryImplementationStep]
1043
+ };
1044
+ }
830
1045
 
831
1046
  if (lightweightGoal) {
832
1047
  return {
833
1048
  summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
834
- steps: [primaryImplementationStep, testerStep]
1049
+ steps: hasTester ? [primaryImplementationStep, testerStep] : [primaryImplementationStep]
835
1050
  };
836
1051
  }
837
1052
 
838
1053
  return {
839
1054
  summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
840
- steps: [...implementationSteps.slice(0, 6), reviewerStep, testerStep]
1055
+ steps: [
1056
+ ...implementationSteps.slice(0, 6),
1057
+ ...(hasReviewer ? [reviewerStep] : []),
1058
+ ...(testerStep ? [testerStep] : [])
1059
+ ]
841
1060
  };
842
1061
  }
843
1062
 
@@ -906,19 +1125,23 @@ function extractAcceptanceStatusItems(text = '') {
906
1125
  }
907
1126
 
908
1127
  function buildAutoPlanSystemSummary(auto) {
909
- const statusTitle =
1128
+ const baseStatusTitle =
910
1129
  auto.failedCount > 0 ? 'Auto plan finished with failures' : auto.warningCount > 0 ? 'Auto plan finished with warnings' : 'Auto plan finished';
1130
+ const statusTitle =
1131
+ auto.approvalStatus === 'pending' ? `${baseStatusTitle} (waiting for /plan approve)` : baseStatusTitle;
911
1132
  const lines = [
912
1133
  statusTitle,
913
- `File: ${auto.filePath}`,
1134
+ `Plan File: ${auto.filePath}`,
914
1135
  `Plan Summary: ${auto.summary || '-'}`,
915
1136
  `Final Summary: ${auto.finalSummary || auto.summary || '-'}`,
916
- `Approval: ${auto.approvalStatus || 'not_required'}`,
917
- `Steps: ${auto.steps.length} total`,
918
- `Completed: ${auto.completedCount}`,
919
- `Warnings: ${auto.warningCount}`,
920
- `Failed: ${auto.failedCount}`
1137
+ `Approval: ${auto.approvalStatus || 'not_required'}`
921
1138
  ];
1139
+ if (auto.approvalStatus !== 'pending') {
1140
+ lines.push(`Steps: ${auto.steps.length} total`);
1141
+ lines.push(`Completed: ${auto.completedCount}`);
1142
+ lines.push(`Warnings: ${auto.warningCount}`);
1143
+ lines.push(`Failed: ${auto.failedCount}`);
1144
+ }
922
1145
  if (auto.warningTitles?.length) {
923
1146
  lines.push(`Warning steps: ${auto.warningTitles.slice(0, 5).join(', ')}`);
924
1147
  }
@@ -926,7 +1149,7 @@ function buildAutoPlanSystemSummary(auto) {
926
1149
  lines.push(`Failed steps: ${auto.failedTitles.slice(0, 5).join(', ')}`);
927
1150
  }
928
1151
  if (auto.approvalStatus === 'pending') {
929
- lines.push('Next: review the plan summary, then use /plan approve to start implementation or /plan stay to keep planning.');
1152
+ lines.push('Next: review the plan summary, then use /plan approve to start implementation, /plan auto run <goal> to plan and run in one step next time, or /plan stay to keep planning.');
930
1153
  }
931
1154
  return lines.join('\n');
932
1155
  }
@@ -1002,6 +1225,7 @@ async function buildAutoPlanFinalSummary({
1002
1225
 
1003
1226
  try {
1004
1227
  const result = await createChatCompletion({
1228
+ sdkProvider: config.sdk?.provider,
1005
1229
  baseUrl: config.gateway.base_url,
1006
1230
  apiKey: config.gateway.api_key,
1007
1231
  model: model || config.model.name,
@@ -1102,6 +1326,7 @@ async function buildSpecWithModel({
1102
1326
  ].join('\n');
1103
1327
 
1104
1328
  const result = await createChatCompletion({
1329
+ sdkProvider: config.sdk?.provider,
1105
1330
  baseUrl: config.gateway.base_url,
1106
1331
  apiKey: config.gateway.api_key,
1107
1332
  model: model || config.model.name,
@@ -1161,6 +1386,7 @@ async function buildPlanFromSpecWithModel({
1161
1386
  ].join('\n');
1162
1387
 
1163
1388
  const result = await createChatCompletion({
1389
+ sdkProvider: config.sdk?.provider,
1164
1390
  baseUrl: config.gateway.base_url,
1165
1391
  apiKey: config.gateway.api_key,
1166
1392
  model: model || config.model.name,
@@ -1276,6 +1502,7 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
1276
1502
  const snapshot = {
1277
1503
  sessionId: currentSession?.id || '',
1278
1504
  mode: executionMode || config.execution?.mode || 'auto',
1505
+ sdkProvider: config.sdk?.provider || 'openai-compatible',
1279
1506
  model: model || config.model?.name || '',
1280
1507
  maxContextTokens
1281
1508
  };
@@ -1336,7 +1563,7 @@ function buildPendingPlanApprovalMessage(planState) {
1336
1563
  const lines = [
1337
1564
  'Plan approval is still pending.',
1338
1565
  `Goal: ${planState?.goal || '-'}`,
1339
- `Plan file: ${planState?.filePath || '-'}`,
1566
+ `Plan File: ${planState?.filePath || '-'}`,
1340
1567
  `Summary: ${planState?.finalSummary || planState?.summary || '-'}`,
1341
1568
  'Use /plan approve to start implementation, or /plan stay to keep refining the plan first.'
1342
1569
  ];
@@ -1344,6 +1571,7 @@ function buildPendingPlanApprovalMessage(planState) {
1344
1571
  }
1345
1572
 
1346
1573
  function buildApprovedPlanExecutionPrompt(planState, approvalText = '') {
1574
+ const requirementPacket = buildGoalRequirementPacket(planState?.goal || '', 'coder');
1347
1575
  const lines = [
1348
1576
  'Approved implementation plan:',
1349
1577
  `Original goal: ${planState?.goal || '-'}`,
@@ -1351,6 +1579,7 @@ function buildApprovedPlanExecutionPrompt(planState, approvalText = '') {
1351
1579
  `Plan summary: ${planState?.summary || '-'}`,
1352
1580
  `Final planning summary: ${planState?.finalSummary || planState?.summary || '-'}`,
1353
1581
  `User approval: ${String(approvalText || '').trim() || 'approved'}`,
1582
+ requirementPacket,
1354
1583
  Array.isArray(planState?.steps) && planState.steps.length > 0 ? 'Planned steps:' : '',
1355
1584
  ...(Array.isArray(planState?.steps)
1356
1585
  ? planState.steps.slice(0, 8).map((step, index) => `${index + 1}. [${step.role}] ${step.title} :: ${step.task}`)
@@ -1518,14 +1747,19 @@ async function askModel({
1518
1747
 
1519
1748
  const projectContextSnippet = await buildProjectContextSnippet(process.cwd(), text).catch(() => '');
1520
1749
  const effectiveSystemPrompt = projectContextSnippet
1521
- ? `${systemPrompt}\n\n${projectContextSnippet}\n\nUse this project context as lightweight guidance. Query the project index before broad globs or reading many files, then use targeted reads for fresh verification.`
1750
+ ? `${systemPrompt}\n\n${projectContextSnippet}\n\nUse this project context as lightweight guidance and verify important details with fresh reads when needed.`
1522
1751
  : systemPrompt;
1523
1752
 
1524
1753
  const { definitions, handlers, formatters, deferredDefinitions } = getBuiltinTools({
1525
1754
  workspaceRoot: process.cwd(),
1526
1755
  config,
1527
1756
  sessionId: session.id,
1528
- onSystemEvent: onAgentEvent
1757
+ onSystemEvent: onAgentEvent,
1758
+ getTodos: () => normalizeTodos(session.todos),
1759
+ onTodosUpdate: (todos) => {
1760
+ session.todos = normalizeTodos(todos);
1761
+ scheduleSessionSave();
1762
+ }
1529
1763
  });
1530
1764
 
1531
1765
  let activeAssistantIndex = -1;
@@ -1595,6 +1829,7 @@ async function askModel({
1595
1829
  };
1596
1830
 
1597
1831
  const result = await createChatCompletionStream({
1832
+ sdkProvider: config.sdk?.provider,
1598
1833
  baseUrl: config.gateway.base_url,
1599
1834
  apiKey: config.gateway.api_key,
1600
1835
  model: selectedModel,
@@ -1719,12 +1954,26 @@ async function buildAutoPlanAndRun({
1719
1954
  model,
1720
1955
  systemPrompt,
1721
1956
  onAgentEvent,
1722
- sessionId
1957
+ sessionId,
1958
+ taskClass
1723
1959
  }) {
1960
+ const normalizedTaskClass = taskClass || classifyPlanTaskClass(goal);
1724
1961
  const requirementPacket = buildGoalRequirementPacket(goal, 'planner');
1725
1962
  const plannerPrompt = [
1726
1963
  buildAutoPlanPlannerGuidance(),
1727
- 'Return strict JSON only with shape {"summary":"...","steps":[{"title":"...","role":"planner|coder|reviewer|tester","task":"..."}]}. No markdown. Always include final reviewer and tester steps.'
1964
+ 'Planning policy:',
1965
+ '- First classify the user goal as one of: advisory, implementation, or verification-heavy.',
1966
+ '- advisory = analysis, review, audit, optimization suggestions, architecture feedback, brainstorming, planning, or recommendation requests.',
1967
+ '- implementation = add/build/create/implement/refactor/fix/update/change behavior in code or files.',
1968
+ '- verification-heavy = the user explicitly asks to run tests, verify findings, reproduce a bug, prove a claim, or validate a result.',
1969
+ '- For advisory goals, prefer only planner and coder roles. Do not use reviewer or tester unless the user explicitly asks for verification or review as a separate deliverable.',
1970
+ '- For advisory goals, do not emit generic filler steps such as "Test and verify", "Review recommendations", or other template-only steps.',
1971
+ '- For implementation goals, reviewer and tester are optional support roles, not defaults. Only include them when they clearly add value.',
1972
+ '- Every step title must be concrete and tied to the goal. Avoid vague titles like "Initial analysis", "Review recommendations", or "Test and verify" unless the user explicitly requested those activities.',
1973
+ '- If the task is purely to inspect the current project and suggest improvements, a lean 2-step or 3-step plan is preferred.',
1974
+ '- Example advisory roles: planner -> inspect project shape, coder -> synthesize findings and prioritized recommendations.',
1975
+ '- Example implementation roles: planner -> inspect target area, coder -> implement change, tester -> verify changed behavior.',
1976
+ 'Return strict JSON only with shape {"summary":"...","steps":[{"title":"...","role":"planner|coder|reviewer|tester","task":"..."}]}. No markdown.'
1728
1977
  ].join('\n');
1729
1978
  let autoPlan = {
1730
1979
  summary: `Auto plan for: ${goal}`,
@@ -1739,6 +1988,7 @@ async function buildAutoPlanAndRun({
1739
1988
  let planningError = '';
1740
1989
  try {
1741
1990
  const planning = await createChatCompletion({
1991
+ sdkProvider: config.sdk?.provider,
1742
1992
  baseUrl: config.gateway.base_url,
1743
1993
  apiKey: config.gateway.api_key,
1744
1994
  model: model || config.model.name,
@@ -1749,10 +1999,15 @@ async function buildAutoPlanAndRun({
1749
1999
  content: [
1750
2000
  'Create an execution plan and assign best sub-agent role for each step.',
1751
2001
  'Return strict JSON only with shape {"summary":"...","steps":[{"title":"...","role":"planner|coder|reviewer|tester","task":"..."}]}. No markdown.',
1752
- 'Always include final reviewer and tester steps unless the task is explicitly tiny.',
2002
+ 'The available roles are planner, coder, reviewer, and tester. Use only the roles the task actually needs.',
2003
+ `Task class: ${normalizedTaskClass}`,
2004
+ 'Before choosing roles, decide whether the request is advisory, implementation, or verification-heavy.',
1753
2005
  requirementPacket,
1754
2006
  'The first step should usually inspect or clarify the target area before implementation.',
1755
- 'The final steps must include review and testing/verification unless the goal is a tiny single-change task, in which case you may keep only one implementation step plus one testing/verification step.',
2007
+ 'For analysis, recommendation, optimization, audit, or project-review goals, keep the plan lean and usually limit it to planner/coder.',
2008
+ 'Do not include reviewer/tester for advisory goals unless the user explicitly asks to validate, verify, or independently review the findings.',
2009
+ 'Avoid template-only titles like "Initial analysis", "Review recommendations", or "Test and verify" for advisory goals.',
2010
+ 'For implementation-heavy changes, prefer review and/or testing steps near the end only when they materially improve confidence.',
1756
2011
  'Prefer 3-5 steps total.'
1757
2012
  ]
1758
2013
  .filter(Boolean)
@@ -1866,6 +2121,17 @@ export async function createChatRuntime({
1866
2121
  summary: initialIndex.summary
1867
2122
  });
1868
2123
  }
2124
+ const initialTodos = normalizeTodos(session?.todos);
2125
+ if (initialTodos.length > 0) {
2126
+ startupEvents.push({
2127
+ type: 'tool',
2128
+ id: `startup-todos-${String(session?.id || 'session')}`,
2129
+ name: 'update_todos',
2130
+ status: 'done',
2131
+ arguments: { todos: initialTodos },
2132
+ summary: `${initialTodos.length} todo item(s)`
2133
+ });
2134
+ }
1869
2135
  let currentSession = session;
1870
2136
  let config = initialConfig;
1871
2137
  const baseSystemPrompt = systemPrompt;
@@ -1891,14 +2157,43 @@ export async function createChatRuntime({
1891
2157
  messageCount: Array.isArray(currentSession.messages) ? currentSession.messages.length : 0
1892
2158
  }
1893
2159
  ];
2160
+ try {
2161
+ const initialSessions = await listSessions(100);
2162
+ if (initialSessions.length > 0) {
2163
+ const merged = [
2164
+ {
2165
+ id: currentSession.id,
2166
+ messageCount: Array.isArray(currentSession.messages) ? currentSession.messages.length : 0
2167
+ },
2168
+ ...initialSessions.map((session) => ({
2169
+ id: session.id,
2170
+ messageCount: Number(session.messageCount || 0)
2171
+ }))
2172
+ ];
2173
+ const deduped = [];
2174
+ const seen = new Set();
2175
+ for (const session of merged) {
2176
+ const id = String(session.id || '').trim();
2177
+ if (!id || seen.has(id)) continue;
2178
+ seen.add(id);
2179
+ deduped.push(session);
2180
+ }
2181
+ historySessionCache = deduped;
2182
+ historyIdCache = deduped.map((session) => session.id);
2183
+ }
2184
+ } catch {
2185
+ // keep startup resilient even if historical sessions cannot be listed
2186
+ }
1894
2187
 
1895
2188
  const configKeyHints = [
1896
2189
  'gateway.base_url',
1897
2190
  'gateway.api_key',
1898
2191
  'model.name',
2192
+ 'ui.language',
1899
2193
  'ui.reply_language',
1900
2194
  'execution.mode',
1901
2195
  'shell.default',
2196
+ 'sdk.provider',
1902
2197
  'gateway.timeout_ms',
1903
2198
  'gateway.max_retries',
1904
2199
  'model.max_context_tokens',
@@ -1923,9 +2218,9 @@ export async function createChatRuntime({
1923
2218
  '/help',
1924
2219
  '/status',
1925
2220
  '/config',
2221
+ '/memory',
1926
2222
  '/mode',
1927
2223
  '/plan',
1928
- '/tasks',
1929
2224
  '/history',
1930
2225
  '/checkpoint',
1931
2226
  '/agents',
@@ -1934,30 +2229,25 @@ export async function createChatRuntime({
1934
2229
  '/retry'
1935
2230
  ];
1936
2231
  const configSubcommandPriority = ['/config set', '/config get', '/config list', '/config reset'];
1937
- const configSubcommandDescriptions = {
1938
- '/config set': 'update a config value',
1939
- '/config get': 'show a config value',
1940
- '/config list': 'print the full config',
1941
- '/config reset': 'reset config to defaults'
1942
- };
1943
2232
 
1944
2233
  const listCommandNames = () => {
2234
+ const completionCopy = getCompletionCopy(config.ui?.language);
1945
2235
  const builtins = [
1946
- { name: 'help', description: 'show chat help' },
1947
- { name: 'exit', description: 'exit chat' },
1948
- { name: 'commands', description: 'list slash/custom commands' },
1949
- { name: 'status', description: 'show runtime status (mode/model/session)' },
1950
- { name: 'mode', description: 'set execution mode: normal|auto|plan' },
1951
- { name: 'compact', description: 'compress message context' },
1952
- { name: 'tasks', description: 'task board management' },
1953
- { name: 'checkpoint', description: 'create/list/load conversation checkpoints' },
1954
- { name: 'spec', description: 'create a spec markdown file in .codemini/specs' },
1955
- { name: 'plan', description: 'create an implementation plan markdown file in .codemini/plans' },
1956
- { name: 'agents', description: 'run/list sub-agent roles' },
1957
- { name: 'config', description: 'set/get/list/reset config values' },
1958
- { name: 'history', description: 'list/resume sessions' },
1959
- { name: 'debug', description: 'runtime debug switches' },
1960
- { name: 'retry', description: 'retry the last user request' }
2236
+ { name: 'help', description: completionCopy.commands.help },
2237
+ { name: 'exit', description: completionCopy.commands.exit },
2238
+ { name: 'commands', description: completionCopy.commands.commands },
2239
+ { name: 'status', description: completionCopy.commands.status },
2240
+ { name: 'mode', description: completionCopy.commands.mode },
2241
+ { name: 'compact', description: completionCopy.commands.compact },
2242
+ { name: 'checkpoint', description: completionCopy.commands.checkpoint },
2243
+ { name: 'spec', description: completionCopy.commands.spec },
2244
+ { name: 'plan', description: completionCopy.commands.plan },
2245
+ { name: 'agents', description: completionCopy.commands.agents },
2246
+ { name: 'config', description: completionCopy.commands.config },
2247
+ { name: 'memory', description: completionCopy.commands.memory },
2248
+ { name: 'history', description: completionCopy.commands.history },
2249
+ { name: 'debug', description: completionCopy.commands.debug },
2250
+ { name: 'retry', description: completionCopy.commands.retry }
1961
2251
  ];
1962
2252
  const out = [];
1963
2253
  for (const cmd of commands.values()) {
@@ -1991,8 +2281,8 @@ export async function createChatRuntime({
1991
2281
  ];
1992
2282
 
1993
2283
  const historyTemplates = ['/history list', '/history current', '/history resume <session_id>'];
2284
+ const memoryTemplates = ['/memory list <scope>', '/memory search <scope> <query>', '/memory forget <scope> <id>'];
1994
2285
  const modeTemplates = ['/mode normal', '/mode auto', '/mode plan'];
1995
- const taskTemplates = ['/tasks', '/tasks add <title>', '/tasks start <id>', '/tasks done <id>', '/tasks remove <id>', '/tasks clear'];
1996
2286
  const checkpointTemplates = [
1997
2287
  '/checkpoint create <name>',
1998
2288
  '/checkpoint list',
@@ -2000,15 +2290,15 @@ export async function createChatRuntime({
2000
2290
  '/checkpoint load <id>'
2001
2291
  ];
2002
2292
  const specTemplates = ['/spec <topic>'];
2003
- const planTemplates = ['/plan <goal>', '/plan auto <goal>', '/plan from-spec <spec-path?>'];
2293
+ const planTemplates = ['/plan <goal>', '/plan auto <goal>', '/plan auto run <goal>', '/plan approve', '/plan from-spec <spec-path?>'];
2004
2294
  const agentTemplates = ['/agents list', '/agents run planner <task>', '/agents run coder <task>', '/agents run reviewer <task>', '/agents run tester <task>'];
2005
2295
  const debugTemplates = ['/debug keys on', '/debug keys off', '/debug keys status'];
2006
2296
  const compactTemplates = compactOptions.map((opt) => `/compact ${opt}`);
2007
2297
  const slashTemplates = [
2008
2298
  ...configTemplates,
2299
+ ...memoryTemplates,
2009
2300
  ...historyTemplates,
2010
2301
  ...modeTemplates,
2011
- ...taskTemplates,
2012
2302
  ...checkpointTemplates,
2013
2303
  ...specTemplates,
2014
2304
  ...planTemplates,
@@ -2041,6 +2331,9 @@ export async function createChatRuntime({
2041
2331
  const getCompletionOptions = (rawInput) => {
2042
2332
  const input = String(rawInput || '');
2043
2333
  if (!input.startsWith('/')) return [];
2334
+ const completionCopy = getCompletionCopy(config.ui?.language);
2335
+ const configSubcommandDescriptions = completionCopy.configSubcommands;
2336
+ const planSubcommandDescriptions = completionCopy.planSubcommands || {};
2044
2337
 
2045
2338
  const hasTrailingSpace = /\s$/.test(input);
2046
2339
  const body = input.slice(1);
@@ -2048,9 +2341,9 @@ export async function createChatRuntime({
2048
2341
  const commandPart = tokens[0] || '';
2049
2342
  const commandHasSubcommands = new Set([
2050
2343
  'config',
2344
+ 'memory',
2051
2345
  'compact',
2052
2346
  'mode',
2053
- 'tasks',
2054
2347
  'checkpoint',
2055
2348
  'plan',
2056
2349
  'agents',
@@ -2065,19 +2358,21 @@ export async function createChatRuntime({
2065
2358
  registerSuggestion(`/${entry.name}`, entry.description || '');
2066
2359
  }
2067
2360
  for (const template of configTemplates) {
2068
- registerSuggestion(template, configSubcommandDescriptions[template] || 'config command');
2069
- }
2070
- for (const template of historyTemplates) registerSuggestion(template, 'history command');
2071
- for (const template of modeTemplates) registerSuggestion(template, 'switch execution mode');
2072
- for (const template of taskTemplates) registerSuggestion(template, 'task board command');
2073
- for (const template of checkpointTemplates) registerSuggestion(template, 'checkpoint command');
2074
- for (const template of specTemplates) registerSuggestion(template, 'create a spec file');
2075
- for (const template of planTemplates) registerSuggestion(template, 'planning command');
2076
- for (const template of agentTemplates) registerSuggestion(template, 'sub-agent command');
2077
- for (const template of debugTemplates) registerSuggestion(template, 'debug command');
2078
- for (const template of compactTemplates) registerSuggestion(template, 'context compaction command');
2079
- registerSuggestion('/retry', 'retry the last user request');
2080
- registerSuggestion('/status', 'show runtime status');
2361
+ registerSuggestion(template, configSubcommandDescriptions[template] || completionCopy.generic.configCommand);
2362
+ }
2363
+ for (const template of memoryTemplates) registerSuggestion(template, completionCopy.generic.memoryCommand);
2364
+ for (const template of historyTemplates) registerSuggestion(template, completionCopy.generic.historyCommand);
2365
+ for (const template of modeTemplates) registerSuggestion(template, completionCopy.generic.modeCommand);
2366
+ for (const template of checkpointTemplates) registerSuggestion(template, completionCopy.generic.checkpointCommand);
2367
+ for (const template of specTemplates) registerSuggestion(template, completionCopy.generic.specCommand);
2368
+ for (const template of planTemplates) {
2369
+ registerSuggestion(template, planSubcommandDescriptions[template] || completionCopy.generic.planCommand);
2370
+ }
2371
+ for (const template of agentTemplates) registerSuggestion(template, completionCopy.generic.agentCommand);
2372
+ for (const template of debugTemplates) registerSuggestion(template, completionCopy.generic.debugCommand);
2373
+ for (const template of compactTemplates) registerSuggestion(template, completionCopy.generic.compactCommand);
2374
+ registerSuggestion('/retry', completionCopy.generic.retryCommand);
2375
+ registerSuggestion('/status', completionCopy.generic.statusCommand);
2081
2376
 
2082
2377
  if (!commandPart) {
2083
2378
  return materializeSuggestions(prioritizeByPreferredOrder(
@@ -2105,7 +2400,7 @@ export async function createChatRuntime({
2105
2400
  return materializeSuggestions(prioritizeByPreferredOrder(
2106
2401
  ['set', 'get', 'list', 'reset']
2107
2402
  .filter((s) => s.startsWith(subcommand))
2108
- .map((s) => registerSuggestion(`/config ${s}`, configSubcommandDescriptions[`/config ${s}`] || 'config command').value),
2403
+ .map((s) => registerSuggestion(`/config ${s}`, configSubcommandDescriptions[`/config ${s}`] || completionCopy.generic.configCommand).value),
2109
2404
  configSubcommandPriority
2110
2405
  ));
2111
2406
  }
@@ -2114,77 +2409,84 @@ export async function createChatRuntime({
2114
2409
  const keyPrefix = tokens.length >= 3 ? tokens[2] || '' : '';
2115
2410
  return configKeyHints
2116
2411
  .filter((k) => k.startsWith(keyPrefix))
2117
- .map((k) => registerSuggestion(`/config get ${k}`, describeConfigKey(k, 'get')));
2412
+ .map((k) => registerSuggestion(`/config get ${k}`, describeConfigKey(k, 'get', config.ui?.language)));
2118
2413
  }
2119
2414
  if (subcommand === 'set') {
2120
2415
  const keyPrefix = tokens.length >= 3 ? tokens[2] || '' : '';
2121
2416
  return configKeyHints
2122
2417
  .filter((k) => k.startsWith(keyPrefix))
2123
- .map((k) => registerSuggestion(`/config set ${k} `, describeConfigKey(k, 'set')));
2418
+ .map((k) => registerSuggestion(`/config set ${k} `, describeConfigKey(k, 'set', config.ui?.language)));
2124
2419
  }
2125
2420
 
2126
2421
  return materializeSuggestions(configTemplates);
2127
2422
  }
2128
2423
 
2424
+ if (commandPart === 'memory') {
2425
+ const sub = tokens[1] || '';
2426
+ if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
2427
+ return ['list', 'search', 'forget']
2428
+ .filter((item) => item.startsWith(sub))
2429
+ .map((item) => registerSuggestion(`/memory ${item}`, completionCopy.generic.memoryCommand));
2430
+ }
2431
+ const scope = tokens[2] || '';
2432
+ if (['list', 'search', 'forget'].includes(sub) && (tokens.length === 2 || (tokens.length === 3 && !hasTrailingSpace))) {
2433
+ return ['user', 'global', 'project']
2434
+ .filter((item) => item.startsWith(scope))
2435
+ .map((item) => registerSuggestion(`/memory ${sub} ${item}${sub === 'list' ? '' : ' '}`, completionCopy.generic.memoryCommand));
2436
+ }
2437
+ return materializeSuggestions(memoryTemplates);
2438
+ }
2439
+
2129
2440
  if (commandPart === 'compact') {
2130
2441
  const joined = tokens.slice(1).join(' ');
2131
2442
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
2132
2443
  return compactOptions
2133
2444
  .filter((opt) => opt.startsWith(joined) || joined === '')
2134
- .map((opt) => registerSuggestion(`/compact ${opt}`, 'context compaction command'));
2445
+ .map((opt) => registerSuggestion(`/compact ${opt}`, completionCopy.generic.compactCommand));
2135
2446
  }
2136
2447
  return compactOptions
2137
2448
  .filter((opt) => opt.includes(joined) || joined === '')
2138
- .map((opt) => registerSuggestion(`/compact ${opt}`, 'context compaction command'));
2449
+ .map((opt) => registerSuggestion(`/compact ${opt}`, completionCopy.generic.compactCommand));
2139
2450
  }
2140
2451
 
2141
2452
  if (commandPart === 'retry') {
2142
- return [registerSuggestion('/retry', 'retry the last user request')];
2453
+ return [registerSuggestion('/retry', completionCopy.generic.retryCommand)];
2143
2454
  }
2144
2455
  if (commandPart === 'status') {
2145
- return [registerSuggestion('/status', 'show runtime status')];
2456
+ return [registerSuggestion('/status', completionCopy.generic.statusCommand)];
2146
2457
  }
2147
2458
  if (commandPart === 'mode') {
2148
2459
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
2149
2460
  const sub = tokens[1] || '';
2150
2461
  return ['normal', 'auto', 'plan']
2151
2462
  .filter((m) => m.startsWith(sub))
2152
- .map((m) => registerSuggestion(`/mode ${m}`, 'switch execution mode'));
2463
+ .map((m) => registerSuggestion(`/mode ${m}`, completionCopy.generic.modeCommand));
2153
2464
  }
2154
2465
  return materializeSuggestions(modeTemplates);
2155
2466
  }
2156
- if (commandPart === 'tasks') {
2157
- if (tokens.length <= 2 && !hasTrailingSpace) {
2158
- const sub = tokens[1] || '';
2159
- return ['add', 'start', 'done', 'remove', 'rm', 'clear']
2160
- .filter((s) => s.startsWith(sub))
2161
- .map((s) => registerSuggestion(`/tasks ${s}`, 'task board command'));
2162
- }
2163
- return materializeSuggestions(taskTemplates);
2164
- }
2165
2467
  if (commandPart === 'checkpoint') {
2166
2468
  if (tokens.length <= 2 && !hasTrailingSpace) {
2167
2469
  const sub = tokens[1] || '';
2168
2470
  if (sub === 'list') {
2169
2471
  return ['--all']
2170
- .map((v) => registerSuggestion(`/checkpoint list ${v}`, 'checkpoint command'));
2472
+ .map((v) => registerSuggestion(`/checkpoint list ${v}`, completionCopy.generic.checkpointCommand));
2171
2473
  }
2172
2474
  return ['create', 'list', 'load']
2173
2475
  .filter((s) => s.startsWith(sub))
2174
- .map((s) => registerSuggestion(`/checkpoint ${s}`, 'checkpoint command'));
2476
+ .map((s) => registerSuggestion(`/checkpoint ${s}`, completionCopy.generic.checkpointCommand));
2175
2477
  }
2176
2478
  if (tokens[1] === 'list') {
2177
2479
  const hint = tokens[2] || '';
2178
2480
  return ['--all']
2179
2481
  .filter((v) => v.startsWith(hint))
2180
- .map((v) => registerSuggestion(`/checkpoint list ${v}`, 'checkpoint command'));
2482
+ .map((v) => registerSuggestion(`/checkpoint list ${v}`, completionCopy.generic.checkpointCommand));
2181
2483
  }
2182
2484
  if (tokens[1] === 'load') {
2183
2485
  if (tokens.length >= 3) {
2184
2486
  const hint = tokens[3] || '';
2185
2487
  return ['--all']
2186
2488
  .filter((v) => v.startsWith(hint))
2187
- .map((v) => registerSuggestion(`/checkpoint load ${tokens[2]} ${v}`, 'checkpoint command'));
2489
+ .map((v) => registerSuggestion(`/checkpoint load ${tokens[2]} ${v}`, completionCopy.generic.checkpointCommand));
2188
2490
  }
2189
2491
  }
2190
2492
  return materializeSuggestions(checkpointTemplates);
@@ -2193,11 +2495,25 @@ export async function createChatRuntime({
2193
2495
  return materializeSuggestions(specTemplates);
2194
2496
  }
2195
2497
  if (commandPart === 'plan') {
2498
+ if (tokens[1] === 'auto' && (tokens.length === 2 || (tokens.length === 3 && !hasTrailingSpace))) {
2499
+ const sub = tokens[2] || '';
2500
+ return ['run']
2501
+ .filter((s) => s.startsWith(sub))
2502
+ .map((s) => registerSuggestion(`/plan auto ${s} `, planSubcommandDescriptions[`/plan auto ${s} <goal>`] || completionCopy.generic.planCommand));
2503
+ }
2196
2504
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
2197
2505
  const sub = tokens[1] || '';
2198
- return ['auto', 'from-spec']
2506
+ return ['auto', 'approve', 'from-spec']
2199
2507
  .filter((s) => s.startsWith(sub))
2200
- .map((s) => registerSuggestion(`/plan ${s}`, 'planning command'));
2508
+ .map((s) =>
2509
+ registerSuggestion(
2510
+ `/plan ${s}`,
2511
+ planSubcommandDescriptions[`/plan ${s}`] ||
2512
+ planSubcommandDescriptions[`/plan ${s} <goal>`] ||
2513
+ planSubcommandDescriptions[`/plan ${s} <spec-path?>`] ||
2514
+ completionCopy.generic.planCommand
2515
+ )
2516
+ );
2201
2517
  }
2202
2518
  return materializeSuggestions(planTemplates);
2203
2519
  }
@@ -2206,17 +2522,17 @@ export async function createChatRuntime({
2206
2522
  const sub = tokens[1] || '';
2207
2523
  if (sub === 'run') {
2208
2524
  return ['planner', 'coder', 'reviewer', 'tester']
2209
- .map((r) => registerSuggestion(`/agents run ${r} `, 'sub-agent command'));
2525
+ .map((r) => registerSuggestion(`/agents run ${r} `, completionCopy.generic.agentCommand));
2210
2526
  }
2211
2527
  return ['list', 'run']
2212
2528
  .filter((s) => s.startsWith(sub))
2213
- .map((s) => registerSuggestion(`/agents ${s}`, 'sub-agent command'));
2529
+ .map((s) => registerSuggestion(`/agents ${s}`, completionCopy.generic.agentCommand));
2214
2530
  }
2215
2531
  if (tokens[1] === 'run') {
2216
2532
  const rolePrefix = tokens[2] || '';
2217
2533
  return ['planner', 'coder', 'reviewer', 'tester']
2218
2534
  .filter((r) => r.startsWith(rolePrefix))
2219
- .map((r) => registerSuggestion(`/agents run ${r} `, 'sub-agent command'));
2535
+ .map((r) => registerSuggestion(`/agents run ${r} `, completionCopy.generic.agentCommand));
2220
2536
  }
2221
2537
  return materializeSuggestions(agentTemplates);
2222
2538
  }
@@ -2230,13 +2546,13 @@ export async function createChatRuntime({
2230
2546
  .map((session) => ({
2231
2547
  value: `/history resume ${session.id}`,
2232
2548
  display: `/history resume ${session.id} · ${Number(session.messageCount || 0)} msgs`,
2233
- description: 'resume a saved session'
2549
+ description: completionCopy.generic.resumeSession
2234
2550
  }));
2235
2551
  if (dynamic.length > 0) return dynamic;
2236
2552
  }
2237
2553
  return ['list', 'current', 'resume']
2238
2554
  .filter((s) => s.startsWith(sub))
2239
- .map((s) => registerSuggestion(`/history ${s}`, 'history command'));
2555
+ .map((s) => registerSuggestion(`/history ${s}`, completionCopy.generic.historyCommand));
2240
2556
  }
2241
2557
  if (sub === 'resume') {
2242
2558
  const idPrefix = tokens[2] || '';
@@ -2245,7 +2561,7 @@ export async function createChatRuntime({
2245
2561
  .map((session) => ({
2246
2562
  value: `/history resume ${session.id}`,
2247
2563
  display: `/history resume ${session.id} · ${Number(session.messageCount || 0)} msgs`,
2248
- description: 'resume a saved session'
2564
+ description: completionCopy.generic.resumeSession
2249
2565
  }));
2250
2566
  if (dynamic.length > 0) return dynamic;
2251
2567
  return materializeSuggestions(historyTemplates);
@@ -2258,17 +2574,17 @@ export async function createChatRuntime({
2258
2574
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
2259
2575
  if (sub === 'keys') {
2260
2576
  return ['on', 'off', 'status']
2261
- .map((v) => registerSuggestion(`/debug keys ${v}`, 'keyboard debug command'));
2577
+ .map((v) => registerSuggestion(`/debug keys ${v}`, completionCopy.generic.keyboardDebugCommand));
2262
2578
  }
2263
2579
  return ['keys']
2264
2580
  .filter((s) => s.startsWith(sub))
2265
- .map((s) => registerSuggestion(`/debug ${s}`, 'debug command'));
2581
+ .map((s) => registerSuggestion(`/debug ${s}`, completionCopy.generic.debugCommand));
2266
2582
  }
2267
2583
  if (sub === 'keys') {
2268
2584
  const action = tokens[2] || '';
2269
2585
  return ['on', 'off', 'status']
2270
2586
  .filter((v) => v.startsWith(action))
2271
- .map((v) => registerSuggestion(`/debug keys ${v}`, 'keyboard debug command'));
2587
+ .map((v) => registerSuggestion(`/debug keys ${v}`, completionCopy.generic.keyboardDebugCommand));
2272
2588
  }
2273
2589
  return materializeSuggestions(debugTemplates);
2274
2590
  }
@@ -2286,6 +2602,17 @@ export async function createChatRuntime({
2286
2602
  await saveSession(currentSession);
2287
2603
  };
2288
2604
 
2605
+ const buildActiveSystemPrompt = async () => {
2606
+ const soulPrompt = await buildSystemPromptWithSoul(baseSystemPrompt, config);
2607
+ const memorySnapshot = await buildMemorySnapshot({
2608
+ config,
2609
+ workspaceRoot: process.cwd()
2610
+ }).catch(() => '');
2611
+ const memoryGuide =
2612
+ 'Persistent memory stores durable preferences and stable workflow knowledge. Verify changeable details from files, and only write memory for future-useful, non-sensitive facts.';
2613
+ return [soulPrompt, memorySnapshot, memoryGuide].filter(Boolean).join('\n\n');
2614
+ };
2615
+
2289
2616
  const isImmediateLocalInput = (line) => {
2290
2617
  const parsedInput = parseInput(line);
2291
2618
  if (parsedInput.type !== 'slash') return false;
@@ -2301,9 +2628,9 @@ export async function createChatRuntime({
2301
2628
  'commands',
2302
2629
  'status',
2303
2630
  'mode',
2304
- 'tasks',
2305
2631
  'checkpoint',
2306
2632
  'history',
2633
+ 'memory',
2307
2634
  'config',
2308
2635
  'compact',
2309
2636
  'debug'
@@ -2312,7 +2639,7 @@ export async function createChatRuntime({
2312
2639
  };
2313
2640
 
2314
2641
  const submit = async (line, onAgentEvent) => {
2315
- const activeReplySystemPrompt = await buildSystemPromptWithSoul(baseSystemPrompt, config);
2642
+ const activeReplySystemPrompt = await buildActiveSystemPrompt();
2316
2643
  try {
2317
2644
  await appendInputHistory(line);
2318
2645
  } catch {
@@ -2331,14 +2658,14 @@ export async function createChatRuntime({
2331
2658
  if (parsedInput.command === 'help') {
2332
2659
  return {
2333
2660
  type: 'system',
2334
- text: 'Commands: /help /exit /commands /status /mode /compact /tasks /checkpoint /spec /plan /agents /config /history /debug /retry /<custom> !<shell>'
2661
+ text: 'Commands: /help /exit /commands /status /mode /compact /checkpoint /spec /plan /agents /config /memory /history /debug /retry /<custom> !<shell>'
2335
2662
  };
2336
2663
  }
2337
2664
  if (parsedInput.command === 'status') {
2338
- const taskCount = (await loadTasks(process.cwd(), currentSession.id)).length;
2665
+ const todoCount = countActiveTodos(currentSession.todos);
2339
2666
  return {
2340
2667
  type: 'system',
2341
- text: `mode=${executionMode} | model=${model || config.model.name} | max_ctx=${effectiveMaxContextTokens(config)} | session=${currentSession.id} | tasks=${taskCount}`
2668
+ text: `mode=${executionMode} | model=${model || config.model.name} | max_ctx=${effectiveMaxContextTokens(config)} | session=${currentSession.id} | todos=${todoCount}`
2342
2669
  };
2343
2670
  }
2344
2671
  if (parsedInput.command === 'mode') {
@@ -2356,74 +2683,15 @@ export async function createChatRuntime({
2356
2683
  await persistLocalExchange(line, text);
2357
2684
  return { type: 'system', text };
2358
2685
  }
2359
- if (parsedInput.command === 'tasks') {
2360
- const sub = (parsedInput.args[0] || '').trim().toLowerCase();
2361
- if (!sub) {
2362
- const tasks = await loadTasks(process.cwd(), currentSession.id);
2363
- if (tasks.length === 0) return { type: 'system', text: 'No tasks' };
2364
- const rows = tasks.map((t, idx) => `${idx + 1}. ${t.id} | ${t.status} | ${t.title}`);
2365
- return { type: 'system', text: rows.join('\n') };
2366
- }
2367
- if (sub === 'add') {
2368
- const title = parsedInput.args.slice(1).join(' ').trim();
2369
- if (!title) return { type: 'system', text: 'Usage: /tasks add <title>' };
2370
- const created = await createTasks([{ title }], process.cwd(), currentSession.id);
2371
- const text = `Created task: ${created[0]?.id || '-'} | ${title}`;
2372
- await persistLocalExchange(line, text);
2373
- return { type: 'system', text };
2374
- }
2375
- if (sub === 'start') {
2376
- const id = parsedInput.args[1];
2377
- if (!id) return { type: 'system', text: 'Usage: /tasks start <id>' };
2378
- const updated = await updateTask(id, { status: 'in_progress' }, process.cwd(), currentSession.id);
2379
- if (!updated) return { type: 'system', text: `Task not found: ${id}` };
2380
- const text = `Task in progress: ${id}`;
2381
- await persistLocalExchange(line, text);
2382
- return { type: 'system', text };
2383
- }
2384
- if (sub === 'done') {
2385
- const id = parsedInput.args[1];
2386
- if (!id) return { type: 'system', text: 'Usage: /tasks done <id>' };
2387
- const updated = await updateTask(id, { status: 'completed' }, process.cwd(), currentSession.id);
2388
- if (!updated) return { type: 'system', text: `Task not found: ${id}` };
2389
- const text = `Task completed: ${id}`;
2390
- await persistLocalExchange(line, text);
2391
- return { type: 'system', text };
2392
- }
2393
- if (sub === 'remove' || sub === 'rm') {
2394
- const id = parsedInput.args[1];
2395
- if (!id) return { type: 'system', text: 'Usage: /tasks remove <id>' };
2396
- const result = await deleteTasks([id], process.cwd(), currentSession.id);
2397
- const text = `Removed=${result.removed}, Remaining=${result.remaining}`;
2398
- await persistLocalExchange(line, text);
2399
- return { type: 'system', text };
2400
- }
2401
- if (sub === 'clear') {
2402
- await clearTasks(process.cwd(), currentSession.id);
2403
- const text = 'All tasks cleared';
2404
- await persistLocalExchange(line, text);
2405
- return { type: 'system', text };
2406
- }
2407
- // shorthand: /tasks implement x
2408
- const title = parsedInput.args.join(' ').trim();
2409
- if (title) {
2410
- const created = await createTasks([{ title }], process.cwd(), currentSession.id);
2411
- const text = `Created task: ${created[0]?.id || '-'} | ${title}`;
2412
- await persistLocalExchange(line, text);
2413
- return { type: 'system', text };
2414
- }
2415
- }
2416
2686
  if (parsedInput.command === 'checkpoint') {
2417
2687
  const sub = (parsedInput.args[0] || 'list').trim().toLowerCase();
2418
2688
  if (sub === 'create') {
2419
2689
  const name = parsedInput.args.slice(1).join(' ').trim();
2420
- const tasks = await loadTasks(process.cwd(), currentSession.id);
2421
2690
  const cp = await createCheckpoint(
2422
2691
  {
2423
2692
  name,
2424
2693
  session: currentSession,
2425
- config,
2426
- tasks
2694
+ config
2427
2695
  },
2428
2696
  process.cwd()
2429
2697
  );
@@ -2458,17 +2726,6 @@ export async function createChatRuntime({
2458
2726
  config = cp.config;
2459
2727
  executionMode = config.execution?.mode || executionMode;
2460
2728
  }
2461
- if (Array.isArray(cp?.tasks)) {
2462
- await clearTasks(process.cwd(), currentSession.id);
2463
- if (cp.tasks.length > 0) {
2464
- // restore with new ids to avoid stale references
2465
- await createTasks(
2466
- cp.tasks.map((t) => ({ title: t.title, description: t.description })),
2467
- process.cwd(),
2468
- currentSession.id
2469
- );
2470
- }
2471
- }
2472
2729
  const text = `Checkpoint loaded: ${id}`;
2473
2730
  await persistLocalExchange(line, text, { includeUser: false });
2474
2731
  return { type: 'system', text };
@@ -2505,8 +2762,9 @@ export async function createChatRuntime({
2505
2762
  if (parsedInput.command === 'plan') {
2506
2763
  const sub = (parsedInput.args[0] || '').trim().toLowerCase();
2507
2764
  if (sub === 'auto') {
2508
- const goal = parsedInput.args.slice(1).join(' ').trim();
2509
- if (!goal) return { type: 'system', text: 'Usage: /plan auto <goal>' };
2765
+ const runImmediately = (parsedInput.args[1] || '').trim().toLowerCase() === 'run';
2766
+ const goal = parsedInput.args.slice(runImmediately ? 2 : 1).join(' ').trim();
2767
+ if (!goal) return { type: 'system', text: 'Usage: /plan auto <goal> | /plan auto run <goal>' };
2510
2768
  const auto = await buildAutoPlanAndRun({
2511
2769
  goal,
2512
2770
  session: currentSession,
@@ -2514,8 +2772,35 @@ export async function createChatRuntime({
2514
2772
  model,
2515
2773
  systemPrompt: activeReplySystemPrompt,
2516
2774
  onAgentEvent,
2517
- sessionId: currentSession.id
2775
+ sessionId: currentSession.id,
2776
+ taskClass: classifyPlanTaskClass(goal)
2518
2777
  });
2778
+ if (runImmediately) {
2779
+ const result = await askModel({
2780
+ text: buildApprovedPlanExecutionPrompt(
2781
+ {
2782
+ status: 'approved',
2783
+ source: 'auto',
2784
+ goal,
2785
+ filePath: auto.filePath,
2786
+ summary: auto.summary || '',
2787
+ finalSummary: auto.finalSummary || auto.summary || '',
2788
+ steps: Array.isArray(auto.steps) ? auto.steps : []
2789
+ },
2790
+ '/plan auto run'
2791
+ ),
2792
+ session: currentSession,
2793
+ config,
2794
+ model,
2795
+ systemPrompt: activeReplySystemPrompt,
2796
+ onAgentEvent,
2797
+ executionMode: 'auto'
2798
+ });
2799
+ currentSession.planState = null;
2800
+ executionMode = 'auto';
2801
+ await saveSession(currentSession);
2802
+ return { type: 'assistant', text: result.text };
2803
+ }
2519
2804
  currentSession.planState = {
2520
2805
  status: 'pending_approval',
2521
2806
  source: 'auto',
@@ -2595,7 +2880,7 @@ export async function createChatRuntime({
2595
2880
  }
2596
2881
 
2597
2882
  const goal = parsedInput.args.join(' ').trim();
2598
- if (!goal) return { type: 'system', text: 'Usage: /plan <goal> | /plan auto <goal> | /plan from-spec <spec-path?>' };
2883
+ if (!goal) return { type: 'system', text: 'Usage: /plan <goal> | /plan auto <goal> | /plan auto run <goal> | /plan from-spec <spec-path?>' };
2599
2884
  const content = buildPlanTemplate(goal);
2600
2885
  const filePath = await writeMarkdownInProjectDir(
2601
2886
  'plans',
@@ -2690,11 +2975,55 @@ export async function createChatRuntime({
2690
2975
  ];
2691
2976
  return {
2692
2977
  type: 'system',
2693
- text: `Switched to session: ${targetId} (${loaded.messages.length} messages)`
2978
+ text: `Switched to session: ${targetId} (${loaded.messages.length} messages)`,
2979
+ restoredMessages: structuredClone(loaded.messages || [])
2694
2980
  };
2695
2981
  }
2696
2982
  return { type: 'system', text: `Unknown /history subcommand: ${sub}` };
2697
2983
  }
2984
+ if (parsedInput.command === 'memory') {
2985
+ const sub = String(parsedInput.args[0] || '').trim().toLowerCase();
2986
+ if (!sub) {
2987
+ return { type: 'system', text: 'Usage: /memory list <user|global|project> | /memory search <scope> <query> | /memory forget <scope> <id>' };
2988
+ }
2989
+ if (sub === 'list') {
2990
+ const scope = String(parsedInput.args[1] || '').trim().toLowerCase();
2991
+ if (!['user', 'global', 'project'].includes(scope)) {
2992
+ return { type: 'system', text: 'Usage: /memory list <user|global|project>' };
2993
+ }
2994
+ const items = await listMemories({ scope, workspaceRoot: process.cwd() });
2995
+ if (items.length === 0) return { type: 'system', text: `No ${scope} memories found.` };
2996
+ return {
2997
+ type: 'system',
2998
+ text: items.map((item) => `${item.id} | ${item.kind} | ${item.content}`).join('\n')
2999
+ };
3000
+ }
3001
+ if (sub === 'search') {
3002
+ const scope = String(parsedInput.args[1] || '').trim().toLowerCase();
3003
+ const query = parsedInput.args.slice(2).join(' ').trim();
3004
+ if (!['user', 'global', 'project'].includes(scope) || !query) {
3005
+ return { type: 'system', text: 'Usage: /memory search <user|global|project> <query>' };
3006
+ }
3007
+ const items = await searchMemories({ scope, query, workspaceRoot: process.cwd() });
3008
+ if (items.length === 0) return { type: 'system', text: `No ${scope} memories matched: ${query}` };
3009
+ return {
3010
+ type: 'system',
3011
+ text: items.map((item) => `${item.id} | ${item.kind} | ${item.content}`).join('\n')
3012
+ };
3013
+ }
3014
+ if (sub === 'forget') {
3015
+ const scope = String(parsedInput.args[1] || '').trim().toLowerCase();
3016
+ const id = String(parsedInput.args[2] || '').trim();
3017
+ if (!['user', 'global', 'project'].includes(scope) || !id) {
3018
+ return { type: 'system', text: 'Usage: /memory forget <user|global|project> <id>' };
3019
+ }
3020
+ const result = await forgetMemory({ scope, id, workspaceRoot: process.cwd() });
3021
+ const text = `Removed ${Number(result.removed || 0)} ${scope} memory item(s)`;
3022
+ await persistLocalExchange(line, text, { includeUser: false });
3023
+ return { type: 'system', text };
3024
+ }
3025
+ return { type: 'system', text: `Unknown /memory subcommand: ${sub}` };
3026
+ }
2698
3027
  if (parsedInput.command === 'retry') {
2699
3028
  const lastUser = [...currentSession.messages].reverse().find((m) => m.role === 'user');
2700
3029
  if (!lastUser?.content) {
@@ -2925,7 +3254,8 @@ export async function createChatRuntime({
2925
3254
  model,
2926
3255
  systemPrompt: activeReplySystemPrompt,
2927
3256
  onAgentEvent,
2928
- sessionId: currentSession.id
3257
+ sessionId: currentSession.id,
3258
+ taskClass: classifyPlanTaskClass(expandedText)
2929
3259
  });
2930
3260
  currentSession.planState = {
2931
3261
  status: 'pending_approval',
@@ -2949,7 +3279,11 @@ export async function createChatRuntime({
2949
3279
  names: selectedAutoSkills
2950
3280
  });
2951
3281
  }
2952
- const routedSystemPrompt = buildAutoSkillSystemPrompt(activeReplySystemPrompt, commands, config, expandedText);
3282
+ const skillPrompt = buildAutoSkillSystemPrompt(activeReplySystemPrompt, commands, config, expandedText);
3283
+ const routedSystemPrompt =
3284
+ autoRoute.mode === 'direct_medium'
3285
+ ? buildMediumTaskSystemPrompt(skillPrompt)
3286
+ : skillPrompt;
2953
3287
  const result = await askModel({
2954
3288
  text: expandedText,
2955
3289
  session: currentSession,