codemini-cli 0.1.11 → 0.1.13

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 {
26
26
  estimateMessagesTokens,
27
27
  parseCompactArgs
28
28
  } from './context-compact.js';
29
+ import { buildSystemPromptWithReplyLanguage } from './reply-language.js';
29
30
  import { buildSystemPromptWithSoul } from './soul.js';
30
31
 
31
32
  function toOpenAIMessages(sessionMessages) {
@@ -60,6 +61,46 @@ function nowStamp() {
60
61
  return new Date().toISOString().replace(/[:.]/g, '-');
61
62
  }
62
63
 
64
+ function prioritizeByPreferredOrder(items, preferredOrder) {
65
+ const source = Array.isArray(items) ? items : [];
66
+ const priorities = new Map((Array.isArray(preferredOrder) ? preferredOrder : []).map((value, index) => [value, index]));
67
+ return [...source].sort((left, right) => {
68
+ const leftRank = priorities.has(left) ? priorities.get(left) : Number.MAX_SAFE_INTEGER;
69
+ const rightRank = priorities.has(right) ? priorities.get(right) : Number.MAX_SAFE_INTEGER;
70
+ if (leftRank !== rightRank) return leftRank - rightRank;
71
+ return source.indexOf(left) - source.indexOf(right);
72
+ });
73
+ }
74
+
75
+ function describeConfigKey(key, mode = 'set') {
76
+ const labelMap = {
77
+ 'gateway.base_url': 'gateway base URL',
78
+ 'gateway.api_key': 'gateway API key',
79
+ 'gateway.timeout_ms': 'gateway timeout in milliseconds',
80
+ 'gateway.max_retries': 'gateway retry count',
81
+ 'model.name': 'active model name',
82
+ 'model.max_context_tokens': 'model context token limit',
83
+ 'ui.reply_language': 'reply language',
84
+ 'execution.mode': 'execution mode',
85
+ 'execution.always_allow_tools': 'always-allowed tools',
86
+ 'execution.max_steps': 'maximum tool steps',
87
+ 'context.preflight_trigger_pct': 'preflight compact threshold',
88
+ 'context.hard_limit_pct': 'hard compact threshold',
89
+ 'context.tool_result_max_chars': 'tool result character limit',
90
+ 'context.read_file_default_lines': 'default read_file line window',
91
+ 'context.read_file_max_chars': 'read_file character limit',
92
+ 'sessions.max_sessions': 'stored session limit',
93
+ 'sessions.retention_days': 'session retention days',
94
+ 'shell.default': 'default shell',
95
+ 'shell.timeout_ms': 'shell timeout in milliseconds',
96
+ 'context.max_tokens': 'context token budget',
97
+ 'policy.safe_mode': 'safe mode switch',
98
+ 'policy.allow_dangerous_commands': 'dangerous command allowance'
99
+ };
100
+ const label = labelMap[key] || key;
101
+ return mode === 'get' ? `show the ${label}` : `set the ${label}`;
102
+ }
103
+
63
104
  const SUB_AGENT_ROLES = ['planner', 'coder', 'reviewer', 'tester'];
64
105
  const SUB_AGENT_CONTEXT_MAX_MESSAGES = 4;
65
106
  const SUB_AGENT_CONTEXT_MAX_CHARS = 1200;
@@ -76,6 +117,8 @@ function getSubAgentRolePrompt(role) {
76
117
  'You are a review sub-agent. Focus on bugs, regressions, edge cases, and missing tests.',
77
118
  'Start with the focused files or directories handed to you. Do not roam unrelated parts of the repo unless the handed-off evidence is insufficient.',
78
119
  'Use this exact output structure:',
120
+ 'Acceptance Status:',
121
+ '- <met|unmet|unverified> :: <acceptance checklist item or "none">',
79
122
  'Findings:',
80
123
  '- <bug, regression, risk, or "none">',
81
124
  'Verified:',
@@ -92,6 +135,8 @@ function getSubAgentRolePrompt(role) {
92
135
  'Prefer running concrete verification commands over only suggesting them.',
93
136
  'Start with the focused files or directories handed to you. Verify those artifacts first before scanning the wider repo.',
94
137
  'Use this exact output structure:',
138
+ 'Acceptance Status:',
139
+ '- <met|unmet|unverified> :: <acceptance checklist item or "none">',
95
140
  'Verified:',
96
141
  '- <commands run and evidence>',
97
142
  'Not Verified:',
@@ -277,6 +322,114 @@ function buildFocusedTaskNote(role, focusPaths) {
277
322
  return '';
278
323
  }
279
324
 
325
+ function normalizeGoalClauseText(value) {
326
+ return String(value || '')
327
+ .replace(/^[\s\-*0-9.)、,,:;]+/g, '')
328
+ .replace(/\s+/g, ' ')
329
+ .trim();
330
+ }
331
+
332
+ function sentenceCaseRequirement(value) {
333
+ const text = normalizeGoalClauseText(value);
334
+ if (!text) return '';
335
+ return text.charAt(0).toUpperCase() + text.slice(1);
336
+ }
337
+
338
+ function deriveGoalRequirements(goal) {
339
+ const rawGoal = String(goal || '').trim();
340
+ if (!rawGoal) return [];
341
+
342
+ const normalized = rawGoal
343
+ .replace(/\r\n?/g, '\n')
344
+ .replace(/[;。]/g, ',')
345
+ .replace(/\band then\b/gi, ',')
346
+ .replace(/\bthen\b/gi, ',')
347
+ .replace(/\bplus\b/gi, ',')
348
+ .replace(/\s+(?:and|并且|而且|以及)\s+/gi, ', ')
349
+ .replace(/\n+/g, ', ');
350
+
351
+ const roughParts = normalized
352
+ .split(/\s*,\s*/)
353
+ .map((part) => normalizeGoalClauseText(part))
354
+ .filter(Boolean);
355
+
356
+ const requirements = [];
357
+ const seen = new Set();
358
+
359
+ for (const part of roughParts) {
360
+ const lowered = part.toLowerCase();
361
+ if (/\btrim\b/.test(lowered) && !/\bwhitespace\b/.test(lowered)) {
362
+ const label = 'Trim whitespace in the returned greeting';
363
+ if (!seen.has(label)) {
364
+ seen.add(label);
365
+ requirements.push(label);
366
+ }
367
+ continue;
368
+ }
369
+ if (/\btrim\b/.test(lowered) && /\bwhitespace\b/.test(lowered)) {
370
+ const label = 'Trim whitespace in the returned greeting';
371
+ if (!seen.has(label)) {
372
+ seen.add(label);
373
+ requirements.push(label);
374
+ }
375
+ continue;
376
+ }
377
+ if (/(exclamation mark|感叹号|!)/i.test(part)) {
378
+ const label = 'Preserve the exclamation mark';
379
+ if (!seen.has(label)) {
380
+ seen.add(label);
381
+ requirements.push(label);
382
+ }
383
+ continue;
384
+ }
385
+ const label = sentenceCaseRequirement(part);
386
+ if (!label || seen.has(label)) continue;
387
+ seen.add(label);
388
+ requirements.push(label);
389
+ }
390
+
391
+ if (requirements.length === 0) {
392
+ return [sentenceCaseRequirement(rawGoal)].filter(Boolean);
393
+ }
394
+ return requirements.slice(0, 6);
395
+ }
396
+
397
+ function isLightweightAutoPlanGoal(goal, requirements = []) {
398
+ const text = String(goal || '').trim();
399
+ if (!text) return false;
400
+ if (requirements.length !== 1) return false;
401
+ if (text.length > 140) return false;
402
+ if (/\b(plan|spec|design|architecture|roadmap|strategy|migration|refactor)\b/i.test(text)) return false;
403
+ if (/\b(ensure|verify|review|test|validate|make sure|confirm)\b/i.test(text)) return false;
404
+ if (/[;。]/.test(text)) return false;
405
+ return /\b(add|update|fix|rename|trim|export|create|remove|change|implement)\b/i.test(text);
406
+ }
407
+
408
+ function buildGoalRequirementPacket(goal, role) {
409
+ const rawGoal = trimInlineText(goal, 800);
410
+ if (!rawGoal) return '';
411
+ const requirements = deriveGoalRequirements(goal);
412
+ const lines = ['Original goal:', rawGoal];
413
+ if (requirements.length > 0) {
414
+ lines.push('Acceptance checklist:');
415
+ for (const requirement of requirements) {
416
+ lines.push(`- ${requirement}`);
417
+ }
418
+ }
419
+ if (role === 'reviewer') {
420
+ lines.push('Review against the original goal, not just local code quality.');
421
+ lines.push('Check each acceptance item explicitly before deciding there are no findings.');
422
+ lines.push('If any requested behavior is missing, incorrect, or only partially implemented, report it in Findings.');
423
+ } else if (role === 'tester') {
424
+ lines.push('Verify the implementation against the original goal, not just syntax or smoke checks.');
425
+ lines.push('Check each acceptance item explicitly before calling the work verified.');
426
+ lines.push('If any requested behavior is unverified or contradicted by evidence, list it under Not Verified or Failures.');
427
+ } else if (role === 'coder') {
428
+ lines.push('Implement against the acceptance checklist, not only the broad wording of the goal.');
429
+ }
430
+ return lines.join('\n');
431
+ }
432
+
280
433
  async function pathExists(targetPath) {
281
434
  try {
282
435
  await fs.access(targetPath);
@@ -466,7 +619,16 @@ function normalizeAutoPlan(parsed, goal) {
466
619
 
467
620
  function enforceAutoPlanGuardrailSteps(plan, goal) {
468
621
  const source = Array.isArray(plan?.steps) ? plan.steps : [];
622
+ const requirements = deriveGoalRequirements(goal);
623
+ const lightweightGoal = isLightweightAutoPlanGoal(goal, requirements);
469
624
  const implementationSteps = source.filter((step) => step.role !== 'reviewer' && step.role !== 'tester');
625
+ const primaryImplementationStep =
626
+ implementationSteps.find((step) => step.role === 'coder') ||
627
+ implementationSteps[0] || {
628
+ title: 'Implement requested change',
629
+ role: 'coder',
630
+ task: `Implement the requested change for: ${goal}`
631
+ };
470
632
  const reviewerStep = source.find((step) => step.role === 'reviewer') || {
471
633
  title: 'Review implementation',
472
634
  role: 'reviewer',
@@ -478,6 +640,13 @@ function enforceAutoPlanGuardrailSteps(plan, goal) {
478
640
  task: `Test and verify the completed work for: ${goal}. Start with the artifacts produced by earlier implementation steps, run the most relevant checks available, report concrete evidence, and call out anything still unverified.`
479
641
  };
480
642
 
643
+ if (lightweightGoal) {
644
+ return {
645
+ summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
646
+ steps: [primaryImplementationStep, testerStep]
647
+ };
648
+ }
649
+
481
650
  return {
482
651
  summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
483
652
  steps: [...implementationSteps.slice(0, 6), reviewerStep, testerStep]
@@ -487,18 +656,75 @@ function enforceAutoPlanGuardrailSteps(plan, goal) {
487
656
  function looksLikeSuccessfulStepOutput(text = '') {
488
657
  const value = String(text || '').trim();
489
658
  if (!value) return false;
490
- if (/(^|\n)\s*(error|failures?)\s*:\s*(?!none\b)/i.test(value)) return false;
491
- if (/(^|\n)\s*next action\s*:\s*-\s*retry\b/i.test(value)) return false;
659
+ const failureBullet = extractSectionFirstBullet(value, 'Failures');
660
+ const errorBullet = extractSectionFirstBullet(value, 'Error');
661
+ const nextActionBullet = extractSectionFirstBullet(value, 'Next Action');
662
+ const acceptanceFailures = extractAcceptanceStatusItems(value).filter((item) => item.status !== 'met');
663
+ if (errorBullet && !/^none\b/i.test(errorBullet)) return false;
664
+ if (failureBullet && !/^none\b/i.test(failureBullet)) return false;
665
+ if (acceptanceFailures.length > 0) return false;
666
+ if (nextActionBullet && /^retry\b/i.test(nextActionBullet)) return false;
492
667
  return true;
493
668
  }
494
669
 
670
+ function stepOutputHasFailureSignals(role, text = '') {
671
+ const value = String(text || '').trim();
672
+ if (!value) return true;
673
+ const errorBullet = extractSectionFirstBullet(value, 'Error');
674
+ const failureBullet = extractSectionFirstBullet(value, 'Failures');
675
+ const findingsBullet = extractSectionFirstBullet(value, 'Findings');
676
+ const nextActionBullet = extractSectionFirstBullet(value, 'Next Action');
677
+ const acceptanceFailures = extractAcceptanceStatusItems(value).filter((item) => item.status !== 'met');
678
+ if (errorBullet && !/^none\b/i.test(errorBullet)) return true;
679
+ if (failureBullet && !/^none\b/i.test(failureBullet)) return true;
680
+ if (acceptanceFailures.length > 0) return true;
681
+ if (role === 'reviewer' && findingsBullet && !/^none\b/i.test(findingsBullet)) return true;
682
+ if (nextActionBullet && /^(fix|retry|correct|repair)\b/i.test(nextActionBullet)) return true;
683
+ return false;
684
+ }
685
+
686
+ function extractSectionFirstBullet(text = '', heading = '') {
687
+ const escaped = String(heading || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
688
+ const match = String(text || '').match(new RegExp(String.raw`(^|\n)\s*${escaped}\s*:\s*(?:\n|\r\n?)+\s*-\s*([^\n\r]+)`, 'i'));
689
+ return String(match?.[2] || '').trim();
690
+ }
691
+
692
+ function extractSectionBullets(text = '', heading = '') {
693
+ const escaped = String(heading || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
694
+ const value = String(text || '');
695
+ const headingMatch = value.match(new RegExp(String.raw`(^|\n)\s*${escaped}\s*:\s*(?:\n|\r\n?)`, 'i'));
696
+ if (!headingMatch || headingMatch.index == null) return [];
697
+ const start = headingMatch.index + headingMatch[0].length;
698
+ const after = value.slice(start);
699
+ const nextHeading = after.search(/\n\s*[A-Za-z][A-Za-z ]+\s*:\s*(?:\n|\r\n?)/);
700
+ const section = nextHeading === -1 ? after : after.slice(0, nextHeading);
701
+ return section
702
+ .split(/\r?\n/)
703
+ .map((line) => line.match(/^\s*-\s*(.+)$/)?.[1]?.trim() || '')
704
+ .filter(Boolean);
705
+ }
706
+
707
+ function extractAcceptanceStatusItems(text = '') {
708
+ return extractSectionBullets(text, 'Acceptance Status')
709
+ .map((item) => {
710
+ const match = String(item).match(/^(met|unmet|unverified)\s*::\s*(.+)$/i);
711
+ if (!match) return null;
712
+ return {
713
+ status: String(match[1] || '').toLowerCase(),
714
+ label: String(match[2] || '').trim()
715
+ };
716
+ })
717
+ .filter(Boolean);
718
+ }
719
+
495
720
  function buildAutoPlanSystemSummary(auto) {
496
721
  const statusTitle =
497
722
  auto.failedCount > 0 ? 'Auto plan finished with failures' : auto.warningCount > 0 ? 'Auto plan finished with warnings' : 'Auto plan finished';
498
723
  const lines = [
499
724
  statusTitle,
500
725
  `File: ${auto.filePath}`,
501
- `Summary: ${auto.summary || '-'}`,
726
+ `Plan Summary: ${auto.summary || '-'}`,
727
+ `Final Summary: ${auto.finalSummary || auto.summary || '-'}`,
502
728
  `Steps: ${auto.steps.length} total`,
503
729
  `Completed: ${auto.completedCount}`,
504
730
  `Warnings: ${auto.warningCount}`,
@@ -513,6 +739,99 @@ function buildAutoPlanSystemSummary(auto) {
513
739
  return lines.join('\n');
514
740
  }
515
741
 
742
+ function buildAutoPlanFinalSummaryUserPrompt({ goal, autoPlan, runItems, planningError }) {
743
+ const lines = [];
744
+ lines.push('Create a final execution summary for an auto-generated implementation/test plan.');
745
+ lines.push('Keep it concise, high-signal, and outcome-focused.');
746
+ lines.push('Include: overall result, what was verified, what is still pending, and the best next action.');
747
+ lines.push('Use plain text only. Do not use markdown fences.');
748
+ lines.push('');
749
+ lines.push(`Goal: ${goal}`);
750
+ lines.push(`Plan Summary: ${autoPlan?.summary || `Auto plan for: ${goal}`}`);
751
+ if (planningError) {
752
+ lines.push(`Planning Error: ${planningError}`);
753
+ }
754
+ lines.push('');
755
+ lines.push('Executed Steps:');
756
+ runItems.forEach((item, idx) => {
757
+ lines.push(`${idx + 1}. [${item.role}] ${item.title}`);
758
+ if (item.failed) {
759
+ lines.push(`Status: failed`);
760
+ } else if (item.warning) {
761
+ lines.push(`Status: warning`);
762
+ } else {
763
+ lines.push(`Status: completed`);
764
+ }
765
+ if (item.error) {
766
+ lines.push(`Error: ${item.error}`);
767
+ }
768
+ if (item.warning) {
769
+ lines.push(`Warning: ${item.warning}`);
770
+ }
771
+ lines.push(`Output: ${trimInlineText(item.output || '(empty)', 500)}`);
772
+ if (Array.isArray(item.artifactPaths) && item.artifactPaths.length > 0) {
773
+ lines.push(`Artifacts: ${item.artifactPaths.slice(0, 5).join(', ')}`);
774
+ }
775
+ lines.push('');
776
+ });
777
+ return lines.join('\n').trim();
778
+ }
779
+
780
+ async function buildAutoPlanFinalSummary({
781
+ goal,
782
+ autoPlan,
783
+ runItems,
784
+ planningError,
785
+ config,
786
+ model,
787
+ systemPrompt
788
+ }) {
789
+ const fallbackParts = [];
790
+ if (runItems.some((item) => item.failed || item.error)) {
791
+ fallbackParts.push('Execution finished with failed steps.');
792
+ } else if (runItems.some((item) => item.warning)) {
793
+ fallbackParts.push('Execution finished with warnings.');
794
+ } else {
795
+ fallbackParts.push('Execution finished successfully.');
796
+ }
797
+ const verifiedTitles = runItems.filter((item) => !item.failed).map((item) => item.title);
798
+ const pendingTitles = runItems.filter((item) => item.failed || item.warning).map((item) => item.title);
799
+ if (verifiedTitles.length > 0) {
800
+ fallbackParts.push(`Completed: ${verifiedTitles.slice(0, 4).join(', ')}.`);
801
+ }
802
+ if (pendingTitles.length > 0) {
803
+ fallbackParts.push(`Needs follow-up: ${pendingTitles.slice(0, 4).join(', ')}.`);
804
+ }
805
+ const fallbackSummary = fallbackParts.join(' ');
806
+
807
+ if (runItems.some((item) => item.failed || item.error)) {
808
+ return fallbackSummary;
809
+ }
810
+
811
+ try {
812
+ const result = await createChatCompletion({
813
+ baseUrl: config.gateway.base_url,
814
+ apiKey: config.gateway.api_key,
815
+ model: model || config.model.name,
816
+ messages: [
817
+ {
818
+ role: 'system',
819
+ content: `${systemPrompt}\nYou are writing the final execution summary for a completed auto plan. Focus on closure, verification status, and the next action.`
820
+ },
821
+ {
822
+ role: 'user',
823
+ content: buildAutoPlanFinalSummaryUserPrompt({ goal, autoPlan, runItems, planningError })
824
+ }
825
+ ],
826
+ timeoutMs: config.gateway.timeout_ms || 90000,
827
+ maxRetries: config.gateway.max_retries ?? 2
828
+ });
829
+ return trimInlineText(result.text || '', 600) || fallbackSummary;
830
+ } catch {
831
+ return fallbackSummary;
832
+ }
833
+ }
834
+
516
835
  async function writeMarkdownInCoderDir(subDir, title, body, fallbackName, sessionId) {
517
836
  const parts = [process.cwd(), '.coder', subDir];
518
837
  if (sessionId) parts.push(String(sessionId));
@@ -943,6 +1262,7 @@ async function askModel({
943
1262
  async function runSubAgentTask({
944
1263
  role,
945
1264
  task,
1265
+ goal = '',
946
1266
  priorSteps = [],
947
1267
  parentSession,
948
1268
  config,
@@ -957,8 +1277,18 @@ async function runSubAgentTask({
957
1277
  const handoffPacket = buildStepArtifactPacket(priorSteps, role);
958
1278
  const handoffFocusPaths = collectStepArtifacts(priorSteps, role)?.focusPaths || [];
959
1279
  const focusedTaskNote = buildFocusedTaskNote(role, handoffFocusPaths);
1280
+ const goalRequirementPacket = buildGoalRequirementPacket(goal, role);
960
1281
  const verificationPacket = role === 'tester' ? await buildTesterVerificationPacket(handoffFocusPaths) : '';
961
- const scopedTask = [contextPacket, evidencePacket, handoffPacket, verificationPacket, focusedTaskNote, 'Task:', task]
1282
+ const scopedTask = [
1283
+ contextPacket,
1284
+ goalRequirementPacket,
1285
+ evidencePacket,
1286
+ handoffPacket,
1287
+ verificationPacket,
1288
+ focusedTaskNote,
1289
+ 'Task:',
1290
+ task
1291
+ ]
962
1292
  .filter(Boolean)
963
1293
  .join('\n\n');
964
1294
  let blockedCount = 0;
@@ -1015,6 +1345,7 @@ async function buildAutoPlanAndRun({
1015
1345
  onAgentEvent,
1016
1346
  sessionId
1017
1347
  }) {
1348
+ const requirementPacket = buildGoalRequirementPacket(goal, 'planner');
1018
1349
  const plannerPrompt =
1019
1350
  'Return strict JSON only with shape {"summary":"...","steps":[{"title":"...","role":"planner|coder|reviewer|tester","task":"..."}]}. No markdown. Always include final reviewer and tester steps.';
1020
1351
  let autoPlan = {
@@ -1037,7 +1368,13 @@ async function buildAutoPlanAndRun({
1037
1368
  { role: 'system', content: `${systemPrompt}\n${plannerPrompt}` },
1038
1369
  {
1039
1370
  role: 'user',
1040
- content: `Create an execution plan and assign best sub-agent role for each step.\nGoal: ${goal}\nThe final steps must include review and testing/verification.`
1371
+ content: [
1372
+ 'Create an execution plan and assign best sub-agent role for each step.',
1373
+ requirementPacket,
1374
+ '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.'
1375
+ ]
1376
+ .filter(Boolean)
1377
+ .join('\n')
1041
1378
  }
1042
1379
  ],
1043
1380
  timeoutMs: config.gateway.timeout_ms || 90000,
@@ -1050,18 +1387,20 @@ async function buildAutoPlanAndRun({
1050
1387
  }
1051
1388
 
1052
1389
  const runItems = [];
1390
+ const totalPlanSteps = autoPlan.steps.length + 1;
1053
1391
  for (let i = 0; i < autoPlan.steps.length; i += 1) {
1054
1392
  const step = autoPlan.steps[i];
1055
1393
  if (onAgentEvent) {
1056
1394
  onAgentEvent({
1057
1395
  type: 'assistant:delta',
1058
- text: `\n[plan] Step ${i + 1}/${autoPlan.steps.length} -> ${step.role}: ${step.title}\n`
1396
+ text: `\n[plan] Step ${i + 1}/${totalPlanSteps} -> ${step.role}: ${step.title}\n`
1059
1397
  });
1060
1398
  }
1061
1399
  try {
1062
1400
  const stepResult = await runSubAgentTask({
1063
1401
  role: step.role,
1064
1402
  task: step.task,
1403
+ goal,
1065
1404
  priorSteps: runItems,
1066
1405
  parentSession: session,
1067
1406
  config,
@@ -1070,14 +1409,20 @@ async function buildAutoPlanAndRun({
1070
1409
  onAgentEvent
1071
1410
  });
1072
1411
  const outputLooksSuccessful = looksLikeSuccessfulStepOutput(stepResult.text);
1412
+ const outputHasFailureSignals = stepOutputHasFailureSignals(step.role, stepResult.text);
1073
1413
  const warningParts = [];
1074
1414
  if (stepResult.blockedCount > 0) warningParts.push(`${stepResult.blockedCount} blocked tool call(s)`);
1075
1415
  if (stepResult.toolErrorCount > 0) warningParts.push(`${stepResult.toolErrorCount} tool error(s)`);
1076
1416
  const warning = warningParts.length > 0 ? `sub-agent recovered after ${warningParts.join(', ')}` : '';
1077
- const failed = stepResult.hasErrorLine || (!outputLooksSuccessful && (stepResult.blockedCount > 0 || stepResult.toolErrorCount > 0));
1417
+ const failed =
1418
+ stepResult.hasErrorLine ||
1419
+ outputHasFailureSignals ||
1420
+ (!outputLooksSuccessful && (stepResult.blockedCount > 0 || stepResult.toolErrorCount > 0));
1078
1421
  let error = '';
1079
1422
  if (stepResult.hasErrorLine) {
1080
1423
  error = 'sub-agent output contains error line(s)';
1424
+ } else if (outputHasFailureSignals) {
1425
+ error = 'sub-agent output reports unmet requirements or failed verification';
1081
1426
  } else if (failed && stepResult.blockedCount > 0) {
1082
1427
  error = `sub-agent ended with ${stepResult.blockedCount} blocked tool call(s)`;
1083
1428
  } else if (failed && stepResult.toolErrorCount > 0) {
@@ -1106,11 +1451,30 @@ async function buildAutoPlanAndRun({
1106
1451
  const warningItems = runItems.filter((s) => !s.failed && s.warning);
1107
1452
  const completedItems = runItems.filter((s) => !s.failed);
1108
1453
 
1454
+ if (onAgentEvent) {
1455
+ onAgentEvent({
1456
+ type: 'assistant:delta',
1457
+ text: `\n[plan] Step ${totalPlanSteps}/${totalPlanSteps} -> summarizer: Final summary\n`
1458
+ });
1459
+ }
1460
+ const finalSummary = await buildAutoPlanFinalSummary({
1461
+ goal,
1462
+ autoPlan,
1463
+ runItems,
1464
+ planningError,
1465
+ config,
1466
+ model,
1467
+ systemPrompt
1468
+ });
1469
+
1109
1470
  const lines = [];
1110
1471
  lines.push(`# Auto Plan: ${goal}`);
1111
1472
  lines.push('');
1112
1473
  lines.push(`## Summary`);
1113
1474
  lines.push(autoPlan.summary || `Auto plan for: ${goal}`);
1475
+ lines.push('');
1476
+ lines.push('## Final Summary');
1477
+ lines.push(finalSummary || '(empty)');
1114
1478
  if (planningError) {
1115
1479
  lines.push('');
1116
1480
  lines.push(`Planning Error: ${planningError}`);
@@ -1152,6 +1516,7 @@ async function buildAutoPlanAndRun({
1152
1516
  return {
1153
1517
  filePath,
1154
1518
  summary: autoPlan.summary,
1519
+ finalSummary,
1155
1520
  steps: autoPlan.steps,
1156
1521
  completedCount: completedItems.length,
1157
1522
  warningCount: warningItems.length,
@@ -1213,11 +1578,13 @@ export async function createChatRuntime({
1213
1578
  const configKeyHints = [
1214
1579
  'gateway.base_url',
1215
1580
  'gateway.api_key',
1581
+ 'model.name',
1582
+ 'ui.reply_language',
1583
+ 'execution.mode',
1584
+ 'shell.default',
1216
1585
  'gateway.timeout_ms',
1217
1586
  'gateway.max_retries',
1218
- 'model.name',
1219
1587
  'model.max_context_tokens',
1220
- 'execution.mode',
1221
1588
  'execution.always_allow_tools',
1222
1589
  'execution.max_steps',
1223
1590
  'context.preflight_trigger_pct',
@@ -1227,13 +1594,34 @@ export async function createChatRuntime({
1227
1594
  'context.read_file_max_chars',
1228
1595
  'sessions.max_sessions',
1229
1596
  'sessions.retention_days',
1230
- 'shell.default',
1231
1597
  'shell.timeout_ms',
1232
1598
  'context.max_tokens',
1233
1599
  'policy.safe_mode',
1234
1600
  'policy.allow_dangerous_commands'
1235
1601
  ];
1236
1602
 
1603
+ const commandPriorityOrder = [
1604
+ '/help',
1605
+ '/status',
1606
+ '/config',
1607
+ '/mode',
1608
+ '/plan',
1609
+ '/tasks',
1610
+ '/history',
1611
+ '/checkpoint',
1612
+ '/agents',
1613
+ '/compact',
1614
+ '/debug',
1615
+ '/retry'
1616
+ ];
1617
+ const configSubcommandPriority = ['/config set', '/config get', '/config list', '/config reset'];
1618
+ const configSubcommandDescriptions = {
1619
+ '/config set': 'update a config value',
1620
+ '/config get': 'show a config value',
1621
+ '/config list': 'print the full config',
1622
+ '/config reset': 'reset config to defaults'
1623
+ };
1624
+
1237
1625
  const listCommandNames = () => {
1238
1626
  const builtins = [
1239
1627
  { name: 'help', description: 'show chat help' },
@@ -1312,10 +1700,23 @@ export async function createChatRuntime({
1312
1700
  '/status'
1313
1701
  ];
1314
1702
  const compactKey = (value) => String(value || '').toLowerCase().replace(/[\/\s<>?]/g, '');
1703
+ const commandDescriptions = new Map();
1704
+ const registerSuggestion = (value, description = '') => {
1705
+ commandDescriptions.set(value, description);
1706
+ return { value, description };
1707
+ };
1708
+ const materializeSuggestions = (items) =>
1709
+ (Array.isArray(items) ? items : []).map((item) => {
1710
+ if (item && typeof item === 'object' && 'value' in item) return item;
1711
+ const value = String(item || '');
1712
+ return { value, description: commandDescriptions.get(value) || '' };
1713
+ });
1315
1714
  const matchCompactTemplates = (value) => {
1316
1715
  const needle = compactKey(value);
1317
1716
  if (!needle) return [];
1318
- return slashTemplates.filter((template) => compactKey(template).startsWith(needle));
1717
+ return materializeSuggestions(
1718
+ slashTemplates.filter((template) => compactKey(template).startsWith(needle))
1719
+ );
1319
1720
  };
1320
1721
 
1321
1722
  const getCompletionOptions = (rawInput) => {
@@ -1327,26 +1728,53 @@ export async function createChatRuntime({
1327
1728
  const tokens = body.trim().split(/\s+/).filter(Boolean);
1328
1729
  const commandPart = tokens[0] || '';
1329
1730
 
1330
- const allCommands = listCommandNames().map((c) => c.name);
1731
+ const allCommandEntries = listCommandNames();
1732
+ const allCommands = allCommandEntries.map((c) => c.name);
1733
+ for (const entry of allCommandEntries) {
1734
+ registerSuggestion(`/${entry.name}`, entry.description || '');
1735
+ }
1736
+ for (const template of configTemplates) {
1737
+ registerSuggestion(template, configSubcommandDescriptions[template] || 'config command');
1738
+ }
1739
+ for (const template of historyTemplates) registerSuggestion(template, 'history command');
1740
+ for (const template of modeTemplates) registerSuggestion(template, 'switch execution mode');
1741
+ for (const template of taskTemplates) registerSuggestion(template, 'task board command');
1742
+ for (const template of checkpointTemplates) registerSuggestion(template, 'checkpoint command');
1743
+ for (const template of specTemplates) registerSuggestion(template, 'create a spec file');
1744
+ for (const template of planTemplates) registerSuggestion(template, 'planning command');
1745
+ for (const template of agentTemplates) registerSuggestion(template, 'sub-agent command');
1746
+ for (const template of debugTemplates) registerSuggestion(template, 'debug command');
1747
+ for (const template of compactTemplates) registerSuggestion(template, 'context compaction command');
1748
+ registerSuggestion('/retry', 'retry the last user request');
1749
+ registerSuggestion('/status', 'show runtime status');
1331
1750
 
1332
1751
  if (!commandPart) {
1333
- return allCommands.map((name) => `/${name}`);
1752
+ return materializeSuggestions(prioritizeByPreferredOrder(
1753
+ allCommands.map((name) => `/${name}`),
1754
+ commandPriorityOrder
1755
+ ));
1334
1756
  }
1335
1757
 
1336
1758
  if (tokens.length === 1 && !hasTrailingSpace) {
1337
- const direct = allCommands
1338
- .filter((name) => name.startsWith(commandPart))
1339
- .map((name) => `/${name}`);
1340
- if (direct.length > 0) return direct;
1759
+ const direct = prioritizeByPreferredOrder(
1760
+ allCommands
1761
+ .filter((name) => name.startsWith(commandPart))
1762
+ .map((name) => `/${name}`),
1763
+ commandPriorityOrder
1764
+ );
1765
+ if (direct.length > 0) return materializeSuggestions(direct);
1341
1766
  return matchCompactTemplates(input);
1342
1767
  }
1343
1768
 
1344
1769
  if (commandPart === 'config') {
1345
1770
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
1346
1771
  const sub = tokens[1] || '';
1347
- return ['list', 'get', 'set', 'reset']
1348
- .filter((s) => s.startsWith(sub))
1349
- .map((s) => `/config ${s}`);
1772
+ return materializeSuggestions(prioritizeByPreferredOrder(
1773
+ ['set', 'get', 'list', 'reset']
1774
+ .filter((s) => s.startsWith(sub))
1775
+ .map((s) => registerSuggestion(`/config ${s}`, configSubcommandDescriptions[`/config ${s}`] || 'config command').value),
1776
+ configSubcommandPriority
1777
+ ));
1350
1778
  }
1351
1779
 
1352
1780
  const sub = tokens[1] || '';
@@ -1354,96 +1782,98 @@ export async function createChatRuntime({
1354
1782
  const keyPrefix = tokens[2] || '';
1355
1783
  return configKeyHints
1356
1784
  .filter((k) => k.startsWith(keyPrefix))
1357
- .map((k) => `/config get ${k}`);
1785
+ .map((k) => registerSuggestion(`/config get ${k}`, describeConfigKey(k, 'get')));
1358
1786
  }
1359
1787
  if (sub === 'set') {
1360
1788
  const keyPrefix = tokens[2] || '';
1361
1789
  return configKeyHints
1362
1790
  .filter((k) => k.startsWith(keyPrefix))
1363
- .map((k) => `/config set ${k} `);
1791
+ .map((k) => registerSuggestion(`/config set ${k} `, describeConfigKey(k, 'set')));
1364
1792
  }
1365
1793
 
1366
- return configTemplates;
1794
+ return materializeSuggestions(configTemplates);
1367
1795
  }
1368
1796
 
1369
1797
  if (commandPart === 'compact') {
1370
1798
  const joined = tokens.slice(1).join(' ');
1371
1799
  return compactOptions
1372
1800
  .filter((opt) => opt.includes(joined) || joined === '')
1373
- .map((opt) => `/compact ${opt}`);
1801
+ .map((opt) => registerSuggestion(`/compact ${opt}`, 'context compaction command'));
1374
1802
  }
1375
1803
 
1376
1804
  if (commandPart === 'retry') {
1377
- return ['/retry'];
1805
+ return [registerSuggestion('/retry', 'retry the last user request')];
1378
1806
  }
1379
1807
  if (commandPart === 'status') {
1380
- return ['/status'];
1808
+ return [registerSuggestion('/status', 'show runtime status')];
1381
1809
  }
1382
1810
  if (commandPart === 'mode') {
1383
1811
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
1384
1812
  const sub = tokens[1] || '';
1385
1813
  return ['normal', 'auto', 'plan']
1386
1814
  .filter((m) => m.startsWith(sub))
1387
- .map((m) => `/mode ${m}`);
1815
+ .map((m) => registerSuggestion(`/mode ${m}`, 'switch execution mode'));
1388
1816
  }
1389
- return modeTemplates;
1817
+ return materializeSuggestions(modeTemplates);
1390
1818
  }
1391
1819
  if (commandPart === 'tasks') {
1392
1820
  if (tokens.length <= 2 && !hasTrailingSpace) {
1393
1821
  const sub = tokens[1] || '';
1394
1822
  return ['add', 'start', 'done', 'remove', 'rm', 'clear']
1395
1823
  .filter((s) => s.startsWith(sub))
1396
- .map((s) => `/tasks ${s}`);
1824
+ .map((s) => registerSuggestion(`/tasks ${s}`, 'task board command'));
1397
1825
  }
1398
- return taskTemplates;
1826
+ return materializeSuggestions(taskTemplates);
1399
1827
  }
1400
1828
  if (commandPart === 'checkpoint') {
1401
1829
  if (tokens.length <= 2 && !hasTrailingSpace) {
1402
1830
  const sub = tokens[1] || '';
1403
1831
  return ['create', 'list', 'load']
1404
1832
  .filter((s) => s.startsWith(sub))
1405
- .map((s) => `/checkpoint ${s}`);
1833
+ .map((s) => registerSuggestion(`/checkpoint ${s}`, 'checkpoint command'));
1406
1834
  }
1407
1835
  if (tokens[1] === 'list') {
1408
1836
  const hint = tokens[2] || '';
1409
- return ['--all'].filter((v) => v.startsWith(hint)).map((v) => `/checkpoint list ${v}`);
1837
+ return ['--all']
1838
+ .filter((v) => v.startsWith(hint))
1839
+ .map((v) => registerSuggestion(`/checkpoint list ${v}`, 'checkpoint command'));
1410
1840
  }
1411
1841
  if (tokens[1] === 'load') {
1412
1842
  if (tokens.length >= 3) {
1413
1843
  const hint = tokens[3] || '';
1414
1844
  return ['--all']
1415
1845
  .filter((v) => v.startsWith(hint))
1416
- .map((v) => `/checkpoint load ${tokens[2]} ${v}`);
1846
+ .map((v) => registerSuggestion(`/checkpoint load ${tokens[2]} ${v}`, 'checkpoint command'));
1417
1847
  }
1418
1848
  }
1419
- return checkpointTemplates;
1849
+ return materializeSuggestions(checkpointTemplates);
1420
1850
  }
1421
1851
  if (commandPart === 'spec') {
1422
- return specTemplates;
1852
+ return materializeSuggestions(specTemplates);
1423
1853
  }
1424
1854
  if (commandPart === 'plan') {
1425
1855
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
1426
1856
  const sub = tokens[1] || '';
1427
1857
  return ['auto', 'from-spec']
1428
1858
  .filter((s) => s.startsWith(sub))
1429
- .map((s) => `/plan ${s}`);
1859
+ .map((s) => registerSuggestion(`/plan ${s}`, 'planning command'));
1430
1860
  }
1431
- return planTemplates;
1861
+ return materializeSuggestions(planTemplates);
1432
1862
  }
1433
1863
  if (commandPart === 'agents') {
1434
1864
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
1435
1865
  const sub = tokens[1] || '';
1436
1866
  return ['list', 'run']
1437
1867
  .filter((s) => s.startsWith(sub))
1438
- .map((s) => `/agents ${s}`);
1868
+ .map((s) => registerSuggestion(`/agents ${s}`, 'sub-agent command'));
1439
1869
  }
1440
1870
  if (tokens[1] === 'run') {
1441
1871
  const rolePrefix = tokens[2] || '';
1442
1872
  return ['planner', 'coder', 'reviewer', 'tester']
1443
1873
  .filter((r) => r.startsWith(rolePrefix))
1444
- .map((r) => `/agents run ${r} `);
1874
+ .map((r) => registerSuggestion(`/agents run ${r} `, 'sub-agent command'));
1445
1875
  }
1446
- return agentTemplates;
1876
+ return materializeSuggestions(agentTemplates);
1447
1877
  }
1448
1878
 
1449
1879
  if (commandPart === 'history') {
@@ -1460,12 +1890,13 @@ export async function createChatRuntime({
1460
1890
  .filter((session) => String(session.id || '').startsWith(idPrefix))
1461
1891
  .map((session) => ({
1462
1892
  value: `/history resume ${session.id}`,
1463
- display: `/history resume ${session.id} · ${Number(session.messageCount || 0)} msgs`
1893
+ display: `/history resume ${session.id} · ${Number(session.messageCount || 0)} msgs`,
1894
+ description: 'resume a saved session'
1464
1895
  }));
1465
1896
  if (dynamic.length > 0) return dynamic;
1466
- return historyTemplates;
1897
+ return materializeSuggestions(historyTemplates);
1467
1898
  }
1468
- return historyTemplates;
1899
+ return materializeSuggestions(historyTemplates);
1469
1900
  }
1470
1901
 
1471
1902
  if (commandPart === 'debug') {
@@ -1475,9 +1906,9 @@ export async function createChatRuntime({
1475
1906
  const action = tokens[2] || '';
1476
1907
  return ['on', 'off', 'status']
1477
1908
  .filter((v) => v.startsWith(action))
1478
- .map((v) => `/debug keys ${v}`);
1909
+ .map((v) => registerSuggestion(`/debug keys ${v}`, 'keyboard debug command'));
1479
1910
  }
1480
- return debugTemplates;
1911
+ return materializeSuggestions(debugTemplates);
1481
1912
  }
1482
1913
 
1483
1914
  return [];
@@ -1519,7 +1950,7 @@ export async function createChatRuntime({
1519
1950
  };
1520
1951
 
1521
1952
  const submit = async (line, onAgentEvent) => {
1522
- const activeBaseSystemPrompt = baseSystemPrompt;
1953
+ const activeBaseSystemPrompt = buildSystemPromptWithReplyLanguage(baseSystemPrompt, config);
1523
1954
  const activeReplySystemPrompt = await buildSystemPromptWithSoul(baseSystemPrompt, config);
1524
1955
  try {
1525
1956
  await appendInputHistory(line);