codemini-cli 0.3.6 → 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.
@@ -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 /plan approve)` : baseStatusTitle;
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 /plan approve to start implementation, /plan auto run <goal> to plan and run in one step next time, or /plan stay to keep planning.');
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 approve to start implementation, or /plan stay to keep refining the plan first.'
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(`## Summary`);
2575
- lines.push(autoPlan.summary || `Auto plan for: ${goal}`);
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('Pending user approval before implementation.');
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('- Plan created and waiting for execution.');
2669
+ lines.push(progressLine);
2602
2670
  lines.push(PLAN_MEMORY_MARKERS.progress[1]);
2671
+ return lines.join('\n');
2672
+ }
2603
2673
 
2604
- const filePath = await writeMarkdownInProjectDir(
2605
- 'plans',
2606
- `${goal}-auto`,
2607
- lines.join('\n'),
2608
- 'plan-auto',
2609
- sessionId
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
- filePath,
2613
- summary: autoPlan.summary,
2614
- finalSummary,
2615
- approvalStatus: 'pending',
2616
- steps: autoPlan.steps,
2617
- completedCount: 0,
2618
- warningCount: planningError ? 1 : 0,
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 auto run <goal>', '/plan approve', '/plan from-spec <spec-path?>'];
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
- await appendInputHistory(line);
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 runImmediately = (parsedInput.args[1] || '').trim().toLowerCase() === 'run';
3333
- const goal = parsedInput.args.slice(runImmediately ? 2 : 1).join(' ').trim();
3334
- if (!goal) return { type: 'system', text: 'Usage: /plan auto <goal> | /plan auto run <goal>' };
3335
- if (runImmediately) {
3336
- await persistUserExchange(line);
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> or /plan <goal> first.' };
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 auto run <goal> | /plan from-spec <spec-path?>' };
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
- if (session?.planState && typeof session.planState === 'object') {
81
- out.planState = {
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: