codemini-cli 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
 
@@ -103,6 +110,7 @@ function getCompletionCopy(language = 'zh') {
103
110
  'context.tool_result_max_chars': '工具结果字符上限',
104
111
  'context.read_file_default_lines': 'read_file 默认行数窗口',
105
112
  'context.read_file_max_chars': 'read_file 字符上限',
113
+ 'context.prompt_budget_audit': 'Prompt 预算审计开关',
106
114
  'sessions.max_sessions': '会话保留上限',
107
115
  'sessions.retention_days': '会话保留天数',
108
116
  'shell.default': '默认 shell',
@@ -120,7 +128,8 @@ function getCompletionCopy(language = 'zh') {
120
128
  'execution.mode': '可选:auto | normal | plan',
121
129
  'shell.default': '常用:bash | powershell',
122
130
  'policy.safe_mode': '可选:true | false',
123
- 'policy.allow_dangerous_commands': '可选:true | false'
131
+ 'policy.allow_dangerous_commands': '可选:true | false',
132
+ 'context.prompt_budget_audit': '可选:true | false'
124
133
  },
125
134
  describeSet: (label, hint) => `设置${label}${hint ? `(${hint})` : ''}`,
126
135
  describeGet: (label, hint) => `查看${label}${hint ? `(${hint})` : ''}`,
@@ -153,12 +162,14 @@ function getCompletionCopy(language = 'zh') {
153
162
  config: '设置/读取/列出/重置配置',
154
163
  memory: '查看/搜索/删除持久记忆',
155
164
  dream: '整理记忆收件箱(dream consolidation)',
165
+ reflect: '复盘成功链路并生成可审阅 skill 草稿',
156
166
  history: '查看/恢复会话',
157
167
  debug: '运行时调试开关',
158
168
  retry: '重试上一条用户请求',
159
169
  stop: '中止当前回答',
160
170
  new: '开始新会话',
161
171
  yes: '确认当前待审批计划并开始执行',
172
+ no: '放弃当前待审批事项',
162
173
  edit: '修改当前待审批计划',
163
174
  reject: '拒绝当前待审批计划'
164
175
  },
@@ -172,6 +183,7 @@ function getCompletionCopy(language = 'zh') {
172
183
  agentCommand: '子代理命令',
173
184
  memoryCommand: '记忆命令',
174
185
  dreamCommand: '记忆整理命令',
186
+ reflectCommand: '复盘生成 skill 草稿',
175
187
  debugCommand: '调试命令',
176
188
  keyboardDebugCommand: '键盘调试命令',
177
189
  compactCommand: '上下文压缩命令',
@@ -200,6 +212,7 @@ function getCompletionCopy(language = 'zh') {
200
212
  'context.tool_result_max_chars': 'tool result character limit',
201
213
  'context.read_file_default_lines': 'default read_file line window',
202
214
  'context.read_file_max_chars': 'read_file character limit',
215
+ 'context.prompt_budget_audit': 'prompt budget audit switch',
203
216
  'sessions.max_sessions': 'stored session limit',
204
217
  'sessions.retention_days': 'session retention days',
205
218
  'shell.default': 'default shell',
@@ -217,7 +230,8 @@ function getCompletionCopy(language = 'zh') {
217
230
  'execution.mode': 'options: auto | normal | plan',
218
231
  'shell.default': 'common: bash | powershell',
219
232
  'policy.safe_mode': 'options: true | false',
220
- 'policy.allow_dangerous_commands': 'options: true | false'
233
+ 'policy.allow_dangerous_commands': 'options: true | false',
234
+ 'context.prompt_budget_audit': 'options: true | false'
221
235
  },
222
236
  describeSet: (label, hint) => `set the ${label}${hint ? ` (${hint})` : ''}`,
223
237
  describeGet: (label, hint) => `show the ${label}${hint ? ` (${hint})` : ''}`,
@@ -250,12 +264,14 @@ function getCompletionCopy(language = 'zh') {
250
264
  config: 'set/get/list/reset config values',
251
265
  memory: 'list/search/delete persistent memories',
252
266
  dream: 'consolidate memory inbox (dream)',
267
+ reflect: 'reflect on a successful workflow and draft a reusable skill',
253
268
  history: 'list/resume sessions',
254
269
  debug: 'runtime debug switches',
255
270
  retry: 'retry the last user request',
256
271
  stop: 'stop the current response',
257
272
  new: 'start a new session',
258
273
  yes: 'approve the pending plan and start execution',
274
+ no: 'discard the pending item',
259
275
  edit: 'revise the pending plan',
260
276
  reject: 'reject the pending plan'
261
277
  },
@@ -269,6 +285,7 @@ function getCompletionCopy(language = 'zh') {
269
285
  agentCommand: 'sub-agent command',
270
286
  memoryCommand: 'memory command',
271
287
  dreamCommand: 'dream consolidation command',
288
+ reflectCommand: 'reflect skill draft command',
272
289
  debugCommand: 'debug command',
273
290
  keyboardDebugCommand: 'keyboard debug command',
274
291
  compactCommand: 'context compaction command',
@@ -288,9 +305,10 @@ function describeConfigKey(key, mode = 'set', language = 'zh') {
288
305
  return mode === 'get' ? copy.describeGet(label, hint) : copy.describeSet(label, hint);
289
306
  }
290
307
 
291
- const SUB_AGENT_ROLES = ['planner', 'coder', 'reviewer', 'tester', 'summarizer'];
308
+ const SUB_AGENT_ROLES = ['planner', 'advisor', 'coder', 'reviewer', 'tester', 'summarizer'];
292
309
  export const ROLE_TOOL_POLICY = {
293
310
  planner: ['read', 'grep', 'list', 'query_project_index', 'tool_search', 'glob', 'ast_query', 'read_ast_node', 'web_fetch', 'web_search', 'read_plan', 'update_plan'],
311
+ advisor: ['read', 'grep', 'list', 'query_project_index', 'tool_search', 'read_plan'],
294
312
  coder: ['read', 'grep', 'list', 'edit', 'write', 'delete', 'run', 'ast_query', 'read_ast_node', 'glob', 'tool_search', 'web_fetch', 'web_search', 'update_todos', 'read_plan', 'update_plan'],
295
313
  reviewer: ['read', 'grep', 'list', 'glob', 'tool_search', 'ast_query', 'read_ast_node', 'read_plan'],
296
314
  tester: ['read', 'grep', 'list', 'run', 'glob', 'tool_search', 'read_plan'],
@@ -338,6 +356,25 @@ export function getSubAgentRolePrompt(role) {
338
356
  'Do not add a closing summary or "Next Action" — the pipeline handles what comes next.'
339
357
  ].join('\n');
340
358
  }
359
+ if (role === 'advisor') {
360
+ return [
361
+ 'You are the advisor in a multi-step agent pipeline.',
362
+ 'Your job: analyze the handed-off context and produce recommendations, tradeoffs, and evidence.',
363
+ 'Do not edit files, write code, delete files, or run commands.',
364
+ 'Output format — keep it short and direct:',
365
+ 'Findings:',
366
+ '- <important observation, constraint, or "none">',
367
+ 'Recommendations:',
368
+ '- <prioritized recommendation or "none">',
369
+ 'Tradeoffs:',
370
+ '- <important tradeoff or "none">',
371
+ 'Evidence:',
372
+ '- <files, docs, or observations checked>',
373
+ 'Open Questions:',
374
+ '- <blocking uncertainty or "none">',
375
+ 'Do not summarize your own work or add closing remarks — just deliver the structured advisory handoff and stop.'
376
+ ].join('\n');
377
+ }
341
378
  if (role === 'tester') {
342
379
  return [
343
380
  'You are the tester in a multi-step agent pipeline.',
@@ -413,7 +450,10 @@ function buildPipelineStepGuidance({ role, stepIndex, totalSteps, isFirst, isLas
413
450
  lines.push('- If you discover something new, record it under the requested headings instead of burying it in prose.');
414
451
  lines.push('- Continue the established direction unless you have concrete contradictory evidence.');
415
452
  lines.push('- Output only what the next step needs to know. Skip obvious observations.');
416
- if (isLast) {
453
+ if (role !== 'summarizer') {
454
+ lines.push('- Do not produce a final overall summary; the final summarizer step owns synthesis.');
455
+ }
456
+ if (isLast && role === 'summarizer') {
417
457
  lines.push('- Since you are the final step, give a concise overall verdict the user can act on.');
418
458
  }
419
459
  return lines.join('\n');
@@ -704,6 +744,9 @@ function buildGoalRequirementPacket(goal, role) {
704
744
  lines.push('Verify the implementation against the original goal, not just syntax or smoke checks.');
705
745
  lines.push('Check each acceptance item explicitly before calling the work verified.');
706
746
  lines.push('If any requested behavior is unverified or contradicted by evidence, list it under Not Verified or Failures.');
747
+ } else if (role === 'advisor') {
748
+ lines.push('Advise against the acceptance checklist and original goal, not generic best practices.');
749
+ lines.push('Prioritize concrete recommendations, evidence, and tradeoffs. Do not implement changes.');
707
750
  } else if (role === 'coder') {
708
751
  lines.push('Implement against the acceptance checklist, not only the broad wording of the goal.');
709
752
  }
@@ -719,10 +762,11 @@ function buildAutoPlanPlannerGuidance() {
719
762
  '- Prefer the smallest local approach that satisfies the goal.',
720
763
  '- Do not output multiple alternative branches in the final plan.',
721
764
  '- 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.',
765
+ '- Available sub-agent roles are planner, advisor, coder, reviewer, tester, and summarizer. Use only the non-summary roles the task actually needs.',
766
+ '- 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.',
767
+ '- Do not ask planner, advisor, coder, reviewer, or tester steps to produce the final summary. They should write detailed step results for the summarizer.',
724
768
  '- For implementation-heavy or risky changes, prefer adding review and/or verification steps.',
725
- '- For analysis, recommendation, or planning-only goals, you may omit reviewer/tester if they do not add value.',
769
+ '- For analysis, recommendation, optimization, architecture feedback, or planning-only goals, prefer advisor over coder and omit reviewer/tester if they do not add value.',
726
770
  '- Prefer 3-5 steps total unless the task is clearly larger.',
727
771
  '- Keep the plan ordered, implementation-oriented, and easy for small sub-agents to follow.'
728
772
  ].join('\n');
@@ -740,6 +784,9 @@ function buildAutoPlanExecutionGuidance(role) {
740
784
  if (role === 'coder') {
741
785
  common.push('- Keep edits tightly scoped to the chosen plan direction.');
742
786
  common.push('- Avoid speculative cleanup or unrelated improvements.');
787
+ } else if (role === 'advisor') {
788
+ common.push('- Produce advisory findings and recommendations only; do not modify files or run commands.');
789
+ common.push('- Ground every recommendation in inspected evidence or mark it as an assumption.');
743
790
  } else if (role === 'reviewer') {
744
791
  common.push('- Review against the chosen plan direction and the acceptance checklist.');
745
792
  common.push('- Call out missing requested behavior, regression risk, and unverified claims.');
@@ -1040,7 +1087,12 @@ async function buildTesterVerificationPacket(focusPaths = []) {
1040
1087
  return lines.join('\n');
1041
1088
  }
1042
1089
 
1043
- function isSkillEnabled(config, name) {
1090
+ function isBundledSkillCommand(command) {
1091
+ return command?.metadata?.type === 'skill' && command?.source === 'bundled-skill';
1092
+ }
1093
+
1094
+ function isSkillEnabled(config, name, command = null) {
1095
+ if (isBundledSkillCommand(command)) return true;
1044
1096
  return config.skills?.enabled?.[name] !== false;
1045
1097
  }
1046
1098
 
@@ -1048,6 +1100,10 @@ function selectAutoSkillNames(text = '') {
1048
1100
  const input = String(text || '').toLowerCase();
1049
1101
  const selected = ['superpowers-lite'];
1050
1102
 
1103
+ const explicitGrillMe =
1104
+ /\bgr+ill\s+me\b|\bpressure[- ]?test\b|\bstress[- ]?test\b|\bchallenge\s+(?:this|my|me)\b|\btear\s+(?:this|my)\s+.*apart\b/i.test(
1105
+ input
1106
+ ) || /(拷问|质询|压力测试|挑战一下|挑刺|狠狠审|喷一下|怼一下)/i.test(input);
1051
1107
  const explicitBrainstorm =
1052
1108
  /(brainstorm|头脑风暴|方案|思路|设计一下|设计方案|怎么做|如何做|approach|options?)/i.test(input);
1053
1109
  const ambiguitySignals =
@@ -1066,6 +1122,9 @@ function selectAutoSkillNames(text = '') {
1066
1122
  if (explicitBrainstorm || (ambiguitySignals && featureRequest) || greenfieldBuildRequest) {
1067
1123
  selected.push('brainstorm');
1068
1124
  }
1125
+ if (explicitGrillMe) {
1126
+ selected.push('grill-me');
1127
+ }
1069
1128
  return selected;
1070
1129
  }
1071
1130
 
@@ -1165,7 +1224,7 @@ function buildMediumTaskSystemPrompt(systemPrompt) {
1165
1224
  }
1166
1225
 
1167
1226
  function buildAutoSkillSystemPrompt(baseSystemPrompt, commands, config, text) {
1168
- const selected = classifyAutoRoute(text).selectedSkills.filter((name) => isSkillEnabled(config, name));
1227
+ const selected = classifyAutoRoute(text).selectedSkills.filter((name) => isSkillEnabled(config, name, commands.get(name)));
1169
1228
  if (selected.length === 0) return baseSystemPrompt;
1170
1229
 
1171
1230
  const blocks = [];
@@ -1244,13 +1303,34 @@ function summarizeGoalForStepTitle(goal, fallback = 'requested change') {
1244
1303
  function buildFallbackAutoPlan(goal) {
1245
1304
  const requirements = deriveGoalRequirements(goal);
1246
1305
  const lightweightGoal = isLightweightAutoPlanGoal(goal, requirements);
1306
+ const taskClass = classifyPlanTaskClass(goal);
1247
1307
  const focus = summarizeGoalForStepTitle(goal);
1248
1308
  const summary =
1249
1309
  requirements.length > 0
1250
1310
  ? `Auto fallback plan for: ${requirements.join('; ')}`
1251
1311
  : `Auto fallback plan for: ${goal}`;
1252
1312
 
1313
+ if (taskClass === 'advisory') {
1314
+ return {
1315
+ summary,
1316
+ steps: [
1317
+ {
1318
+ title: 'Inspect the target area',
1319
+ role: 'planner',
1320
+ task: `Inspect the relevant project context for: ${goal}. Identify constraints, evidence, and likely high-value advisory areas.`
1321
+ },
1322
+ {
1323
+ title: `Advise on ${focus}`,
1324
+ role: 'advisor',
1325
+ task: `Analyze the findings for: ${goal}. Produce prioritized recommendations, tradeoffs, evidence, and open questions without implementing changes.`
1326
+ },
1327
+ buildDefaultSummarizerStep(goal)
1328
+ ]
1329
+ };
1330
+ }
1331
+
1253
1332
  if (lightweightGoal) {
1333
+ const summarizerStep = buildDefaultSummarizerStep(goal);
1254
1334
  return {
1255
1335
  summary,
1256
1336
  steps: [
@@ -1263,7 +1343,8 @@ function buildFallbackAutoPlan(goal) {
1263
1343
  title: 'Verify the change',
1264
1344
  role: 'tester',
1265
1345
  task: `Verify the completed change for: ${goal}. Run the most relevant focused checks available and report concrete evidence plus anything still unverified.`
1266
- }
1346
+ },
1347
+ summarizerStep
1267
1348
  ]
1268
1349
  };
1269
1350
  }
@@ -1308,6 +1389,13 @@ function buildFallbackAutoPlan(goal) {
1308
1389
  function buildDefaultSummarizerStep(goal, source = []) {
1309
1390
  const existing = (Array.isArray(source) ? source : []).find((step) => step.role === 'summarizer');
1310
1391
  if (existing?.title && existing?.task) return existing;
1392
+ if (classifyPlanTaskClass(goal) === 'advisory') {
1393
+ return {
1394
+ title: 'Synthesize final findings',
1395
+ role: 'summarizer',
1396
+ task: `Synthesize the advisory findings for: ${goal}. Read the accumulated observations, recommendations, tradeoffs, evidence, and open questions from earlier steps, then produce a concise final summary with the single best next action.`
1397
+ };
1398
+ }
1311
1399
  return {
1312
1400
  title: 'Synthesize final implementation status',
1313
1401
  role: 'summarizer',
@@ -1320,7 +1408,7 @@ function enforceAutoPlanGuardrailSteps(plan, goal) {
1320
1408
  const requirements = deriveGoalRequirements(goal);
1321
1409
  const lightweightGoal = isLightweightAutoPlanGoal(goal, requirements);
1322
1410
  const taskClass = classifyPlanTaskClass(goal);
1323
- const implementationSteps = source.filter((step) => step.role !== 'reviewer' && step.role !== 'tester' && step.role !== 'summarizer');
1411
+ const implementationSteps = source.filter((step) => step.role !== 'advisor' && step.role !== 'reviewer' && step.role !== 'tester' && step.role !== 'summarizer');
1324
1412
  const primaryImplementationStep =
1325
1413
  implementationSteps.find((step) => step.role === 'coder') ||
1326
1414
  implementationSteps[0] || {
@@ -1343,17 +1431,49 @@ function enforceAutoPlanGuardrailSteps(plan, goal) {
1343
1431
  const hasTester = source.some((step) => step.role === 'tester');
1344
1432
 
1345
1433
  if (taskClass === 'advisory') {
1346
- const advisorySteps = source.filter((step) => step.role === 'planner' || step.role === 'coder');
1434
+ const advisorySteps = source
1435
+ .filter((step) => step.role === 'planner' || step.role === 'advisor' || step.role === 'coder')
1436
+ .map((step) =>
1437
+ step.role === 'coder'
1438
+ ? {
1439
+ ...step,
1440
+ role: 'advisor',
1441
+ title: /^implement\b/i.test(String(step.title || '')) ? 'Advise on requested goal' : step.title
1442
+ }
1443
+ : step
1444
+ );
1445
+ const hasAdvisor = advisorySteps.some((step) => step.role === 'advisor');
1446
+ const baseSteps =
1447
+ advisorySteps.length > 0
1448
+ ? advisorySteps.slice(0, 6)
1449
+ : [
1450
+ {
1451
+ title: 'Advise on requested goal',
1452
+ role: 'advisor',
1453
+ task: `Analyze the goal and recommend the highest-value next steps for: ${goal}`
1454
+ }
1455
+ ];
1456
+ const finalSteps = hasAdvisor
1457
+ ? baseSteps
1458
+ : [
1459
+ ...baseSteps,
1460
+ {
1461
+ title: 'Advise on requested goal',
1462
+ role: 'advisor',
1463
+ task: `Analyze the goal and recommend the highest-value next steps for: ${goal}`
1464
+ }
1465
+ ];
1347
1466
  return {
1348
1467
  summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
1349
- steps: advisorySteps.length > 0 ? advisorySteps.slice(0, 6) : [primaryImplementationStep]
1468
+ steps: [...finalSteps, summarizerStep]
1350
1469
  };
1351
1470
  }
1352
1471
 
1353
1472
  if (lightweightGoal) {
1473
+ const baseSteps = hasTester ? [primaryImplementationStep, testerStep] : [primaryImplementationStep];
1354
1474
  return {
1355
1475
  summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
1356
- steps: hasTester ? [primaryImplementationStep, testerStep] : [primaryImplementationStep]
1476
+ steps: [...baseSteps, summarizerStep]
1357
1477
  };
1358
1478
  }
1359
1479
 
@@ -1362,11 +1482,9 @@ function enforceAutoPlanGuardrailSteps(plan, goal) {
1362
1482
  ...(hasReviewer ? [reviewerStep] : []),
1363
1483
  ...(testerStep ? [testerStep] : [])
1364
1484
  ];
1365
- const needsSummarizer = executionSteps.length >= 3;
1366
-
1367
1485
  return {
1368
1486
  summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
1369
- steps: needsSummarizer ? [...executionSteps, summarizerStep] : executionSteps
1487
+ steps: [...executionSteps, summarizerStep]
1370
1488
  };
1371
1489
  }
1372
1490
 
@@ -1618,7 +1736,7 @@ async function buildAutoPlanFinalSummary({
1618
1736
  content: buildAutoPlanFinalSummaryUserPrompt({ goal, autoPlan, runItems, planningError })
1619
1737
  }
1620
1738
  ],
1621
- timeoutMs: config.gateway.timeout_ms || 90000,
1739
+ timeoutMs: config.gateway.timeout_ms || 1800000,
1622
1740
  maxRetries: config.gateway.max_retries ?? 2
1623
1741
  });
1624
1742
  return trimInline(result.text || '', 600) || fallbackSummary;
@@ -1656,35 +1774,51 @@ async function removePlanFileIfPresent(planState) {
1656
1774
 
1657
1775
  function buildSpecTemplate(topic) {
1658
1776
  return `
1659
- # Spec: ${topic}
1777
+ # ${topic} Design
1660
1778
 
1661
- ## 1. Background
1662
- - Why this work is needed
1663
- - Existing pain points
1779
+ ## Summary
1780
+ - Problem statement
1781
+ - Desired outcome
1782
+ - Why this is worth doing
1664
1783
 
1665
- ## 2. Goals
1784
+ ## Goals
1666
1785
  - Primary goal
1667
- - Non-goals
1786
+ - Secondary goals
1787
+
1788
+ ## Non-Goals
1789
+ - Out-of-scope behavior
1790
+ - Explicitly rejected approaches
1668
1791
 
1669
- ## 3. Scope
1670
- - In scope
1671
- - Out of scope
1792
+ ## User Experience / Command Behavior
1793
+ - User-facing commands or flows
1794
+ - Review or approval behavior
1795
+ - Expected outputs
1672
1796
 
1673
- ## 4. Requirements
1797
+ ## Architecture
1798
+ - Main modules and responsibilities
1799
+ - Data flow
1800
+ - Integration points
1801
+
1802
+ ## Data / State Model
1803
+ - New or changed state
1804
+ - Persistence locations
1805
+ - Lifecycle and cleanup behavior
1806
+
1807
+ ## Safety Rules
1808
+ - Guardrails
1809
+ - Permission or approval requirements
1810
+ - Failure behavior
1811
+
1812
+ ## Requirements
1674
1813
  - Functional requirements
1675
1814
  - Non-functional requirements
1676
1815
  - Win10 compatibility requirements
1677
1816
 
1678
- ## 5. Design
1679
- - Architecture sketch
1680
- - Data flow
1681
- - Key interfaces/commands
1682
-
1683
- ## 6. Risks and Mitigations
1817
+ ## Risks and Mitigations
1684
1818
  - Risk
1685
1819
  - Mitigation
1686
1820
 
1687
- ## 7. Validation
1821
+ ## Testing / Validation
1688
1822
  - Test strategy
1689
1823
  - Acceptance checklist
1690
1824
  `;
@@ -1703,17 +1837,20 @@ async function buildSpecWithModel({
1703
1837
  systemPrompt
1704
1838
  }) {
1705
1839
  const prompt = [
1706
- 'Write a practical engineering spec in markdown.',
1840
+ 'Write a practical engineering spec in markdown, like an implementation-ready design document.',
1707
1841
  '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.'
1842
+ '# <Feature> Design',
1843
+ '## Summary',
1844
+ '## Goals',
1845
+ '## Non-Goals',
1846
+ '## User Experience / Command Behavior',
1847
+ '## Architecture',
1848
+ '## Data / State Model',
1849
+ '## Safety Rules',
1850
+ '## Requirements',
1851
+ '## Risks and Mitigations',
1852
+ '## Testing / Validation',
1853
+ 'Make it concrete, scoped, and suitable for turning into a sub-agent implementation plan.'
1717
1854
  ].join('\n');
1718
1855
 
1719
1856
  const result = await createChatCompletion({
@@ -1725,7 +1862,7 @@ async function buildSpecWithModel({
1725
1862
  { role: 'system', content: `${systemPrompt}\n${prompt}` },
1726
1863
  { role: 'user', content: `Topic: ${topic}` }
1727
1864
  ],
1728
- timeoutMs: config.gateway.timeout_ms || 90000,
1865
+ timeoutMs: config.gateway.timeout_ms || 1800000,
1729
1866
  maxRetries: config.gateway.max_retries ?? 2
1730
1867
  });
1731
1868
  return String(result.text || '').trim();
@@ -1788,7 +1925,7 @@ async function buildPlanFromSpecWithModel({
1788
1925
  content: `Spec path: ${specPath || '(inline)'}\n\nProject implementation constraints:\n${projectConstraints}\n\n${specText}`
1789
1926
  }
1790
1927
  ],
1791
- timeoutMs: config.gateway.timeout_ms || 90000,
1928
+ timeoutMs: config.gateway.timeout_ms || 1800000,
1792
1929
  maxRetries: config.gateway.max_retries ?? 2
1793
1930
  });
1794
1931
  return String(result.text || '').trim();
@@ -1886,6 +2023,72 @@ function effectiveMaxContextTokens(config) {
1886
2023
  return 32000;
1887
2024
  }
1888
2025
 
2026
+ function estimateTextTokens(role, content) {
2027
+ const text = String(content || '');
2028
+ if (!text) return 0;
2029
+ return estimateMessagesTokens([{ role, content: text }]);
2030
+ }
2031
+
2032
+ function makePromptBudgetComponent(name, role, content) {
2033
+ const text = String(content || '');
2034
+ return {
2035
+ name,
2036
+ chars: text.length,
2037
+ estimated_tokens: estimateTextTokens(role, text)
2038
+ };
2039
+ }
2040
+
2041
+ function buildPromptBudgetAudit({
2042
+ systemPrompt = '',
2043
+ projectContextSnippet = '',
2044
+ projectContextGuidance = '',
2045
+ messages = [],
2046
+ toolDefinitions = [],
2047
+ config = {}
2048
+ }) {
2049
+ const toolSchemaText = JSON.stringify(toolDefinitions || []);
2050
+ const messageTexts = (Array.isArray(messages) ? messages : []).map((message) => ({
2051
+ role: message?.role || 'user',
2052
+ content: message?.content || ''
2053
+ }));
2054
+ const components = [
2055
+ makePromptBudgetComponent('system_prompt', 'system', systemPrompt),
2056
+ makePromptBudgetComponent('project_context', 'system', projectContextSnippet),
2057
+ makePromptBudgetComponent('project_context_guidance', 'system', projectContextGuidance),
2058
+ {
2059
+ name: 'message_history',
2060
+ chars: messageTexts.reduce((total, message) => total + String(message.content || '').length, 0),
2061
+ estimated_tokens: estimateMessagesTokens(messageTexts)
2062
+ },
2063
+ makePromptBudgetComponent('tool_schemas', 'system', toolSchemaText)
2064
+ ];
2065
+ const totalChars = components.reduce((total, component) => total + component.chars, 0);
2066
+ const totalTokens = components.reduce((total, component) => total + component.estimated_tokens, 0);
2067
+ const maxContextTokens = effectiveMaxContextTokens(config);
2068
+ const contextUsagePct =
2069
+ maxContextTokens > 0 ? Math.min(100, Math.max(0, (totalTokens / maxContextTokens) * 100)) : 0;
2070
+ return {
2071
+ components,
2072
+ total: {
2073
+ chars: totalChars,
2074
+ estimated_tokens: totalTokens
2075
+ },
2076
+ max_context_tokens: maxContextTokens,
2077
+ context_usage_pct: contextUsagePct
2078
+ };
2079
+ }
2080
+
2081
+ function summarizePromptBudgetAudit(audit) {
2082
+ const totalTokens = audit?.total?.estimated_tokens || 0;
2083
+ const maxContextTokens = audit?.max_context_tokens || 0;
2084
+ const pct = Number(audit?.context_usage_pct || 0).toFixed(1);
2085
+ const components = (audit?.components || [])
2086
+ .filter((component) => component.estimated_tokens > 0)
2087
+ .map((component) => `${component.name}=${component.estimated_tokens}`)
2088
+ .join(', ');
2089
+ return `prompt budget: ${totalTokens}/${maxContextTokens} est tokens (${pct}%)${components ? `; ${components}` : ''}`;
2090
+ }
2091
+
1889
2092
  function buildRuntimeStateSnapshot({ currentSession, config, model, executionMode, extraSession }) {
1890
2093
  const parentTokens = estimateMessagesTokens(currentSession?.messages || []);
1891
2094
  const subTokens = extraSession ? estimateMessagesTokens(extraSession.messages || []) : 0;
@@ -1896,6 +2099,7 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
1896
2099
  sessionId: currentSession?.id || '',
1897
2100
  mode: executionMode || config.execution?.mode || 'auto',
1898
2101
  sdkProvider: config.sdk?.provider || 'openai-compatible',
2102
+ agentRole: 'general',
1899
2103
  model: model || config.model?.name || '',
1900
2104
  maxContextTokens
1901
2105
  };
@@ -1914,6 +2118,11 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
1914
2118
  value: currentSession?.planState?.status === 'pending_approval',
1915
2119
  enumerable: false,
1916
2120
  writable: false
2121
+ },
2122
+ pendingReflectSkill: {
2123
+ value: currentSession?.planState?.status === 'pending_reflect_skill',
2124
+ enumerable: false,
2125
+ writable: false
1917
2126
  }
1918
2127
  });
1919
2128
  return snapshot;
@@ -1940,6 +2149,10 @@ function hasPendingPlanApproval(session) {
1940
2149
  return session?.planState?.status === 'pending_approval';
1941
2150
  }
1942
2151
 
2152
+ function hasPendingReflectSkill(session) {
2153
+ return session?.planState?.status === 'pending_reflect_skill';
2154
+ }
2155
+
1943
2156
  function isApprovalText(text = '') {
1944
2157
  const value = String(text || '').trim().toLowerCase();
1945
2158
  if (!value) return false;
@@ -1976,6 +2189,28 @@ function buildPendingPlanApprovalMessage(planState) {
1976
2189
  return lines.join('\n');
1977
2190
  }
1978
2191
 
2192
+ function buildPendingReflectSkillMessage(reflectState) {
2193
+ const candidates = Array.isArray(reflectState?.candidates) ? reflectState.candidates : [];
2194
+ if (candidates.length === 0) {
2195
+ return 'Reflect found no reusable skill candidate.';
2196
+ }
2197
+ const lines = [
2198
+ 'Reflect skill draft pending.',
2199
+ `Scope: ${reflectState?.targetScope || 'project'}`
2200
+ ];
2201
+ for (const candidate of candidates) {
2202
+ lines.push('');
2203
+ lines.push(`[${candidate.id || 1}] ${candidate.name}`);
2204
+ lines.push(`Confidence: ${Number(candidate.confidence ?? 0.75).toFixed(2)}`);
2205
+ lines.push(`Target: ${candidate.targetPath || '-'}`);
2206
+ lines.push('');
2207
+ lines.push(String(candidate.content || '').trim());
2208
+ }
2209
+ lines.push('');
2210
+ lines.push('Use /yes to write this skill, /edit <feedback> to revise it, or /no to discard it.');
2211
+ return lines.join('\n');
2212
+ }
2213
+
1979
2214
  function buildApprovedPlanExecutionPrompt(planState, approvalText = '') {
1980
2215
  const requirementPacket = buildGoalRequirementPacket(planState?.goal || '', 'coder');
1981
2216
  const lines = [
@@ -2167,8 +2402,10 @@ async function askModel({
2167
2402
  }
2168
2403
 
2169
2404
  const projectContextSnippet = await buildProjectContextSnippet(process.cwd(), text).catch(() => '');
2405
+ const projectContextGuidance =
2406
+ 'Use this project context as lightweight guidance and verify important details with fresh reads when needed.';
2170
2407
  const effectiveSystemPrompt = projectContextSnippet
2171
- ? `${systemPrompt}\n\n${projectContextSnippet}\n\nUse this project context as lightweight guidance and verify important details with fresh reads when needed.`
2408
+ ? `${systemPrompt}\n\n${projectContextSnippet}\n\n${projectContextGuidance}`
2172
2409
  : systemPrompt;
2173
2410
 
2174
2411
  const { definitions, handlers, formatters, deferredDefinitions, dispose: disposeTools } = getBuiltinTools({
@@ -2198,6 +2435,31 @@ async function askModel({
2198
2435
  ? Object.fromEntries(Object.entries(deferredDefinitions).filter(([name]) => allowedTools.includes(name)))
2199
2436
  : deferredDefinitions;
2200
2437
 
2438
+ if (config.context?.prompt_budget_audit === true && onAgentEvent) {
2439
+ const auditId = `prompt-budget-${Date.now()}`;
2440
+ const audit = buildPromptBudgetAudit({
2441
+ systemPrompt,
2442
+ projectContextSnippet,
2443
+ projectContextGuidance: projectContextSnippet ? projectContextGuidance : '',
2444
+ messages: session.messages.filter((m) => m.role !== 'system'),
2445
+ toolDefinitions: filteredDefinitions,
2446
+ config
2447
+ });
2448
+ onAgentEvent({
2449
+ type: 'system_tool:start',
2450
+ id: auditId,
2451
+ name: 'prompt_budget',
2452
+ summary: 'calculating prompt budget'
2453
+ });
2454
+ onAgentEvent({
2455
+ type: 'system_tool:end',
2456
+ id: auditId,
2457
+ name: 'prompt_budget',
2458
+ summary: summarizePromptBudgetAudit(audit),
2459
+ details: audit
2460
+ });
2461
+ }
2462
+
2201
2463
  let activeAssistantIndex = -1;
2202
2464
  const wrappedAgentEvent = (event) => {
2203
2465
  // Always accumulate messages in session (for token tracking), only save when persisting
@@ -2277,7 +2539,7 @@ async function askModel({
2277
2539
  model: selectedModel,
2278
2540
  messages,
2279
2541
  tools,
2280
- timeoutMs: config.gateway.timeout_ms || 90000,
2542
+ timeoutMs: config.gateway.timeout_ms || 1800000,
2281
2543
  maxRetries: config.gateway.max_retries ?? 2,
2282
2544
  signal,
2283
2545
  onTextDelta: (delta) => {
@@ -2375,6 +2637,12 @@ async function runSubAgentTask({
2375
2637
  }
2376
2638
  } catch {}
2377
2639
  }
2640
+ if (
2641
+ role !== 'summarizer' &&
2642
+ ['assistant:start', 'assistant:delta', 'assistant:response', 'assistant:tool_call_delta'].includes(String(evt?.type || ''))
2643
+ ) {
2644
+ return;
2645
+ }
2378
2646
  if (onAgentEvent) onAgentEvent(evt);
2379
2647
  };
2380
2648
  const roleAllowedTools = ROLE_TOOL_POLICY[role];
@@ -2503,7 +2771,14 @@ async function executePlanWithSubAgents({
2503
2771
  );
2504
2772
  }
2505
2773
 
2506
- if (stepRecord.failed && i < steps.length - 1) break;
2774
+ if (stepRecord.failed && i < steps.length - 1) {
2775
+ const summarizerIndex = steps.findIndex((candidate, index) => index > i && candidate.role === 'summarizer');
2776
+ if (summarizerIndex > i) {
2777
+ i = summarizerIndex - 1;
2778
+ continue;
2779
+ }
2780
+ break;
2781
+ }
2507
2782
  }
2508
2783
 
2509
2784
  const summaryLines = [];
@@ -2557,14 +2832,15 @@ async function buildAutoPlanAndRun({
2557
2832
  '- advisory = analysis, review, audit, optimization suggestions, architecture feedback, brainstorming, planning, or recommendation requests.',
2558
2833
  '- implementation = add/build/create/implement/refactor/fix/update/change behavior in code or files.',
2559
2834
  '- verification-heavy = the user explicitly asks to run tests, verify findings, reproduce a bug, prove a claim, or validate a result.',
2560
- '- 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.',
2835
+ '- For advisory goals, prefer planner and advisor roles. Do not use coder unless the plan will actually modify code or files.',
2836
+ '- For advisory goals, do not use reviewer or tester unless the user explicitly asks for verification or review as a separate deliverable.',
2561
2837
  '- For advisory goals, do not emit generic filler steps such as "Test and verify", "Review recommendations", or other template-only steps.',
2562
2838
  '- For implementation goals, reviewer and tester are optional support roles, not defaults. Only include them when they clearly add value.',
2563
2839
  '- 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.',
2564
2840
  '- If the task is purely to inspect the current project and suggest improvements, a lean 2-step or 3-step plan is preferred.',
2565
- '- Example advisory roles: planner -> inspect project shape, coder -> synthesize findings and prioritized recommendations.',
2841
+ '- Example advisory roles: planner -> inspect project shape, advisor -> synthesize findings and prioritized recommendations.',
2566
2842
  '- Example implementation roles: planner -> inspect target area, coder -> implement change, tester -> verify changed behavior.',
2567
- 'Return strict JSON only with shape {"summary":"...","steps":[{"title":"...","role":"planner|coder|reviewer|tester|summarizer","task":"..."}]}. No markdown.'
2843
+ 'Return strict JSON only with shape {"summary":"...","steps":[{"title":"...","role":"planner|advisor|coder|reviewer|tester|summarizer","task":"..."}]}. No markdown.'
2568
2844
  ].join('\n');
2569
2845
  let autoPlan = {
2570
2846
  summary: `Auto plan for: ${goal}`,
@@ -2589,14 +2865,15 @@ async function buildAutoPlanAndRun({
2589
2865
  role: 'user',
2590
2866
  content: [
2591
2867
  'Create an execution plan and assign best sub-agent role for each step.',
2592
- '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.',
2868
+ 'Return strict JSON only with shape {"summary":"...","steps":[{"title":"...","role":"planner|advisor|coder|reviewer|tester|summarizer","task":"..."}]}. No markdown.',
2869
+ 'The available roles are planner, advisor, coder, reviewer, tester, and summarizer. Use only the non-summary roles the task actually needs.',
2870
+ 'Always include a summarizer as the final step. The summarizer synthesizes prior step results without re-analyzing.',
2871
+ 'Planner, advisor, coder, reviewer, and tester steps should write detailed step results, not final summaries.',
2595
2872
  `Task class: ${normalizedTaskClass}`,
2596
2873
  'Before choosing roles, decide whether the request is advisory, implementation, or verification-heavy.',
2597
2874
  requirementPacket,
2598
2875
  'The first step should usually inspect or clarify the target area before implementation.',
2599
- 'For analysis, recommendation, optimization, audit, or project-review goals, keep the plan lean and usually limit it to planner/coder.',
2876
+ 'For analysis, recommendation, optimization, audit, or project-review goals, keep the plan lean and usually limit it to planner/advisor.',
2600
2877
  'Do not include reviewer/tester for advisory goals unless the user explicitly asks to validate, verify, or independently review the findings.',
2601
2878
  'Avoid template-only titles like "Initial analysis", "Review recommendations", or "Test and verify" for advisory goals.',
2602
2879
  'For implementation-heavy changes, prefer review and/or testing steps near the end only when they materially improve confidence.',
@@ -2606,7 +2883,7 @@ async function buildAutoPlanAndRun({
2606
2883
  .join('\n')
2607
2884
  }
2608
2885
  ],
2609
- timeoutMs: config.gateway.timeout_ms || 90000,
2886
+ timeoutMs: config.gateway.timeout_ms || 1800000,
2610
2887
  maxRetries: config.gateway.max_retries ?? 2
2611
2888
  });
2612
2889
  const parsed = extractJsonBlock(planning.text || '');
@@ -2707,8 +2984,9 @@ async function revisePendingPlanWithModel({
2707
2984
  const prompt = [
2708
2985
  buildAutoPlanPlannerGuidance(),
2709
2986
  'You are revising an existing plan based on explicit user feedback.',
2710
- '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.'
2987
+ 'Return strict JSON only with shape {"summary":"...","steps":[{"title":"...","role":"planner|advisor|coder|reviewer|tester|summarizer","task":"..."}]}. No markdown.',
2988
+ 'Keep roles minimal and only include steps that materially help the goal.',
2989
+ 'Always keep a summarizer as the final step.'
2712
2990
  ].join('\n');
2713
2991
  const result = await createChatCompletion({
2714
2992
  sdkProvider: config.sdk?.provider,
@@ -2730,7 +3008,7 @@ async function revisePendingPlanWithModel({
2730
3008
  ].join('\n')
2731
3009
  }
2732
3010
  ],
2733
- timeoutMs: config.gateway.timeout_ms || 90000,
3011
+ timeoutMs: config.gateway.timeout_ms || 1800000,
2734
3012
  maxRetries: config.gateway.max_retries ?? 2
2735
3013
  });
2736
3014
  const parsed = extractJsonBlock(result.text || '');
@@ -2782,6 +3060,44 @@ async function handleShellInput(shellText, config) {
2782
3060
  return { text: chunks.join('\n') };
2783
3061
  }
2784
3062
 
3063
+ function formatHistoryTimestamp(value) {
3064
+ const raw = String(value || '').trim();
3065
+ if (!raw) return 'updated unknown';
3066
+ const parsed = new Date(raw);
3067
+ if (Number.isNaN(parsed.getTime())) return `updated ${raw}`;
3068
+ return `updated ${parsed.toISOString().slice(0, 16).replace('T', ' ')}`;
3069
+ }
3070
+
3071
+ function compactHistoryPreview(value, maxChars = 72) {
3072
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
3073
+ if (!text) return '(no preview)';
3074
+ if (text.length <= maxChars) return text;
3075
+ return `${text.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
3076
+ }
3077
+
3078
+ function formatHistoryList({ currentSession, sessions }) {
3079
+ const currentMessages = Array.isArray(currentSession?.messages) ? currentSession.messages.length : 0;
3080
+ const lines = [
3081
+ `Current session ${currentSession.id}`,
3082
+ `Messages ${currentMessages}`,
3083
+ '',
3084
+ 'Recent sessions'
3085
+ ];
3086
+
3087
+ for (const [index, session] of sessions.entries()) {
3088
+ const count = Number(session.messageCount || 0);
3089
+ lines.push(
3090
+ `${index + 1}. ${session.id}`,
3091
+ ` ${count} ${count === 1 ? 'msg' : 'msgs'} | ${formatHistoryTimestamp(session.updatedAt)}`,
3092
+ ` ${compactHistoryPreview(session.preview)}`,
3093
+ ` resume: /history resume ${session.id}`
3094
+ );
3095
+ }
3096
+
3097
+ lines.push('', 'Tip: use /history resume <session_id>');
3098
+ return lines.join('\n');
3099
+ }
3100
+
2785
3101
  export async function createChatRuntime({
2786
3102
  session,
2787
3103
  config: initialConfig,
@@ -2830,6 +3146,13 @@ export async function createChatRuntime({
2830
3146
  executionMode = 'plan';
2831
3147
  }
2832
3148
  const commands = await loadCommandsAndSkills();
3149
+ const reloadCommandsAndSkills = async () => {
3150
+ const next = await loadCommandsAndSkills();
3151
+ commands.clear();
3152
+ for (const [name, command] of next.entries()) {
3153
+ commands.set(name, command);
3154
+ }
3155
+ };
2833
3156
 
2834
3157
  // Set up tool result store under session directory
2835
3158
  const sessionResultsDir = path.join(getSessionsDir(), String(currentSession.id));
@@ -2940,6 +3263,7 @@ export async function createChatRuntime({
2940
3263
  { name: 'config', description: completionCopy.commands.config },
2941
3264
  { name: 'memory', description: completionCopy.commands.memory },
2942
3265
  { name: 'dream', description: completionCopy.commands.dream },
3266
+ { name: 'reflect', description: completionCopy.commands.reflect },
2943
3267
  { name: 'history', description: completionCopy.commands.history },
2944
3268
  { name: 'debug', description: completionCopy.commands.debug },
2945
3269
  { name: 'retry', description: completionCopy.commands.retry },
@@ -2948,7 +3272,7 @@ export async function createChatRuntime({
2948
3272
  ];
2949
3273
  const out = [];
2950
3274
  for (const cmd of commands.values()) {
2951
- if (cmd.metadata.type === 'skill' && config.skills?.enabled?.[cmd.name] === false) {
3275
+ if (cmd.metadata.type === 'skill' && !isSkillEnabled(config, cmd.name, cmd)) {
2952
3276
  continue;
2953
3277
  }
2954
3278
  out.push({
@@ -2988,9 +3312,10 @@ export async function createChatRuntime({
2988
3312
  ];
2989
3313
  const specTemplates = ['/spec <topic>'];
2990
3314
  const planTemplates = ['/plan <goal>', '/plan auto <goal>', '/plan approve', '/plan from-spec <spec-path?>'];
2991
- const agentTemplates = ['/agents list', '/agents run planner <task>', '/agents run coder <task>', '/agents run reviewer <task>', '/agents run tester <task>', '/agents run summarizer <task>'];
3315
+ const agentTemplates = ['/agents list', '/agents run planner <task>', '/agents run advisor <task>', '/agents run coder <task>', '/agents run reviewer <task>', '/agents run tester <task>', '/agents run summarizer <task>'];
2992
3316
  const debugTemplates = ['/debug keys on', '/debug keys off', '/debug keys status'];
2993
3317
  const dreamTemplates = ['/dream', '/dream --dry-run', '/dream --scope=project', '/dream --scope=global'];
3318
+ const reflectTemplates = ['/reflect', '/reflect --scope=global <request>', '/reflect <request>'];
2994
3319
  const compactTemplates = compactOptions.map((opt) => `/compact ${opt}`);
2995
3320
  const slashTemplates = [
2996
3321
  ...configTemplates,
@@ -3003,6 +3328,7 @@ export async function createChatRuntime({
3003
3328
  ...agentTemplates,
3004
3329
  ...debugTemplates,
3005
3330
  ...dreamTemplates,
3331
+ ...reflectTemplates,
3006
3332
  ...compactTemplates,
3007
3333
  '/retry',
3008
3334
  '/status'
@@ -3070,6 +3396,7 @@ export async function createChatRuntime({
3070
3396
  for (const template of agentTemplates) registerSuggestion(template, completionCopy.generic.agentCommand);
3071
3397
  for (const template of debugTemplates) registerSuggestion(template, completionCopy.generic.debugCommand);
3072
3398
  for (const template of dreamTemplates) registerSuggestion(template, completionCopy.generic.dreamCommand);
3399
+ for (const template of reflectTemplates) registerSuggestion(template, completionCopy.generic.reflectCommand);
3073
3400
  for (const template of compactTemplates) registerSuggestion(template, completionCopy.generic.compactCommand);
3074
3401
  registerSuggestion('/retry', completionCopy.generic.retryCommand);
3075
3402
  registerSuggestion('/status', completionCopy.generic.statusCommand);
@@ -3215,7 +3542,7 @@ export async function createChatRuntime({
3215
3542
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
3216
3543
  const sub = tokens[1] || '';
3217
3544
  if (sub === 'run') {
3218
- return ['planner', 'coder', 'reviewer', 'tester', 'summarizer']
3545
+ return SUB_AGENT_ROLES
3219
3546
  .map((r) => registerSuggestion(`/agents run ${r} `, completionCopy.generic.agentCommand));
3220
3547
  }
3221
3548
  return ['list', 'run']
@@ -3224,7 +3551,7 @@ export async function createChatRuntime({
3224
3551
  }
3225
3552
  if (tokens[1] === 'run') {
3226
3553
  const rolePrefix = tokens[2] || '';
3227
- return ['planner', 'coder', 'reviewer', 'tester']
3554
+ return SUB_AGENT_ROLES
3228
3555
  .filter((r) => r.startsWith(rolePrefix))
3229
3556
  .map((r) => registerSuggestion(`/agents run ${r} `, completionCopy.generic.agentCommand));
3230
3557
  }
@@ -3355,6 +3682,27 @@ export async function createChatRuntime({
3355
3682
  const saveDirectMemoryPrompt = async (text) => {
3356
3683
  const direct = classifyDirectMemoryPrompt(text);
3357
3684
  if (!direct) return null;
3685
+ const existing = await listMemories({
3686
+ scope: direct.scope,
3687
+ workspaceRoot: process.cwd()
3688
+ }).catch(() => []);
3689
+ const directText = String(direct.content || '').toLowerCase();
3690
+ const directTokens = new Set(directText.match(/[a-z0-9_\u4e00-\u9fa5]+/g) || []);
3691
+ const directAsciiTokens = new Set(directText.match(/[a-z0-9_]{4,}/g) || []);
3692
+ const overlapsExisting = existing.some((item) => {
3693
+ const existingText = `${item.content || ''} ${item.summary || ''}`.toLowerCase();
3694
+ for (const token of directAsciiTokens) {
3695
+ if (existingText.includes(token)) return true;
3696
+ }
3697
+ let hits = 0;
3698
+ for (const token of directTokens) {
3699
+ if (token.length < 2) continue;
3700
+ if (existingText.includes(token)) hits += 1;
3701
+ if (hits >= 2) return true;
3702
+ }
3703
+ return false;
3704
+ });
3705
+ if (overlapsExisting) return null;
3358
3706
  return rememberMemory({
3359
3707
  scope: direct.scope,
3360
3708
  content: direct.content,
@@ -3495,14 +3843,14 @@ export async function createChatRuntime({
3495
3843
  if (parsedInput.command === 'help') {
3496
3844
  return {
3497
3845
  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>'
3846
+ 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
3847
  };
3500
3848
  }
3501
3849
  if (parsedInput.command === 'status') {
3502
3850
  const todoCount = countActiveTodos(currentSession.todos);
3503
3851
  return {
3504
3852
  type: 'system',
3505
- text: `mode=${executionMode} | model=${model || config.model.name} | max_ctx=${effectiveMaxContextTokens(config)} | session=${currentSession.id} | todos=${todoCount}`
3853
+ text: `mode=${executionMode} | role=general | model=${model || config.model.name} | max_ctx=${effectiveMaxContextTokens(config)} | session=${currentSession.id} | todos=${todoCount}`
3506
3854
  };
3507
3855
  }
3508
3856
  if (parsedInput.command === 'mode') {
@@ -3521,6 +3869,27 @@ export async function createChatRuntime({
3521
3869
  return { type: 'system', text };
3522
3870
  }
3523
3871
  if (parsedInput.command === 'yes') {
3872
+ if (hasPendingReflectSkill(currentSession)) {
3873
+ const state = { ...currentSession.planState };
3874
+ const candidate = Array.isArray(state.candidates) ? state.candidates[0] : null;
3875
+ if (!candidate) {
3876
+ currentSession.planState = null;
3877
+ const text = 'No reflect skill draft to write.';
3878
+ await persistLocalExchange(line, text, { includeUser: false });
3879
+ return { type: 'system', text };
3880
+ }
3881
+ const written = await writeReflectSkillDraft({
3882
+ draft: candidate,
3883
+ scope: state.targetScope || 'project',
3884
+ workspaceRoot: process.cwd()
3885
+ });
3886
+ currentSession.planState = null;
3887
+ executionMode = 'auto';
3888
+ await reloadCommandsAndSkills();
3889
+ const text = `Reflect skill written and loaded: /${written.draft.name}\nPath: ${written.filePath}`;
3890
+ await persistLocalExchange(line, text, { includeUser: false });
3891
+ return { type: 'system', text };
3892
+ }
3524
3893
  if (!hasPendingPlanApproval(currentSession)) {
3525
3894
  return { type: 'system', text: 'No pending plan approval. Use /plan auto <goal> first.' };
3526
3895
  }
@@ -3544,6 +3913,35 @@ export async function createChatRuntime({
3544
3913
  return { type: 'assistant', text: result.text, aborted: !!result.aborted };
3545
3914
  }
3546
3915
  if (parsedInput.command === 'edit') {
3916
+ if (hasPendingReflectSkill(currentSession)) {
3917
+ const feedback = parsedInput.args.join(' ').trim();
3918
+ if (!feedback) {
3919
+ return { type: 'system', text: 'Usage: /edit <feedback>' };
3920
+ }
3921
+ const state = { ...currentSession.planState };
3922
+ const previousDraft = Array.isArray(state.candidates) ? state.candidates[0] : null;
3923
+ const drafts = await buildReflectSkillDraft({
3924
+ request: state.request || '',
3925
+ scope: state.targetScope || 'project',
3926
+ session: currentSession,
3927
+ config,
3928
+ model,
3929
+ systemPrompt: activeReplySystemPrompt,
3930
+ previousDraft,
3931
+ feedback
3932
+ });
3933
+ currentSession.planState = {
3934
+ ...state,
3935
+ candidates: attachReflectTargets({
3936
+ candidates: drafts,
3937
+ scope: state.targetScope || 'project',
3938
+ workspaceRoot: process.cwd()
3939
+ })
3940
+ };
3941
+ const text = `Reflect skill draft revised.\n${buildPendingReflectSkillMessage(currentSession.planState)}`;
3942
+ await persistLocalExchange(line, text);
3943
+ return { type: 'system', text };
3944
+ }
3547
3945
  if (!hasPendingPlanApproval(currentSession)) {
3548
3946
  return { type: 'system', text: 'No pending plan approval. Use /plan auto <goal> first.' };
3549
3947
  }
@@ -3564,6 +3962,23 @@ export async function createChatRuntime({
3564
3962
  await persistLocalExchange(line, text);
3565
3963
  return { type: 'system', text };
3566
3964
  }
3965
+ if (parsedInput.command === 'no') {
3966
+ if (hasPendingReflectSkill(currentSession)) {
3967
+ currentSession.planState = null;
3968
+ executionMode = 'auto';
3969
+ const text = 'Reflect skill draft discarded.';
3970
+ await persistLocalExchange(line, text, { includeUser: false });
3971
+ return { type: 'system', text };
3972
+ }
3973
+ if (hasPendingPlanApproval(currentSession)) {
3974
+ currentSession.planState = null;
3975
+ executionMode = 'auto';
3976
+ const text = 'Pending plan rejected and cleared.';
3977
+ await persistLocalExchange(line, text, { includeUser: false });
3978
+ return { type: 'system', text };
3979
+ }
3980
+ return { type: 'system', text: 'No pending reflect skill draft.' };
3981
+ }
3567
3982
  if (parsedInput.command === 'reject') {
3568
3983
  if (!hasPendingPlanApproval(currentSession)) {
3569
3984
  return { type: 'system', text: 'No pending plan approval.' };
@@ -3776,7 +4191,7 @@ export async function createChatRuntime({
3776
4191
  if (sub === 'list') {
3777
4192
  return {
3778
4193
  type: 'system',
3779
- text: 'Sub-agent roles: planner, coder, reviewer, tester, summarizer\nUse: /agents run <role> <task>'
4194
+ text: 'Sub-agent roles: planner, advisor, coder, reviewer, tester, summarizer\nUse: /agents run <role> <task>'
3780
4195
  };
3781
4196
  }
3782
4197
  if (sub === 'run') {
@@ -3784,7 +4199,7 @@ export async function createChatRuntime({
3784
4199
  const task = parsedInput.args.slice(2).join(' ').trim();
3785
4200
  if (!role || !task) return { type: 'system', text: 'Usage: /agents run <role> <task>' };
3786
4201
  if (!SUB_AGENT_ROLES.includes(role)) {
3787
- return { type: 'system', text: 'Unknown role. Allowed: planner|coder|reviewer|tester|summarizer' };
4202
+ return { type: 'system', text: 'Unknown role. Allowed: planner|advisor|coder|reviewer|tester|summarizer' };
3788
4203
  }
3789
4204
  const output = await runSubAgentTask({
3790
4205
  role,
@@ -3822,13 +4237,9 @@ export async function createChatRuntime({
3822
4237
  messageCount: Number(s.messageCount || 0)
3823
4238
  }));
3824
4239
  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
4240
  return {
3830
4241
  type: 'system',
3831
- text: `Current: ${currentSession.id}\n${rows.join('\n')}\nUse /history resume <session_id>`
4242
+ text: formatHistoryList({ currentSession, sessions })
3832
4243
  };
3833
4244
  }
3834
4245
  if (sub === 'current') {
@@ -3971,6 +4382,37 @@ export async function createChatRuntime({
3971
4382
  return { type: 'system', text: `Dream failed: ${err.message}` };
3972
4383
  }
3973
4384
  }
4385
+ if (parsedInput.command === 'reflect') {
4386
+ const parsedReflect = parseReflectScope(parsedInput.args);
4387
+ const drafts = await buildReflectSkillDraft({
4388
+ request: parsedReflect.request,
4389
+ scope: parsedReflect.scope,
4390
+ session: currentSession,
4391
+ config,
4392
+ model,
4393
+ systemPrompt: activeReplySystemPrompt
4394
+ });
4395
+ const candidates = attachReflectTargets({
4396
+ candidates: drafts,
4397
+ scope: parsedReflect.scope,
4398
+ workspaceRoot: process.cwd()
4399
+ });
4400
+ if (candidates.length === 0) {
4401
+ const text = 'Reflect found no reusable skill candidate.';
4402
+ await persistLocalExchange(line, text);
4403
+ return { type: 'system', text };
4404
+ }
4405
+ currentSession.planState = {
4406
+ status: 'pending_reflect_skill',
4407
+ source: 'reflect',
4408
+ targetScope: parsedReflect.scope,
4409
+ request: parsedReflect.request,
4410
+ candidates
4411
+ };
4412
+ const text = buildPendingReflectSkillMessage(currentSession.planState);
4413
+ await persistLocalExchange(line, text);
4414
+ return { type: 'system', text };
4415
+ }
3974
4416
  if (parsedInput.command === 'retry') {
3975
4417
  const lastUser = [...currentSession.messages].reverse().find((m) => m.role === 'user');
3976
4418
  if (!lastUser?.content) {
@@ -4096,7 +4538,7 @@ export async function createChatRuntime({
4096
4538
  if (!custom) {
4097
4539
  return { type: 'system', text: `Unknown slash command: /${parsedInput.command}` };
4098
4540
  }
4099
- if (custom.metadata.type === 'skill' && config.skills?.enabled?.[custom.name] === false) {
4541
+ if (custom.metadata.type === 'skill' && !isSkillEnabled(config, custom.name, custom)) {
4100
4542
  return { type: 'system', text: `Skill is disabled: ${custom.name}` };
4101
4543
  }
4102
4544
 
@@ -4218,7 +4660,6 @@ export async function createChatRuntime({
4218
4660
  }
4219
4661
 
4220
4662
  const expandedText = await expandFileMentions(parsedInput.text, process.cwd());
4221
- await saveDirectMemoryPrompt(expandedText);
4222
4663
  const autoRoute = classifyAutoRoute(expandedText);
4223
4664
  if (autoRoute.autoPlan) {
4224
4665
  await maybeAutoDreamFromRuntime();
@@ -4247,7 +4688,7 @@ export async function createChatRuntime({
4247
4688
  return { type: 'system', text };
4248
4689
  }
4249
4690
 
4250
- const selectedAutoSkills = autoRoute.selectedSkills.filter((name) => isSkillEnabled(config, name));
4691
+ const selectedAutoSkills = autoRoute.selectedSkills.filter((name) => isSkillEnabled(config, name, commands.get(name)));
4251
4692
  if (selectedAutoSkills.length > 0 && onAgentEvent) {
4252
4693
  onAgentEvent({
4253
4694
  type: 'skill:auto',
@@ -4270,6 +4711,7 @@ export async function createChatRuntime({
4270
4711
  executionMode,
4271
4712
  signal
4272
4713
  });
4714
+ await saveDirectMemoryPrompt(expandedText);
4273
4715
  await captureUserPromptForDream(expandedText);
4274
4716
  return { type: 'assistant', text: result.text, aborted: !!result.aborted };
4275
4717
  };