@calliopelabs/cli 2.2.0 → 2.3.0

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.
Files changed (105) hide show
  1. package/dist/agents/council-types.d.ts +2 -0
  2. package/dist/agents/council-types.d.ts.map +1 -1
  3. package/dist/agents/council-types.js.map +1 -1
  4. package/dist/agents/council.d.ts +5 -0
  5. package/dist/agents/council.d.ts.map +1 -1
  6. package/dist/agents/council.js +150 -14
  7. package/dist/agents/council.js.map +1 -1
  8. package/dist/agents/orchestrator.d.ts +7 -0
  9. package/dist/agents/orchestrator.d.ts.map +1 -1
  10. package/dist/agents/orchestrator.js +48 -7
  11. package/dist/agents/orchestrator.js.map +1 -1
  12. package/dist/agents/swarm-types.d.ts +1 -0
  13. package/dist/agents/swarm-types.d.ts.map +1 -1
  14. package/dist/agents/swarm.d.ts +5 -0
  15. package/dist/agents/swarm.d.ts.map +1 -1
  16. package/dist/agents/swarm.js +85 -17
  17. package/dist/agents/swarm.js.map +1 -1
  18. package/dist/agents/types.d.ts +1 -0
  19. package/dist/agents/types.d.ts.map +1 -1
  20. package/dist/agents/types.js.map +1 -1
  21. package/dist/api-server.d.ts.map +1 -1
  22. package/dist/api-server.js +1 -1
  23. package/dist/api-server.js.map +1 -1
  24. package/dist/auto-compressor.d.ts +14 -0
  25. package/dist/auto-compressor.d.ts.map +1 -1
  26. package/dist/auto-compressor.js +58 -5
  27. package/dist/auto-compressor.js.map +1 -1
  28. package/dist/background-jobs.d.ts.map +1 -1
  29. package/dist/background-jobs.js +8 -3
  30. package/dist/background-jobs.js.map +1 -1
  31. package/dist/bin.js +4 -0
  32. package/dist/bin.js.map +1 -1
  33. package/dist/cli/agent.d.ts.map +1 -1
  34. package/dist/cli/agent.js +111 -59
  35. package/dist/cli/agent.js.map +1 -1
  36. package/dist/cli/commands.d.ts.map +1 -1
  37. package/dist/cli/commands.js +119 -11
  38. package/dist/cli/commands.js.map +1 -1
  39. package/dist/cli/index.d.ts.map +1 -1
  40. package/dist/cli/index.js +23 -5
  41. package/dist/cli/index.js.map +1 -1
  42. package/dist/cli/types.d.ts +3 -0
  43. package/dist/cli/types.d.ts.map +1 -1
  44. package/dist/cli/types.js +2 -1
  45. package/dist/cli/types.js.map +1 -1
  46. package/dist/config.d.ts +1 -0
  47. package/dist/config.d.ts.map +1 -1
  48. package/dist/config.js +13 -1
  49. package/dist/config.js.map +1 -1
  50. package/dist/env-expansion.d.ts +15 -0
  51. package/dist/env-expansion.d.ts.map +1 -0
  52. package/dist/env-expansion.js +43 -0
  53. package/dist/env-expansion.js.map +1 -0
  54. package/dist/headless.d.ts.map +1 -1
  55. package/dist/headless.js +3 -2
  56. package/dist/headless.js.map +1 -1
  57. package/dist/iteration-ledger.d.ts +111 -2
  58. package/dist/iteration-ledger.d.ts.map +1 -1
  59. package/dist/iteration-ledger.js +327 -19
  60. package/dist/iteration-ledger.js.map +1 -1
  61. package/dist/iteration-limit.d.ts +5 -0
  62. package/dist/iteration-limit.d.ts.map +1 -0
  63. package/dist/iteration-limit.js +17 -0
  64. package/dist/iteration-limit.js.map +1 -0
  65. package/dist/mcp.d.ts.map +1 -1
  66. package/dist/mcp.js +6 -1
  67. package/dist/mcp.js.map +1 -1
  68. package/dist/memory.d.ts.map +1 -1
  69. package/dist/memory.js +4 -9
  70. package/dist/memory.js.map +1 -1
  71. package/dist/prevent-sleep.d.ts +10 -0
  72. package/dist/prevent-sleep.d.ts.map +1 -0
  73. package/dist/prevent-sleep.js +85 -0
  74. package/dist/prevent-sleep.js.map +1 -0
  75. package/dist/providers/compat.d.ts.map +1 -1
  76. package/dist/providers/compat.js +21 -6
  77. package/dist/providers/compat.js.map +1 -1
  78. package/dist/providers/openai-compat-shims.d.ts +31 -0
  79. package/dist/providers/openai-compat-shims.d.ts.map +1 -0
  80. package/dist/providers/openai-compat-shims.js +179 -0
  81. package/dist/providers/openai-compat-shims.js.map +1 -0
  82. package/dist/providers/openai.d.ts.map +1 -1
  83. package/dist/storage.d.ts +20 -1
  84. package/dist/storage.d.ts.map +1 -1
  85. package/dist/storage.js +101 -9
  86. package/dist/storage.js.map +1 -1
  87. package/dist/tools.js +1 -1
  88. package/dist/tools.js.map +1 -1
  89. package/dist/types.d.ts.map +1 -1
  90. package/dist/types.js +5 -8
  91. package/dist/types.js.map +1 -1
  92. package/dist/ui/agent.d.ts.map +1 -1
  93. package/dist/ui/agent.js +140 -77
  94. package/dist/ui/agent.js.map +1 -1
  95. package/dist/ui/commands.d.ts +2 -0
  96. package/dist/ui/commands.d.ts.map +1 -1
  97. package/dist/ui/commands.js +244 -29
  98. package/dist/ui/commands.js.map +1 -1
  99. package/dist/ui/completions.d.ts.map +1 -1
  100. package/dist/ui/completions.js +2 -0
  101. package/dist/ui/completions.js.map +1 -1
  102. package/dist/ui/index.d.ts.map +1 -1
  103. package/dist/ui/index.js +52 -14
  104. package/dist/ui/index.js.map +1 -1
  105. package/package.json +1 -1
@@ -28,6 +28,74 @@ import { getTerminalImageInfo, getImageModeLabel, renderSkinBanner, renderTransi
28
28
  import { applyThemePack, listThemePacks, getCurrentPack, getCompanionMode, setCompanionMode, getThemePack } from '../hud/theme-packs/api.js';
29
29
  import { getModelContextLimit } from '../model-detection.js';
30
30
  import { resetContextWarnings } from './context.js';
31
+ import * as memory from '../memory.js';
32
+ import { resolveIterationLimit, formatIterationLimit, isFiniteIterationLimit, } from '../iteration-limit.js';
33
+ // Builds the full system prompt including memory context (project + global).
34
+ // dir should be the project directory for the active/resumed session.
35
+ function buildFullSystemPrompt(persona, dir) {
36
+ const base = getSystemPrompt(persona);
37
+ const mem = memory.buildMemoryContext(dir);
38
+ return mem.trim() ? base + '\n\n--- Project Context ---\n' + mem : base;
39
+ }
40
+ function getActiveProjectDir(ctx) {
41
+ return ctx.sessionRef.current?.projectPath ?? process.cwd();
42
+ }
43
+ function formatLedgerDuration(durationMs) {
44
+ if (durationMs < 1000)
45
+ return `${durationMs}ms`;
46
+ if (durationMs < 60_000)
47
+ return `${(durationMs / 1000).toFixed(1)}s`;
48
+ return `${(durationMs / 60_000).toFixed(1)}m`;
49
+ }
50
+ function formatSessionLogLimit(limit) {
51
+ return limit > 0 ? String(limit) : 'unlimited';
52
+ }
53
+ function formatLedgerRun(run) {
54
+ const iterationCount = Math.max(0, (run.entryCountAtEnd ?? run.entryCountAtStart) - run.entryCountAtStart);
55
+ const parts = [`${run.kind}`, `[${run.status}]`];
56
+ if (iterationCount > 0) {
57
+ parts.push(`${iterationCount} iteration${iterationCount === 1 ? '' : 's'}`);
58
+ }
59
+ if (run.maxIterations != null && Number.isFinite(run.maxIterations)) {
60
+ parts.push(`max ${run.maxIterations}`);
61
+ }
62
+ else if (run.maxIterations === null) {
63
+ parts.push('unlimited');
64
+ }
65
+ const prompt = run.prompt.length > 90 ? `${run.prompt.slice(0, 90)}...` : run.prompt;
66
+ let line = `${parts.join(' ')} — ${prompt}`;
67
+ if (run.errorSummary) {
68
+ line += ` (${run.errorSummary})`;
69
+ }
70
+ return line;
71
+ }
72
+ function watchAsyncLedgerRun(ledger, kind, prompt, getStatus) {
73
+ if (!ledger)
74
+ return;
75
+ const runId = ledger.startRun(kind, prompt);
76
+ void (async () => {
77
+ for (;;) {
78
+ const current = getStatus();
79
+ if (!current) {
80
+ ledger.finishRun(runId, 'failed', { errorSummary: 'Run state no longer available' });
81
+ return;
82
+ }
83
+ if (current.status === 'completed') {
84
+ ledger.finishRun(runId, 'completed');
85
+ return;
86
+ }
87
+ if (current.status === 'failed') {
88
+ ledger.finishRun(runId, 'failed', { errorSummary: current.error });
89
+ return;
90
+ }
91
+ if (current.status === 'cancelled') {
92
+ ledger.finishRun(runId, 'cancelled', { errorSummary: current.error || 'Cancelled' });
93
+ return;
94
+ }
95
+ await new Promise(resolve => setTimeout(resolve, 500));
96
+ }
97
+ })();
98
+ }
31
99
  // ============================================================================
32
100
  // handleCommand
33
101
  // ============================================================================
@@ -57,6 +125,7 @@ export async function handleCommand(cmd, ctx) {
57
125
 
58
126
  --- Session & State ---
59
127
  /session [list|info|fork|save] - Session management (/sessions)
128
+ /log [summary|tail|failures|reset] - Iteration/run log
60
129
  /resume [sessionId] - Resume session (restores full context)
61
130
  /checkpoint [list|clear] - File checkpoints (/cp)
62
131
  /restore <path> [index] - Restore file from checkpoint
@@ -103,8 +172,8 @@ export async function handleCommand(cmd, ctx) {
103
172
  --- Multi-Agent ---
104
173
  /agents - Sub-agent status (--agents mode)
105
174
  /swarm [start|coord|status] - Agent swarms & coordination
106
- /loop [prompt] [n] - Iterative agent loop
107
- /cancel-loop - Stop running loop (/stop)
175
+ /loop [prompt] - Iterative agent loop (default: unlimited)
176
+ /cancel-loop - Stop running loop (/stop, /breakloop)
108
177
 
109
178
  --- System ---
110
179
  /status - Show status (/s)
@@ -202,7 +271,7 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
202
271
  if (parts[1] && ['calliope', 'muse', 'minimal'].includes(parts[1])) {
203
272
  const p = parts[1];
204
273
  ctx.setPersona(p);
205
- ctx.llmMessages.current = [{ role: 'system', content: getSystemPrompt(p) }];
274
+ ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(p, getActiveProjectDir(ctx)) }];
206
275
  ctx.addMessage('system', `Persona: ${p}`);
207
276
  }
208
277
  else {
@@ -212,7 +281,8 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
212
281
  case '/clear':
213
282
  case '/c':
214
283
  ctx.setMessages([]);
215
- ctx.llmMessages.current = [{ role: 'system', content: getSystemPrompt(ctx.persona) }];
284
+ ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
285
+ ctx.ledger?.reset();
216
286
  ctx.setStats({ inputTokens: 0, outputTokens: 0, cost: 0, messageCount: 0 });
217
287
  resetContextWarnings(); // Reset context warning state
218
288
  break;
@@ -344,7 +414,7 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
344
414
  break;
345
415
  }
346
416
  case '/config':
347
- ctx.addMessage('system', `Config: ${config.getConfigPath()}\nProviders: ${config.getConfiguredProviders().join(', ') || 'none'}\nmaxIterations: ${config.get('maxIterations')}`);
417
+ ctx.addMessage('system', `Config: ${config.getConfigPath()}\nProviders: ${config.getConfiguredProviders().join(', ') || 'none'}\nmaxIterations: ${config.get('maxIterations')}\nsessionLogLimit: ${formatSessionLogLimit(config.get('sessionLogLimit'))} (set > 0 to cap)`);
348
418
  break;
349
419
  case '/agents': {
350
420
  if (!ctx.agtermEnabled) {
@@ -797,6 +867,7 @@ Edit the YAML to customize members, strategy, and coordination settings.`);
797
867
  ctx.addMessage('system', `Usage: /set <key> <value>
798
868
  Available keys:
799
869
  maxIterations <number> - Max agent iterations (current: ${config.get('maxIterations')})
870
+ sessionLogLimit <number> - Cap retained session log items (current: ${formatSessionLogLimit(config.get('sessionLogLimit'))}, 0 = unlimited)
800
871
  persona <name> - calliope, muse, minimal
801
872
  fancyOutput <bool> - true/false`);
802
873
  break;
@@ -804,12 +875,22 @@ Available keys:
804
875
  try {
805
876
  if (key === 'maxIterations') {
806
877
  const num = parseInt(value, 10);
807
- if (isNaN(num) || num < 1 || num > 10000) {
808
- ctx.addMessage('error', 'maxIterations must be 1-10000');
878
+ if (isNaN(num) || num < 0 || num > 1000000) {
879
+ ctx.addMessage('error', 'maxIterations must be 0-1000000 (0 = unlimited)');
809
880
  break;
810
881
  }
811
882
  config.set('maxIterations', num);
812
- ctx.addMessage('system', `\u2713 maxIterations set to ${num}`);
883
+ ctx.addMessage('system', `\u2713 maxIterations set to ${formatIterationLimit(resolveIterationLimit(num))}`);
884
+ }
885
+ else if (key === 'sessionLogLimit') {
886
+ const num = parseInt(value, 10);
887
+ if (isNaN(num) || num < 0 || num > 100000) {
888
+ ctx.addMessage('error', 'sessionLogLimit must be 0-100000 (0 = unlimited)');
889
+ break;
890
+ }
891
+ config.set('sessionLogLimit', num);
892
+ ctx.ledger?.setRetentionLimit(num);
893
+ ctx.addMessage('system', `\u2713 sessionLogLimit set to ${num === 0 ? 'unlimited (set > 0 to cap)' : num}`);
813
894
  }
814
895
  else if (key === 'persona') {
815
896
  if (!['calliope', 'muse', 'minimal'].includes(value)) {
@@ -947,6 +1028,10 @@ Usage:
947
1028
  }
948
1029
  case '/loop': {
949
1030
  // Parse /loop "<prompt>" [--max-iterations N] [--completion-promise "text"]
1031
+ if (ctx.loopActive) {
1032
+ ctx.addMessage('system', 'Loop already running. Use /breakloop to stop it first.');
1033
+ break;
1034
+ }
950
1035
  const loopArgs = parts.slice(1).join(' ');
951
1036
  const maxIterMatch = loopArgs.match(/--max-iterations\s+(\d+)/);
952
1037
  const completionMatch = loopArgs.match(/--completion-promise\s+"([^"]+)"/);
@@ -963,23 +1048,28 @@ Usage:
963
1048
  Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE"`);
964
1049
  break;
965
1050
  }
1051
+ const defaultMaxIterations = resolveIterationLimit(config.get('maxIterations'));
1052
+ const loopMaxIterations = maxIterMatch
1053
+ ? resolveIterationLimit(parseInt(maxIterMatch[1], 10))
1054
+ : defaultMaxIterations;
966
1055
  // Start the loop
967
1056
  ctx.setLoopActive(true);
968
1057
  ctx.setLoopPrompt(prompt);
969
- ctx.setLoopMaxIterations(maxIterMatch ? parseInt(maxIterMatch[1], 10) : 100);
1058
+ ctx.setLoopMaxIterations(loopMaxIterations);
970
1059
  ctx.setLoopCompletionPromise(completionMatch ? completionMatch[1] : undefined);
971
1060
  ctx.setLoopIteration(0);
972
1061
  ctx.loopCancelledRef.current = false;
973
1062
  ctx.addMessage('system', `\u{1F504} Agent Loop Started
974
1063
  Prompt: "${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}"
975
- Max iterations: ${maxIterMatch ? maxIterMatch[1] : '100'}
976
- ${completionMatch ? `Completion promise: "${completionMatch[1]}"` : 'No completion promise (runs until max iterations)'}
977
- Use /cancel-loop to stop`);
1064
+ Max iterations: ${formatIterationLimit(loopMaxIterations)}
1065
+ ${completionMatch ? `Completion promise: "${completionMatch[1]}"` : isFiniteIterationLimit(loopMaxIterations) ? 'No completion promise (runs until max iterations)' : 'No completion promise (runs until stopped)'}
1066
+ Use /breakloop to stop`);
978
1067
  // Start the loop execution (non-blocking)
979
- ctx.runLoop(prompt, maxIterMatch ? parseInt(maxIterMatch[1], 10) : 100, completionMatch?.[1]);
1068
+ ctx.runLoop(prompt, loopMaxIterations, completionMatch?.[1]);
980
1069
  break;
981
1070
  }
982
1071
  case '/cancel-loop':
1072
+ case '/breakloop':
983
1073
  case '/stop':
984
1074
  if (ctx.loopActive) {
985
1075
  ctx.loopCancelledRef.current = true;
@@ -1272,7 +1362,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1272
1362
  case '/branch': {
1273
1363
  const branching = await import('../branching.js');
1274
1364
  const subCmd = parts[1];
1275
- const sessionId = ctx.sessionRef.current?.id || `session_${Date.now()}`;
1365
+ const sessionId = ctx.sessionRef.current?.id || storage.createSessionId();
1276
1366
  if (subCmd === 'list' || !subCmd) {
1277
1367
  const tree = branching.getBranchTree(sessionId);
1278
1368
  ctx.addMessage('system', `Branches:\n${tree}`);
@@ -1399,7 +1489,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1399
1489
  const newComp = getCurrentCompanion();
1400
1490
  if (newComp.name === subCmd) {
1401
1491
  config.set('activeCompanion', subCmd);
1402
- ctx.llmMessages.current = [{ role: 'system', content: getSystemPrompt(ctx.persona) }];
1492
+ ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
1403
1493
  ctx.addMessage('system', `Companion set to: ${subCmd} \u2014 "${newComp.greeting}"`);
1404
1494
  }
1405
1495
  else {
@@ -1480,7 +1570,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1480
1570
  : pack.companions.immersive;
1481
1571
  config.set('activeCompanion', companion.name);
1482
1572
  // Reset LLM system prompt to use the companion's persona
1483
- ctx.llmMessages.current = [{ role: 'system', content: getSystemPrompt(ctx.persona) }];
1573
+ ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
1484
1574
  ctx.addMessage('system', `Theme pack: ${subCmd}\n` +
1485
1575
  ` Skin: ${pack.skin.name}, Palette: ${pack.palette.name}, Companion: ${companion.name}\n` +
1486
1576
  ` "${companion.greeting}"`);
@@ -1499,7 +1589,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1499
1589
  const pack = getCurrentPack();
1500
1590
  config.set('companionIntensity', 'professional');
1501
1591
  config.set('activeCompanion', pack.companions.professional.name);
1502
- ctx.llmMessages.current = [{ role: 'system', content: getSystemPrompt(ctx.persona) }];
1592
+ ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
1503
1593
  ctx.addMessage('system', `Switched to professional mode — ${pack.companions.professional.description}`);
1504
1594
  }
1505
1595
  else {
@@ -1512,7 +1602,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1512
1602
  const pack = getCurrentPack();
1513
1603
  config.set('companionIntensity', 'immersive');
1514
1604
  config.set('activeCompanion', pack.companions.immersive.name);
1515
- ctx.llmMessages.current = [{ role: 'system', content: getSystemPrompt(ctx.persona) }];
1605
+ ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
1516
1606
  ctx.addMessage('system', `Switched to immersive mode — ${pack.companions.immersive.description}`);
1517
1607
  }
1518
1608
  else {
@@ -1768,7 +1858,22 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1768
1858
  if (session) {
1769
1859
  const savedMessages = storage.loadMessageHistory();
1770
1860
  const savedCount = savedMessages ? savedMessages.length : 0;
1771
- ctx.addMessage('system', `Session: ${session.projectName}\nID: ${session.id}\nCreated: ${new Date(session.createdAt).toLocaleString()}\nMessages: ${session.messageCount}\nSaved LLM messages: ${savedCount}`);
1861
+ const ledgerTotals = ctx.ledger?.getTotals();
1862
+ const latestRun = ctx.ledger?.getLatestRun();
1863
+ const lines = [
1864
+ `Session: ${session.projectName}`,
1865
+ `ID: ${session.id}`,
1866
+ `Created: ${new Date(session.createdAt).toLocaleString()}`,
1867
+ `Messages: ${session.messageCount}`,
1868
+ `Saved LLM messages: ${savedCount}`,
1869
+ ];
1870
+ if (ledgerTotals) {
1871
+ lines.push(`Iterations logged: ${ledgerTotals.iterations}`, `Failed approaches: ${ctx.ledger?.getFailedApproachCount() ?? 0}`);
1872
+ }
1873
+ if (latestRun) {
1874
+ lines.push(`Latest run: ${formatLedgerRun(latestRun)}`);
1875
+ }
1876
+ ctx.addMessage('system', lines.join('\n'));
1772
1877
  }
1773
1878
  else {
1774
1879
  ctx.addMessage('system', 'No active session.');
@@ -1782,6 +1887,9 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1782
1887
  else {
1783
1888
  // Save current messages before forking
1784
1889
  storage.saveMessageHistory(ctx.llmMessages.current);
1890
+ if (ctx.ledger) {
1891
+ storage.saveIterationLedger(ctx.ledger);
1892
+ }
1785
1893
  const forked = storage.forkSession(session.projectPath);
1786
1894
  if (forked) {
1787
1895
  ctx.sessionRef.current = forked;
@@ -1794,12 +1902,89 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1794
1902
  }
1795
1903
  else if (parts[1] === 'save') {
1796
1904
  storage.saveMessageHistory(ctx.llmMessages.current);
1797
- ctx.addMessage('system', `Saved ${ctx.llmMessages.current.length} LLM messages to session.`);
1905
+ if (ctx.ledger) {
1906
+ storage.saveIterationLedger(ctx.ledger);
1907
+ }
1908
+ ctx.addMessage('system', `Saved ${ctx.llmMessages.current.length} LLM messages and current log state to session.`);
1798
1909
  }
1799
1910
  else {
1800
1911
  ctx.addMessage('system', 'Usage: /session [list|info|fork|save] or just /sessions');
1801
1912
  }
1802
1913
  break;
1914
+ case '/log': {
1915
+ if (!ctx.ledger) {
1916
+ ctx.addMessage('system', 'No session log available.');
1917
+ break;
1918
+ }
1919
+ const subCmd = parts[1] || 'summary';
1920
+ if (subCmd === 'summary') {
1921
+ const totals = ctx.ledger.getTotals();
1922
+ const runs = ctx.ledger.getRuns(5);
1923
+ const allFailures = ctx.ledger.getFailedApproaches();
1924
+ const failures = allFailures.slice(-5);
1925
+ const lines = [
1926
+ 'Session Log',
1927
+ `Iterations: ${totals.iterations}`,
1928
+ `Failed approaches: ${ctx.ledger.getFailedApproachCount()}`,
1929
+ `Tokens: ${totals.totalTokens}`,
1930
+ `Cost: $${totals.totalCost.toFixed(4)}`,
1931
+ `Duration: ${formatLedgerDuration(totals.totalDurationMs)}`,
1932
+ ];
1933
+ if (runs.length > 0) {
1934
+ lines.push('', 'Recent runs:');
1935
+ for (const run of runs) {
1936
+ lines.push(` - ${formatLedgerRun(run)}`);
1937
+ }
1938
+ }
1939
+ if (failures.length > 0) {
1940
+ lines.push('', 'Recent failures:');
1941
+ for (const failure of failures) {
1942
+ lines.push(` - #${failure.iteration} ${failure.description} — ${failure.reason}`);
1943
+ }
1944
+ }
1945
+ ctx.addMessage('system', lines.join('\n'));
1946
+ }
1947
+ else if (subCmd === 'tail') {
1948
+ const limit = parts[2] ? parseInt(parts[2], 10) : 10;
1949
+ if (isNaN(limit) || limit <= 0 || limit > 100) {
1950
+ ctx.addMessage('error', 'Usage: /log tail [1-100]');
1951
+ break;
1952
+ }
1953
+ const entries = ctx.ledger.getEntries().slice(-limit);
1954
+ if (entries.length === 0) {
1955
+ ctx.addMessage('system', 'No logged iterations yet.');
1956
+ break;
1957
+ }
1958
+ const lines = ['Recent iterations:'];
1959
+ for (const entry of entries) {
1960
+ const actions = entry.actions.length > 0
1961
+ ? entry.actions.map(action => `${action.tool}(${action.args})${action.result === 'error' ? ' FAILED' : action.result === 'blocked' ? ' BLOCKED' : ''}`).join(', ')
1962
+ : 'no tool actions';
1963
+ lines.push(` #${entry.iteration} [${entry.outcome}] ${formatLedgerDuration(entry.durationMs)} — ${actions}`);
1964
+ }
1965
+ ctx.addMessage('system', lines.join('\n'));
1966
+ }
1967
+ else if (subCmd === 'failures') {
1968
+ const failures = ctx.ledger.getFailedApproaches();
1969
+ if (failures.length === 0) {
1970
+ ctx.addMessage('system', 'No failed approaches recorded.');
1971
+ break;
1972
+ }
1973
+ const lines = ['Failed approaches:'];
1974
+ for (const failure of failures.slice(-10)) {
1975
+ lines.push(` - #${failure.iteration} ${failure.description} — ${failure.reason}`);
1976
+ }
1977
+ ctx.addMessage('system', lines.join('\n'));
1978
+ }
1979
+ else if (subCmd === 'reset') {
1980
+ ctx.ledger.reset();
1981
+ ctx.addMessage('system', 'Session log reset.');
1982
+ }
1983
+ else {
1984
+ ctx.addMessage('system', 'Usage: /log [summary|tail [N]|failures|reset]');
1985
+ }
1986
+ break;
1987
+ }
1803
1988
  case '/todo': {
1804
1989
  const subCommand = parts[1];
1805
1990
  if (subCommand === 'add' && parts.length > 2) {
@@ -2371,6 +2556,20 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2371
2556
  // Resume a session by loading saved LLM message history
2372
2557
  // Usage: /resume [sessionId] - resume a specific session, or current session if no ID
2373
2558
  const targetSessionId = parts[1];
2559
+ if (targetSessionId) {
2560
+ const resumedSession = storage.setCurrentSessionById(targetSessionId);
2561
+ if (!resumedSession) {
2562
+ ctx.addMessage('system', `Session not found: ${targetSessionId}`);
2563
+ break;
2564
+ }
2565
+ ctx.sessionRef.current = resumedSession;
2566
+ }
2567
+ if (ctx.ledger) {
2568
+ ctx.ledger.loadSnapshot(storage.loadIterationLedger(targetSessionId || ctx.sessionRef.current?.id));
2569
+ if (ctx.sessionRef.current?.id) {
2570
+ storage.saveIterationLedger(ctx.ledger, ctx.sessionRef.current.id);
2571
+ }
2572
+ }
2374
2573
  // Try loading full message history first (preferred - preserves tool calls etc.)
2375
2574
  const savedMessages = storage.loadMessageHistory(targetSessionId);
2376
2575
  if (savedMessages && savedMessages.length > 0) {
@@ -2384,11 +2583,16 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2384
2583
  }
2385
2584
  else {
2386
2585
  // Fall back to chat.log history (legacy format, user/assistant only)
2387
- const history = storage.getChatHistory(20);
2586
+ const history = storage.getChatHistory(20, targetSessionId);
2388
2587
  if (history.length === 0) {
2389
2588
  ctx.addMessage('system', 'No previous messages to resume. Start a conversation first, messages are auto-saved.');
2390
2589
  }
2391
2590
  else {
2591
+ ctx.llmMessages.current.length = 0;
2592
+ ctx.llmMessages.current.push({
2593
+ role: 'system',
2594
+ content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)),
2595
+ });
2392
2596
  for (const msg of history) {
2393
2597
  if (msg.role === 'user' || msg.role === 'assistant') {
2394
2598
  ctx.llmMessages.current.push({
@@ -2523,6 +2727,7 @@ Usage: /smart [on|off|cost <0-1>|test <message>]
2523
2727
  case '/swarm':
2524
2728
  case '/council': {
2525
2729
  const subCmd = parts[1];
2730
+ const swarmCwd = ctx.sessionRef.current?.projectPath ?? process.cwd();
2526
2731
  if (!ctx.agtermEnabled) {
2527
2732
  ctx.addMessage('system', 'Agents mode not enabled. Start with --agents flag to use agent swarms.');
2528
2733
  break;
@@ -2603,7 +2808,7 @@ Usage: /smart [on|off|cost <0-1>|test <message>]
2603
2808
  try {
2604
2809
  let session;
2605
2810
  if (template) {
2606
- session = await councilManager.startFromTemplate(template, cleanPrompt);
2811
+ session = await councilManager.startFromTemplate(template, cleanPrompt, swarmCwd);
2607
2812
  }
2608
2813
  else {
2609
2814
  const { randomUUID } = await import('crypto');
@@ -2612,8 +2817,9 @@ Usage: /smart [on|off|cost <0-1>|test <message>]
2612
2817
  { id: randomUUID(), name: 'Agent B', agent: 'claude', weight: 1.0 },
2613
2818
  { id: randomUUID(), name: 'Agent C', agent: 'claude', weight: 1.0 },
2614
2819
  ];
2615
- session = await councilManager.startCouncil(cleanPrompt, { mode, members });
2820
+ session = await councilManager.startCouncil(cleanPrompt, { mode, members }, swarmCwd);
2616
2821
  }
2822
+ watchAsyncLedgerRun(ctx.ledger, 'council', cleanPrompt, () => councilManager.getSession(session.id));
2617
2823
  ctx.addMessage('system', `\u2713 Swarm coordination started: ${session.id.slice(0, 8)}\nMode: ${session.config.mode}\nAgents: ${session.config.members.map(m => m.name).join(', ')}\n\nUse /swarm coord status ${session.id.slice(0, 8)} to check progress.`);
2618
2824
  }
2619
2825
  catch (err) {
@@ -2674,7 +2880,8 @@ Options:
2674
2880
  cleanPrompt = cleanPrompt.replace(aggMatch[0], '').trim();
2675
2881
  }
2676
2882
  try {
2677
- const session = await swarmManager.startSwarm(cleanPrompt, { decomposition: strategy, aggregation });
2883
+ const session = await swarmManager.startSwarm(cleanPrompt, { decomposition: strategy, aggregation }, swarmCwd);
2884
+ watchAsyncLedgerRun(ctx.ledger, 'swarm', cleanPrompt, () => swarmManager.getSession(session.id));
2678
2885
  ctx.addMessage('system', `\u2713 Swarm started: ${session.id.slice(0, 8)}\nStrategy: ${strategy} \u2192 ${aggregation}\nStatus: ${session.status}\n\nUse /swarm status ${session.id.slice(0, 8)} to check progress.`);
2679
2886
  }
2680
2887
  catch (err) {
@@ -2858,14 +3065,16 @@ Requires --agents flag.`);
2858
3065
  runJob(bgJob.id, async (prompt, signal) => {
2859
3066
  const { chat } = await import('../providers/index.js');
2860
3067
  const { TOOLS } = await import('../tools.js');
2861
- const { getSystemPrompt: getSysPrompt } = await import('../types.js');
3068
+ const { executeTool: execTool } = await import('../tools.js');
3069
+ const cwd = ctx.sessionRef.current?.projectPath || process.cwd();
3070
+ const maxIterations = resolveIterationLimit(config.get('maxIterations'));
2862
3071
  const bgMessages = [
2863
- { role: 'system', content: getSysPrompt(ctx.persona) },
3072
+ { role: 'system', content: buildFullSystemPrompt(ctx.persona, cwd) },
2864
3073
  { role: 'user', content: prompt },
2865
3074
  ];
2866
3075
  let iterations = 0;
2867
3076
  let lastContent = '';
2868
- while (iterations < 20 && !signal.aborted) {
3077
+ while (!signal.aborted && iterations < maxIterations) {
2869
3078
  iterations++;
2870
3079
  const response = await chat(ctx.provider, bgMessages, TOOLS, ctx.model);
2871
3080
  if (!response.toolCalls?.length) {
@@ -2873,12 +3082,18 @@ Requires --agents flag.`);
2873
3082
  break;
2874
3083
  }
2875
3084
  bgMessages.push({ role: 'assistant', content: response.content, toolCalls: response.toolCalls });
2876
- const { executeTool: execTool } = await import('../tools.js');
2877
3085
  for (const tc of response.toolCalls) {
2878
- const result = await execTool(tc, process.cwd());
3086
+ const result = await execTool(tc, cwd);
2879
3087
  bgMessages.push({ role: 'tool', content: result.result, toolCallId: tc.id });
3088
+ if (signal.aborted)
3089
+ break;
2880
3090
  }
2881
3091
  }
3092
+ if (signal.aborted) {
3093
+ const error = new Error('Background job cancelled');
3094
+ error.name = 'AbortError';
3095
+ throw error;
3096
+ }
2882
3097
  return { result: lastContent, iterations };
2883
3098
  }).then(completed => {
2884
3099
  ctx.addMessage('system', `Background job ${completed.id} ${completed.status}: ${completed.result?.slice(0, 200) || completed.error || 'done'}`);