codemini-cli 0.4.1 → 0.4.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.
@@ -1,6 +1,7 @@
1
1
  import { parseInput } from './input-parser.js';
2
2
  import { loadCommandsAndSkills, renderCommandPrompt } from './command-loader.js';
3
- import { runAgentLoop, setResultDir, clearResultStore } from './agent-loop.js';
3
+ import { runAgentLoop } from './agent-loop.js';
4
+ import { setResultDir, clearResultStore } from './tool-result-store.js';
4
5
  import { trimInline, normalizePath } from './string-utils.js';
5
6
  import fs from 'node:fs/promises';
6
7
  import path from 'node:path';
@@ -29,6 +30,12 @@ import { forgetMemory, listMemories, rememberMemory, searchMemories, captureToIn
29
30
  import { runDreamConsolidation } from './dream-consolidate.js';
30
31
  import { normalizePlanState } from './plan-state.js';
31
32
  import { countActiveTodos, normalizeTodos } from './todo-state.js';
33
+ import {
34
+ attachReflectTargets,
35
+ buildReflectSkillDraft,
36
+ parseReflectScope,
37
+ writeReflectSkillDraft
38
+ } from './reflect-skill.js';
32
39
 
33
40
  const STREAM_SAVE_DEBOUNCE_MS = 120;
34
41
 
@@ -153,12 +160,14 @@ function getCompletionCopy(language = 'zh') {
153
160
  config: '设置/读取/列出/重置配置',
154
161
  memory: '查看/搜索/删除持久记忆',
155
162
  dream: '整理记忆收件箱(dream consolidation)',
163
+ reflect: '复盘成功链路并生成可审阅 skill 草稿',
156
164
  history: '查看/恢复会话',
157
165
  debug: '运行时调试开关',
158
166
  retry: '重试上一条用户请求',
159
167
  stop: '中止当前回答',
160
168
  new: '开始新会话',
161
169
  yes: '确认当前待审批计划并开始执行',
170
+ no: '放弃当前待审批事项',
162
171
  edit: '修改当前待审批计划',
163
172
  reject: '拒绝当前待审批计划'
164
173
  },
@@ -172,6 +181,7 @@ function getCompletionCopy(language = 'zh') {
172
181
  agentCommand: '子代理命令',
173
182
  memoryCommand: '记忆命令',
174
183
  dreamCommand: '记忆整理命令',
184
+ reflectCommand: '复盘生成 skill 草稿',
175
185
  debugCommand: '调试命令',
176
186
  keyboardDebugCommand: '键盘调试命令',
177
187
  compactCommand: '上下文压缩命令',
@@ -250,12 +260,14 @@ function getCompletionCopy(language = 'zh') {
250
260
  config: 'set/get/list/reset config values',
251
261
  memory: 'list/search/delete persistent memories',
252
262
  dream: 'consolidate memory inbox (dream)',
263
+ reflect: 'reflect on a successful workflow and draft a reusable skill',
253
264
  history: 'list/resume sessions',
254
265
  debug: 'runtime debug switches',
255
266
  retry: 'retry the last user request',
256
267
  stop: 'stop the current response',
257
268
  new: 'start a new session',
258
269
  yes: 'approve the pending plan and start execution',
270
+ no: 'discard the pending item',
259
271
  edit: 'revise the pending plan',
260
272
  reject: 'reject the pending plan'
261
273
  },
@@ -269,6 +281,7 @@ function getCompletionCopy(language = 'zh') {
269
281
  agentCommand: 'sub-agent command',
270
282
  memoryCommand: 'memory command',
271
283
  dreamCommand: 'dream consolidation command',
284
+ reflectCommand: 'reflect skill draft command',
272
285
  debugCommand: 'debug command',
273
286
  keyboardDebugCommand: 'keyboard debug command',
274
287
  compactCommand: 'context compaction command',
@@ -413,7 +426,10 @@ function buildPipelineStepGuidance({ role, stepIndex, totalSteps, isFirst, isLas
413
426
  lines.push('- If you discover something new, record it under the requested headings instead of burying it in prose.');
414
427
  lines.push('- Continue the established direction unless you have concrete contradictory evidence.');
415
428
  lines.push('- Output only what the next step needs to know. Skip obvious observations.');
416
- if (isLast) {
429
+ if (role !== 'summarizer') {
430
+ lines.push('- Do not produce a final overall summary; the final summarizer step owns synthesis.');
431
+ }
432
+ if (isLast && role === 'summarizer') {
417
433
  lines.push('- Since you are the final step, give a concise overall verdict the user can act on.');
418
434
  }
419
435
  return lines.join('\n');
@@ -719,8 +735,9 @@ function buildAutoPlanPlannerGuidance() {
719
735
  '- Prefer the smallest local approach that satisfies the goal.',
720
736
  '- Do not output multiple alternative branches in the final plan.',
721
737
  '- Do not assume implementation should begin before the plan is coherent.',
722
- '- Available sub-agent roles are planner, coder, reviewer, tester, and summarizer. Use only the roles the task actually needs.',
723
- '- The summarizer role reads accumulated step results from the plan file and synthesizes a final summary. It does NOT re-analyze the codebase. Prefer summarizer as the final step for multi-step plans.',
738
+ '- Available sub-agent roles are planner, coder, reviewer, tester, and summarizer. Use only the non-summary roles the task actually needs.',
739
+ '- Always include a summarizer as the final step. The summarizer reads accumulated step results from the plan file and synthesizes the final summary. It does NOT re-analyze the codebase.',
740
+ '- Do not ask planner, coder, reviewer, or tester steps to produce the final summary. They should write detailed step results for the summarizer.',
724
741
  '- For implementation-heavy or risky changes, prefer adding review and/or verification steps.',
725
742
  '- For analysis, recommendation, or planning-only goals, you may omit reviewer/tester if they do not add value.',
726
743
  '- Prefer 3-5 steps total unless the task is clearly larger.',
@@ -1040,7 +1057,12 @@ async function buildTesterVerificationPacket(focusPaths = []) {
1040
1057
  return lines.join('\n');
1041
1058
  }
1042
1059
 
1043
- function isSkillEnabled(config, name) {
1060
+ function isBundledSkillCommand(command) {
1061
+ return command?.metadata?.type === 'skill' && command?.source === 'bundled-skill';
1062
+ }
1063
+
1064
+ function isSkillEnabled(config, name, command = null) {
1065
+ if (isBundledSkillCommand(command)) return true;
1044
1066
  return config.skills?.enabled?.[name] !== false;
1045
1067
  }
1046
1068
 
@@ -1165,7 +1187,7 @@ function buildMediumTaskSystemPrompt(systemPrompt) {
1165
1187
  }
1166
1188
 
1167
1189
  function buildAutoSkillSystemPrompt(baseSystemPrompt, commands, config, text) {
1168
- const selected = classifyAutoRoute(text).selectedSkills.filter((name) => isSkillEnabled(config, name));
1190
+ const selected = classifyAutoRoute(text).selectedSkills.filter((name) => isSkillEnabled(config, name, commands.get(name)));
1169
1191
  if (selected.length === 0) return baseSystemPrompt;
1170
1192
 
1171
1193
  const blocks = [];
@@ -1251,6 +1273,7 @@ function buildFallbackAutoPlan(goal) {
1251
1273
  : `Auto fallback plan for: ${goal}`;
1252
1274
 
1253
1275
  if (lightweightGoal) {
1276
+ const summarizerStep = buildDefaultSummarizerStep(goal);
1254
1277
  return {
1255
1278
  summary,
1256
1279
  steps: [
@@ -1263,7 +1286,8 @@ function buildFallbackAutoPlan(goal) {
1263
1286
  title: 'Verify the change',
1264
1287
  role: 'tester',
1265
1288
  task: `Verify the completed change for: ${goal}. Run the most relevant focused checks available and report concrete evidence plus anything still unverified.`
1266
- }
1289
+ },
1290
+ summarizerStep
1267
1291
  ]
1268
1292
  };
1269
1293
  }
@@ -1344,16 +1368,18 @@ function enforceAutoPlanGuardrailSteps(plan, goal) {
1344
1368
 
1345
1369
  if (taskClass === 'advisory') {
1346
1370
  const advisorySteps = source.filter((step) => step.role === 'planner' || step.role === 'coder');
1371
+ const baseSteps = advisorySteps.length > 0 ? advisorySteps.slice(0, 6) : [primaryImplementationStep];
1347
1372
  return {
1348
1373
  summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
1349
- steps: advisorySteps.length > 0 ? advisorySteps.slice(0, 6) : [primaryImplementationStep]
1374
+ steps: [...baseSteps, summarizerStep]
1350
1375
  };
1351
1376
  }
1352
1377
 
1353
1378
  if (lightweightGoal) {
1379
+ const baseSteps = hasTester ? [primaryImplementationStep, testerStep] : [primaryImplementationStep];
1354
1380
  return {
1355
1381
  summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
1356
- steps: hasTester ? [primaryImplementationStep, testerStep] : [primaryImplementationStep]
1382
+ steps: [...baseSteps, summarizerStep]
1357
1383
  };
1358
1384
  }
1359
1385
 
@@ -1362,11 +1388,9 @@ function enforceAutoPlanGuardrailSteps(plan, goal) {
1362
1388
  ...(hasReviewer ? [reviewerStep] : []),
1363
1389
  ...(testerStep ? [testerStep] : [])
1364
1390
  ];
1365
- const needsSummarizer = executionSteps.length >= 3;
1366
-
1367
1391
  return {
1368
1392
  summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
1369
- steps: needsSummarizer ? [...executionSteps, summarizerStep] : executionSteps
1393
+ steps: [...executionSteps, summarizerStep]
1370
1394
  };
1371
1395
  }
1372
1396
 
@@ -1656,35 +1680,51 @@ async function removePlanFileIfPresent(planState) {
1656
1680
 
1657
1681
  function buildSpecTemplate(topic) {
1658
1682
  return `
1659
- # Spec: ${topic}
1683
+ # ${topic} Design
1660
1684
 
1661
- ## 1. Background
1662
- - Why this work is needed
1663
- - Existing pain points
1685
+ ## Summary
1686
+ - Problem statement
1687
+ - Desired outcome
1688
+ - Why this is worth doing
1664
1689
 
1665
- ## 2. Goals
1690
+ ## Goals
1666
1691
  - Primary goal
1667
- - Non-goals
1692
+ - Secondary goals
1668
1693
 
1669
- ## 3. Scope
1670
- - In scope
1671
- - Out of scope
1694
+ ## Non-Goals
1695
+ - Out-of-scope behavior
1696
+ - Explicitly rejected approaches
1672
1697
 
1673
- ## 4. Requirements
1698
+ ## User Experience / Command Behavior
1699
+ - User-facing commands or flows
1700
+ - Review or approval behavior
1701
+ - Expected outputs
1702
+
1703
+ ## Architecture
1704
+ - Main modules and responsibilities
1705
+ - Data flow
1706
+ - Integration points
1707
+
1708
+ ## Data / State Model
1709
+ - New or changed state
1710
+ - Persistence locations
1711
+ - Lifecycle and cleanup behavior
1712
+
1713
+ ## Safety Rules
1714
+ - Guardrails
1715
+ - Permission or approval requirements
1716
+ - Failure behavior
1717
+
1718
+ ## Requirements
1674
1719
  - Functional requirements
1675
1720
  - Non-functional requirements
1676
1721
  - Win10 compatibility requirements
1677
1722
 
1678
- ## 5. Design
1679
- - Architecture sketch
1680
- - Data flow
1681
- - Key interfaces/commands
1682
-
1683
- ## 6. Risks and Mitigations
1723
+ ## Risks and Mitigations
1684
1724
  - Risk
1685
1725
  - Mitigation
1686
1726
 
1687
- ## 7. Validation
1727
+ ## Testing / Validation
1688
1728
  - Test strategy
1689
1729
  - Acceptance checklist
1690
1730
  `;
@@ -1703,17 +1743,20 @@ async function buildSpecWithModel({
1703
1743
  systemPrompt
1704
1744
  }) {
1705
1745
  const prompt = [
1706
- 'Write a practical engineering spec in markdown.',
1746
+ 'Write a practical engineering spec in markdown, like an implementation-ready design document.',
1707
1747
  'Use these sections exactly:',
1708
- '# Spec: <title>',
1709
- '## 1. Background',
1710
- '## 2. Goals',
1711
- '## 3. Scope',
1712
- '## 4. Requirements',
1713
- '## 5. Design',
1714
- '## 6. Risks and Mitigations',
1715
- '## 7. Validation',
1716
- 'Make it implementation-oriented and suitable for a Win10-first internal coding CLI.'
1748
+ '# <Feature> Design',
1749
+ '## Summary',
1750
+ '## Goals',
1751
+ '## Non-Goals',
1752
+ '## User Experience / Command Behavior',
1753
+ '## Architecture',
1754
+ '## Data / State Model',
1755
+ '## Safety Rules',
1756
+ '## Requirements',
1757
+ '## Risks and Mitigations',
1758
+ '## Testing / Validation',
1759
+ 'Make it concrete, scoped, and suitable for turning into a sub-agent implementation plan.'
1717
1760
  ].join('\n');
1718
1761
 
1719
1762
  const result = await createChatCompletion({
@@ -1914,6 +1957,11 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
1914
1957
  value: currentSession?.planState?.status === 'pending_approval',
1915
1958
  enumerable: false,
1916
1959
  writable: false
1960
+ },
1961
+ pendingReflectSkill: {
1962
+ value: currentSession?.planState?.status === 'pending_reflect_skill',
1963
+ enumerable: false,
1964
+ writable: false
1917
1965
  }
1918
1966
  });
1919
1967
  return snapshot;
@@ -1940,6 +1988,10 @@ function hasPendingPlanApproval(session) {
1940
1988
  return session?.planState?.status === 'pending_approval';
1941
1989
  }
1942
1990
 
1991
+ function hasPendingReflectSkill(session) {
1992
+ return session?.planState?.status === 'pending_reflect_skill';
1993
+ }
1994
+
1943
1995
  function isApprovalText(text = '') {
1944
1996
  const value = String(text || '').trim().toLowerCase();
1945
1997
  if (!value) return false;
@@ -1976,6 +2028,28 @@ function buildPendingPlanApprovalMessage(planState) {
1976
2028
  return lines.join('\n');
1977
2029
  }
1978
2030
 
2031
+ function buildPendingReflectSkillMessage(reflectState) {
2032
+ const candidates = Array.isArray(reflectState?.candidates) ? reflectState.candidates : [];
2033
+ if (candidates.length === 0) {
2034
+ return 'Reflect found no reusable skill candidate.';
2035
+ }
2036
+ const lines = [
2037
+ 'Reflect skill draft pending.',
2038
+ `Scope: ${reflectState?.targetScope || 'project'}`
2039
+ ];
2040
+ for (const candidate of candidates) {
2041
+ lines.push('');
2042
+ lines.push(`[${candidate.id || 1}] ${candidate.name}`);
2043
+ lines.push(`Confidence: ${Number(candidate.confidence ?? 0.75).toFixed(2)}`);
2044
+ lines.push(`Target: ${candidate.targetPath || '-'}`);
2045
+ lines.push('');
2046
+ lines.push(String(candidate.content || '').trim());
2047
+ }
2048
+ lines.push('');
2049
+ lines.push('Use /yes to write this skill, /edit <feedback> to revise it, or /no to discard it.');
2050
+ return lines.join('\n');
2051
+ }
2052
+
1979
2053
  function buildApprovedPlanExecutionPrompt(planState, approvalText = '') {
1980
2054
  const requirementPacket = buildGoalRequirementPacket(planState?.goal || '', 'coder');
1981
2055
  const lines = [
@@ -2375,6 +2449,12 @@ async function runSubAgentTask({
2375
2449
  }
2376
2450
  } catch {}
2377
2451
  }
2452
+ if (
2453
+ role !== 'summarizer' &&
2454
+ ['assistant:start', 'assistant:delta', 'assistant:response', 'assistant:tool_call_delta'].includes(String(evt?.type || ''))
2455
+ ) {
2456
+ return;
2457
+ }
2378
2458
  if (onAgentEvent) onAgentEvent(evt);
2379
2459
  };
2380
2460
  const roleAllowedTools = ROLE_TOOL_POLICY[role];
@@ -2503,7 +2583,14 @@ async function executePlanWithSubAgents({
2503
2583
  );
2504
2584
  }
2505
2585
 
2506
- if (stepRecord.failed && i < steps.length - 1) break;
2586
+ if (stepRecord.failed && i < steps.length - 1) {
2587
+ const summarizerIndex = steps.findIndex((candidate, index) => index > i && candidate.role === 'summarizer');
2588
+ if (summarizerIndex > i) {
2589
+ i = summarizerIndex - 1;
2590
+ continue;
2591
+ }
2592
+ break;
2593
+ }
2507
2594
  }
2508
2595
 
2509
2596
  const summaryLines = [];
@@ -2590,8 +2677,9 @@ async function buildAutoPlanAndRun({
2590
2677
  content: [
2591
2678
  'Create an execution plan and assign best sub-agent role for each step.',
2592
2679
  'Return strict JSON only with shape {"summary":"...","steps":[{"title":"...","role":"planner|coder|reviewer|tester|summarizer","task":"..."}]}. No markdown.',
2593
- 'The available roles are planner, coder, reviewer, tester, and summarizer. Use only the roles the task actually needs.',
2594
- 'The summarizer role synthesizes prior step results without re-analyzing. Use it as the final step for plans with 3+ steps.',
2680
+ 'The available roles are planner, coder, reviewer, tester, and summarizer. Use only the non-summary roles the task actually needs.',
2681
+ 'Always include a summarizer as the final step. The summarizer synthesizes prior step results without re-analyzing.',
2682
+ 'Planner, coder, reviewer, and tester steps should write detailed step results, not final summaries.',
2595
2683
  `Task class: ${normalizedTaskClass}`,
2596
2684
  'Before choosing roles, decide whether the request is advisory, implementation, or verification-heavy.',
2597
2685
  requirementPacket,
@@ -2708,7 +2796,8 @@ async function revisePendingPlanWithModel({
2708
2796
  buildAutoPlanPlannerGuidance(),
2709
2797
  'You are revising an existing plan based on explicit user feedback.',
2710
2798
  'Return strict JSON only with shape {"summary":"...","steps":[{"title":"...","role":"planner|coder|reviewer|tester|summarizer","task":"..."}]}. No markdown.',
2711
- 'Keep roles minimal and only include steps that materially help the goal.'
2799
+ 'Keep roles minimal and only include steps that materially help the goal.',
2800
+ 'Always keep a summarizer as the final step.'
2712
2801
  ].join('\n');
2713
2802
  const result = await createChatCompletion({
2714
2803
  sdkProvider: config.sdk?.provider,
@@ -2782,6 +2871,44 @@ async function handleShellInput(shellText, config) {
2782
2871
  return { text: chunks.join('\n') };
2783
2872
  }
2784
2873
 
2874
+ function formatHistoryTimestamp(value) {
2875
+ const raw = String(value || '').trim();
2876
+ if (!raw) return 'updated unknown';
2877
+ const parsed = new Date(raw);
2878
+ if (Number.isNaN(parsed.getTime())) return `updated ${raw}`;
2879
+ return `updated ${parsed.toISOString().slice(0, 16).replace('T', ' ')}`;
2880
+ }
2881
+
2882
+ function compactHistoryPreview(value, maxChars = 72) {
2883
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
2884
+ if (!text) return '(no preview)';
2885
+ if (text.length <= maxChars) return text;
2886
+ return `${text.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
2887
+ }
2888
+
2889
+ function formatHistoryList({ currentSession, sessions }) {
2890
+ const currentMessages = Array.isArray(currentSession?.messages) ? currentSession.messages.length : 0;
2891
+ const lines = [
2892
+ `Current session ${currentSession.id}`,
2893
+ `Messages ${currentMessages}`,
2894
+ '',
2895
+ 'Recent sessions'
2896
+ ];
2897
+
2898
+ for (const [index, session] of sessions.entries()) {
2899
+ const count = Number(session.messageCount || 0);
2900
+ lines.push(
2901
+ `${index + 1}. ${session.id}`,
2902
+ ` ${count} ${count === 1 ? 'msg' : 'msgs'} | ${formatHistoryTimestamp(session.updatedAt)}`,
2903
+ ` ${compactHistoryPreview(session.preview)}`,
2904
+ ` resume: /history resume ${session.id}`
2905
+ );
2906
+ }
2907
+
2908
+ lines.push('', 'Tip: use /history resume <session_id>');
2909
+ return lines.join('\n');
2910
+ }
2911
+
2785
2912
  export async function createChatRuntime({
2786
2913
  session,
2787
2914
  config: initialConfig,
@@ -2830,6 +2957,13 @@ export async function createChatRuntime({
2830
2957
  executionMode = 'plan';
2831
2958
  }
2832
2959
  const commands = await loadCommandsAndSkills();
2960
+ const reloadCommandsAndSkills = async () => {
2961
+ const next = await loadCommandsAndSkills();
2962
+ commands.clear();
2963
+ for (const [name, command] of next.entries()) {
2964
+ commands.set(name, command);
2965
+ }
2966
+ };
2833
2967
 
2834
2968
  // Set up tool result store under session directory
2835
2969
  const sessionResultsDir = path.join(getSessionsDir(), String(currentSession.id));
@@ -2940,6 +3074,7 @@ export async function createChatRuntime({
2940
3074
  { name: 'config', description: completionCopy.commands.config },
2941
3075
  { name: 'memory', description: completionCopy.commands.memory },
2942
3076
  { name: 'dream', description: completionCopy.commands.dream },
3077
+ { name: 'reflect', description: completionCopy.commands.reflect },
2943
3078
  { name: 'history', description: completionCopy.commands.history },
2944
3079
  { name: 'debug', description: completionCopy.commands.debug },
2945
3080
  { name: 'retry', description: completionCopy.commands.retry },
@@ -2948,7 +3083,7 @@ export async function createChatRuntime({
2948
3083
  ];
2949
3084
  const out = [];
2950
3085
  for (const cmd of commands.values()) {
2951
- if (cmd.metadata.type === 'skill' && config.skills?.enabled?.[cmd.name] === false) {
3086
+ if (cmd.metadata.type === 'skill' && !isSkillEnabled(config, cmd.name, cmd)) {
2952
3087
  continue;
2953
3088
  }
2954
3089
  out.push({
@@ -2991,6 +3126,7 @@ export async function createChatRuntime({
2991
3126
  const agentTemplates = ['/agents list', '/agents run planner <task>', '/agents run coder <task>', '/agents run reviewer <task>', '/agents run tester <task>', '/agents run summarizer <task>'];
2992
3127
  const debugTemplates = ['/debug keys on', '/debug keys off', '/debug keys status'];
2993
3128
  const dreamTemplates = ['/dream', '/dream --dry-run', '/dream --scope=project', '/dream --scope=global'];
3129
+ const reflectTemplates = ['/reflect', '/reflect --scope=global <request>', '/reflect <request>'];
2994
3130
  const compactTemplates = compactOptions.map((opt) => `/compact ${opt}`);
2995
3131
  const slashTemplates = [
2996
3132
  ...configTemplates,
@@ -3003,6 +3139,7 @@ export async function createChatRuntime({
3003
3139
  ...agentTemplates,
3004
3140
  ...debugTemplates,
3005
3141
  ...dreamTemplates,
3142
+ ...reflectTemplates,
3006
3143
  ...compactTemplates,
3007
3144
  '/retry',
3008
3145
  '/status'
@@ -3070,6 +3207,7 @@ export async function createChatRuntime({
3070
3207
  for (const template of agentTemplates) registerSuggestion(template, completionCopy.generic.agentCommand);
3071
3208
  for (const template of debugTemplates) registerSuggestion(template, completionCopy.generic.debugCommand);
3072
3209
  for (const template of dreamTemplates) registerSuggestion(template, completionCopy.generic.dreamCommand);
3210
+ for (const template of reflectTemplates) registerSuggestion(template, completionCopy.generic.reflectCommand);
3073
3211
  for (const template of compactTemplates) registerSuggestion(template, completionCopy.generic.compactCommand);
3074
3212
  registerSuggestion('/retry', completionCopy.generic.retryCommand);
3075
3213
  registerSuggestion('/status', completionCopy.generic.statusCommand);
@@ -3355,6 +3493,27 @@ export async function createChatRuntime({
3355
3493
  const saveDirectMemoryPrompt = async (text) => {
3356
3494
  const direct = classifyDirectMemoryPrompt(text);
3357
3495
  if (!direct) return null;
3496
+ const existing = await listMemories({
3497
+ scope: direct.scope,
3498
+ workspaceRoot: process.cwd()
3499
+ }).catch(() => []);
3500
+ const directText = String(direct.content || '').toLowerCase();
3501
+ const directTokens = new Set(directText.match(/[a-z0-9_\u4e00-\u9fa5]+/g) || []);
3502
+ const directAsciiTokens = new Set(directText.match(/[a-z0-9_]{4,}/g) || []);
3503
+ const overlapsExisting = existing.some((item) => {
3504
+ const existingText = `${item.content || ''} ${item.summary || ''}`.toLowerCase();
3505
+ for (const token of directAsciiTokens) {
3506
+ if (existingText.includes(token)) return true;
3507
+ }
3508
+ let hits = 0;
3509
+ for (const token of directTokens) {
3510
+ if (token.length < 2) continue;
3511
+ if (existingText.includes(token)) hits += 1;
3512
+ if (hits >= 2) return true;
3513
+ }
3514
+ return false;
3515
+ });
3516
+ if (overlapsExisting) return null;
3358
3517
  return rememberMemory({
3359
3518
  scope: direct.scope,
3360
3519
  content: direct.content,
@@ -3495,7 +3654,7 @@ export async function createChatRuntime({
3495
3654
  if (parsedInput.command === 'help') {
3496
3655
  return {
3497
3656
  type: 'system',
3498
- text: 'Commands: /help /exit /new /stop /commands /status /mode /compact /checkpoint /spec /plan /yes /edit /reject /agents /config /memory /capture /inbox /dream /history /debug /retry /<custom> !<shell>'
3657
+ text: 'Commands: /help /exit /new /stop /commands /status /mode /compact /checkpoint /spec /plan /yes /no /edit /reject /agents /config /memory /capture /inbox /dream /reflect /history /debug /retry /<custom> !<shell>'
3499
3658
  };
3500
3659
  }
3501
3660
  if (parsedInput.command === 'status') {
@@ -3521,6 +3680,27 @@ export async function createChatRuntime({
3521
3680
  return { type: 'system', text };
3522
3681
  }
3523
3682
  if (parsedInput.command === 'yes') {
3683
+ if (hasPendingReflectSkill(currentSession)) {
3684
+ const state = { ...currentSession.planState };
3685
+ const candidate = Array.isArray(state.candidates) ? state.candidates[0] : null;
3686
+ if (!candidate) {
3687
+ currentSession.planState = null;
3688
+ const text = 'No reflect skill draft to write.';
3689
+ await persistLocalExchange(line, text, { includeUser: false });
3690
+ return { type: 'system', text };
3691
+ }
3692
+ const written = await writeReflectSkillDraft({
3693
+ draft: candidate,
3694
+ scope: state.targetScope || 'project',
3695
+ workspaceRoot: process.cwd()
3696
+ });
3697
+ currentSession.planState = null;
3698
+ executionMode = 'auto';
3699
+ await reloadCommandsAndSkills();
3700
+ const text = `Reflect skill written and loaded: /${written.draft.name}\nPath: ${written.filePath}`;
3701
+ await persistLocalExchange(line, text, { includeUser: false });
3702
+ return { type: 'system', text };
3703
+ }
3524
3704
  if (!hasPendingPlanApproval(currentSession)) {
3525
3705
  return { type: 'system', text: 'No pending plan approval. Use /plan auto <goal> first.' };
3526
3706
  }
@@ -3544,6 +3724,35 @@ export async function createChatRuntime({
3544
3724
  return { type: 'assistant', text: result.text, aborted: !!result.aborted };
3545
3725
  }
3546
3726
  if (parsedInput.command === 'edit') {
3727
+ if (hasPendingReflectSkill(currentSession)) {
3728
+ const feedback = parsedInput.args.join(' ').trim();
3729
+ if (!feedback) {
3730
+ return { type: 'system', text: 'Usage: /edit <feedback>' };
3731
+ }
3732
+ const state = { ...currentSession.planState };
3733
+ const previousDraft = Array.isArray(state.candidates) ? state.candidates[0] : null;
3734
+ const drafts = await buildReflectSkillDraft({
3735
+ request: state.request || '',
3736
+ scope: state.targetScope || 'project',
3737
+ session: currentSession,
3738
+ config,
3739
+ model,
3740
+ systemPrompt: activeReplySystemPrompt,
3741
+ previousDraft,
3742
+ feedback
3743
+ });
3744
+ currentSession.planState = {
3745
+ ...state,
3746
+ candidates: attachReflectTargets({
3747
+ candidates: drafts,
3748
+ scope: state.targetScope || 'project',
3749
+ workspaceRoot: process.cwd()
3750
+ })
3751
+ };
3752
+ const text = `Reflect skill draft revised.\n${buildPendingReflectSkillMessage(currentSession.planState)}`;
3753
+ await persistLocalExchange(line, text);
3754
+ return { type: 'system', text };
3755
+ }
3547
3756
  if (!hasPendingPlanApproval(currentSession)) {
3548
3757
  return { type: 'system', text: 'No pending plan approval. Use /plan auto <goal> first.' };
3549
3758
  }
@@ -3564,6 +3773,23 @@ export async function createChatRuntime({
3564
3773
  await persistLocalExchange(line, text);
3565
3774
  return { type: 'system', text };
3566
3775
  }
3776
+ if (parsedInput.command === 'no') {
3777
+ if (hasPendingReflectSkill(currentSession)) {
3778
+ currentSession.planState = null;
3779
+ executionMode = 'auto';
3780
+ const text = 'Reflect skill draft discarded.';
3781
+ await persistLocalExchange(line, text, { includeUser: false });
3782
+ return { type: 'system', text };
3783
+ }
3784
+ if (hasPendingPlanApproval(currentSession)) {
3785
+ currentSession.planState = null;
3786
+ executionMode = 'auto';
3787
+ const text = 'Pending plan rejected and cleared.';
3788
+ await persistLocalExchange(line, text, { includeUser: false });
3789
+ return { type: 'system', text };
3790
+ }
3791
+ return { type: 'system', text: 'No pending reflect skill draft.' };
3792
+ }
3567
3793
  if (parsedInput.command === 'reject') {
3568
3794
  if (!hasPendingPlanApproval(currentSession)) {
3569
3795
  return { type: 'system', text: 'No pending plan approval.' };
@@ -3822,13 +4048,9 @@ export async function createChatRuntime({
3822
4048
  messageCount: Number(s.messageCount || 0)
3823
4049
  }));
3824
4050
  if (sessions.length === 0) return { type: 'system', text: 'No sessions found' };
3825
- const rows = sessions.map(
3826
- (s, idx) =>
3827
- `${idx + 1}. ${s.id} | msgs:${s.messageCount} | updated:${s.updatedAt || '-'}${s.preview ? ` | ${s.preview}` : ''}`
3828
- );
3829
4051
  return {
3830
4052
  type: 'system',
3831
- text: `Current: ${currentSession.id}\n${rows.join('\n')}\nUse /history resume <session_id>`
4053
+ text: formatHistoryList({ currentSession, sessions })
3832
4054
  };
3833
4055
  }
3834
4056
  if (sub === 'current') {
@@ -3971,6 +4193,37 @@ export async function createChatRuntime({
3971
4193
  return { type: 'system', text: `Dream failed: ${err.message}` };
3972
4194
  }
3973
4195
  }
4196
+ if (parsedInput.command === 'reflect') {
4197
+ const parsedReflect = parseReflectScope(parsedInput.args);
4198
+ const drafts = await buildReflectSkillDraft({
4199
+ request: parsedReflect.request,
4200
+ scope: parsedReflect.scope,
4201
+ session: currentSession,
4202
+ config,
4203
+ model,
4204
+ systemPrompt: activeReplySystemPrompt
4205
+ });
4206
+ const candidates = attachReflectTargets({
4207
+ candidates: drafts,
4208
+ scope: parsedReflect.scope,
4209
+ workspaceRoot: process.cwd()
4210
+ });
4211
+ if (candidates.length === 0) {
4212
+ const text = 'Reflect found no reusable skill candidate.';
4213
+ await persistLocalExchange(line, text);
4214
+ return { type: 'system', text };
4215
+ }
4216
+ currentSession.planState = {
4217
+ status: 'pending_reflect_skill',
4218
+ source: 'reflect',
4219
+ targetScope: parsedReflect.scope,
4220
+ request: parsedReflect.request,
4221
+ candidates
4222
+ };
4223
+ const text = buildPendingReflectSkillMessage(currentSession.planState);
4224
+ await persistLocalExchange(line, text);
4225
+ return { type: 'system', text };
4226
+ }
3974
4227
  if (parsedInput.command === 'retry') {
3975
4228
  const lastUser = [...currentSession.messages].reverse().find((m) => m.role === 'user');
3976
4229
  if (!lastUser?.content) {
@@ -4096,7 +4349,7 @@ export async function createChatRuntime({
4096
4349
  if (!custom) {
4097
4350
  return { type: 'system', text: `Unknown slash command: /${parsedInput.command}` };
4098
4351
  }
4099
- if (custom.metadata.type === 'skill' && config.skills?.enabled?.[custom.name] === false) {
4352
+ if (custom.metadata.type === 'skill' && !isSkillEnabled(config, custom.name, custom)) {
4100
4353
  return { type: 'system', text: `Skill is disabled: ${custom.name}` };
4101
4354
  }
4102
4355
 
@@ -4218,7 +4471,6 @@ export async function createChatRuntime({
4218
4471
  }
4219
4472
 
4220
4473
  const expandedText = await expandFileMentions(parsedInput.text, process.cwd());
4221
- await saveDirectMemoryPrompt(expandedText);
4222
4474
  const autoRoute = classifyAutoRoute(expandedText);
4223
4475
  if (autoRoute.autoPlan) {
4224
4476
  await maybeAutoDreamFromRuntime();
@@ -4247,7 +4499,7 @@ export async function createChatRuntime({
4247
4499
  return { type: 'system', text };
4248
4500
  }
4249
4501
 
4250
- const selectedAutoSkills = autoRoute.selectedSkills.filter((name) => isSkillEnabled(config, name));
4502
+ const selectedAutoSkills = autoRoute.selectedSkills.filter((name) => isSkillEnabled(config, name, commands.get(name)));
4251
4503
  if (selectedAutoSkills.length > 0 && onAgentEvent) {
4252
4504
  onAgentEvent({
4253
4505
  type: 'skill:auto',
@@ -4270,6 +4522,7 @@ export async function createChatRuntime({
4270
4522
  executionMode,
4271
4523
  signal
4272
4524
  });
4525
+ await saveDirectMemoryPrompt(expandedText);
4273
4526
  await captureUserPromptForDream(expandedText);
4274
4527
  return { type: 'assistant', text: result.text, aborted: !!result.aborted };
4275
4528
  };