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.
- package/OPERATIONS.md +4 -2
- package/README.md +83 -5
- package/deployment.md +14 -7
- package/package.json +1 -2
- package/src/cli.js +1 -1
- package/src/commands/skill.js +145 -53
- package/src/core/agent-loop.js +1 -206
- package/src/core/chat-runtime.js +306 -53
- package/src/core/command-loader.js +12 -5
- package/src/core/context-compact.js +2 -1
- package/src/core/dream-audit.js +12 -0
- package/src/core/dream-consolidate.js +131 -59
- package/src/core/dream-evaluator.js +86 -0
- package/src/core/fff-adapter.js +1 -1
- package/src/core/memory-store.js +145 -10
- package/src/core/reflect-skill.js +178 -0
- package/src/core/tool-result-store.js +206 -0
- package/src/core/tools.js +126 -30
- package/src/tui/chat-app.js +247 -27
- package/src/core/provider/anthropic.sdk-backup.js +0 -439
- package/src/core/provider/openai-compatible.sdk-backup.js +0 -412
package/src/core/chat-runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseInput } from './input-parser.js';
|
|
2
2
|
import { loadCommandsAndSkills, renderCommandPrompt } from './command-loader.js';
|
|
3
|
-
import { runAgentLoop
|
|
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 (
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
#
|
|
1683
|
+
# ${topic} Design
|
|
1660
1684
|
|
|
1661
|
-
##
|
|
1662
|
-
-
|
|
1663
|
-
-
|
|
1685
|
+
## Summary
|
|
1686
|
+
- Problem statement
|
|
1687
|
+
- Desired outcome
|
|
1688
|
+
- Why this is worth doing
|
|
1664
1689
|
|
|
1665
|
-
##
|
|
1690
|
+
## Goals
|
|
1666
1691
|
- Primary goal
|
|
1667
|
-
-
|
|
1692
|
+
- Secondary goals
|
|
1668
1693
|
|
|
1669
|
-
##
|
|
1670
|
-
-
|
|
1671
|
-
-
|
|
1694
|
+
## Non-Goals
|
|
1695
|
+
- Out-of-scope behavior
|
|
1696
|
+
- Explicitly rejected approaches
|
|
1672
1697
|
|
|
1673
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
'#
|
|
1709
|
-
'##
|
|
1710
|
-
'##
|
|
1711
|
-
'##
|
|
1712
|
-
'##
|
|
1713
|
-
'##
|
|
1714
|
-
'##
|
|
1715
|
-
'##
|
|
1716
|
-
'
|
|
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)
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
};
|