codemini-cli 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { parseInput } from './input-parser.js';
2
2
  import { loadCommandsAndSkills, renderCommandPrompt } from './command-loader.js';
3
- import { runAgentLoop } from './agent-loop.js';
3
+ import { runAgentLoop, setResultDir, clearResultStore } from './agent-loop.js';
4
4
  import fs from 'node:fs/promises';
5
5
  import path from 'node:path';
6
6
  import {
@@ -28,7 +28,7 @@ import {
28
28
  } from './context-compact.js';
29
29
  import { buildSystemPromptWithReplyLanguage } from './reply-language.js';
30
30
  import { buildSystemPromptWithSoul } from './soul.js';
31
- import { getProjectPlansDir, getProjectSpecsDir, getProjectWorkspaceDir } from './paths.js';
31
+ import { getProjectPlansDir, getProjectSpecsDir, getProjectWorkspaceDir, getSessionsDir } from './paths.js';
32
32
  import { buildProjectContextSnippet, initializeProjectIndex } from './project-index.js';
33
33
 
34
34
  function toOpenAIMessages(sessionMessages) {
@@ -434,11 +434,15 @@ function buildGoalRequirementPacket(goal, role) {
434
434
 
435
435
  function buildAutoPlanPlannerGuidance() {
436
436
  return [
437
+ 'Design a short implementation plan for a small model.',
437
438
  'Auto-plan planning rules:',
439
+ '- Start with a discovery or clarification step when the current implementation is not yet verified.',
438
440
  '- If the goal still leaves room for multiple approaches, choose one practical direction before planning execution.',
439
441
  '- Prefer the smallest local approach that satisfies the goal.',
440
442
  '- Do not output multiple alternative branches in the final plan.',
441
- '- Turn the chosen direction into concrete execution steps for coder, reviewer, and tester.',
443
+ '- Do not assume implementation should begin before the plan is coherent.',
444
+ '- Turn the chosen direction into concrete execution steps for planner, coder, reviewer, and tester.',
445
+ '- Prefer 3-5 steps total unless the task is clearly larger.',
442
446
  '- Keep the plan ordered, implementation-oriented, and easy for small sub-agents to follow.'
443
447
  ].join('\n');
444
448
  }
@@ -594,8 +598,79 @@ function selectAutoSkillNames(text = '') {
594
598
  return selected;
595
599
  }
596
600
 
601
+ function shouldAutoPlan(text = '') {
602
+ const input = String(text || '').trim();
603
+ if (!input) return false;
604
+
605
+ const lower = input.toLowerCase();
606
+ const explicitPlanning =
607
+ /(\/plan\b|plan first|make a plan|implementation plan|先做计划|先出方案|先规划|先计划)/i.test(lower);
608
+ if (explicitPlanning) return false;
609
+
610
+ const simpleSkip =
611
+ /(typo|readme|console\.log|log this|rename\s+\w+|one line|small tweak|tiny fix|格式化|拼写|注释|文案|小改|微调)/i.test(
612
+ lower
613
+ );
614
+ if (simpleSkip) return false;
615
+
616
+ const discussionFirst =
617
+ /(brainstorm|头脑风暴|方案|思路|怎么做|如何做|which (?:approach|option|way)|best way|trade-?off|not sure|unsure|unclear|whether it should|要不要|不确定|先别写|先不要写|先讨论|先想一下)/i.test(
618
+ lower
619
+ );
620
+ if (discussionFirst) return false;
621
+
622
+ const implementationRequest =
623
+ /\b(add|build|create|implement|support|introduce|design|refactor|rework|migrate|change|update|rewrite|restructure)\b/i.test(
624
+ lower
625
+ ) ||
626
+ /(新增|增加|实现|支持|设计|重构|改造|迁移|调整|重写|重做)/i.test(lower);
627
+ if (!implementationRequest) return false;
628
+
629
+ const nonTrivialSignals =
630
+ /\b(auth|authentication|workflow|flow|system|architecture|api|endpoint|state management|cache|caching|database|migration|service|shared helper|helper module|refactor|multi[- ]file|across files|with tests?|and tests?|with validation|error handling)\b/i.test(
631
+ lower
632
+ ) ||
633
+ /(架构|流程|系统|接口|缓存|数据库|迁移|服务|共享|模块|跨文件|测试|校验|错误处理)/i.test(lower);
634
+
635
+ const multipleActions = /\b(and|plus|also|while|along with)\b/i.test(lower) || /[,、;;].+/.test(input);
636
+ const singleFileScoped =
637
+ /\b(?:in|inside|within|only in)\s+[-_/.\w]+\.(?:[cm]?[jt]sx?|py|go|rb|java|rs|php|md)\b/i.test(lower) ||
638
+ /\b(?:src|app|lib|tests?)\/[-_/.\w]+\.(?:[cm]?[jt]sx?|py|go|rb|java|rs|php|md)\b/i.test(lower);
639
+
640
+ if (singleFileScoped && !multipleActions) return false;
641
+ if (singleFileScoped && !nonTrivialSignals) return false;
642
+
643
+ return nonTrivialSignals || (multipleActions && !singleFileScoped);
644
+ }
645
+
646
+ function classifyAutoRoute(text = '') {
647
+ const selectedSkills = selectAutoSkillNames(text);
648
+ const hasBrainstorm = selectedSkills.includes('brainstorm');
649
+ if (hasBrainstorm) {
650
+ return {
651
+ mode: 'brainstorm',
652
+ autoPlan: false,
653
+ selectedSkills
654
+ };
655
+ }
656
+
657
+ if (shouldAutoPlan(text)) {
658
+ return {
659
+ mode: 'auto_plan',
660
+ autoPlan: true,
661
+ selectedSkills: ['superpowers-lite']
662
+ };
663
+ }
664
+
665
+ return {
666
+ mode: 'direct',
667
+ autoPlan: false,
668
+ selectedSkills
669
+ };
670
+ }
671
+
597
672
  function buildAutoSkillSystemPrompt(baseSystemPrompt, commands, config, text) {
598
- const selected = selectAutoSkillNames(text).filter((name) => isSkillEnabled(config, name));
673
+ const selected = classifyAutoRoute(text).selectedSkills.filter((name) => isSkillEnabled(config, name));
599
674
  if (selected.length === 0) return baseSystemPrompt;
600
675
 
601
676
  const blocks = [];
@@ -662,6 +737,74 @@ function normalizeAutoPlan(parsed, goal) {
662
737
  return enforceAutoPlanGuardrailSteps(basePlan, goal);
663
738
  }
664
739
 
740
+ function summarizeGoalForStepTitle(goal, fallback = 'requested change') {
741
+ const text = String(goal || '')
742
+ .replace(/\s+/g, ' ')
743
+ .trim();
744
+ if (!text) return fallback;
745
+ const compact = text.length > 72 ? `${text.slice(0, 69).trimEnd()}...` : text;
746
+ return compact;
747
+ }
748
+
749
+ function buildFallbackAutoPlan(goal) {
750
+ const requirements = deriveGoalRequirements(goal);
751
+ const lightweightGoal = isLightweightAutoPlanGoal(goal, requirements);
752
+ const focus = summarizeGoalForStepTitle(goal);
753
+ const summary =
754
+ requirements.length > 0
755
+ ? `Auto fallback plan for: ${requirements.join('; ')}`
756
+ : `Auto fallback plan for: ${goal}`;
757
+
758
+ if (lightweightGoal) {
759
+ return {
760
+ summary,
761
+ steps: [
762
+ {
763
+ title: `Implement ${focus}`,
764
+ role: 'coder',
765
+ task: `Implement the requested change for: ${goal}. Follow the acceptance checklist and keep the change narrowly scoped.`
766
+ },
767
+ {
768
+ title: 'Verify the change',
769
+ role: 'tester',
770
+ task: `Verify the completed change for: ${goal}. Run the most relevant focused checks available and report concrete evidence plus anything still unverified.`
771
+ }
772
+ ]
773
+ };
774
+ }
775
+
776
+ return {
777
+ summary,
778
+ steps: [
779
+ {
780
+ title: 'Inspect the target area',
781
+ role: 'planner',
782
+ task: `Inspect the existing code paths, affected files, and current behavior for: ${goal}. Identify constraints, dependencies, and any compatibility risks before implementation.`
783
+ },
784
+ {
785
+ title: `Implement ${focus}`,
786
+ role: 'coder',
787
+ task: `Implement the requested changes for: ${goal}. Keep the behavior aligned with the acceptance checklist and preserve existing external behavior unless the goal explicitly changes it.`
788
+ },
789
+ {
790
+ title: 'Update or add focused verification',
791
+ role: 'coder',
792
+ task: `Add or update the most relevant tests and focused verification coverage for: ${goal}. Prefer narrow checks tied to the changed files and flows.`
793
+ },
794
+ {
795
+ title: 'Review for regressions and gaps',
796
+ role: 'reviewer',
797
+ task: `Review the completed work for: ${goal}. Start with the changed files, then check regressions, risky assumptions, backward compatibility, and missing edge cases.`
798
+ },
799
+ {
800
+ title: 'Verify the changed flows',
801
+ role: 'tester',
802
+ task: `Verify the completed work for: ${goal}. Run the most relevant checks available, report concrete evidence, and call out anything still not verified.`
803
+ }
804
+ ]
805
+ };
806
+ }
807
+
665
808
  function enforceAutoPlanGuardrailSteps(plan, goal) {
666
809
  const source = Array.isArray(plan?.steps) ? plan.steps : [];
667
810
  const requirements = deriveGoalRequirements(goal);
@@ -770,6 +913,7 @@ function buildAutoPlanSystemSummary(auto) {
770
913
  `File: ${auto.filePath}`,
771
914
  `Plan Summary: ${auto.summary || '-'}`,
772
915
  `Final Summary: ${auto.finalSummary || auto.summary || '-'}`,
916
+ `Approval: ${auto.approvalStatus || 'not_required'}`,
773
917
  `Steps: ${auto.steps.length} total`,
774
918
  `Completed: ${auto.completedCount}`,
775
919
  `Warnings: ${auto.warningCount}`,
@@ -781,6 +925,9 @@ function buildAutoPlanSystemSummary(auto) {
781
925
  if (auto.failedTitles?.length) {
782
926
  lines.push(`Failed steps: ${auto.failedTitles.slice(0, 5).join(', ')}`);
783
927
  }
928
+ if (auto.approvalStatus === 'pending') {
929
+ lines.push('Next: review the plan summary, then use /plan approve to start implementation or /plan stay to keep planning.');
930
+ }
784
931
  return lines.join('\n');
785
932
  }
786
933
 
@@ -1142,6 +1289,11 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
1142
1289
  value: contextUsagePct,
1143
1290
  enumerable: false,
1144
1291
  writable: false
1292
+ },
1293
+ pendingPlanApproval: {
1294
+ value: currentSession?.planState?.status === 'pending_approval',
1295
+ enumerable: false,
1296
+ writable: false
1145
1297
  }
1146
1298
  });
1147
1299
  return snapshot;
@@ -1164,6 +1316,61 @@ function stampedMessage(role, content, extra = {}) {
1164
1316
  };
1165
1317
  }
1166
1318
 
1319
+ function hasPendingPlanApproval(session) {
1320
+ return session?.planState?.status === 'pending_approval';
1321
+ }
1322
+
1323
+ function isApprovalText(text = '') {
1324
+ const value = String(text || '').trim().toLowerCase();
1325
+ if (!value) return false;
1326
+ return /^(yes|y|ok|okay|approve|approved|continue|proceed|go ahead|start|开始|继续|可以|同意|批准|通过|按这个做)$/.test(value);
1327
+ }
1328
+
1329
+ function isStayInPlanText(text = '') {
1330
+ const value = String(text || '').trim().toLowerCase();
1331
+ if (!value) return false;
1332
+ return /^(stay|keep planning|keep in plan mode|not yet|wait|先别|先等等|继续计划|继续讨论|继续规划|暂不批准)$/.test(value);
1333
+ }
1334
+
1335
+ function buildPendingPlanApprovalMessage(planState) {
1336
+ const lines = [
1337
+ 'Plan approval is still pending.',
1338
+ `Goal: ${planState?.goal || '-'}`,
1339
+ `Plan file: ${planState?.filePath || '-'}`,
1340
+ `Summary: ${planState?.finalSummary || planState?.summary || '-'}`,
1341
+ 'Use /plan approve to start implementation, or /plan stay to keep refining the plan first.'
1342
+ ];
1343
+ return lines.join('\n');
1344
+ }
1345
+
1346
+ function buildApprovedPlanExecutionPrompt(planState, approvalText = '') {
1347
+ const lines = [
1348
+ 'Approved implementation plan:',
1349
+ `Original goal: ${planState?.goal || '-'}`,
1350
+ `Plan file: ${planState?.filePath || '-'}`,
1351
+ `Plan summary: ${planState?.summary || '-'}`,
1352
+ `Final planning summary: ${planState?.finalSummary || planState?.summary || '-'}`,
1353
+ `User approval: ${String(approvalText || '').trim() || 'approved'}`,
1354
+ Array.isArray(planState?.steps) && planState.steps.length > 0 ? 'Planned steps:' : '',
1355
+ ...(Array.isArray(planState?.steps)
1356
+ ? planState.steps.slice(0, 8).map((step, index) => `${index + 1}. [${step.role}] ${step.title} :: ${step.task}`)
1357
+ : []),
1358
+ 'Proceed with implementation now.',
1359
+ 'Follow the approved direction unless a blocking contradiction appears.',
1360
+ 'Output rules for this implementation phase:',
1361
+ '- Be concise and practical.',
1362
+ '- Do not celebrate, praise, or use emojis.',
1363
+ '- Do not restate the full plan back to the user.',
1364
+ '- If the work is already done, say so briefly and cite the verification evidence.',
1365
+ '- After implementation or verification, prefer a short result summary in 3-6 lines.',
1366
+ '- If the work is complete, use this exact structure:',
1367
+ 'Status: <done|partial|blocked>',
1368
+ 'Verified: <tests, checks, or evidence>',
1369
+ 'Next: <none or the single next action>'
1370
+ ];
1371
+ return lines.join('\n');
1372
+ }
1373
+
1167
1374
  async function resolveSpecPath(rawArg = '', sessionId = '') {
1168
1375
  const input = String(rawArg || '').trim();
1169
1376
  const roots = [
@@ -1314,7 +1521,7 @@ async function askModel({
1314
1521
  ? `${systemPrompt}\n\n${projectContextSnippet}\n\nUse this project context as lightweight guidance. Prefer tools for fresh verification before assuming details.`
1315
1522
  : systemPrompt;
1316
1523
 
1317
- const { definitions, handlers } = getBuiltinTools({
1524
+ const { definitions, handlers, formatters, deferredDefinitions } = getBuiltinTools({
1318
1525
  workspaceRoot: process.cwd(),
1319
1526
  config,
1320
1527
  sessionId: session.id,
@@ -1376,6 +1583,8 @@ async function askModel({
1376
1583
  alwaysAllowTools:
1377
1584
  alwaysAllowTools || config.execution?.always_allow_tools || ['run', 'read', 'write'],
1378
1585
  toolResultMaxChars: config.context?.tool_result_max_chars || 12000,
1586
+ toolFormatters: formatters,
1587
+ deferredDefinitions,
1379
1588
  requestCompletion: async ({ messages, tools, model: selectedModel }) => {
1380
1589
  if (onAgentEvent) onAgentEvent({ type: 'assistant:start' });
1381
1590
  return createChatCompletionStream({
@@ -1491,7 +1700,6 @@ async function runSubAgentTask({
1491
1700
 
1492
1701
  async function buildAutoPlanAndRun({
1493
1702
  goal,
1494
- session,
1495
1703
  config,
1496
1704
  model,
1497
1705
  systemPrompt,
@@ -1525,8 +1733,12 @@ async function buildAutoPlanAndRun({
1525
1733
  role: 'user',
1526
1734
  content: [
1527
1735
  'Create an execution plan and assign best sub-agent role for each step.',
1736
+ 'Return strict JSON only with shape {"summary":"...","steps":[{"title":"...","role":"planner|coder|reviewer|tester","task":"..."}]}. No markdown.',
1737
+ 'Always include final reviewer and tester steps unless the task is explicitly tiny.',
1528
1738
  requirementPacket,
1529
- 'The final steps must include review and testing/verification unless the goal is a tiny single-change task, in which case you may keep only one implementation step plus one testing/verification step.'
1739
+ 'The first step should usually inspect or clarify the target area before implementation.',
1740
+ 'The final steps must include review and testing/verification unless the goal is a tiny single-change task, in which case you may keep only one implementation step plus one testing/verification step.',
1741
+ 'Prefer 3-5 steps total.'
1530
1742
  ]
1531
1743
  .filter(Boolean)
1532
1744
  .join('\n')
@@ -1539,89 +1751,22 @@ async function buildAutoPlanAndRun({
1539
1751
  autoPlan = normalizeAutoPlan(parsed, goal);
1540
1752
  } catch (err) {
1541
1753
  planningError = String(err?.message || err || 'planning failed');
1754
+ autoPlan = buildFallbackAutoPlan(goal);
1542
1755
  }
1543
1756
 
1544
- const runItems = [];
1545
- const totalPlanSteps = autoPlan.steps.length + 1;
1546
1757
  for (let i = 0; i < autoPlan.steps.length; i += 1) {
1547
1758
  const step = autoPlan.steps[i];
1548
1759
  if (onAgentEvent) {
1549
1760
  onAgentEvent({
1550
1761
  type: 'assistant:delta',
1551
- text: `\n[plan] Step ${i + 1}/${totalPlanSteps} -> ${step.role}: ${step.title}\n`
1552
- });
1553
- }
1554
- try {
1555
- const stepResult = await runSubAgentTask({
1556
- role: step.role,
1557
- task: step.task,
1558
- goal,
1559
- priorSteps: runItems,
1560
- parentSession: session,
1561
- config,
1562
- model,
1563
- systemPrompt,
1564
- onAgentEvent,
1565
- extraRolePrompt: buildAutoPlanExecutionGuidance(step.role)
1566
- });
1567
- const outputLooksSuccessful = looksLikeSuccessfulStepOutput(stepResult.text);
1568
- const outputHasFailureSignals = stepOutputHasFailureSignals(step.role, stepResult.text);
1569
- const warningParts = [];
1570
- if (stepResult.blockedCount > 0) warningParts.push(`${stepResult.blockedCount} blocked tool call(s)`);
1571
- if (stepResult.toolErrorCount > 0) warningParts.push(`${stepResult.toolErrorCount} tool error(s)`);
1572
- const warning = warningParts.length > 0 ? `sub-agent recovered after ${warningParts.join(', ')}` : '';
1573
- const failed =
1574
- stepResult.hasErrorLine ||
1575
- outputHasFailureSignals ||
1576
- (!outputLooksSuccessful && (stepResult.blockedCount > 0 || stepResult.toolErrorCount > 0));
1577
- let error = '';
1578
- if (stepResult.hasErrorLine) {
1579
- error = 'sub-agent output contains error line(s)';
1580
- } else if (outputHasFailureSignals) {
1581
- error = 'sub-agent output reports unmet requirements or failed verification';
1582
- } else if (failed && stepResult.blockedCount > 0) {
1583
- error = `sub-agent ended with ${stepResult.blockedCount} blocked tool call(s)`;
1584
- } else if (failed && stepResult.toolErrorCount > 0) {
1585
- error = `sub-agent ended with ${stepResult.toolErrorCount} tool error(s)`;
1586
- }
1587
- runItems.push({
1588
- ...step,
1589
- output: stepResult.text,
1590
- error,
1591
- warning,
1592
- failed,
1593
- artifactPaths: stepResult.artifactPaths || []
1594
- });
1595
- } catch (err) {
1596
- runItems.push({
1597
- ...step,
1598
- output: '',
1599
- error: String(err?.message || err || 'sub-agent step failed'),
1600
- warning: '',
1601
- failed: true
1762
+ text: `\n[plan] Step ${i + 1}/${autoPlan.steps.length} -> ${step.role}: ${step.title}\n`
1602
1763
  });
1603
1764
  }
1604
1765
  }
1605
1766
 
1606
- const failedItems = runItems.filter((s) => s.failed || s.error);
1607
- const warningItems = runItems.filter((s) => !s.failed && s.warning);
1608
- const completedItems = runItems.filter((s) => !s.failed);
1609
-
1610
- if (onAgentEvent) {
1611
- onAgentEvent({
1612
- type: 'assistant:delta',
1613
- text: `\n[plan] Step ${totalPlanSteps}/${totalPlanSteps} -> summarizer: Final summary\n`
1614
- });
1615
- }
1616
- const finalSummary = await buildAutoPlanFinalSummary({
1617
- goal,
1618
- autoPlan,
1619
- runItems,
1620
- planningError,
1621
- config,
1622
- model,
1623
- systemPrompt
1624
- });
1767
+ const finalSummary = planningError
1768
+ ? `Plan created with fallback guidance because planning hit an error: ${planningError}`
1769
+ : 'Plan created and waiting for approval before implementation.';
1625
1770
 
1626
1771
  const lines = [];
1627
1772
  lines.push(`# Auto Plan: ${goal}`);
@@ -1642,25 +1787,8 @@ async function buildAutoPlanAndRun({
1642
1787
  lines.push(` - task: ${s.task}`);
1643
1788
  });
1644
1789
  lines.push('');
1645
- lines.push('## Sub-Agent Outputs');
1646
- runItems.forEach((s, idx) => {
1647
- lines.push(`### ${idx + 1}. [${s.role}] ${s.title}`);
1648
- if (s.error) {
1649
- lines.push(`Error: ${s.error}`);
1650
- if (s.output) {
1651
- lines.push('');
1652
- lines.push(s.output);
1653
- }
1654
- lines.push('');
1655
- return;
1656
- }
1657
- if (s.warning) {
1658
- lines.push(`Note: ${s.warning}`);
1659
- lines.push('');
1660
- }
1661
- lines.push(s.output || '(empty)');
1662
- lines.push('');
1663
- });
1790
+ lines.push('## Approval');
1791
+ lines.push('Pending user approval before implementation.');
1664
1792
 
1665
1793
  const filePath = await writeMarkdownInProjectDir(
1666
1794
  'plans',
@@ -1673,12 +1801,13 @@ async function buildAutoPlanAndRun({
1673
1801
  filePath,
1674
1802
  summary: autoPlan.summary,
1675
1803
  finalSummary,
1804
+ approvalStatus: 'pending',
1676
1805
  steps: autoPlan.steps,
1677
- completedCount: completedItems.length,
1678
- warningCount: warningItems.length,
1679
- failedCount: failedItems.length,
1680
- warningTitles: warningItems.map((s) => `${s.role}:${s.title}`),
1681
- failedTitles: failedItems.map((s) => `${s.role}:${s.title}`)
1806
+ completedCount: 0,
1807
+ warningCount: planningError ? 1 : 0,
1808
+ failedCount: 0,
1809
+ warningTitles: planningError ? ['planner:fallback-plan'] : [],
1810
+ failedTitles: []
1682
1811
  };
1683
1812
  }
1684
1813
 
@@ -1726,7 +1855,14 @@ export async function createChatRuntime({
1726
1855
  let config = initialConfig;
1727
1856
  const baseSystemPrompt = systemPrompt;
1728
1857
  let executionMode = config.execution?.mode || 'auto';
1858
+ if (hasPendingPlanApproval(currentSession)) {
1859
+ executionMode = 'plan';
1860
+ }
1729
1861
  const commands = await loadCommandsAndSkills();
1862
+
1863
+ // Set up tool result store under session directory
1864
+ const sessionResultsDir = path.join(getSessionsDir(), String(currentSession.id));
1865
+ setResultDir(sessionResultsDir);
1730
1866
  const compactState = {
1731
1867
  backupMessages: null,
1732
1868
  autoEnabled: true,
@@ -2366,6 +2502,16 @@ export async function createChatRuntime({
2366
2502
  onAgentEvent,
2367
2503
  sessionId: currentSession.id
2368
2504
  });
2505
+ currentSession.planState = {
2506
+ status: 'pending_approval',
2507
+ source: 'auto',
2508
+ goal,
2509
+ filePath: auto.filePath,
2510
+ summary: auto.summary || '',
2511
+ finalSummary: auto.finalSummary || auto.summary || '',
2512
+ steps: Array.isArray(auto.steps) ? auto.steps : []
2513
+ };
2514
+ executionMode = 'plan';
2369
2515
  const text = buildAutoPlanSystemSummary(auto);
2370
2516
  await persistLocalExchange(line, text);
2371
2517
  return {
@@ -2373,6 +2519,33 @@ export async function createChatRuntime({
2373
2519
  text
2374
2520
  };
2375
2521
  }
2522
+ if (sub === 'approve') {
2523
+ if (!hasPendingPlanApproval(currentSession)) {
2524
+ return { type: 'system', text: 'No pending plan approval. Use /plan auto <goal> or /plan <goal> first.' };
2525
+ }
2526
+ const planState = { ...currentSession.planState };
2527
+ const result = await askModel({
2528
+ text: buildApprovedPlanExecutionPrompt(planState, '/plan approve'),
2529
+ session: currentSession,
2530
+ config,
2531
+ model,
2532
+ systemPrompt: activeReplySystemPrompt,
2533
+ onAgentEvent,
2534
+ executionMode: 'auto'
2535
+ });
2536
+ currentSession.planState = null;
2537
+ executionMode = 'auto';
2538
+ await saveSession(currentSession);
2539
+ return { type: 'assistant', text: result.text };
2540
+ }
2541
+ if (sub === 'stay') {
2542
+ if (!hasPendingPlanApproval(currentSession)) {
2543
+ return { type: 'system', text: 'No pending plan approval.' };
2544
+ }
2545
+ const text = buildPendingPlanApprovalMessage(currentSession.planState);
2546
+ await persistLocalExchange(line, text);
2547
+ return { type: 'system', text };
2548
+ }
2376
2549
  if (sub === 'from-spec') {
2377
2550
  const specArg = parsedInput.args.slice(1).join(' ').trim();
2378
2551
  const specPath = await resolveSpecPath(specArg, currentSession.id);
@@ -2492,6 +2665,10 @@ export async function createChatRuntime({
2492
2665
  if (!targetId) return { type: 'system', text: 'Usage: /history resume <session_id>' };
2493
2666
  const loaded = await loadSession(targetId);
2494
2667
  currentSession = loaded;
2668
+ setResultDir(path.join(getSessionsDir(), String(targetId)));
2669
+ if (hasPendingPlanApproval(currentSession)) {
2670
+ executionMode = 'plan';
2671
+ }
2495
2672
  if (!historyIdCache.includes(targetId)) historyIdCache.unshift(targetId);
2496
2673
  historySessionCache = [
2497
2674
  { id: targetId, messageCount: Array.isArray(loaded.messages) ? loaded.messages.length : 0 },
@@ -2631,6 +2808,7 @@ export async function createChatRuntime({
2631
2808
  renderCommandPrompt(custom, []),
2632
2809
  'Explicit brainstorm mode:',
2633
2810
  '- Ask exactly one clarifying question first if any important uncertainty remains.',
2811
+ '- Stop after the question and wait for the user\'s answer before continuing.',
2634
2812
  '- Do not inspect the repo or generate code unless the user explicitly asks for that.',
2635
2813
  '- If you recommend an option, present it as a suggested decision rather than a final choice for the user.',
2636
2814
  parsedInput.args.length > 0 ? `Current question:\n${parsedInput.args.join(' ')}` : ''
@@ -2669,6 +2847,34 @@ export async function createChatRuntime({
2669
2847
  return { type: 'assistant', text: result.text };
2670
2848
  }
2671
2849
 
2850
+ if (hasPendingPlanApproval(currentSession)) {
2851
+ if (isApprovalText(parsedInput.text)) {
2852
+ const planState = { ...currentSession.planState };
2853
+ const result = await askModel({
2854
+ text: buildApprovedPlanExecutionPrompt(planState, parsedInput.text),
2855
+ session: currentSession,
2856
+ config,
2857
+ model,
2858
+ systemPrompt: activeReplySystemPrompt,
2859
+ onAgentEvent,
2860
+ executionMode: 'auto'
2861
+ });
2862
+ currentSession.planState = null;
2863
+ executionMode = 'auto';
2864
+ await saveSession(currentSession);
2865
+ return { type: 'assistant', text: result.text };
2866
+ }
2867
+ if (isStayInPlanText(parsedInput.text)) {
2868
+ const text = buildPendingPlanApprovalMessage(currentSession.planState);
2869
+ await persistLocalExchange(line, text);
2870
+ return { type: 'system', text };
2871
+ }
2872
+ return {
2873
+ type: 'system',
2874
+ text: buildPendingPlanApprovalMessage(currentSession.planState)
2875
+ };
2876
+ }
2877
+
2672
2878
  if (compactState.autoEnabled) {
2673
2879
  const currentTokens = estimateMessagesTokens(currentSession.messages);
2674
2880
  const maxTokens = effectiveMaxContextTokens(config);
@@ -2696,7 +2902,33 @@ export async function createChatRuntime({
2696
2902
  }
2697
2903
 
2698
2904
  const expandedText = await expandFileMentions(parsedInput.text, process.cwd());
2699
- const selectedAutoSkills = selectAutoSkillNames(expandedText).filter((name) => isSkillEnabled(config, name));
2905
+ const autoRoute = classifyAutoRoute(expandedText);
2906
+ if (autoRoute.autoPlan) {
2907
+ const auto = await buildAutoPlanAndRun({
2908
+ goal: expandedText,
2909
+ session: currentSession,
2910
+ config,
2911
+ model,
2912
+ systemPrompt: activeBaseSystemPrompt,
2913
+ onAgentEvent,
2914
+ sessionId: currentSession.id
2915
+ });
2916
+ currentSession.planState = {
2917
+ status: 'pending_approval',
2918
+ source: 'auto',
2919
+ goal: expandedText,
2920
+ filePath: auto.filePath,
2921
+ summary: auto.summary || '',
2922
+ finalSummary: auto.finalSummary || auto.summary || '',
2923
+ steps: Array.isArray(auto.steps) ? auto.steps : []
2924
+ };
2925
+ executionMode = 'plan';
2926
+ const text = buildAutoPlanSystemSummary(auto);
2927
+ await persistLocalExchange(line, text);
2928
+ return { type: 'system', text };
2929
+ }
2930
+
2931
+ const selectedAutoSkills = autoRoute.selectedSkills.filter((name) => isSkillEnabled(config, name));
2700
2932
  if (selectedAutoSkills.length > 0 && onAgentEvent) {
2701
2933
  onAgentEvent({
2702
2934
  type: 'skill:auto',
@@ -1,3 +1,5 @@
1
+ import { summarizeToolResult, trimInline } from './agent-loop.js';
2
+
1
3
  function textFromContent(content) {
2
4
  if (typeof content === 'string') return content;
3
5
  if (Array.isArray(content)) {
@@ -30,11 +32,39 @@ function modeToKeepRecent(mode) {
30
32
 
31
33
  function buildLocalSummary(messages) {
32
34
  const lines = [];
33
- const limit = 12;
35
+ const limit = 16;
34
36
  for (const msg of messages.slice(-limit)) {
37
+ if (msg.role === 'tool') {
38
+ // Try to parse tool result as JSON for semantic summary
39
+ const text = textFromContent(msg.content);
40
+ let parsed;
41
+ try { parsed = JSON.parse(text); } catch { parsed = null; }
42
+ if (parsed && typeof parsed === 'object') {
43
+ const summary = summarizeToolResult(parsed);
44
+ lines.push(`- tool_result: ${summary}`);
45
+ } else {
46
+ const clipped = text.length > 120 ? `${text.slice(0, 117)}...` : text;
47
+ lines.push(`- tool_result: ${clipped}`);
48
+ }
49
+ continue;
50
+ }
51
+ if (msg.role === 'assistant') {
52
+ const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
53
+ const toolCallCount = Array.isArray(msg.tool_calls) ? msg.tool_calls.length : 0;
54
+ const toolInfo = toolCallCount > 0 ? ` [called ${toolCallCount} tool(s)]` : '';
55
+ const clipped = text.length > 300 ? `${text.slice(0, 297)}...` : text;
56
+ lines.push(`- assistant: ${clipped}${toolInfo}`);
57
+ continue;
58
+ }
59
+ if (msg.role === 'user') {
60
+ const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
61
+ const clipped = text.length > 200 ? `${text.slice(0, 197)}...` : text;
62
+ lines.push(`- user: ${clipped}`);
63
+ continue;
64
+ }
35
65
  const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
36
66
  if (!text) continue;
37
- const clipped = text.length > 160 ? `${text.slice(0, 160)}...` : text;
67
+ const clipped = text.length > 160 ? `${text.slice(0, 157)}...` : text;
38
68
  lines.push(`- ${msg.role}: ${clipped}`);
39
69
  }
40
70
  return `Context Summary\n${lines.join('\n')}`.trim();
@@ -1,5 +1,26 @@
1
+ import os from 'node:os';
2
+ import fs from 'node:fs';
1
3
  import { getShellSystemPrompt } from './shell-profile.js';
2
4
 
5
+ function getEnvBlock() {
6
+ const cwd = process.cwd();
7
+ let isGitRepo = false;
8
+ try {
9
+ fs.accessSync(`${cwd}/.git`);
10
+ isGitRepo = true;
11
+ } catch {}
12
+
13
+ return `<env>
14
+ Working directory: ${cwd}
15
+ Is directory a git repo: ${isGitRepo ? 'Yes' : 'No'}
16
+ Platform: ${process.platform}
17
+ Shell: ${os.userInfo().shell || 'unknown'}
18
+ OS Version: ${os.version || os.release()}
19
+ </env>`;
20
+ }
21
+
3
22
  export function buildDefaultSystemPrompt(config = {}) {
4
- return `${getShellSystemPrompt(config?.shell?.default)} If a command or tool is blocked or fails, inspect the error and retry with allowed commands or tools. For AST-scoped edits, if edit rejects a call because kind=replace_block or ast_target is missing or stale, fix the tool arguments and retry instead of switching to a broader text edit. Do not claim filesystem access is impossible unless the allowed search/read tools also fail.`;
23
+ return `${getShellSystemPrompt(config?.shell?.default)}
24
+
25
+ ${getEnvBlock()}`;
5
26
  }