codemini-cli 0.3.0 → 0.3.2

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,7 +6,7 @@ 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';
@@ -30,6 +30,8 @@ import { buildSystemPromptWithReplyLanguage } from './reply-language.js';
30
30
  import { buildSystemPromptWithSoul } from './soul.js';
31
31
  import { getProjectPlansDir, getProjectSpecsDir, getProjectWorkspaceDir, getSessionsDir } from './paths.js';
32
32
  import { buildProjectContextSnippet, initializeProjectIndex } from './project-index.js';
33
+ import { buildMemorySnapshot } from './memory-prompt.js';
34
+ import { forgetMemory, listMemories, searchMemories } from './memory-store.js';
33
35
 
34
36
  function toOpenAIMessages(sessionMessages) {
35
37
  const mapped = [];
@@ -74,35 +76,199 @@ function prioritizeByPreferredOrder(items, preferredOrder) {
74
76
  });
75
77
  }
76
78
 
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}`;
79
+ function normalizeUiLocale(value) {
80
+ return String(value || '').toLowerCase().startsWith('en') ? 'en' : 'zh';
81
+ }
82
+
83
+ function getCompletionCopy(language = 'zh') {
84
+ const lang = normalizeUiLocale(language);
85
+ return {
86
+ zh: {
87
+ configLabels: {
88
+ 'gateway.base_url': '网关基础 URL',
89
+ 'gateway.api_key': '网关 API Key',
90
+ 'sdk.provider': 'SDK provider',
91
+ 'gateway.timeout_ms': '网关超时时间(毫秒)',
92
+ 'gateway.max_retries': '网关重试次数',
93
+ 'model.name': '当前模型名称',
94
+ 'model.max_context_tokens': '模型上下文 token 上限',
95
+ 'ui.language': '界面语言',
96
+ 'ui.reply_language': '回复语言',
97
+ 'execution.mode': '执行模式',
98
+ 'execution.always_allow_tools': '始终允许的工具列表',
99
+ 'execution.max_steps': '最大工具步骤数',
100
+ 'context.preflight_trigger_pct': '预压缩阈值',
101
+ 'context.hard_limit_pct': '硬压缩阈值',
102
+ 'context.tool_result_max_chars': '工具结果字符上限',
103
+ 'context.read_file_default_lines': 'read_file 默认行数窗口',
104
+ 'context.read_file_max_chars': 'read_file 字符上限',
105
+ 'sessions.max_sessions': '会话保留上限',
106
+ 'sessions.retention_days': '会话保留天数',
107
+ 'shell.default': '默认 shell',
108
+ 'shell.timeout_ms': 'shell 超时时间(毫秒)',
109
+ 'context.max_tokens': '上下文 token 预算',
110
+ 'soul.preset': 'soul 预设',
111
+ 'soul.custom_path': '自定义 soul 路径',
112
+ 'policy.safe_mode': '安全模式开关',
113
+ 'policy.allow_dangerous_commands': '危险命令开关'
114
+ },
115
+ optionHints: {
116
+ 'sdk.provider': '可选:openai-compatible | anthropic',
117
+ 'ui.language': '可选:zh | en',
118
+ 'ui.reply_language': '可选:zh | en',
119
+ 'execution.mode': '可选:auto | normal | plan',
120
+ 'shell.default': '常用:bash | powershell',
121
+ 'policy.safe_mode': '可选:true | false',
122
+ 'policy.allow_dangerous_commands': '可选:true | false'
123
+ },
124
+ describeSet: (label, hint) => `设置${label}${hint ? `(${hint})` : ''}`,
125
+ describeGet: (label, hint) => `查看${label}${hint ? `(${hint})` : ''}`,
126
+ configSubcommands: {
127
+ '/config set': '设置配置项',
128
+ '/config get': '查看配置项',
129
+ '/config list': '查看完整配置',
130
+ '/config reset': '重置为默认配置'
131
+ },
132
+ planSubcommands: {
133
+ '/plan <goal>': '创建一个人工审阅的实施计划',
134
+ '/plan auto <goal>': '自动生成计划并等待你确认执行',
135
+ '/plan auto run <goal>': '自动生成计划后立即继续执行',
136
+ '/plan approve': '批准当前待确认的计划并开始执行',
137
+ '/plan from-spec <spec-path?>': '从 spec 文件生成实施计划'
138
+ },
139
+ commands: {
140
+ help: '显示聊天帮助',
141
+ exit: '退出聊天',
142
+ commands: '列出 slash/自定义命令',
143
+ status: '查看运行状态(mode/model/session)',
144
+ mode: '设置执行模式:normal|auto|plan',
145
+ compact: '压缩消息上下文',
146
+ tasks: '任务面板管理',
147
+ checkpoint: '创建/查看/加载检查点',
148
+ spec: '在 .codemini/specs 中创建 spec',
149
+ plan: '在 .codemini/plans 中创建实施计划',
150
+ agents: '列出/运行子代理角色',
151
+ config: '设置/读取/列出/重置配置',
152
+ memory: '查看/搜索/删除持久记忆',
153
+ history: '查看/恢复会话',
154
+ debug: '运行时调试开关',
155
+ retry: '重试上一条用户请求'
156
+ },
157
+ generic: {
158
+ configCommand: '配置命令',
159
+ historyCommand: '历史会话命令',
160
+ modeCommand: '切换执行模式',
161
+ taskCommand: '任务面板命令',
162
+ checkpointCommand: '检查点命令',
163
+ specCommand: '创建 spec 文件',
164
+ planCommand: '规划命令',
165
+ agentCommand: '子代理命令',
166
+ memoryCommand: '记忆命令',
167
+ debugCommand: '调试命令',
168
+ keyboardDebugCommand: '键盘调试命令',
169
+ compactCommand: '上下文压缩命令',
170
+ retryCommand: '重试上一条用户请求',
171
+ statusCommand: '查看运行状态',
172
+ resumeSession: '恢复一个已保存的会话'
173
+ }
174
+ },
175
+ en: {
176
+ configLabels: {
177
+ 'gateway.base_url': 'gateway base URL',
178
+ 'gateway.api_key': 'gateway API key',
179
+ 'sdk.provider': 'SDK provider',
180
+ 'gateway.timeout_ms': 'gateway timeout in milliseconds',
181
+ 'gateway.max_retries': 'gateway retry count',
182
+ 'model.name': 'active model name',
183
+ 'model.max_context_tokens': 'model context token limit',
184
+ 'ui.language': 'UI language',
185
+ 'ui.reply_language': 'reply language',
186
+ 'execution.mode': 'execution mode',
187
+ 'execution.always_allow_tools': 'always-allowed tools',
188
+ 'execution.max_steps': 'maximum tool steps',
189
+ 'context.preflight_trigger_pct': 'preflight compact threshold',
190
+ 'context.hard_limit_pct': 'hard compact threshold',
191
+ 'context.tool_result_max_chars': 'tool result character limit',
192
+ 'context.read_file_default_lines': 'default read_file line window',
193
+ 'context.read_file_max_chars': 'read_file character limit',
194
+ 'sessions.max_sessions': 'stored session limit',
195
+ 'sessions.retention_days': 'session retention days',
196
+ 'shell.default': 'default shell',
197
+ 'shell.timeout_ms': 'shell timeout in milliseconds',
198
+ 'context.max_tokens': 'context token budget',
199
+ 'soul.preset': 'soul preset',
200
+ 'soul.custom_path': 'custom soul prompt path',
201
+ 'policy.safe_mode': 'safe mode switch',
202
+ 'policy.allow_dangerous_commands': 'dangerous command allowance'
203
+ },
204
+ optionHints: {
205
+ 'sdk.provider': 'options: openai-compatible | anthropic',
206
+ 'ui.language': 'options: zh | en',
207
+ 'ui.reply_language': 'options: zh | en',
208
+ 'execution.mode': 'options: auto | normal | plan',
209
+ 'shell.default': 'common: bash | powershell',
210
+ 'policy.safe_mode': 'options: true | false',
211
+ 'policy.allow_dangerous_commands': 'options: true | false'
212
+ },
213
+ describeSet: (label, hint) => `set the ${label}${hint ? ` (${hint})` : ''}`,
214
+ describeGet: (label, hint) => `show the ${label}${hint ? ` (${hint})` : ''}`,
215
+ configSubcommands: {
216
+ '/config set': 'update a config value',
217
+ '/config get': 'show a config value',
218
+ '/config list': 'print the full config',
219
+ '/config reset': 'reset config to defaults'
220
+ },
221
+ planSubcommands: {
222
+ '/plan <goal>': 'create an implementation plan for manual review',
223
+ '/plan auto <goal>': 'generate a plan and wait for your approval',
224
+ '/plan auto run <goal>': 'generate a plan and continue execution immediately',
225
+ '/plan approve': 'approve the pending plan and start execution',
226
+ '/plan from-spec <spec-path?>': 'generate an implementation plan from a spec file'
227
+ },
228
+ commands: {
229
+ help: 'show chat help',
230
+ exit: 'exit chat',
231
+ commands: 'list slash/custom commands',
232
+ status: 'show runtime status (mode/model/session)',
233
+ mode: 'set execution mode: normal|auto|plan',
234
+ compact: 'compress message context',
235
+ tasks: 'task board management',
236
+ checkpoint: 'create/list/load conversation checkpoints',
237
+ spec: 'create a spec markdown file in .codemini/specs',
238
+ plan: 'create an implementation plan markdown file in .codemini/plans',
239
+ agents: 'run/list sub-agent roles',
240
+ config: 'set/get/list/reset config values',
241
+ memory: 'list/search/delete persistent memories',
242
+ history: 'list/resume sessions',
243
+ debug: 'runtime debug switches',
244
+ retry: 'retry the last user request'
245
+ },
246
+ generic: {
247
+ configCommand: 'config command',
248
+ historyCommand: 'history command',
249
+ modeCommand: 'switch execution mode',
250
+ taskCommand: 'task board command',
251
+ checkpointCommand: 'checkpoint command',
252
+ specCommand: 'create a spec file',
253
+ planCommand: 'planning command',
254
+ agentCommand: 'sub-agent command',
255
+ memoryCommand: 'memory command',
256
+ debugCommand: 'debug command',
257
+ keyboardDebugCommand: 'keyboard debug command',
258
+ compactCommand: 'context compaction command',
259
+ retryCommand: 'retry the last user request',
260
+ statusCommand: 'show runtime status',
261
+ resumeSession: 'resume a saved session'
262
+ }
263
+ }
264
+ }[lang];
265
+ }
266
+
267
+ function describeConfigKey(key, mode = 'set', language = 'zh') {
268
+ const copy = getCompletionCopy(language);
269
+ const label = copy.configLabels[key] || key;
270
+ const hint = copy.optionHints[key] || '';
271
+ return mode === 'get' ? copy.describeGet(label, hint) : copy.describeSet(label, hint);
106
272
  }
107
273
 
108
274
  const SUB_AGENT_ROLES = ['planner', 'coder', 'reviewer', 'tester'];
@@ -407,6 +573,24 @@ function isLightweightAutoPlanGoal(goal, requirements = []) {
407
573
  return /\b(add|update|fix|rename|trim|export|create|remove|change|implement)\b/i.test(text);
408
574
  }
409
575
 
576
+ function classifyPlanTaskClass(goal = '') {
577
+ const text = String(goal || '').trim();
578
+ const lowerGoal = text.toLowerCase();
579
+ const advisory =
580
+ /\b(analyze|analysis|review|audit|inspect|assess|recommend|recommendation|optimization|optimize|improve|suggest|brainstorm|plan|feedback)\b/i.test(lowerGoal) ||
581
+ /(分析|审查|审计|检查|评估|建议|优化|优化点|优化建议|改进|改进点|规划|方案|看一下|看看|有哪些问题|有什么问题)/.test(text);
582
+ const implementation =
583
+ /\b(add|build|create|implement|support|introduce|refactor|rewrite|rework|migrate|change|update|fix)\b/i.test(lowerGoal) ||
584
+ /(新增|增加|实现|支持|重构|重写|改造|迁移|修改|更新|修复)/.test(text);
585
+ const verificationHeavy =
586
+ /\b(test|verify|validation|validate|prove|confirm|reproduce|check coverage)\b/i.test(lowerGoal) ||
587
+ /(测试|验证|校验|确认|复现|覆盖率)/.test(text);
588
+
589
+ if (verificationHeavy) return 'verification-heavy';
590
+ if (advisory && !implementation) return 'advisory';
591
+ return 'implementation';
592
+ }
593
+
410
594
  function buildGoalRequirementPacket(goal, role) {
411
595
  const rawGoal = trimInlineText(goal, 800);
412
596
  if (!rawGoal) return '';
@@ -441,7 +625,9 @@ function buildAutoPlanPlannerGuidance() {
441
625
  '- Prefer the smallest local approach that satisfies the goal.',
442
626
  '- Do not output multiple alternative branches in the final plan.',
443
627
  '- 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.',
628
+ '- Available sub-agent roles are planner, coder, reviewer, and tester. Use only the roles the task actually needs.',
629
+ '- For implementation-heavy or risky changes, prefer adding review and/or verification steps.',
630
+ '- For analysis, recommendation, or planning-only goals, you may omit reviewer/tester if they do not add value.',
445
631
  '- Prefer 3-5 steps total unless the task is clearly larger.',
446
632
  '- Keep the plan ordered, implementation-oriented, and easy for small sub-agents to follow.'
447
633
  ].join('\n');
@@ -598,49 +784,56 @@ function selectAutoSkillNames(text = '') {
598
784
  return selected;
599
785
  }
600
786
 
601
- function shouldAutoPlan(text = '') {
787
+ function classifyTaskComplexity(text = '') {
602
788
  const input = String(text || '').trim();
603
- if (!input) return false;
789
+ if (!input) return 'simple';
604
790
 
605
791
  const lower = input.toLowerCase();
606
792
  const explicitPlanning =
607
793
  /(\/plan\b|plan first|make a plan|implementation plan|先做计划|先出方案|先规划|先计划)/i.test(lower);
608
- if (explicitPlanning) return false;
794
+ if (explicitPlanning) return 'complex';
609
795
 
610
796
  const simpleSkip =
611
797
  /(typo|readme|console\.log|log this|rename\s+\w+|one line|small tweak|tiny fix|格式化|拼写|注释|文案|小改|微调)/i.test(
612
798
  lower
613
799
  );
614
- if (simpleSkip) return false;
800
+ if (simpleSkip) return 'simple';
615
801
 
616
802
  const discussionFirst =
617
803
  /(brainstorm|头脑风暴|方案|思路|怎么做|如何做|which (?:approach|option|way)|best way|trade-?off|not sure|unsure|unclear|whether it should|要不要|不确定|先别写|先不要写|先讨论|先想一下)/i.test(
618
804
  lower
619
805
  );
620
- if (discussionFirst) return false;
806
+ if (discussionFirst) return 'simple';
621
807
 
622
808
  const implementationRequest =
623
809
  /\b(add|build|create|implement|support|introduce|design|refactor|rework|migrate|change|update|rewrite|restructure)\b/i.test(
624
810
  lower
625
811
  ) ||
626
812
  /(新增|增加|实现|支持|设计|重构|改造|迁移|调整|重写|重做)/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);
813
+ if (!implementationRequest) return 'simple';
634
814
 
815
+ const broadSignalPattern =
816
+ /\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;
817
+ const broadSignals = lower.match(broadSignalPattern) || [];
635
818
  const multipleActions = /\b(and|plus|also|while|along with)\b/i.test(lower) || /[,、;;].+/.test(input);
636
819
  const singleFileScoped =
637
820
  /\b(?:in|inside|within|only in)\s+[-_/.\w]+\.(?:[cm]?[jt]sx?|py|go|rb|java|rs|php|md)\b/i.test(lower) ||
638
821
  /\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);
822
+ const fileMentions = (lower.match(/[-_/.\w]+\.(?:[cm]?[jt]sx?|py|go|rb|java|rs|php|md)\b/g) || []).length;
823
+ const multiFileScope =
824
+ fileMentions >= 2 ||
825
+ /\b(across|multiple files?|cross-file|cross file)\b/i.test(lower) ||
826
+ /跨文件|多文件/.test(input);
827
+ const verificationHeavy = /\b(with tests?|and tests?|verify|validation|error handling|error recovery)\b/i.test(lower) || /测试|验证|校验|错误处理|错误恢复/.test(input);
828
+ const architectureHeavy =
829
+ broadSignals.length >= 3 ||
830
+ /\b(architecture|workflow|migration|state management|session state|integration)\b/i.test(lower) ||
831
+ /架构|流程|迁移|状态/.test(input);
832
+
833
+ if (singleFileScoped && !multipleActions && !verificationHeavy) return 'simple';
834
+ if (architectureHeavy && (multiFileScope || multipleActions || verificationHeavy)) return 'complex';
835
+ if (multiFileScope || verificationHeavy || multipleActions) return 'medium';
836
+ return 'simple';
644
837
  }
645
838
 
646
839
  function classifyAutoRoute(text = '') {
@@ -650,25 +843,42 @@ function classifyAutoRoute(text = '') {
650
843
  return {
651
844
  mode: 'brainstorm',
652
845
  autoPlan: false,
653
- selectedSkills
846
+ selectedSkills,
847
+ complexity: 'discussion'
654
848
  };
655
849
  }
656
850
 
657
- if (shouldAutoPlan(text)) {
851
+ const complexity = classifyTaskComplexity(text);
852
+ if (complexity === 'complex') {
658
853
  return {
659
854
  mode: 'auto_plan',
660
855
  autoPlan: true,
661
- selectedSkills: ['superpowers-lite']
856
+ selectedSkills: ['superpowers-lite'],
857
+ complexity
662
858
  };
663
859
  }
664
860
 
665
861
  return {
666
- mode: 'direct',
862
+ mode: complexity === 'medium' ? 'direct_medium' : 'direct',
667
863
  autoPlan: false,
668
- selectedSkills
864
+ selectedSkills,
865
+ complexity
669
866
  };
670
867
  }
671
868
 
869
+ function buildMediumTaskSystemPrompt(systemPrompt) {
870
+ const guidance = [
871
+ 'Task Mode: medium',
872
+ 'Execution guidance:',
873
+ '- Give a brief execution outline before coding.',
874
+ '- Keep the outline concise and focused on touched files/behaviors.',
875
+ '- Then implement directly instead of entering pending plan approval.',
876
+ '- Verify the changed behavior before finishing.',
877
+ '- If major ambiguity appears mid-task, say so clearly and ask for a plan instead of guessing.'
878
+ ].join('\n');
879
+ return `${systemPrompt}\n\n${guidance}`;
880
+ }
881
+
672
882
  function buildAutoSkillSystemPrompt(baseSystemPrompt, commands, config, text) {
673
883
  const selected = classifyAutoRoute(text).selectedSkills.filter((name) => isSkillEnabled(config, name));
674
884
  if (selected.length === 0) return baseSystemPrompt;
@@ -809,6 +1019,7 @@ function enforceAutoPlanGuardrailSteps(plan, goal) {
809
1019
  const source = Array.isArray(plan?.steps) ? plan.steps : [];
810
1020
  const requirements = deriveGoalRequirements(goal);
811
1021
  const lightweightGoal = isLightweightAutoPlanGoal(goal, requirements);
1022
+ const taskClass = classifyPlanTaskClass(goal);
812
1023
  const implementationSteps = source.filter((step) => step.role !== 'reviewer' && step.role !== 'tester');
813
1024
  const primaryImplementationStep =
814
1025
  implementationSteps.find((step) => step.role === 'coder') ||
@@ -827,17 +1038,31 @@ function enforceAutoPlanGuardrailSteps(plan, goal) {
827
1038
  role: 'tester',
828
1039
  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
1040
  };
1041
+ const hasReviewer = source.some((step) => step.role === 'reviewer');
1042
+ const hasTester = source.some((step) => step.role === 'tester');
1043
+
1044
+ if (taskClass === 'advisory') {
1045
+ const advisorySteps = source.filter((step) => step.role === 'planner' || step.role === 'coder');
1046
+ return {
1047
+ summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
1048
+ steps: advisorySteps.length > 0 ? advisorySteps.slice(0, 6) : [primaryImplementationStep]
1049
+ };
1050
+ }
830
1051
 
831
1052
  if (lightweightGoal) {
832
1053
  return {
833
1054
  summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
834
- steps: [primaryImplementationStep, testerStep]
1055
+ steps: hasTester ? [primaryImplementationStep, testerStep] : [primaryImplementationStep]
835
1056
  };
836
1057
  }
837
1058
 
838
1059
  return {
839
1060
  summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
840
- steps: [...implementationSteps.slice(0, 6), reviewerStep, testerStep]
1061
+ steps: [
1062
+ ...implementationSteps.slice(0, 6),
1063
+ ...(hasReviewer ? [reviewerStep] : []),
1064
+ ...(testerStep ? [testerStep] : [])
1065
+ ]
841
1066
  };
842
1067
  }
843
1068
 
@@ -906,19 +1131,23 @@ function extractAcceptanceStatusItems(text = '') {
906
1131
  }
907
1132
 
908
1133
  function buildAutoPlanSystemSummary(auto) {
909
- const statusTitle =
1134
+ const baseStatusTitle =
910
1135
  auto.failedCount > 0 ? 'Auto plan finished with failures' : auto.warningCount > 0 ? 'Auto plan finished with warnings' : 'Auto plan finished';
1136
+ const statusTitle =
1137
+ auto.approvalStatus === 'pending' ? `${baseStatusTitle} (waiting for /plan approve)` : baseStatusTitle;
911
1138
  const lines = [
912
1139
  statusTitle,
913
- `File: ${auto.filePath}`,
1140
+ `Plan File: ${auto.filePath}`,
914
1141
  `Plan Summary: ${auto.summary || '-'}`,
915
1142
  `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}`
1143
+ `Approval: ${auto.approvalStatus || 'not_required'}`
921
1144
  ];
1145
+ if (auto.approvalStatus !== 'pending') {
1146
+ lines.push(`Steps: ${auto.steps.length} total`);
1147
+ lines.push(`Completed: ${auto.completedCount}`);
1148
+ lines.push(`Warnings: ${auto.warningCount}`);
1149
+ lines.push(`Failed: ${auto.failedCount}`);
1150
+ }
922
1151
  if (auto.warningTitles?.length) {
923
1152
  lines.push(`Warning steps: ${auto.warningTitles.slice(0, 5).join(', ')}`);
924
1153
  }
@@ -926,7 +1155,7 @@ function buildAutoPlanSystemSummary(auto) {
926
1155
  lines.push(`Failed steps: ${auto.failedTitles.slice(0, 5).join(', ')}`);
927
1156
  }
928
1157
  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.');
1158
+ 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
1159
  }
931
1160
  return lines.join('\n');
932
1161
  }
@@ -1002,6 +1231,7 @@ async function buildAutoPlanFinalSummary({
1002
1231
 
1003
1232
  try {
1004
1233
  const result = await createChatCompletion({
1234
+ sdkProvider: config.sdk?.provider,
1005
1235
  baseUrl: config.gateway.base_url,
1006
1236
  apiKey: config.gateway.api_key,
1007
1237
  model: model || config.model.name,
@@ -1102,6 +1332,7 @@ async function buildSpecWithModel({
1102
1332
  ].join('\n');
1103
1333
 
1104
1334
  const result = await createChatCompletion({
1335
+ sdkProvider: config.sdk?.provider,
1105
1336
  baseUrl: config.gateway.base_url,
1106
1337
  apiKey: config.gateway.api_key,
1107
1338
  model: model || config.model.name,
@@ -1161,6 +1392,7 @@ async function buildPlanFromSpecWithModel({
1161
1392
  ].join('\n');
1162
1393
 
1163
1394
  const result = await createChatCompletion({
1395
+ sdkProvider: config.sdk?.provider,
1164
1396
  baseUrl: config.gateway.base_url,
1165
1397
  apiKey: config.gateway.api_key,
1166
1398
  model: model || config.model.name,
@@ -1276,6 +1508,7 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
1276
1508
  const snapshot = {
1277
1509
  sessionId: currentSession?.id || '',
1278
1510
  mode: executionMode || config.execution?.mode || 'auto',
1511
+ sdkProvider: config.sdk?.provider || 'openai-compatible',
1279
1512
  model: model || config.model?.name || '',
1280
1513
  maxContextTokens
1281
1514
  };
@@ -1336,7 +1569,7 @@ function buildPendingPlanApprovalMessage(planState) {
1336
1569
  const lines = [
1337
1570
  'Plan approval is still pending.',
1338
1571
  `Goal: ${planState?.goal || '-'}`,
1339
- `Plan file: ${planState?.filePath || '-'}`,
1572
+ `Plan File: ${planState?.filePath || '-'}`,
1340
1573
  `Summary: ${planState?.finalSummary || planState?.summary || '-'}`,
1341
1574
  'Use /plan approve to start implementation, or /plan stay to keep refining the plan first.'
1342
1575
  ];
@@ -1344,6 +1577,7 @@ function buildPendingPlanApprovalMessage(planState) {
1344
1577
  }
1345
1578
 
1346
1579
  function buildApprovedPlanExecutionPrompt(planState, approvalText = '') {
1580
+ const requirementPacket = buildGoalRequirementPacket(planState?.goal || '', 'coder');
1347
1581
  const lines = [
1348
1582
  'Approved implementation plan:',
1349
1583
  `Original goal: ${planState?.goal || '-'}`,
@@ -1351,6 +1585,7 @@ function buildApprovedPlanExecutionPrompt(planState, approvalText = '') {
1351
1585
  `Plan summary: ${planState?.summary || '-'}`,
1352
1586
  `Final planning summary: ${planState?.finalSummary || planState?.summary || '-'}`,
1353
1587
  `User approval: ${String(approvalText || '').trim() || 'approved'}`,
1588
+ requirementPacket,
1354
1589
  Array.isArray(planState?.steps) && planState.steps.length > 0 ? 'Planned steps:' : '',
1355
1590
  ...(Array.isArray(planState?.steps)
1356
1591
  ? planState.steps.slice(0, 8).map((step, index) => `${index + 1}. [${step.role}] ${step.title} :: ${step.task}`)
@@ -1595,6 +1830,7 @@ async function askModel({
1595
1830
  };
1596
1831
 
1597
1832
  const result = await createChatCompletionStream({
1833
+ sdkProvider: config.sdk?.provider,
1598
1834
  baseUrl: config.gateway.base_url,
1599
1835
  apiKey: config.gateway.api_key,
1600
1836
  model: selectedModel,
@@ -1719,12 +1955,26 @@ async function buildAutoPlanAndRun({
1719
1955
  model,
1720
1956
  systemPrompt,
1721
1957
  onAgentEvent,
1722
- sessionId
1958
+ sessionId,
1959
+ taskClass
1723
1960
  }) {
1961
+ const normalizedTaskClass = taskClass || classifyPlanTaskClass(goal);
1724
1962
  const requirementPacket = buildGoalRequirementPacket(goal, 'planner');
1725
1963
  const plannerPrompt = [
1726
1964
  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.'
1965
+ 'Planning policy:',
1966
+ '- First classify the user goal as one of: advisory, implementation, or verification-heavy.',
1967
+ '- advisory = analysis, review, audit, optimization suggestions, architecture feedback, brainstorming, planning, or recommendation requests.',
1968
+ '- implementation = add/build/create/implement/refactor/fix/update/change behavior in code or files.',
1969
+ '- verification-heavy = the user explicitly asks to run tests, verify findings, reproduce a bug, prove a claim, or validate a result.',
1970
+ '- 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.',
1971
+ '- For advisory goals, do not emit generic filler steps such as "Test and verify", "Review recommendations", or other template-only steps.',
1972
+ '- For implementation goals, reviewer and tester are optional support roles, not defaults. Only include them when they clearly add value.',
1973
+ '- 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.',
1974
+ '- If the task is purely to inspect the current project and suggest improvements, a lean 2-step or 3-step plan is preferred.',
1975
+ '- Example advisory roles: planner -> inspect project shape, coder -> synthesize findings and prioritized recommendations.',
1976
+ '- Example implementation roles: planner -> inspect target area, coder -> implement change, tester -> verify changed behavior.',
1977
+ 'Return strict JSON only with shape {"summary":"...","steps":[{"title":"...","role":"planner|coder|reviewer|tester","task":"..."}]}. No markdown.'
1728
1978
  ].join('\n');
1729
1979
  let autoPlan = {
1730
1980
  summary: `Auto plan for: ${goal}`,
@@ -1739,6 +1989,7 @@ async function buildAutoPlanAndRun({
1739
1989
  let planningError = '';
1740
1990
  try {
1741
1991
  const planning = await createChatCompletion({
1992
+ sdkProvider: config.sdk?.provider,
1742
1993
  baseUrl: config.gateway.base_url,
1743
1994
  apiKey: config.gateway.api_key,
1744
1995
  model: model || config.model.name,
@@ -1749,10 +2000,15 @@ async function buildAutoPlanAndRun({
1749
2000
  content: [
1750
2001
  'Create an execution plan and assign best sub-agent role for each step.',
1751
2002
  '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.',
2003
+ 'The available roles are planner, coder, reviewer, and tester. Use only the roles the task actually needs.',
2004
+ `Task class: ${normalizedTaskClass}`,
2005
+ 'Before choosing roles, decide whether the request is advisory, implementation, or verification-heavy.',
1753
2006
  requirementPacket,
1754
2007
  '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.',
2008
+ 'For analysis, recommendation, optimization, audit, or project-review goals, keep the plan lean and usually limit it to planner/coder.',
2009
+ 'Do not include reviewer/tester for advisory goals unless the user explicitly asks to validate, verify, or independently review the findings.',
2010
+ 'Avoid template-only titles like "Initial analysis", "Review recommendations", or "Test and verify" for advisory goals.',
2011
+ 'For implementation-heavy changes, prefer review and/or testing steps near the end only when they materially improve confidence.',
1756
2012
  'Prefer 3-5 steps total.'
1757
2013
  ]
1758
2014
  .filter(Boolean)
@@ -1891,14 +2147,43 @@ export async function createChatRuntime({
1891
2147
  messageCount: Array.isArray(currentSession.messages) ? currentSession.messages.length : 0
1892
2148
  }
1893
2149
  ];
2150
+ try {
2151
+ const initialSessions = await listSessions(100);
2152
+ if (initialSessions.length > 0) {
2153
+ const merged = [
2154
+ {
2155
+ id: currentSession.id,
2156
+ messageCount: Array.isArray(currentSession.messages) ? currentSession.messages.length : 0
2157
+ },
2158
+ ...initialSessions.map((session) => ({
2159
+ id: session.id,
2160
+ messageCount: Number(session.messageCount || 0)
2161
+ }))
2162
+ ];
2163
+ const deduped = [];
2164
+ const seen = new Set();
2165
+ for (const session of merged) {
2166
+ const id = String(session.id || '').trim();
2167
+ if (!id || seen.has(id)) continue;
2168
+ seen.add(id);
2169
+ deduped.push(session);
2170
+ }
2171
+ historySessionCache = deduped;
2172
+ historyIdCache = deduped.map((session) => session.id);
2173
+ }
2174
+ } catch {
2175
+ // keep startup resilient even if historical sessions cannot be listed
2176
+ }
1894
2177
 
1895
2178
  const configKeyHints = [
1896
2179
  'gateway.base_url',
1897
2180
  'gateway.api_key',
1898
2181
  'model.name',
2182
+ 'ui.language',
1899
2183
  'ui.reply_language',
1900
2184
  'execution.mode',
1901
2185
  'shell.default',
2186
+ 'sdk.provider',
1902
2187
  'gateway.timeout_ms',
1903
2188
  'gateway.max_retries',
1904
2189
  'model.max_context_tokens',
@@ -1923,6 +2208,7 @@ export async function createChatRuntime({
1923
2208
  '/help',
1924
2209
  '/status',
1925
2210
  '/config',
2211
+ '/memory',
1926
2212
  '/mode',
1927
2213
  '/plan',
1928
2214
  '/tasks',
@@ -1934,30 +2220,26 @@ export async function createChatRuntime({
1934
2220
  '/retry'
1935
2221
  ];
1936
2222
  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
2223
 
1944
2224
  const listCommandNames = () => {
2225
+ const completionCopy = getCompletionCopy(config.ui?.language);
1945
2226
  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' }
2227
+ { name: 'help', description: completionCopy.commands.help },
2228
+ { name: 'exit', description: completionCopy.commands.exit },
2229
+ { name: 'commands', description: completionCopy.commands.commands },
2230
+ { name: 'status', description: completionCopy.commands.status },
2231
+ { name: 'mode', description: completionCopy.commands.mode },
2232
+ { name: 'compact', description: completionCopy.commands.compact },
2233
+ { name: 'tasks', description: completionCopy.commands.tasks },
2234
+ { name: 'checkpoint', description: completionCopy.commands.checkpoint },
2235
+ { name: 'spec', description: completionCopy.commands.spec },
2236
+ { name: 'plan', description: completionCopy.commands.plan },
2237
+ { name: 'agents', description: completionCopy.commands.agents },
2238
+ { name: 'config', description: completionCopy.commands.config },
2239
+ { name: 'memory', description: completionCopy.commands.memory },
2240
+ { name: 'history', description: completionCopy.commands.history },
2241
+ { name: 'debug', description: completionCopy.commands.debug },
2242
+ { name: 'retry', description: completionCopy.commands.retry }
1961
2243
  ];
1962
2244
  const out = [];
1963
2245
  for (const cmd of commands.values()) {
@@ -1991,6 +2273,7 @@ export async function createChatRuntime({
1991
2273
  ];
1992
2274
 
1993
2275
  const historyTemplates = ['/history list', '/history current', '/history resume <session_id>'];
2276
+ const memoryTemplates = ['/memory list <scope>', '/memory search <scope> <query>', '/memory forget <scope> <id>'];
1994
2277
  const modeTemplates = ['/mode normal', '/mode auto', '/mode plan'];
1995
2278
  const taskTemplates = ['/tasks', '/tasks add <title>', '/tasks start <id>', '/tasks done <id>', '/tasks remove <id>', '/tasks clear'];
1996
2279
  const checkpointTemplates = [
@@ -2000,12 +2283,13 @@ export async function createChatRuntime({
2000
2283
  '/checkpoint load <id>'
2001
2284
  ];
2002
2285
  const specTemplates = ['/spec <topic>'];
2003
- const planTemplates = ['/plan <goal>', '/plan auto <goal>', '/plan from-spec <spec-path?>'];
2286
+ const planTemplates = ['/plan <goal>', '/plan auto <goal>', '/plan auto run <goal>', '/plan approve', '/plan from-spec <spec-path?>'];
2004
2287
  const agentTemplates = ['/agents list', '/agents run planner <task>', '/agents run coder <task>', '/agents run reviewer <task>', '/agents run tester <task>'];
2005
2288
  const debugTemplates = ['/debug keys on', '/debug keys off', '/debug keys status'];
2006
2289
  const compactTemplates = compactOptions.map((opt) => `/compact ${opt}`);
2007
2290
  const slashTemplates = [
2008
2291
  ...configTemplates,
2292
+ ...memoryTemplates,
2009
2293
  ...historyTemplates,
2010
2294
  ...modeTemplates,
2011
2295
  ...taskTemplates,
@@ -2041,6 +2325,9 @@ export async function createChatRuntime({
2041
2325
  const getCompletionOptions = (rawInput) => {
2042
2326
  const input = String(rawInput || '');
2043
2327
  if (!input.startsWith('/')) return [];
2328
+ const completionCopy = getCompletionCopy(config.ui?.language);
2329
+ const configSubcommandDescriptions = completionCopy.configSubcommands;
2330
+ const planSubcommandDescriptions = completionCopy.planSubcommands || {};
2044
2331
 
2045
2332
  const hasTrailingSpace = /\s$/.test(input);
2046
2333
  const body = input.slice(1);
@@ -2048,6 +2335,7 @@ export async function createChatRuntime({
2048
2335
  const commandPart = tokens[0] || '';
2049
2336
  const commandHasSubcommands = new Set([
2050
2337
  'config',
2338
+ 'memory',
2051
2339
  'compact',
2052
2340
  'mode',
2053
2341
  'tasks',
@@ -2065,19 +2353,22 @@ export async function createChatRuntime({
2065
2353
  registerSuggestion(`/${entry.name}`, entry.description || '');
2066
2354
  }
2067
2355
  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');
2356
+ registerSuggestion(template, configSubcommandDescriptions[template] || completionCopy.generic.configCommand);
2357
+ }
2358
+ for (const template of memoryTemplates) registerSuggestion(template, completionCopy.generic.memoryCommand);
2359
+ for (const template of historyTemplates) registerSuggestion(template, completionCopy.generic.historyCommand);
2360
+ for (const template of modeTemplates) registerSuggestion(template, completionCopy.generic.modeCommand);
2361
+ for (const template of taskTemplates) registerSuggestion(template, completionCopy.generic.taskCommand);
2362
+ for (const template of checkpointTemplates) registerSuggestion(template, completionCopy.generic.checkpointCommand);
2363
+ for (const template of specTemplates) registerSuggestion(template, completionCopy.generic.specCommand);
2364
+ for (const template of planTemplates) {
2365
+ registerSuggestion(template, planSubcommandDescriptions[template] || completionCopy.generic.planCommand);
2366
+ }
2367
+ for (const template of agentTemplates) registerSuggestion(template, completionCopy.generic.agentCommand);
2368
+ for (const template of debugTemplates) registerSuggestion(template, completionCopy.generic.debugCommand);
2369
+ for (const template of compactTemplates) registerSuggestion(template, completionCopy.generic.compactCommand);
2370
+ registerSuggestion('/retry', completionCopy.generic.retryCommand);
2371
+ registerSuggestion('/status', completionCopy.generic.statusCommand);
2081
2372
 
2082
2373
  if (!commandPart) {
2083
2374
  return materializeSuggestions(prioritizeByPreferredOrder(
@@ -2105,7 +2396,7 @@ export async function createChatRuntime({
2105
2396
  return materializeSuggestions(prioritizeByPreferredOrder(
2106
2397
  ['set', 'get', 'list', 'reset']
2107
2398
  .filter((s) => s.startsWith(subcommand))
2108
- .map((s) => registerSuggestion(`/config ${s}`, configSubcommandDescriptions[`/config ${s}`] || 'config command').value),
2399
+ .map((s) => registerSuggestion(`/config ${s}`, configSubcommandDescriptions[`/config ${s}`] || completionCopy.generic.configCommand).value),
2109
2400
  configSubcommandPriority
2110
2401
  ));
2111
2402
  }
@@ -2114,42 +2405,58 @@ export async function createChatRuntime({
2114
2405
  const keyPrefix = tokens.length >= 3 ? tokens[2] || '' : '';
2115
2406
  return configKeyHints
2116
2407
  .filter((k) => k.startsWith(keyPrefix))
2117
- .map((k) => registerSuggestion(`/config get ${k}`, describeConfigKey(k, 'get')));
2408
+ .map((k) => registerSuggestion(`/config get ${k}`, describeConfigKey(k, 'get', config.ui?.language)));
2118
2409
  }
2119
2410
  if (subcommand === 'set') {
2120
2411
  const keyPrefix = tokens.length >= 3 ? tokens[2] || '' : '';
2121
2412
  return configKeyHints
2122
2413
  .filter((k) => k.startsWith(keyPrefix))
2123
- .map((k) => registerSuggestion(`/config set ${k} `, describeConfigKey(k, 'set')));
2414
+ .map((k) => registerSuggestion(`/config set ${k} `, describeConfigKey(k, 'set', config.ui?.language)));
2124
2415
  }
2125
2416
 
2126
2417
  return materializeSuggestions(configTemplates);
2127
2418
  }
2128
2419
 
2420
+ if (commandPart === 'memory') {
2421
+ const sub = tokens[1] || '';
2422
+ if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
2423
+ return ['list', 'search', 'forget']
2424
+ .filter((item) => item.startsWith(sub))
2425
+ .map((item) => registerSuggestion(`/memory ${item}`, completionCopy.generic.memoryCommand));
2426
+ }
2427
+ const scope = tokens[2] || '';
2428
+ if (['list', 'search', 'forget'].includes(sub) && (tokens.length === 2 || (tokens.length === 3 && !hasTrailingSpace))) {
2429
+ return ['user', 'global', 'project']
2430
+ .filter((item) => item.startsWith(scope))
2431
+ .map((item) => registerSuggestion(`/memory ${sub} ${item}${sub === 'list' ? '' : ' '}`, completionCopy.generic.memoryCommand));
2432
+ }
2433
+ return materializeSuggestions(memoryTemplates);
2434
+ }
2435
+
2129
2436
  if (commandPart === 'compact') {
2130
2437
  const joined = tokens.slice(1).join(' ');
2131
2438
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
2132
2439
  return compactOptions
2133
2440
  .filter((opt) => opt.startsWith(joined) || joined === '')
2134
- .map((opt) => registerSuggestion(`/compact ${opt}`, 'context compaction command'));
2441
+ .map((opt) => registerSuggestion(`/compact ${opt}`, completionCopy.generic.compactCommand));
2135
2442
  }
2136
2443
  return compactOptions
2137
2444
  .filter((opt) => opt.includes(joined) || joined === '')
2138
- .map((opt) => registerSuggestion(`/compact ${opt}`, 'context compaction command'));
2445
+ .map((opt) => registerSuggestion(`/compact ${opt}`, completionCopy.generic.compactCommand));
2139
2446
  }
2140
2447
 
2141
2448
  if (commandPart === 'retry') {
2142
- return [registerSuggestion('/retry', 'retry the last user request')];
2449
+ return [registerSuggestion('/retry', completionCopy.generic.retryCommand)];
2143
2450
  }
2144
2451
  if (commandPart === 'status') {
2145
- return [registerSuggestion('/status', 'show runtime status')];
2452
+ return [registerSuggestion('/status', completionCopy.generic.statusCommand)];
2146
2453
  }
2147
2454
  if (commandPart === 'mode') {
2148
2455
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
2149
2456
  const sub = tokens[1] || '';
2150
2457
  return ['normal', 'auto', 'plan']
2151
2458
  .filter((m) => m.startsWith(sub))
2152
- .map((m) => registerSuggestion(`/mode ${m}`, 'switch execution mode'));
2459
+ .map((m) => registerSuggestion(`/mode ${m}`, completionCopy.generic.modeCommand));
2153
2460
  }
2154
2461
  return materializeSuggestions(modeTemplates);
2155
2462
  }
@@ -2158,7 +2465,7 @@ export async function createChatRuntime({
2158
2465
  const sub = tokens[1] || '';
2159
2466
  return ['add', 'start', 'done', 'remove', 'rm', 'clear']
2160
2467
  .filter((s) => s.startsWith(sub))
2161
- .map((s) => registerSuggestion(`/tasks ${s}`, 'task board command'));
2468
+ .map((s) => registerSuggestion(`/tasks ${s}`, completionCopy.generic.taskCommand));
2162
2469
  }
2163
2470
  return materializeSuggestions(taskTemplates);
2164
2471
  }
@@ -2167,24 +2474,24 @@ export async function createChatRuntime({
2167
2474
  const sub = tokens[1] || '';
2168
2475
  if (sub === 'list') {
2169
2476
  return ['--all']
2170
- .map((v) => registerSuggestion(`/checkpoint list ${v}`, 'checkpoint command'));
2477
+ .map((v) => registerSuggestion(`/checkpoint list ${v}`, completionCopy.generic.checkpointCommand));
2171
2478
  }
2172
2479
  return ['create', 'list', 'load']
2173
2480
  .filter((s) => s.startsWith(sub))
2174
- .map((s) => registerSuggestion(`/checkpoint ${s}`, 'checkpoint command'));
2481
+ .map((s) => registerSuggestion(`/checkpoint ${s}`, completionCopy.generic.checkpointCommand));
2175
2482
  }
2176
2483
  if (tokens[1] === 'list') {
2177
2484
  const hint = tokens[2] || '';
2178
2485
  return ['--all']
2179
2486
  .filter((v) => v.startsWith(hint))
2180
- .map((v) => registerSuggestion(`/checkpoint list ${v}`, 'checkpoint command'));
2487
+ .map((v) => registerSuggestion(`/checkpoint list ${v}`, completionCopy.generic.checkpointCommand));
2181
2488
  }
2182
2489
  if (tokens[1] === 'load') {
2183
2490
  if (tokens.length >= 3) {
2184
2491
  const hint = tokens[3] || '';
2185
2492
  return ['--all']
2186
2493
  .filter((v) => v.startsWith(hint))
2187
- .map((v) => registerSuggestion(`/checkpoint load ${tokens[2]} ${v}`, 'checkpoint command'));
2494
+ .map((v) => registerSuggestion(`/checkpoint load ${tokens[2]} ${v}`, completionCopy.generic.checkpointCommand));
2188
2495
  }
2189
2496
  }
2190
2497
  return materializeSuggestions(checkpointTemplates);
@@ -2193,11 +2500,25 @@ export async function createChatRuntime({
2193
2500
  return materializeSuggestions(specTemplates);
2194
2501
  }
2195
2502
  if (commandPart === 'plan') {
2503
+ if (tokens[1] === 'auto' && (tokens.length === 2 || (tokens.length === 3 && !hasTrailingSpace))) {
2504
+ const sub = tokens[2] || '';
2505
+ return ['run']
2506
+ .filter((s) => s.startsWith(sub))
2507
+ .map((s) => registerSuggestion(`/plan auto ${s} `, planSubcommandDescriptions[`/plan auto ${s} <goal>`] || completionCopy.generic.planCommand));
2508
+ }
2196
2509
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
2197
2510
  const sub = tokens[1] || '';
2198
- return ['auto', 'from-spec']
2511
+ return ['auto', 'approve', 'from-spec']
2199
2512
  .filter((s) => s.startsWith(sub))
2200
- .map((s) => registerSuggestion(`/plan ${s}`, 'planning command'));
2513
+ .map((s) =>
2514
+ registerSuggestion(
2515
+ `/plan ${s}`,
2516
+ planSubcommandDescriptions[`/plan ${s}`] ||
2517
+ planSubcommandDescriptions[`/plan ${s} <goal>`] ||
2518
+ planSubcommandDescriptions[`/plan ${s} <spec-path?>`] ||
2519
+ completionCopy.generic.planCommand
2520
+ )
2521
+ );
2201
2522
  }
2202
2523
  return materializeSuggestions(planTemplates);
2203
2524
  }
@@ -2206,17 +2527,17 @@ export async function createChatRuntime({
2206
2527
  const sub = tokens[1] || '';
2207
2528
  if (sub === 'run') {
2208
2529
  return ['planner', 'coder', 'reviewer', 'tester']
2209
- .map((r) => registerSuggestion(`/agents run ${r} `, 'sub-agent command'));
2530
+ .map((r) => registerSuggestion(`/agents run ${r} `, completionCopy.generic.agentCommand));
2210
2531
  }
2211
2532
  return ['list', 'run']
2212
2533
  .filter((s) => s.startsWith(sub))
2213
- .map((s) => registerSuggestion(`/agents ${s}`, 'sub-agent command'));
2534
+ .map((s) => registerSuggestion(`/agents ${s}`, completionCopy.generic.agentCommand));
2214
2535
  }
2215
2536
  if (tokens[1] === 'run') {
2216
2537
  const rolePrefix = tokens[2] || '';
2217
2538
  return ['planner', 'coder', 'reviewer', 'tester']
2218
2539
  .filter((r) => r.startsWith(rolePrefix))
2219
- .map((r) => registerSuggestion(`/agents run ${r} `, 'sub-agent command'));
2540
+ .map((r) => registerSuggestion(`/agents run ${r} `, completionCopy.generic.agentCommand));
2220
2541
  }
2221
2542
  return materializeSuggestions(agentTemplates);
2222
2543
  }
@@ -2230,13 +2551,13 @@ export async function createChatRuntime({
2230
2551
  .map((session) => ({
2231
2552
  value: `/history resume ${session.id}`,
2232
2553
  display: `/history resume ${session.id} · ${Number(session.messageCount || 0)} msgs`,
2233
- description: 'resume a saved session'
2554
+ description: completionCopy.generic.resumeSession
2234
2555
  }));
2235
2556
  if (dynamic.length > 0) return dynamic;
2236
2557
  }
2237
2558
  return ['list', 'current', 'resume']
2238
2559
  .filter((s) => s.startsWith(sub))
2239
- .map((s) => registerSuggestion(`/history ${s}`, 'history command'));
2560
+ .map((s) => registerSuggestion(`/history ${s}`, completionCopy.generic.historyCommand));
2240
2561
  }
2241
2562
  if (sub === 'resume') {
2242
2563
  const idPrefix = tokens[2] || '';
@@ -2245,7 +2566,7 @@ export async function createChatRuntime({
2245
2566
  .map((session) => ({
2246
2567
  value: `/history resume ${session.id}`,
2247
2568
  display: `/history resume ${session.id} · ${Number(session.messageCount || 0)} msgs`,
2248
- description: 'resume a saved session'
2569
+ description: completionCopy.generic.resumeSession
2249
2570
  }));
2250
2571
  if (dynamic.length > 0) return dynamic;
2251
2572
  return materializeSuggestions(historyTemplates);
@@ -2258,17 +2579,17 @@ export async function createChatRuntime({
2258
2579
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
2259
2580
  if (sub === 'keys') {
2260
2581
  return ['on', 'off', 'status']
2261
- .map((v) => registerSuggestion(`/debug keys ${v}`, 'keyboard debug command'));
2582
+ .map((v) => registerSuggestion(`/debug keys ${v}`, completionCopy.generic.keyboardDebugCommand));
2262
2583
  }
2263
2584
  return ['keys']
2264
2585
  .filter((s) => s.startsWith(sub))
2265
- .map((s) => registerSuggestion(`/debug ${s}`, 'debug command'));
2586
+ .map((s) => registerSuggestion(`/debug ${s}`, completionCopy.generic.debugCommand));
2266
2587
  }
2267
2588
  if (sub === 'keys') {
2268
2589
  const action = tokens[2] || '';
2269
2590
  return ['on', 'off', 'status']
2270
2591
  .filter((v) => v.startsWith(action))
2271
- .map((v) => registerSuggestion(`/debug keys ${v}`, 'keyboard debug command'));
2592
+ .map((v) => registerSuggestion(`/debug keys ${v}`, completionCopy.generic.keyboardDebugCommand));
2272
2593
  }
2273
2594
  return materializeSuggestions(debugTemplates);
2274
2595
  }
@@ -2286,6 +2607,17 @@ export async function createChatRuntime({
2286
2607
  await saveSession(currentSession);
2287
2608
  };
2288
2609
 
2610
+ const buildActiveSystemPrompt = async () => {
2611
+ const soulPrompt = await buildSystemPromptWithSoul(baseSystemPrompt, config);
2612
+ const memorySnapshot = await buildMemorySnapshot({
2613
+ config,
2614
+ workspaceRoot: process.cwd()
2615
+ }).catch(() => '');
2616
+ const memoryGuide =
2617
+ 'Persistent memory is for durable preferences, project conventions, and stable workflow knowledge. Use fresh file reads when the code can verify details. Only write memory when the fact is likely to matter in future sessions, is stable over time, and is not sensitive. Do not store temporary task details, speculative guesses, or secrets. Choose remember_user for user preferences, communication habits, and long-term constraints. Choose remember_global for reusable workflow knowledge that helps across many projects. Choose remember_project for repository-specific conventions, architecture notes, important modules, and local workflow expectations. When memory includes command names, file paths, identifiers, or exact wording, preserve those tokens exactly instead of paraphrasing them.';
2618
+ return [soulPrompt, memorySnapshot, memoryGuide].filter(Boolean).join('\n\n');
2619
+ };
2620
+
2289
2621
  const isImmediateLocalInput = (line) => {
2290
2622
  const parsedInput = parseInput(line);
2291
2623
  if (parsedInput.type !== 'slash') return false;
@@ -2304,6 +2636,7 @@ export async function createChatRuntime({
2304
2636
  'tasks',
2305
2637
  'checkpoint',
2306
2638
  'history',
2639
+ 'memory',
2307
2640
  'config',
2308
2641
  'compact',
2309
2642
  'debug'
@@ -2312,8 +2645,7 @@ export async function createChatRuntime({
2312
2645
  };
2313
2646
 
2314
2647
  const submit = async (line, onAgentEvent) => {
2315
- const activeBaseSystemPrompt = buildSystemPromptWithReplyLanguage(baseSystemPrompt, config);
2316
- const activeReplySystemPrompt = await buildSystemPromptWithSoul(baseSystemPrompt, config);
2648
+ const activeReplySystemPrompt = await buildActiveSystemPrompt();
2317
2649
  try {
2318
2650
  await appendInputHistory(line);
2319
2651
  } catch {
@@ -2332,7 +2664,7 @@ export async function createChatRuntime({
2332
2664
  if (parsedInput.command === 'help') {
2333
2665
  return {
2334
2666
  type: 'system',
2335
- text: 'Commands: /help /exit /commands /status /mode /compact /tasks /checkpoint /spec /plan /agents /config /history /debug /retry /<custom> !<shell>'
2667
+ text: 'Commands: /help /exit /commands /status /mode /compact /tasks /checkpoint /spec /plan /agents /config /memory /history /debug /retry /<custom> !<shell>'
2336
2668
  };
2337
2669
  }
2338
2670
  if (parsedInput.command === 'status') {
@@ -2486,7 +2818,7 @@ export async function createChatRuntime({
2486
2818
  topic,
2487
2819
  config,
2488
2820
  model,
2489
- systemPrompt: activeBaseSystemPrompt
2821
+ systemPrompt: activeReplySystemPrompt
2490
2822
  });
2491
2823
  } catch (err) {
2492
2824
  content = buildSpecTemplate(topic);
@@ -2506,17 +2838,45 @@ export async function createChatRuntime({
2506
2838
  if (parsedInput.command === 'plan') {
2507
2839
  const sub = (parsedInput.args[0] || '').trim().toLowerCase();
2508
2840
  if (sub === 'auto') {
2509
- const goal = parsedInput.args.slice(1).join(' ').trim();
2510
- if (!goal) return { type: 'system', text: 'Usage: /plan auto <goal>' };
2841
+ const runImmediately = (parsedInput.args[1] || '').trim().toLowerCase() === 'run';
2842
+ const goal = parsedInput.args.slice(runImmediately ? 2 : 1).join(' ').trim();
2843
+ if (!goal) return { type: 'system', text: 'Usage: /plan auto <goal> | /plan auto run <goal>' };
2511
2844
  const auto = await buildAutoPlanAndRun({
2512
2845
  goal,
2513
2846
  session: currentSession,
2514
2847
  config,
2515
2848
  model,
2516
- systemPrompt: activeBaseSystemPrompt,
2849
+ systemPrompt: activeReplySystemPrompt,
2517
2850
  onAgentEvent,
2518
- sessionId: currentSession.id
2851
+ sessionId: currentSession.id,
2852
+ taskClass: classifyPlanTaskClass(goal)
2519
2853
  });
2854
+ if (runImmediately) {
2855
+ const result = await askModel({
2856
+ text: buildApprovedPlanExecutionPrompt(
2857
+ {
2858
+ status: 'approved',
2859
+ source: 'auto',
2860
+ goal,
2861
+ filePath: auto.filePath,
2862
+ summary: auto.summary || '',
2863
+ finalSummary: auto.finalSummary || auto.summary || '',
2864
+ steps: Array.isArray(auto.steps) ? auto.steps : []
2865
+ },
2866
+ '/plan auto run'
2867
+ ),
2868
+ session: currentSession,
2869
+ config,
2870
+ model,
2871
+ systemPrompt: activeReplySystemPrompt,
2872
+ onAgentEvent,
2873
+ executionMode: 'auto'
2874
+ });
2875
+ currentSession.planState = null;
2876
+ executionMode = 'auto';
2877
+ await saveSession(currentSession);
2878
+ return { type: 'assistant', text: result.text };
2879
+ }
2520
2880
  currentSession.planState = {
2521
2881
  status: 'pending_approval',
2522
2882
  source: 'auto',
@@ -2577,7 +2937,7 @@ export async function createChatRuntime({
2577
2937
  specPath,
2578
2938
  config,
2579
2939
  model,
2580
- systemPrompt: activeBaseSystemPrompt
2940
+ systemPrompt: activeReplySystemPrompt
2581
2941
  });
2582
2942
  } catch (err) {
2583
2943
  planContent = buildPlanTemplate(specTitle);
@@ -2596,7 +2956,7 @@ export async function createChatRuntime({
2596
2956
  }
2597
2957
 
2598
2958
  const goal = parsedInput.args.join(' ').trim();
2599
- if (!goal) return { type: 'system', text: 'Usage: /plan <goal> | /plan auto <goal> | /plan from-spec <spec-path?>' };
2959
+ if (!goal) return { type: 'system', text: 'Usage: /plan <goal> | /plan auto <goal> | /plan auto run <goal> | /plan from-spec <spec-path?>' };
2600
2960
  const content = buildPlanTemplate(goal);
2601
2961
  const filePath = await writeMarkdownInProjectDir(
2602
2962
  'plans',
@@ -2630,7 +2990,7 @@ export async function createChatRuntime({
2630
2990
  parentSession: currentSession,
2631
2991
  config,
2632
2992
  model,
2633
- systemPrompt: activeBaseSystemPrompt,
2993
+ systemPrompt: activeReplySystemPrompt,
2634
2994
  onAgentEvent
2635
2995
  });
2636
2996
  const text = `[sub-agent:${role}]\n${output.text || output}`;
@@ -2691,11 +3051,55 @@ export async function createChatRuntime({
2691
3051
  ];
2692
3052
  return {
2693
3053
  type: 'system',
2694
- text: `Switched to session: ${targetId} (${loaded.messages.length} messages)`
3054
+ text: `Switched to session: ${targetId} (${loaded.messages.length} messages)`,
3055
+ restoredMessages: structuredClone(loaded.messages || [])
2695
3056
  };
2696
3057
  }
2697
3058
  return { type: 'system', text: `Unknown /history subcommand: ${sub}` };
2698
3059
  }
3060
+ if (parsedInput.command === 'memory') {
3061
+ const sub = String(parsedInput.args[0] || '').trim().toLowerCase();
3062
+ if (!sub) {
3063
+ return { type: 'system', text: 'Usage: /memory list <user|global|project> | /memory search <scope> <query> | /memory forget <scope> <id>' };
3064
+ }
3065
+ if (sub === 'list') {
3066
+ const scope = String(parsedInput.args[1] || '').trim().toLowerCase();
3067
+ if (!['user', 'global', 'project'].includes(scope)) {
3068
+ return { type: 'system', text: 'Usage: /memory list <user|global|project>' };
3069
+ }
3070
+ const items = await listMemories({ scope, workspaceRoot: process.cwd() });
3071
+ if (items.length === 0) return { type: 'system', text: `No ${scope} memories found.` };
3072
+ return {
3073
+ type: 'system',
3074
+ text: items.map((item) => `${item.id} | ${item.kind} | ${item.content}`).join('\n')
3075
+ };
3076
+ }
3077
+ if (sub === 'search') {
3078
+ const scope = String(parsedInput.args[1] || '').trim().toLowerCase();
3079
+ const query = parsedInput.args.slice(2).join(' ').trim();
3080
+ if (!['user', 'global', 'project'].includes(scope) || !query) {
3081
+ return { type: 'system', text: 'Usage: /memory search <user|global|project> <query>' };
3082
+ }
3083
+ const items = await searchMemories({ scope, query, workspaceRoot: process.cwd() });
3084
+ if (items.length === 0) return { type: 'system', text: `No ${scope} memories matched: ${query}` };
3085
+ return {
3086
+ type: 'system',
3087
+ text: items.map((item) => `${item.id} | ${item.kind} | ${item.content}`).join('\n')
3088
+ };
3089
+ }
3090
+ if (sub === 'forget') {
3091
+ const scope = String(parsedInput.args[1] || '').trim().toLowerCase();
3092
+ const id = String(parsedInput.args[2] || '').trim();
3093
+ if (!['user', 'global', 'project'].includes(scope) || !id) {
3094
+ return { type: 'system', text: 'Usage: /memory forget <user|global|project> <id>' };
3095
+ }
3096
+ const result = await forgetMemory({ scope, id, workspaceRoot: process.cwd() });
3097
+ const text = `Removed ${Number(result.removed || 0)} ${scope} memory item(s)`;
3098
+ await persistLocalExchange(line, text, { includeUser: false });
3099
+ return { type: 'system', text };
3100
+ }
3101
+ return { type: 'system', text: `Unknown /memory subcommand: ${sub}` };
3102
+ }
2699
3103
  if (parsedInput.command === 'retry') {
2700
3104
  const lastUser = [...currentSession.messages].reverse().find((m) => m.role === 'user');
2701
3105
  if (!lastUser?.content) {
@@ -2842,7 +3246,7 @@ export async function createChatRuntime({
2842
3246
  session: currentSession,
2843
3247
  config,
2844
3248
  model,
2845
- systemPrompt: activeBaseSystemPrompt,
3249
+ systemPrompt: activeReplySystemPrompt,
2846
3250
  onAgentEvent,
2847
3251
  executionMode
2848
3252
  });
@@ -2924,9 +3328,10 @@ export async function createChatRuntime({
2924
3328
  session: currentSession,
2925
3329
  config,
2926
3330
  model,
2927
- systemPrompt: activeBaseSystemPrompt,
3331
+ systemPrompt: activeReplySystemPrompt,
2928
3332
  onAgentEvent,
2929
- sessionId: currentSession.id
3333
+ sessionId: currentSession.id,
3334
+ taskClass: classifyPlanTaskClass(expandedText)
2930
3335
  });
2931
3336
  currentSession.planState = {
2932
3337
  status: 'pending_approval',
@@ -2950,7 +3355,11 @@ export async function createChatRuntime({
2950
3355
  names: selectedAutoSkills
2951
3356
  });
2952
3357
  }
2953
- const routedSystemPrompt = buildAutoSkillSystemPrompt(activeReplySystemPrompt, commands, config, expandedText);
3358
+ const skillPrompt = buildAutoSkillSystemPrompt(activeReplySystemPrompt, commands, config, expandedText);
3359
+ const routedSystemPrompt =
3360
+ autoRoute.mode === 'direct_medium'
3361
+ ? buildMediumTaskSystemPrompt(skillPrompt)
3362
+ : skillPrompt;
2954
3363
  const result = await askModel({
2955
3364
  text: expandedText,
2956
3365
  session: currentSession,