codemini-cli 0.3.7 → 0.3.8
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 +18 -0
- package/README.md +194 -151
- package/package.json +1 -1
- package/src/commands/chat.js +24 -0
- package/src/commands/run.js +4 -4
- package/src/core/agent-loop.js +11 -1
- package/src/core/chat-runtime.js +239 -79
- package/src/core/plan-state.js +32 -0
- package/src/core/session-store.js +3 -19
- package/src/core/shell-profile.js +1 -0
- package/src/core/tools.js +158 -1
- package/src/tui/chat-app.js +265 -19
package/src/core/chat-runtime.js
CHANGED
|
@@ -26,6 +26,7 @@ import { getProjectPlansDir, getProjectSpecsDir, getProjectWorkspaceDir, getSess
|
|
|
26
26
|
import { buildProjectContextSnippet, initializeProjectIndex } from './project-index.js';
|
|
27
27
|
import { buildMemorySnapshot } from './memory-prompt.js';
|
|
28
28
|
import { forgetMemory, listMemories, searchMemories } from './memory-store.js';
|
|
29
|
+
import { normalizePlanState } from './plan-state.js';
|
|
29
30
|
import { countActiveTodos, normalizeTodos } from './todo-state.js';
|
|
30
31
|
|
|
31
32
|
const STREAM_SAVE_DEBOUNCE_MS = 120;
|
|
@@ -131,8 +132,10 @@ function getCompletionCopy(language = 'zh') {
|
|
|
131
132
|
planSubcommands: {
|
|
132
133
|
'/plan <goal>': '创建一个人工审阅的实施计划',
|
|
133
134
|
'/plan auto <goal>': '自动生成计划并等待你确认执行',
|
|
134
|
-
'/plan auto run <goal>': '自动生成计划后立即继续执行',
|
|
135
135
|
'/plan approve': '批准当前待确认的计划并开始执行',
|
|
136
|
+
'/yes': '确认并执行当前待确认计划',
|
|
137
|
+
'/edit <feedback>': '根据你的反馈修改当前待确认计划',
|
|
138
|
+
'/reject': '拒绝并清空当前待确认计划',
|
|
136
139
|
'/plan from-spec <spec-path?>': '从 spec 文件生成实施计划'
|
|
137
140
|
},
|
|
138
141
|
commands: {
|
|
@@ -151,7 +154,10 @@ function getCompletionCopy(language = 'zh') {
|
|
|
151
154
|
history: '查看/恢复会话',
|
|
152
155
|
debug: '运行时调试开关',
|
|
153
156
|
retry: '重试上一条用户请求',
|
|
154
|
-
stop: '中止当前回答'
|
|
157
|
+
stop: '中止当前回答',
|
|
158
|
+
yes: '确认当前待审批计划并开始执行',
|
|
159
|
+
edit: '修改当前待审批计划',
|
|
160
|
+
reject: '拒绝当前待审批计划'
|
|
155
161
|
},
|
|
156
162
|
generic: {
|
|
157
163
|
configCommand: '配置命令',
|
|
@@ -220,8 +226,10 @@ function getCompletionCopy(language = 'zh') {
|
|
|
220
226
|
planSubcommands: {
|
|
221
227
|
'/plan <goal>': 'create an implementation plan for manual review',
|
|
222
228
|
'/plan auto <goal>': 'generate a plan and wait for your approval',
|
|
223
|
-
'/plan auto run <goal>': 'generate a plan and continue execution immediately',
|
|
224
229
|
'/plan approve': 'approve the pending plan and start execution',
|
|
230
|
+
'/yes': 'approve and execute the pending plan',
|
|
231
|
+
'/edit <feedback>': 'revise the pending plan based on your feedback',
|
|
232
|
+
'/reject': 'reject and clear the pending plan',
|
|
225
233
|
'/plan from-spec <spec-path?>': 'generate an implementation plan from a spec file'
|
|
226
234
|
},
|
|
227
235
|
commands: {
|
|
@@ -240,7 +248,10 @@ function getCompletionCopy(language = 'zh') {
|
|
|
240
248
|
history: 'list/resume sessions',
|
|
241
249
|
debug: 'runtime debug switches',
|
|
242
250
|
retry: 'retry the last user request',
|
|
243
|
-
stop: 'stop the current response'
|
|
251
|
+
stop: 'stop the current response',
|
|
252
|
+
yes: 'approve the pending plan and start execution',
|
|
253
|
+
edit: 'revise the pending plan',
|
|
254
|
+
reject: 'reject the pending plan'
|
|
244
255
|
},
|
|
245
256
|
generic: {
|
|
246
257
|
configCommand: 'config command',
|
|
@@ -272,11 +283,11 @@ function describeConfigKey(key, mode = 'set', language = 'zh') {
|
|
|
272
283
|
|
|
273
284
|
const SUB_AGENT_ROLES = ['planner', 'coder', 'reviewer', 'tester', 'summarizer'];
|
|
274
285
|
const ROLE_TOOL_POLICY = {
|
|
275
|
-
planner: ['read', 'grep', 'list', 'query_project_index', 'tool_search', 'glob', 'ast_query', 'read_ast_node'],
|
|
276
|
-
coder: ['read', 'grep', 'list', 'edit', 'write', 'run', 'ast_query', 'read_ast_node', 'glob', 'tool_search', 'update_todos'],
|
|
277
|
-
reviewer: ['read', 'grep', 'list', 'glob', 'tool_search', 'ast_query', 'read_ast_node'],
|
|
278
|
-
tester: ['read', 'grep', 'list', 'run', 'glob', 'tool_search'],
|
|
279
|
-
summarizer: ['read', 'grep', 'list', 'glob', 'tool_search']
|
|
286
|
+
planner: ['read', 'grep', 'list', 'query_project_index', 'tool_search', 'glob', 'ast_query', 'read_ast_node', 'read_plan', 'update_plan'],
|
|
287
|
+
coder: ['read', 'grep', 'list', 'edit', 'write', 'run', 'ast_query', 'read_ast_node', 'glob', 'tool_search', 'update_todos', 'read_plan', 'update_plan'],
|
|
288
|
+
reviewer: ['read', 'grep', 'list', 'glob', 'tool_search', 'ast_query', 'read_ast_node', 'read_plan'],
|
|
289
|
+
tester: ['read', 'grep', 'list', 'run', 'glob', 'tool_search', 'read_plan'],
|
|
290
|
+
summarizer: ['read', 'grep', 'list', 'glob', 'tool_search', 'read_plan', 'update_plan']
|
|
280
291
|
};
|
|
281
292
|
const SUB_AGENT_CONTEXT_MAX_MESSAGES = 4;
|
|
282
293
|
const SUB_AGENT_CONTEXT_MAX_CHARS = 1200;
|
|
@@ -1479,7 +1490,7 @@ function buildAutoPlanSystemSummary(auto) {
|
|
|
1479
1490
|
const baseStatusTitle =
|
|
1480
1491
|
auto.failedCount > 0 ? 'Auto plan finished with failures' : auto.warningCount > 0 ? 'Auto plan finished with warnings' : 'Auto plan finished';
|
|
1481
1492
|
const statusTitle =
|
|
1482
|
-
auto.approvalStatus === 'pending' ? `${baseStatusTitle} (waiting for /
|
|
1493
|
+
auto.approvalStatus === 'pending' ? `${baseStatusTitle} (waiting for /yes)` : baseStatusTitle;
|
|
1483
1494
|
const lines = [
|
|
1484
1495
|
statusTitle,
|
|
1485
1496
|
`Plan File: ${auto.filePath}`,
|
|
@@ -1504,10 +1515,13 @@ function buildAutoPlanSystemSummary(auto) {
|
|
|
1504
1515
|
lines.push('Plan Steps:');
|
|
1505
1516
|
auto.steps.forEach((s, idx) => {
|
|
1506
1517
|
lines.push(` ${idx + 1}. [${s.role}] ${s.title}`);
|
|
1518
|
+
if (String(s?.task || '').trim()) {
|
|
1519
|
+
lines.push(` - task: ${String(s.task).trim()}`);
|
|
1520
|
+
}
|
|
1507
1521
|
});
|
|
1508
1522
|
}
|
|
1509
1523
|
if (auto.approvalStatus === 'pending') {
|
|
1510
|
-
lines.push('Next: review the plan summary, then use /
|
|
1524
|
+
lines.push('Next: review the plan summary, then use /yes to execute, /edit <feedback> to revise this plan, or /reject to discard it.');
|
|
1511
1525
|
}
|
|
1512
1526
|
return lines.join('\n');
|
|
1513
1527
|
}
|
|
@@ -1910,13 +1924,26 @@ function hasPendingPlanApproval(session) {
|
|
|
1910
1924
|
function isApprovalText(text = '') {
|
|
1911
1925
|
const value = String(text || '').trim().toLowerCase();
|
|
1912
1926
|
if (!value) return false;
|
|
1913
|
-
return /^(yes|y|ok|okay|approve|approved|continue|proceed|go ahead|start|开始|继续|可以|同意|批准|通过|按这个做)$/.test(value);
|
|
1927
|
+
return /^(yes|\/yes|y|ok|okay|approve|approved|continue|proceed|go ahead|start|开始|继续|可以|同意|批准|通过|按这个做)$/.test(value);
|
|
1914
1928
|
}
|
|
1915
1929
|
|
|
1916
1930
|
function isStayInPlanText(text = '') {
|
|
1917
1931
|
const value = String(text || '').trim().toLowerCase();
|
|
1918
1932
|
if (!value) return false;
|
|
1919
|
-
return /^(stay|keep planning|keep in plan mode|not yet|wait|先别|先等等|继续计划|继续讨论|继续规划|暂不批准)$/.test(value);
|
|
1933
|
+
return /^(stay|\/stay|keep planning|keep in plan mode|not yet|wait|先别|先等等|继续计划|继续讨论|继续规划|暂不批准)$/.test(value);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
function isRejectPlanText(text = '') {
|
|
1937
|
+
const value = String(text || '').trim().toLowerCase();
|
|
1938
|
+
if (!value) return false;
|
|
1939
|
+
return /^(\/reject|reject|no|discard|cancel|否决|拒绝|不要了|取消计划)$/.test(value);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
function shouldPersistInputHistory(parsedInput) {
|
|
1943
|
+
if (!parsedInput || parsedInput.type !== 'slash') return true;
|
|
1944
|
+
const command = String(parsedInput.command || '').trim().toLowerCase();
|
|
1945
|
+
// Keep approval-only commands out of input history (↑/↓ should focus on real task prompts).
|
|
1946
|
+
return !['yes', 'no', 'edit', 'reject'].includes(command);
|
|
1920
1947
|
}
|
|
1921
1948
|
|
|
1922
1949
|
function buildPendingPlanApprovalMessage(planState) {
|
|
@@ -1925,7 +1952,7 @@ function buildPendingPlanApprovalMessage(planState) {
|
|
|
1925
1952
|
`Goal: ${planState?.goal || '-'}`,
|
|
1926
1953
|
`Plan File: ${planState?.filePath || '-'}`,
|
|
1927
1954
|
`Summary: ${planState?.finalSummary || planState?.summary || '-'}`,
|
|
1928
|
-
'Use /plan
|
|
1955
|
+
'Use /yes to execute this plan, /edit <feedback> to revise it, or /reject to discard it.'
|
|
1929
1956
|
];
|
|
1930
1957
|
return lines.join('\n');
|
|
1931
1958
|
}
|
|
@@ -2134,6 +2161,11 @@ async function askModel({
|
|
|
2134
2161
|
onTodosUpdate: (todos) => {
|
|
2135
2162
|
session.todos = normalizeTodos(todos);
|
|
2136
2163
|
scheduleSessionSave();
|
|
2164
|
+
},
|
|
2165
|
+
getPlanState: () => normalizePlanState(session.planState),
|
|
2166
|
+
onPlanStateUpdate: (planState) => {
|
|
2167
|
+
session.planState = normalizePlanState(planState);
|
|
2168
|
+
scheduleSessionSave();
|
|
2137
2169
|
}
|
|
2138
2170
|
});
|
|
2139
2171
|
|
|
@@ -2568,11 +2600,47 @@ async function buildAutoPlanAndRun({
|
|
|
2568
2600
|
? `Plan created with fallback guidance because planning hit an error: ${planningError}`
|
|
2569
2601
|
: 'Plan created and waiting for approval before implementation.';
|
|
2570
2602
|
|
|
2603
|
+
const filePath = await writeMarkdownInProjectDir(
|
|
2604
|
+
'plans',
|
|
2605
|
+
`${goal}-auto`,
|
|
2606
|
+
renderAutoPlanMarkdown({
|
|
2607
|
+
goal,
|
|
2608
|
+
autoPlan,
|
|
2609
|
+
finalSummary,
|
|
2610
|
+
planningError,
|
|
2611
|
+
approvalText: 'Pending user approval before implementation.',
|
|
2612
|
+
progressLine: '- Plan created and waiting for execution.'
|
|
2613
|
+
}),
|
|
2614
|
+
'plan-auto',
|
|
2615
|
+
sessionId
|
|
2616
|
+
);
|
|
2617
|
+
return {
|
|
2618
|
+
filePath,
|
|
2619
|
+
summary: autoPlan.summary,
|
|
2620
|
+
finalSummary,
|
|
2621
|
+
approvalStatus: 'pending',
|
|
2622
|
+
steps: autoPlan.steps,
|
|
2623
|
+
completedCount: 0,
|
|
2624
|
+
warningCount: planningError ? 1 : 0,
|
|
2625
|
+
failedCount: 0,
|
|
2626
|
+
warningTitles: planningError ? ['planner:fallback-plan'] : [],
|
|
2627
|
+
failedTitles: []
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
function renderAutoPlanMarkdown({
|
|
2632
|
+
goal,
|
|
2633
|
+
autoPlan,
|
|
2634
|
+
finalSummary,
|
|
2635
|
+
planningError = '',
|
|
2636
|
+
approvalText = 'Pending user approval before implementation.',
|
|
2637
|
+
progressLine = '- Plan created and waiting for execution.'
|
|
2638
|
+
}) {
|
|
2571
2639
|
const lines = [];
|
|
2572
2640
|
lines.push(`# Auto Plan: ${goal}`);
|
|
2573
2641
|
lines.push('');
|
|
2574
|
-
lines.push(
|
|
2575
|
-
lines.push(autoPlan
|
|
2642
|
+
lines.push('## Summary');
|
|
2643
|
+
lines.push(autoPlan?.summary || `Auto plan for: ${goal}`);
|
|
2576
2644
|
lines.push('');
|
|
2577
2645
|
lines.push('## Final Summary');
|
|
2578
2646
|
lines.push(finalSummary || '(empty)');
|
|
@@ -2582,13 +2650,13 @@ async function buildAutoPlanAndRun({
|
|
|
2582
2650
|
}
|
|
2583
2651
|
lines.push('');
|
|
2584
2652
|
lines.push('## Steps');
|
|
2585
|
-
autoPlan.steps.forEach((s, idx) => {
|
|
2653
|
+
(Array.isArray(autoPlan?.steps) ? autoPlan.steps : []).forEach((s, idx) => {
|
|
2586
2654
|
lines.push(`${idx + 1}. [${s.role}] ${s.title}`);
|
|
2587
2655
|
lines.push(` - task: ${s.task}`);
|
|
2588
2656
|
});
|
|
2589
2657
|
lines.push('');
|
|
2590
2658
|
lines.push('## Approval');
|
|
2591
|
-
lines.push(
|
|
2659
|
+
lines.push(approvalText);
|
|
2592
2660
|
lines.push('');
|
|
2593
2661
|
lines.push('## Working Memory');
|
|
2594
2662
|
lines.push('### Findings Ledger');
|
|
@@ -2598,27 +2666,75 @@ async function buildAutoPlanAndRun({
|
|
|
2598
2666
|
lines.push('');
|
|
2599
2667
|
lines.push('### Progress Ledger');
|
|
2600
2668
|
lines.push(PLAN_MEMORY_MARKERS.progress[0]);
|
|
2601
|
-
lines.push(
|
|
2669
|
+
lines.push(progressLine);
|
|
2602
2670
|
lines.push(PLAN_MEMORY_MARKERS.progress[1]);
|
|
2671
|
+
return lines.join('\n');
|
|
2672
|
+
}
|
|
2603
2673
|
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2674
|
+
async function revisePendingPlanWithModel({
|
|
2675
|
+
planState,
|
|
2676
|
+
feedback,
|
|
2677
|
+
config,
|
|
2678
|
+
model,
|
|
2679
|
+
systemPrompt
|
|
2680
|
+
}) {
|
|
2681
|
+
const goal = String(planState?.goal || '').trim();
|
|
2682
|
+
const priorSummary = String(planState?.summary || '').trim();
|
|
2683
|
+
const priorSteps = Array.isArray(planState?.steps) ? planState.steps : [];
|
|
2684
|
+
if (!goal || !feedback) {
|
|
2685
|
+
throw new Error('Plan revision requires both goal and feedback.');
|
|
2686
|
+
}
|
|
2687
|
+
const prompt = [
|
|
2688
|
+
buildAutoPlanPlannerGuidance(),
|
|
2689
|
+
'You are revising an existing plan based on explicit user feedback.',
|
|
2690
|
+
'Return strict JSON only with shape {"summary":"...","steps":[{"title":"...","role":"planner|coder|reviewer|tester|summarizer","task":"..."}]}. No markdown.',
|
|
2691
|
+
'Keep roles minimal and only include steps that materially help the goal.'
|
|
2692
|
+
].join('\n');
|
|
2693
|
+
const result = await createChatCompletion({
|
|
2694
|
+
sdkProvider: config.sdk?.provider,
|
|
2695
|
+
baseUrl: config.gateway.base_url,
|
|
2696
|
+
apiKey: config.gateway.api_key,
|
|
2697
|
+
model: model || config.model.name,
|
|
2698
|
+
messages: [
|
|
2699
|
+
{ role: 'system', content: `${systemPrompt}\n${prompt}` },
|
|
2700
|
+
{
|
|
2701
|
+
role: 'user',
|
|
2702
|
+
content: [
|
|
2703
|
+
`Goal: ${goal}`,
|
|
2704
|
+
`Current summary: ${priorSummary || '-'}`,
|
|
2705
|
+
'Current plan steps:',
|
|
2706
|
+
...priorSteps.map((step, index) => `${index + 1}. [${step.role}] ${step.title} :: ${step.task}`),
|
|
2707
|
+
'',
|
|
2708
|
+
`User revision feedback: ${feedback}`,
|
|
2709
|
+
'Revise the summary and steps accordingly while keeping them executable.'
|
|
2710
|
+
].join('\n')
|
|
2711
|
+
}
|
|
2712
|
+
],
|
|
2713
|
+
timeoutMs: config.gateway.timeout_ms || 90000,
|
|
2714
|
+
maxRetries: config.gateway.max_retries ?? 2
|
|
2715
|
+
});
|
|
2716
|
+
const parsed = extractJsonBlock(result.text || '');
|
|
2717
|
+
const revised = normalizeAutoPlan(parsed, goal);
|
|
2718
|
+
const revisedFinalSummary = `Plan revised based on feedback: ${feedback}`;
|
|
2719
|
+
const planFilePath = String(planState?.filePath || '').trim();
|
|
2720
|
+
if (planFilePath) {
|
|
2721
|
+
const content = renderAutoPlanMarkdown({
|
|
2722
|
+
goal,
|
|
2723
|
+
autoPlan: revised,
|
|
2724
|
+
finalSummary: revisedFinalSummary,
|
|
2725
|
+
approvalText: 'Pending user approval before implementation (revised).',
|
|
2726
|
+
progressLine: `- Plan revised with user feedback: ${feedback}`
|
|
2727
|
+
});
|
|
2728
|
+
await fs.writeFile(planFilePath, `${content.trim()}\n`, 'utf8');
|
|
2729
|
+
}
|
|
2611
2730
|
return {
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
failedCount: 0,
|
|
2620
|
-
warningTitles: planningError ? ['planner:fallback-plan'] : [],
|
|
2621
|
-
failedTitles: []
|
|
2731
|
+
status: 'pending_approval',
|
|
2732
|
+
source: String(planState?.source || 'auto'),
|
|
2733
|
+
goal,
|
|
2734
|
+
filePath: planFilePath,
|
|
2735
|
+
summary: revised.summary || `Auto plan for: ${goal}`,
|
|
2736
|
+
finalSummary: revisedFinalSummary,
|
|
2737
|
+
steps: revised.steps
|
|
2622
2738
|
};
|
|
2623
2739
|
}
|
|
2624
2740
|
|
|
@@ -2675,6 +2791,17 @@ export async function createChatRuntime({
|
|
|
2675
2791
|
summary: `${initialTodos.length} todo item(s)`
|
|
2676
2792
|
});
|
|
2677
2793
|
}
|
|
2794
|
+
const initialPlanState = normalizePlanState(session?.planState);
|
|
2795
|
+
if (initialPlanState) {
|
|
2796
|
+
startupEvents.push({
|
|
2797
|
+
type: 'tool',
|
|
2798
|
+
id: `startup-plan-${String(session?.id || 'session')}`,
|
|
2799
|
+
name: 'update_plan',
|
|
2800
|
+
status: 'done',
|
|
2801
|
+
arguments: { plan: initialPlanState },
|
|
2802
|
+
summary: `plan status=${initialPlanState.status || 'draft'}`
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2678
2805
|
let currentSession = session;
|
|
2679
2806
|
let config = initialConfig;
|
|
2680
2807
|
const baseSystemPrompt = systemPrompt;
|
|
@@ -2834,7 +2961,7 @@ export async function createChatRuntime({
|
|
|
2834
2961
|
'/checkpoint load <id>'
|
|
2835
2962
|
];
|
|
2836
2963
|
const specTemplates = ['/spec <topic>'];
|
|
2837
|
-
const planTemplates = ['/plan <goal>', '/plan auto <goal>', '/plan
|
|
2964
|
+
const planTemplates = ['/plan <goal>', '/plan auto <goal>', '/plan approve', '/plan from-spec <spec-path?>'];
|
|
2838
2965
|
const agentTemplates = ['/agents list', '/agents run planner <task>', '/agents run coder <task>', '/agents run reviewer <task>', '/agents run tester <task>', '/agents run summarizer <task>'];
|
|
2839
2966
|
const debugTemplates = ['/debug keys on', '/debug keys off', '/debug keys status'];
|
|
2840
2967
|
const compactTemplates = compactOptions.map((opt) => `/compact ${opt}`);
|
|
@@ -3039,12 +3166,6 @@ export async function createChatRuntime({
|
|
|
3039
3166
|
return materializeSuggestions(specTemplates);
|
|
3040
3167
|
}
|
|
3041
3168
|
if (commandPart === 'plan') {
|
|
3042
|
-
if (tokens[1] === 'auto' && (tokens.length === 2 || (tokens.length === 3 && !hasTrailingSpace))) {
|
|
3043
|
-
const sub = tokens[2] || '';
|
|
3044
|
-
return ['run']
|
|
3045
|
-
.filter((s) => s.startsWith(sub))
|
|
3046
|
-
.map((s) => registerSuggestion(`/plan auto ${s} `, planSubcommandDescriptions[`/plan auto ${s} <goal>`] || completionCopy.generic.planCommand));
|
|
3047
|
-
}
|
|
3048
3169
|
if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
|
|
3049
3170
|
const sub = tokens[1] || '';
|
|
3050
3171
|
return ['auto', 'approve', 'from-spec']
|
|
@@ -3207,12 +3328,14 @@ export async function createChatRuntime({
|
|
|
3207
3328
|
activeAbortController = new AbortController();
|
|
3208
3329
|
const { signal } = activeAbortController;
|
|
3209
3330
|
const activeReplySystemPrompt = await buildActiveSystemPrompt();
|
|
3331
|
+
const parsedInput = parseInput(line);
|
|
3210
3332
|
try {
|
|
3211
|
-
|
|
3333
|
+
if (shouldPersistInputHistory(parsedInput)) {
|
|
3334
|
+
await appendInputHistory(line);
|
|
3335
|
+
}
|
|
3212
3336
|
} catch {
|
|
3213
3337
|
// Non-fatal: history persistence should not block chat flow.
|
|
3214
3338
|
}
|
|
3215
|
-
const parsedInput = parseInput(line);
|
|
3216
3339
|
if (parsedInput.type === 'empty') {
|
|
3217
3340
|
return { type: 'noop' };
|
|
3218
3341
|
}
|
|
@@ -3225,7 +3348,7 @@ export async function createChatRuntime({
|
|
|
3225
3348
|
if (parsedInput.command === 'help') {
|
|
3226
3349
|
return {
|
|
3227
3350
|
type: 'system',
|
|
3228
|
-
text: 'Commands: /help /exit /stop /commands /status /mode /compact /checkpoint /spec /plan /agents /config /memory /history /debug /retry /<custom> !<shell>'
|
|
3351
|
+
text: 'Commands: /help /exit /stop /commands /status /mode /compact /checkpoint /spec /plan /yes /edit /reject /agents /config /memory /history /debug /retry /<custom> !<shell>'
|
|
3229
3352
|
};
|
|
3230
3353
|
}
|
|
3231
3354
|
if (parsedInput.command === 'status') {
|
|
@@ -3250,6 +3373,59 @@ export async function createChatRuntime({
|
|
|
3250
3373
|
await persistLocalExchange(line, text);
|
|
3251
3374
|
return { type: 'system', text };
|
|
3252
3375
|
}
|
|
3376
|
+
if (parsedInput.command === 'yes') {
|
|
3377
|
+
if (!hasPendingPlanApproval(currentSession)) {
|
|
3378
|
+
return { type: 'system', text: 'No pending plan approval. Use /plan auto <goal> first.' };
|
|
3379
|
+
}
|
|
3380
|
+
await persistUserExchange(line);
|
|
3381
|
+
const planState = { ...currentSession.planState };
|
|
3382
|
+
const result = await executePlanWithSubAgents({
|
|
3383
|
+
planState,
|
|
3384
|
+
parentSession: currentSession,
|
|
3385
|
+
config,
|
|
3386
|
+
model,
|
|
3387
|
+
systemPrompt: baseSystemPrompt,
|
|
3388
|
+
onAgentEvent,
|
|
3389
|
+
signal,
|
|
3390
|
+
onSubSessionActive: (sub) => { activeSubSession = sub; }
|
|
3391
|
+
});
|
|
3392
|
+
activeSubSession = null;
|
|
3393
|
+
currentSession.planState = null;
|
|
3394
|
+
executionMode = 'auto';
|
|
3395
|
+
await persistAssistantExchange(line, result.text || '', { includeUser: false });
|
|
3396
|
+
return { type: 'assistant', text: result.text, aborted: !!result.aborted };
|
|
3397
|
+
}
|
|
3398
|
+
if (parsedInput.command === 'edit') {
|
|
3399
|
+
if (!hasPendingPlanApproval(currentSession)) {
|
|
3400
|
+
return { type: 'system', text: 'No pending plan approval. Use /plan auto <goal> first.' };
|
|
3401
|
+
}
|
|
3402
|
+
const feedback = parsedInput.args.join(' ').trim();
|
|
3403
|
+
if (!feedback) {
|
|
3404
|
+
return { type: 'system', text: 'Usage: /edit <feedback>' };
|
|
3405
|
+
}
|
|
3406
|
+
const revised = await revisePendingPlanWithModel({
|
|
3407
|
+
planState: currentSession.planState,
|
|
3408
|
+
feedback,
|
|
3409
|
+
config,
|
|
3410
|
+
model,
|
|
3411
|
+
systemPrompt: activeReplySystemPrompt
|
|
3412
|
+
});
|
|
3413
|
+
currentSession.planState = revised;
|
|
3414
|
+
executionMode = 'plan';
|
|
3415
|
+
const text = `Plan revised.\n${buildPendingPlanApprovalMessage(currentSession.planState)}`;
|
|
3416
|
+
await persistLocalExchange(line, text);
|
|
3417
|
+
return { type: 'system', text };
|
|
3418
|
+
}
|
|
3419
|
+
if (parsedInput.command === 'reject') {
|
|
3420
|
+
if (!hasPendingPlanApproval(currentSession)) {
|
|
3421
|
+
return { type: 'system', text: 'No pending plan approval.' };
|
|
3422
|
+
}
|
|
3423
|
+
currentSession.planState = null;
|
|
3424
|
+
executionMode = 'auto';
|
|
3425
|
+
const text = 'Pending plan rejected and cleared.';
|
|
3426
|
+
await persistLocalExchange(line, text);
|
|
3427
|
+
return { type: 'system', text };
|
|
3428
|
+
}
|
|
3253
3429
|
if (parsedInput.command === 'checkpoint') {
|
|
3254
3430
|
const sub = (parsedInput.args[0] || 'list').trim().toLowerCase();
|
|
3255
3431
|
if (sub === 'create') {
|
|
@@ -3329,12 +3505,15 @@ export async function createChatRuntime({
|
|
|
3329
3505
|
if (parsedInput.command === 'plan') {
|
|
3330
3506
|
const sub = (parsedInput.args[0] || '').trim().toLowerCase();
|
|
3331
3507
|
if (sub === 'auto') {
|
|
3332
|
-
const
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3508
|
+
const deprecatedRun = (parsedInput.args[1] || '').trim().toLowerCase() === 'run';
|
|
3509
|
+
if (deprecatedRun) {
|
|
3510
|
+
return {
|
|
3511
|
+
type: 'system',
|
|
3512
|
+
text: 'Usage: /plan auto <goal>\n`/plan auto run` was removed. Review the generated plan first, then use /yes to execute, /edit <feedback> to revise, or /reject to discard.'
|
|
3513
|
+
};
|
|
3337
3514
|
}
|
|
3515
|
+
const goal = parsedInput.args.slice(1).join(' ').trim();
|
|
3516
|
+
if (!goal) return { type: 'system', text: 'Usage: /plan auto <goal>' };
|
|
3338
3517
|
const auto = await buildAutoPlanAndRun({
|
|
3339
3518
|
goal,
|
|
3340
3519
|
session: currentSession,
|
|
@@ -3345,32 +3524,6 @@ export async function createChatRuntime({
|
|
|
3345
3524
|
sessionId: currentSession.id,
|
|
3346
3525
|
taskClass: classifyPlanTaskClass(goal)
|
|
3347
3526
|
});
|
|
3348
|
-
if (runImmediately) {
|
|
3349
|
-
const planState = {
|
|
3350
|
-
status: 'approved',
|
|
3351
|
-
source: 'auto',
|
|
3352
|
-
goal,
|
|
3353
|
-
filePath: auto.filePath,
|
|
3354
|
-
summary: auto.summary || '',
|
|
3355
|
-
finalSummary: auto.finalSummary || auto.summary || '',
|
|
3356
|
-
steps: Array.isArray(auto.steps) ? auto.steps : []
|
|
3357
|
-
};
|
|
3358
|
-
const result = await executePlanWithSubAgents({
|
|
3359
|
-
planState,
|
|
3360
|
-
parentSession: currentSession,
|
|
3361
|
-
config,
|
|
3362
|
-
model,
|
|
3363
|
-
systemPrompt: baseSystemPrompt,
|
|
3364
|
-
onAgentEvent,
|
|
3365
|
-
signal,
|
|
3366
|
-
onSubSessionActive: (sub) => { activeSubSession = sub; }
|
|
3367
|
-
});
|
|
3368
|
-
activeSubSession = null;
|
|
3369
|
-
currentSession.planState = null;
|
|
3370
|
-
executionMode = 'auto';
|
|
3371
|
-
await persistAssistantExchange(line, result.text || '', { includeUser: false });
|
|
3372
|
-
return { type: 'assistant', text: result.text, aborted: !!result.aborted };
|
|
3373
|
-
}
|
|
3374
3527
|
currentSession.planState = {
|
|
3375
3528
|
status: 'pending_approval',
|
|
3376
3529
|
source: 'auto',
|
|
@@ -3390,7 +3543,7 @@ export async function createChatRuntime({
|
|
|
3390
3543
|
}
|
|
3391
3544
|
if (sub === 'approve') {
|
|
3392
3545
|
if (!hasPendingPlanApproval(currentSession)) {
|
|
3393
|
-
return { type: 'system', text: 'No pending plan approval. Use /plan auto <goal>
|
|
3546
|
+
return { type: 'system', text: 'No pending plan approval. Use /plan auto <goal> first.' };
|
|
3394
3547
|
}
|
|
3395
3548
|
await persistUserExchange(line);
|
|
3396
3549
|
const planState = { ...currentSession.planState };
|
|
@@ -3453,7 +3606,7 @@ export async function createChatRuntime({
|
|
|
3453
3606
|
}
|
|
3454
3607
|
|
|
3455
3608
|
const goal = parsedInput.args.join(' ').trim();
|
|
3456
|
-
if (!goal) return { type: 'system', text: 'Usage: /plan <goal> | /plan auto <goal> | /plan
|
|
3609
|
+
if (!goal) return { type: 'system', text: 'Usage: /plan <goal> | /plan auto <goal> | /plan from-spec <spec-path?>' };
|
|
3457
3610
|
const content = buildPlanTemplate(goal);
|
|
3458
3611
|
const filePath = await writeMarkdownInProjectDir(
|
|
3459
3612
|
'plans',
|
|
@@ -3792,6 +3945,13 @@ export async function createChatRuntime({
|
|
|
3792
3945
|
await persistLocalExchange(line, text);
|
|
3793
3946
|
return { type: 'system', text };
|
|
3794
3947
|
}
|
|
3948
|
+
if (isRejectPlanText(parsedInput.text)) {
|
|
3949
|
+
currentSession.planState = null;
|
|
3950
|
+
executionMode = 'auto';
|
|
3951
|
+
const text = 'Pending plan rejected and cleared.';
|
|
3952
|
+
await persistLocalExchange(line, text);
|
|
3953
|
+
return { type: 'system', text };
|
|
3954
|
+
}
|
|
3795
3955
|
return {
|
|
3796
3956
|
type: 'system',
|
|
3797
3957
|
text: buildPendingPlanApprovalMessage(currentSession.planState)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const PLAN_STATUS_SET = new Set(['pending_approval', 'approved', 'completed', 'failed', 'draft']);
|
|
2
|
+
|
|
3
|
+
function normalizePlanStatus(value) {
|
|
4
|
+
const status = String(value || '').trim().toLowerCase();
|
|
5
|
+
if (!status) return '';
|
|
6
|
+
if (PLAN_STATUS_SET.has(status)) return status;
|
|
7
|
+
return status;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizePlanStep(step) {
|
|
11
|
+
const title = String(step?.title || '').trim();
|
|
12
|
+
const role = String(step?.role || '').trim();
|
|
13
|
+
const task = String(step?.task || '').trim();
|
|
14
|
+
if (!title && !role && !task) return null;
|
|
15
|
+
return { title, role, task };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizePlanState(value) {
|
|
19
|
+
if (!value || typeof value !== 'object') return null;
|
|
20
|
+
const out = {
|
|
21
|
+
status: normalizePlanStatus(value.status),
|
|
22
|
+
source: String(value.source || '').trim(),
|
|
23
|
+
goal: String(value.goal || '').trim(),
|
|
24
|
+
filePath: String(value.filePath || '').trim(),
|
|
25
|
+
summary: String(value.summary || '').trim(),
|
|
26
|
+
finalSummary: String(value.finalSummary || '').trim()
|
|
27
|
+
};
|
|
28
|
+
if (Array.isArray(value.steps)) {
|
|
29
|
+
out.steps = value.steps.map(normalizePlanStep).filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { getSessionsDir } from './paths.js';
|
|
4
|
+
import { normalizePlanState } from './plan-state.js';
|
|
4
5
|
import { normalizeTodos } from './todo-state.js';
|
|
5
6
|
|
|
6
7
|
const ALLOWED_ROLES = new Set(['system', 'user', 'assistant', 'tool']);
|
|
@@ -77,25 +78,8 @@ function sanitizeSession(session, fallbackId = '') {
|
|
|
77
78
|
|
|
78
79
|
if (session?.model) out.model = String(session.model);
|
|
79
80
|
if (session?.mode) out.mode = String(session.mode);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
status: String(session.planState.status || '').trim(),
|
|
83
|
-
source: String(session.planState.source || '').trim(),
|
|
84
|
-
goal: String(session.planState.goal || '').trim(),
|
|
85
|
-
filePath: String(session.planState.filePath || '').trim(),
|
|
86
|
-
summary: String(session.planState.summary || '').trim(),
|
|
87
|
-
finalSummary: String(session.planState.finalSummary || '').trim()
|
|
88
|
-
};
|
|
89
|
-
if (Array.isArray(session.planState.steps)) {
|
|
90
|
-
out.planState.steps = session.planState.steps
|
|
91
|
-
.map((step) => ({
|
|
92
|
-
title: String(step?.title || '').trim(),
|
|
93
|
-
role: String(step?.role || '').trim(),
|
|
94
|
-
task: String(step?.task || '').trim()
|
|
95
|
-
}))
|
|
96
|
-
.filter((step) => step.title || step.role || step.task);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
81
|
+
const normalizedPlan = normalizePlanState(session?.planState);
|
|
82
|
+
if (normalizedPlan) out.planState = normalizedPlan;
|
|
99
83
|
|
|
100
84
|
const todos = normalizeTodos(session?.todos);
|
|
101
85
|
if (todos.length > 0) out.todos = todos;
|
|
@@ -148,6 +148,7 @@ ALWAYS prefer dedicated tools over raw shell commands:
|
|
|
148
148
|
- Use edit to modify existing files — this is the DEFAULT path for code changes. Demo-style aliases like {file_path:"src/app.ts", old_string:"foo", new_string:"bar"} are accepted
|
|
149
149
|
- Use write only for creating new files or complete rewrites (set full_file_rewrite=true for existing code files). Aliases like {file:"notes.txt", text:"..."} are accepted
|
|
150
150
|
- Use update_todos to manage the session todo checklist for complex work. Provide the full current list each time and usually keep exactly one item in_progress
|
|
151
|
+
- Use read_plan and update_plan to recover or sync structured plan state when plan progress was interrupted (for example by transient gateway/model errors)
|
|
151
152
|
- Use run for shell commands. For long-running processes (dev servers, watchers), set run_in_background=true when you know you do not need the final result immediately. Long-running commands may also be backgrounded automatically
|
|
152
153
|
|
|
153
154
|
Use update_todos with these rules:
|