@calliopelabs/cli 2.2.0 → 2.5.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 (244) hide show
  1. package/README.md +17 -0
  2. package/dist/agents/agent-config-loader.js +1 -1
  3. package/dist/agents/agent-config-presets.js +13 -13
  4. package/dist/agents/agent-config-presets.js.map +1 -1
  5. package/dist/agents/agent-config-types.d.ts +1 -1
  6. package/dist/agents/agent-config-types.d.ts.map +1 -1
  7. package/dist/agents/council-types.d.ts +2 -0
  8. package/dist/agents/council-types.d.ts.map +1 -1
  9. package/dist/agents/council-types.js.map +1 -1
  10. package/dist/agents/council.d.ts +5 -0
  11. package/dist/agents/council.d.ts.map +1 -1
  12. package/dist/agents/council.js +150 -14
  13. package/dist/agents/council.js.map +1 -1
  14. package/dist/agents/dynamic-tools.d.ts.map +1 -1
  15. package/dist/agents/dynamic-tools.js +39 -10
  16. package/dist/agents/dynamic-tools.js.map +1 -1
  17. package/dist/agents/orchestrator.d.ts +7 -0
  18. package/dist/agents/orchestrator.d.ts.map +1 -1
  19. package/dist/agents/orchestrator.js +48 -7
  20. package/dist/agents/orchestrator.js.map +1 -1
  21. package/dist/agents/sdk-backend.js +1 -1
  22. package/dist/agents/sdk-backend.js.map +1 -1
  23. package/dist/agents/swarm-types.d.ts +1 -0
  24. package/dist/agents/swarm-types.d.ts.map +1 -1
  25. package/dist/agents/swarm.d.ts +5 -0
  26. package/dist/agents/swarm.d.ts.map +1 -1
  27. package/dist/agents/swarm.js +85 -17
  28. package/dist/agents/swarm.js.map +1 -1
  29. package/dist/agents/types.d.ts +1 -0
  30. package/dist/agents/types.d.ts.map +1 -1
  31. package/dist/agents/types.js.map +1 -1
  32. package/dist/api-server.d.ts +9 -0
  33. package/dist/api-server.d.ts.map +1 -1
  34. package/dist/api-server.js +75 -4
  35. package/dist/api-server.js.map +1 -1
  36. package/dist/auto-checkpoint.d.ts.map +1 -1
  37. package/dist/auto-checkpoint.js +50 -17
  38. package/dist/auto-checkpoint.js.map +1 -1
  39. package/dist/auto-compressor.d.ts +14 -0
  40. package/dist/auto-compressor.d.ts.map +1 -1
  41. package/dist/auto-compressor.js +67 -10
  42. package/dist/auto-compressor.js.map +1 -1
  43. package/dist/background-jobs.d.ts.map +1 -1
  44. package/dist/background-jobs.js +8 -3
  45. package/dist/background-jobs.js.map +1 -1
  46. package/dist/bin.d.ts +8 -0
  47. package/dist/bin.d.ts.map +1 -1
  48. package/dist/bin.js +63 -4
  49. package/dist/bin.js.map +1 -1
  50. package/dist/branching.d.ts.map +1 -1
  51. package/dist/branching.js +14 -1
  52. package/dist/branching.js.map +1 -1
  53. package/dist/checkpoint.d.ts.map +1 -1
  54. package/dist/checkpoint.js +13 -1
  55. package/dist/checkpoint.js.map +1 -1
  56. package/dist/cli/agent.d.ts.map +1 -1
  57. package/dist/cli/agent.js +130 -62
  58. package/dist/cli/agent.js.map +1 -1
  59. package/dist/cli/commands.d.ts.map +1 -1
  60. package/dist/cli/commands.js +218 -11
  61. package/dist/cli/commands.js.map +1 -1
  62. package/dist/cli/index.d.ts.map +1 -1
  63. package/dist/cli/index.js +55 -6
  64. package/dist/cli/index.js.map +1 -1
  65. package/dist/cli/types.d.ts +3 -0
  66. package/dist/cli/types.d.ts.map +1 -1
  67. package/dist/cli/types.js +2 -1
  68. package/dist/cli/types.js.map +1 -1
  69. package/dist/config.d.ts +1 -0
  70. package/dist/config.d.ts.map +1 -1
  71. package/dist/config.js +15 -3
  72. package/dist/config.js.map +1 -1
  73. package/dist/diff.d.ts.map +1 -1
  74. package/dist/diff.js +42 -4
  75. package/dist/diff.js.map +1 -1
  76. package/dist/env-expansion.d.ts +15 -0
  77. package/dist/env-expansion.d.ts.map +1 -0
  78. package/dist/env-expansion.js +43 -0
  79. package/dist/env-expansion.js.map +1 -0
  80. package/dist/errors.d.ts.map +1 -1
  81. package/dist/errors.js +30 -3
  82. package/dist/errors.js.map +1 -1
  83. package/dist/headless.d.ts.map +1 -1
  84. package/dist/headless.js +59 -4
  85. package/dist/headless.js.map +1 -1
  86. package/dist/hooks.d.ts +8 -2
  87. package/dist/hooks.d.ts.map +1 -1
  88. package/dist/hooks.js +97 -11
  89. package/dist/hooks.js.map +1 -1
  90. package/dist/idle-eviction.d.ts.map +1 -1
  91. package/dist/idle-eviction.js +8 -1
  92. package/dist/idle-eviction.js.map +1 -1
  93. package/dist/iteration-ledger.d.ts +111 -2
  94. package/dist/iteration-ledger.d.ts.map +1 -1
  95. package/dist/iteration-ledger.js +327 -19
  96. package/dist/iteration-ledger.js.map +1 -1
  97. package/dist/iteration-limit.d.ts +5 -0
  98. package/dist/iteration-limit.d.ts.map +1 -0
  99. package/dist/iteration-limit.js +17 -0
  100. package/dist/iteration-limit.js.map +1 -0
  101. package/dist/markdown.d.ts.map +1 -1
  102. package/dist/markdown.js +32 -10
  103. package/dist/markdown.js.map +1 -1
  104. package/dist/mcp.d.ts +35 -5
  105. package/dist/mcp.d.ts.map +1 -1
  106. package/dist/mcp.js +191 -12
  107. package/dist/mcp.js.map +1 -1
  108. package/dist/memory.d.ts.map +1 -1
  109. package/dist/memory.js +4 -9
  110. package/dist/memory.js.map +1 -1
  111. package/dist/model-detection.d.ts +14 -1
  112. package/dist/model-detection.d.ts.map +1 -1
  113. package/dist/model-detection.js +307 -114
  114. package/dist/model-detection.js.map +1 -1
  115. package/dist/model-router.js +7 -7
  116. package/dist/model-router.js.map +1 -1
  117. package/dist/parallel-tools.d.ts +9 -1
  118. package/dist/parallel-tools.d.ts.map +1 -1
  119. package/dist/parallel-tools.js +6 -5
  120. package/dist/parallel-tools.js.map +1 -1
  121. package/dist/plugins.d.ts +37 -0
  122. package/dist/plugins.d.ts.map +1 -1
  123. package/dist/plugins.js +87 -0
  124. package/dist/plugins.js.map +1 -1
  125. package/dist/prevent-sleep.d.ts +10 -0
  126. package/dist/prevent-sleep.d.ts.map +1 -0
  127. package/dist/prevent-sleep.js +85 -0
  128. package/dist/prevent-sleep.js.map +1 -0
  129. package/dist/providers/anthropic.d.ts.map +1 -1
  130. package/dist/providers/anthropic.js +36 -2
  131. package/dist/providers/anthropic.js.map +1 -1
  132. package/dist/providers/bedrock.d.ts.map +1 -1
  133. package/dist/providers/bedrock.js +81 -17
  134. package/dist/providers/bedrock.js.map +1 -1
  135. package/dist/providers/compat.d.ts.map +1 -1
  136. package/dist/providers/compat.js +21 -6
  137. package/dist/providers/compat.js.map +1 -1
  138. package/dist/providers/index.d.ts.map +1 -1
  139. package/dist/providers/index.js +2 -0
  140. package/dist/providers/index.js.map +1 -1
  141. package/dist/providers/openai-compat-shims.d.ts +31 -0
  142. package/dist/providers/openai-compat-shims.d.ts.map +1 -0
  143. package/dist/providers/openai-compat-shims.js +179 -0
  144. package/dist/providers/openai-compat-shims.js.map +1 -0
  145. package/dist/providers/openai.d.ts.map +1 -1
  146. package/dist/providers/types.d.ts.map +1 -1
  147. package/dist/providers/types.js +19 -10
  148. package/dist/providers/types.js.map +1 -1
  149. package/dist/risk.d.ts.map +1 -1
  150. package/dist/risk.js +15 -5
  151. package/dist/risk.js.map +1 -1
  152. package/dist/sandbox-native.d.ts +1 -0
  153. package/dist/sandbox-native.d.ts.map +1 -1
  154. package/dist/sandbox-native.js +37 -5
  155. package/dist/sandbox-native.js.map +1 -1
  156. package/dist/scope.d.ts +10 -0
  157. package/dist/scope.d.ts.map +1 -1
  158. package/dist/scope.js +75 -15
  159. package/dist/scope.js.map +1 -1
  160. package/dist/scuttlebot/client.d.ts +83 -0
  161. package/dist/scuttlebot/client.d.ts.map +1 -0
  162. package/dist/scuttlebot/client.js +350 -0
  163. package/dist/scuttlebot/client.js.map +1 -0
  164. package/dist/scuttlebot/config.d.ts +28 -0
  165. package/dist/scuttlebot/config.d.ts.map +1 -0
  166. package/dist/scuttlebot/config.js +91 -0
  167. package/dist/scuttlebot/config.js.map +1 -0
  168. package/dist/scuttlebot/http-client.d.ts +63 -0
  169. package/dist/scuttlebot/http-client.d.ts.map +1 -0
  170. package/dist/scuttlebot/http-client.js +124 -0
  171. package/dist/scuttlebot/http-client.js.map +1 -0
  172. package/dist/scuttlebot/index.d.ts +13 -0
  173. package/dist/scuttlebot/index.d.ts.map +1 -0
  174. package/dist/scuttlebot/index.js +10 -0
  175. package/dist/scuttlebot/index.js.map +1 -0
  176. package/dist/scuttlebot/irc-client.d.ts +124 -0
  177. package/dist/scuttlebot/irc-client.d.ts.map +1 -0
  178. package/dist/scuttlebot/irc-client.js +599 -0
  179. package/dist/scuttlebot/irc-client.js.map +1 -0
  180. package/dist/skills.d.ts +19 -0
  181. package/dist/skills.d.ts.map +1 -1
  182. package/dist/skills.js +98 -10
  183. package/dist/skills.js.map +1 -1
  184. package/dist/smart-router.js +4 -4
  185. package/dist/smart-router.js.map +1 -1
  186. package/dist/storage.d.ts +18 -3
  187. package/dist/storage.d.ts.map +1 -1
  188. package/dist/storage.js +182 -14
  189. package/dist/storage.js.map +1 -1
  190. package/dist/tools.d.ts.map +1 -1
  191. package/dist/tools.js +233 -39
  192. package/dist/tools.js.map +1 -1
  193. package/dist/trust.d.ts +16 -3
  194. package/dist/trust.d.ts.map +1 -1
  195. package/dist/trust.js +23 -4
  196. package/dist/trust.js.map +1 -1
  197. package/dist/types.d.ts.map +1 -1
  198. package/dist/types.js +18 -12
  199. package/dist/types.js.map +1 -1
  200. package/dist/ui/agent.d.ts +1 -1
  201. package/dist/ui/agent.d.ts.map +1 -1
  202. package/dist/ui/agent.js +175 -121
  203. package/dist/ui/agent.js.map +1 -1
  204. package/dist/ui/chat-input.d.ts +3 -1
  205. package/dist/ui/chat-input.d.ts.map +1 -1
  206. package/dist/ui/chat-input.js +82 -17
  207. package/dist/ui/chat-input.js.map +1 -1
  208. package/dist/ui/commands.d.ts +4 -0
  209. package/dist/ui/commands.d.ts.map +1 -1
  210. package/dist/ui/commands.js +562 -39
  211. package/dist/ui/commands.js.map +1 -1
  212. package/dist/ui/completions.d.ts.map +1 -1
  213. package/dist/ui/completions.js +2 -0
  214. package/dist/ui/completions.js.map +1 -1
  215. package/dist/ui/index.d.ts.map +1 -1
  216. package/dist/ui/index.js +288 -60
  217. package/dist/ui/index.js.map +1 -1
  218. package/dist/ui/input-utils.d.ts +20 -0
  219. package/dist/ui/input-utils.d.ts.map +1 -0
  220. package/dist/ui/input-utils.js +35 -0
  221. package/dist/ui/input-utils.js.map +1 -0
  222. package/dist/ui/messages.d.ts +6 -2
  223. package/dist/ui/messages.d.ts.map +1 -1
  224. package/dist/ui/messages.js +42 -11
  225. package/dist/ui/messages.js.map +1 -1
  226. package/dist/ui/modals.d.ts +21 -1
  227. package/dist/ui/modals.d.ts.map +1 -1
  228. package/dist/ui/modals.js +67 -5
  229. package/dist/ui/modals.js.map +1 -1
  230. package/dist/ui/status-bar.d.ts +4 -1
  231. package/dist/ui/status-bar.d.ts.map +1 -1
  232. package/dist/ui/status-bar.js +12 -1
  233. package/dist/ui/status-bar.js.map +1 -1
  234. package/dist/ui/types.d.ts +3 -0
  235. package/dist/ui/types.d.ts.map +1 -1
  236. package/package.json +4 -7
  237. package/dist/completion.d.ts +0 -75
  238. package/dist/completion.d.ts.map +0 -1
  239. package/dist/completion.js +0 -234
  240. package/dist/completion.js.map +0 -1
  241. package/dist/keyboard.d.ts +0 -57
  242. package/dist/keyboard.d.ts.map +0 -1
  243. package/dist/keyboard.js +0 -265
  244. package/dist/keyboard.js.map +0 -1
@@ -21,6 +21,7 @@ import { getAgentStatusReport, swarmManager, councilManager, COUNCIL_TEMPLATES,
21
21
  import { smartRoute, getDefaultSmartRoutingConfig, detectTaskType } from '../smart-router.js';
22
22
  import { getCurrentSkin, getCurrentPalette, applySkin, applyPalette, listSkins, listPalettes } from '../hud/api.js';
23
23
  import { getCurrentCompanion, applyCompanion, listCompanions, getMoodText } from '../companions.js';
24
+ import { scuttlebotClient } from '../scuttlebot/index.js';
24
25
  import { createJob, runJob, cancelJob, getJob, formatJob, formatJobsList, clearFinishedJobs } from '../background-jobs.js';
25
26
  import { listRecordings, loadRecording, formatRecording, deleteRecording } from '../terminal-recording.js';
26
27
  import { startApiServer, stopApiServer, isApiServerRunning } from '../api-server.js';
@@ -28,6 +29,74 @@ import { getTerminalImageInfo, getImageModeLabel, renderSkinBanner, renderTransi
28
29
  import { applyThemePack, listThemePacks, getCurrentPack, getCompanionMode, setCompanionMode, getThemePack } from '../hud/theme-packs/api.js';
29
30
  import { getModelContextLimit } from '../model-detection.js';
30
31
  import { resetContextWarnings } from './context.js';
32
+ import * as memory from '../memory.js';
33
+ import { resolveIterationLimit, formatIterationLimit, isFiniteIterationLimit, } from '../iteration-limit.js';
34
+ // Builds the full system prompt including memory context (project + global).
35
+ // dir should be the project directory for the active/resumed session.
36
+ function buildFullSystemPrompt(persona, dir) {
37
+ const base = getSystemPrompt(persona);
38
+ const mem = memory.buildMemoryContext(dir);
39
+ return mem.trim() ? base + '\n\n--- Project Context ---\n' + mem : base;
40
+ }
41
+ function getActiveProjectDir(ctx) {
42
+ return ctx.sessionRef.current?.projectPath ?? process.cwd();
43
+ }
44
+ function formatLedgerDuration(durationMs) {
45
+ if (durationMs < 1000)
46
+ return `${durationMs}ms`;
47
+ if (durationMs < 60_000)
48
+ return `${(durationMs / 1000).toFixed(1)}s`;
49
+ return `${(durationMs / 60_000).toFixed(1)}m`;
50
+ }
51
+ function formatSessionLogLimit(limit) {
52
+ return limit > 0 ? String(limit) : 'unlimited';
53
+ }
54
+ function formatLedgerRun(run) {
55
+ const iterationCount = Math.max(0, (run.entryCountAtEnd ?? run.entryCountAtStart) - run.entryCountAtStart);
56
+ const parts = [`${run.kind}`, `[${run.status}]`];
57
+ if (iterationCount > 0) {
58
+ parts.push(`${iterationCount} iteration${iterationCount === 1 ? '' : 's'}`);
59
+ }
60
+ if (run.maxIterations != null && Number.isFinite(run.maxIterations)) {
61
+ parts.push(`max ${run.maxIterations}`);
62
+ }
63
+ else if (run.maxIterations === null) {
64
+ parts.push('unlimited');
65
+ }
66
+ const prompt = run.prompt.length > 90 ? `${run.prompt.slice(0, 90)}...` : run.prompt;
67
+ let line = `${parts.join(' ')} — ${prompt}`;
68
+ if (run.errorSummary) {
69
+ line += ` (${run.errorSummary})`;
70
+ }
71
+ return line;
72
+ }
73
+ function watchAsyncLedgerRun(ledger, kind, prompt, getStatus) {
74
+ if (!ledger)
75
+ return;
76
+ const runId = ledger.startRun(kind, prompt);
77
+ void (async () => {
78
+ for (;;) {
79
+ const current = getStatus();
80
+ if (!current) {
81
+ ledger.finishRun(runId, 'failed', { errorSummary: 'Run state no longer available' });
82
+ return;
83
+ }
84
+ if (current.status === 'completed') {
85
+ ledger.finishRun(runId, 'completed');
86
+ return;
87
+ }
88
+ if (current.status === 'failed') {
89
+ ledger.finishRun(runId, 'failed', { errorSummary: current.error });
90
+ return;
91
+ }
92
+ if (current.status === 'cancelled') {
93
+ ledger.finishRun(runId, 'cancelled', { errorSummary: current.error || 'Cancelled' });
94
+ return;
95
+ }
96
+ await new Promise(resolve => setTimeout(resolve, 500));
97
+ }
98
+ })();
99
+ }
31
100
  // ============================================================================
32
101
  // handleCommand
33
102
  // ============================================================================
@@ -45,7 +114,7 @@ export async function handleCommand(cmd, ctx) {
45
114
  /persona [name] - Switch personality
46
115
  /route [on|off] - Auto model routing (/autoroute)
47
116
  /smart [on|off|cost|test] - Cross-provider smart routing
48
- /breaker [status|resume] - Circuit breaker control (/cb)
117
+ /breaker [status|adjust] - Circuit breaker control (/cb)
49
118
 
50
119
  --- Conversation ---
51
120
  /edit - Edit and resend last message
@@ -57,6 +126,7 @@ export async function handleCommand(cmd, ctx) {
57
126
 
58
127
  --- Session & State ---
59
128
  /session [list|info|fork|save] - Session management (/sessions)
129
+ /log [summary|tail|failures|reset] - Iteration/run log
60
130
  /resume [sessionId] - Resume session (restores full context)
61
131
  /checkpoint [list|clear] - File checkpoints (/cp)
62
132
  /restore <path> [index] - Restore file from checkpoint
@@ -103,8 +173,8 @@ export async function handleCommand(cmd, ctx) {
103
173
  --- Multi-Agent ---
104
174
  /agents - Sub-agent status (--agents mode)
105
175
  /swarm [start|coord|status] - Agent swarms & coordination
106
- /loop [prompt] [n] - Iterative agent loop
107
- /cancel-loop - Stop running loop (/stop)
176
+ /loop [prompt] - Iterative agent loop (default: unlimited)
177
+ /cancel-loop - Stop running loop (/stop, /breakloop)
108
178
 
109
179
  --- System ---
110
180
  /status - Show status (/s)
@@ -125,9 +195,17 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
125
195
  case '/providers':
126
196
  case '/p':
127
197
  if (parts[1]) {
128
- const p = parts[1].toLowerCase();
129
- ctx.setProvider(p);
130
- ctx.addMessage('system', `Provider: ${selectProvider(p)}`);
198
+ const requested = parts[1].toLowerCase();
199
+ const available = getAvailableProviders();
200
+ if (!available.includes(requested)) {
201
+ ctx.addMessage('error', `Provider "${requested}" is not configured. Run /provider (no args) for an interactive picker with setup.`);
202
+ break;
203
+ }
204
+ ctx.setProvider(requested);
205
+ ctx.addMessage('system', `Provider: ${selectProvider(requested)}`);
206
+ }
207
+ else if (ctx.openProviderPicker) {
208
+ ctx.openProviderPicker();
131
209
  }
132
210
  else {
133
211
  ctx.addMessage('system', `Provider: ${ctx.actualProvider} | Available: ${getAvailableProviders().join(', ')}`);
@@ -157,7 +235,7 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
157
235
  else {
158
236
  ctx.addMessage('system', `Fetching models for ${ctx.actualProvider}...`);
159
237
  try {
160
- const models = await getAvailableModels(ctx.actualProvider);
238
+ const models = await getAvailableModels(ctx.actualProvider, { throwOnError: true });
161
239
  if (models.length > 0) {
162
240
  ctx.setAvailableModels(models);
163
241
  ctx.setModalMode('model');
@@ -174,7 +252,7 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
174
252
  case '/models':
175
253
  ctx.addMessage('system', `Fetching models for ${ctx.actualProvider}...`);
176
254
  try {
177
- const models = await getAvailableModels(ctx.actualProvider);
255
+ const models = await getAvailableModels(ctx.actualProvider, { throwOnError: true });
178
256
  if (models.length > 0) {
179
257
  ctx.setAvailableModels(models);
180
258
  ctx.setModalMode('model');
@@ -202,7 +280,7 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
202
280
  if (parts[1] && ['calliope', 'muse', 'minimal'].includes(parts[1])) {
203
281
  const p = parts[1];
204
282
  ctx.setPersona(p);
205
- ctx.llmMessages.current = [{ role: 'system', content: getSystemPrompt(p) }];
283
+ ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(p, getActiveProjectDir(ctx)) }];
206
284
  ctx.addMessage('system', `Persona: ${p}`);
207
285
  }
208
286
  else {
@@ -212,7 +290,8 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
212
290
  case '/clear':
213
291
  case '/c':
214
292
  ctx.setMessages([]);
215
- ctx.llmMessages.current = [{ role: 'system', content: getSystemPrompt(ctx.persona) }];
293
+ ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
294
+ ctx.ledger?.reset();
216
295
  ctx.setStats({ inputTokens: 0, outputTokens: 0, cost: 0, messageCount: 0 });
217
296
  resetContextWarnings(); // Reset context warning state
218
297
  break;
@@ -340,11 +419,95 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
340
419
  case '/status':
341
420
  case '/s': {
342
421
  const imgInfo = getTerminalImageInfo();
343
- ctx.addMessage('system', `${ctx.actualProvider}:${ctx.actualModel} | ${ctx.stats.messageCount} msgs | ${ctx.stats.inputTokens + ctx.stats.outputTokens} tokens | terminal: ${getImageModeLabel(imgInfo.mode)}${imgInfo.truecolor ? ' (truecolor)' : ''} ${imgInfo.width}cols`);
422
+ let statusMsg = `${ctx.actualProvider}:${ctx.actualModel} | ${ctx.stats.messageCount} msgs | ${ctx.stats.inputTokens + ctx.stats.outputTokens} tokens | terminal: ${getImageModeLabel(imgInfo.mode)}${imgInfo.truecolor ? ' (truecolor)' : ''} ${imgInfo.width}cols`;
423
+ // Add scuttlebot status if enabled
424
+ if (scuttlebotClient.isEnabled()) {
425
+ const sbStatus = scuttlebotClient.getStatus();
426
+ statusMsg += `\nScuttlebot: enabled (${sbStatus.nick}) | irc:${sbStatus.config?.ircAddr} | #${sbStatus.config?.channel}`;
427
+ }
428
+ ctx.addMessage('system', statusMsg);
429
+ break;
430
+ }
431
+ case '/scuttlebot': {
432
+ const subCmd = parts[1];
433
+ const sbStatus = scuttlebotClient.getStatus();
434
+ // /scuttlebot enable - enable mid-session
435
+ if (subCmd === 'enable') {
436
+ if (sbStatus.enabled) {
437
+ ctx.addMessage('system', 'Scuttlebot is already enabled.');
438
+ break;
439
+ }
440
+ // Initialize scuttlebot — config is loaded from ~/.config/scuttlebot-relay.env,
441
+ // process.env, and .scuttlebot.yaml inside initialize()
442
+ const sessionId = ctx.sessionRef.current?.id || 'default';
443
+ const cwd = getActiveProjectDir(ctx);
444
+ scuttlebotClient.initialize(sessionId, cwd).then(async (enabled) => {
445
+ if (enabled) {
446
+ const status = scuttlebotClient.getStatus();
447
+ let msg = '✓ Scuttlebot enabled!\n';
448
+ msg += ` Nick: ${status.nick}\n`;
449
+ msg += ` IRC: ${status.config?.ircAddr}\n`;
450
+ msg += ` Channel: #${status.config?.channel}`;
451
+ if (status.config?.channels && status.config.channels.length > 1) {
452
+ msg += `\n Channels: ${status.config.channels.map((c) => '#' + c).join(', ')}`;
453
+ }
454
+ ctx.addMessage('system', msg);
455
+ // Post online status and start routing IRC instructions
456
+ await scuttlebotClient.postOnline();
457
+ ctx.startScuttlebotPolling();
458
+ }
459
+ else {
460
+ ctx.addMessage('system', 'Failed to enable scuttlebot');
461
+ }
462
+ }).catch((err) => {
463
+ ctx.addMessage('system', `Failed to enable scuttlebot: ${err instanceof Error ? err.message : String(err)}`);
464
+ });
465
+ break;
466
+ }
467
+ // /scuttlebot disable - disable mid-session
468
+ if (subCmd === 'disable') {
469
+ if (!sbStatus.enabled) {
470
+ ctx.addMessage('system', 'Scuttlebot is not enabled.');
471
+ break;
472
+ }
473
+ scuttlebotClient.postOffline().then(() => {
474
+ return scuttlebotClient.disconnect();
475
+ }).then(() => {
476
+ ctx.addMessage('system', 'Scuttlebot disabled');
477
+ }).catch((err) => {
478
+ ctx.addMessage('system', `Error disabling scuttlebot: ${err instanceof Error ? err.message : String(err)}`);
479
+ });
480
+ break;
481
+ }
482
+ // Show status
483
+ if (!sbStatus.enabled) {
484
+ ctx.addMessage('system', 'Scuttlebot not enabled.\n\nRun: /scuttlebot enable\n\nConfig is loaded automatically from ~/.config/scuttlebot-relay.env\nChannel is read from .scuttlebot.yaml');
485
+ break;
486
+ }
487
+ let statusText = 'Scuttlebot Status\n────────────────────────────────────────\n';
488
+ statusText += `Enabled: yes\n`;
489
+ statusText += `Nick: ${sbStatus.nick}\n`;
490
+ statusText += `IRC: ${sbStatus.config?.ircAddr}\n`;
491
+ statusText += `Channel: #${sbStatus.config?.channel}\n`;
492
+ statusText += `Connected: ${sbStatus.connected ? 'yes' : 'no'}`;
493
+ if (sbStatus.config?.channels && sbStatus.config.channels.length > 1) {
494
+ statusText += `\nChannels: ${sbStatus.config.channels.map((c) => '#' + c).join(', ')}`;
495
+ }
496
+ statusText += '\n\nCommands:\n /scuttlebot <message> Post a message\n /scuttlebot disable Disable integration';
497
+ ctx.addMessage('system', statusText);
498
+ // Allow manual message posting
499
+ if (subCmd && subCmd !== 'enable' && subCmd !== 'disable') {
500
+ const message = parts.slice(1).join(' ');
501
+ scuttlebotClient.postMessage(message).then(() => {
502
+ ctx.addMessage('system', 'Message posted to scuttlebot.');
503
+ }).catch((err) => {
504
+ ctx.addMessage('system', `Failed to post message: ${err instanceof Error ? err.message : String(err)}`);
505
+ });
506
+ }
344
507
  break;
345
508
  }
346
509
  case '/config':
347
- ctx.addMessage('system', `Config: ${config.getConfigPath()}\nProviders: ${config.getConfiguredProviders().join(', ') || 'none'}\nmaxIterations: ${config.get('maxIterations')}`);
510
+ 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
511
  break;
349
512
  case '/agents': {
350
513
  if (!ctx.agtermEnabled) {
@@ -797,6 +960,7 @@ Edit the YAML to customize members, strategy, and coordination settings.`);
797
960
  ctx.addMessage('system', `Usage: /set <key> <value>
798
961
  Available keys:
799
962
  maxIterations <number> - Max agent iterations (current: ${config.get('maxIterations')})
963
+ sessionLogLimit <number> - Cap retained session log items (current: ${formatSessionLogLimit(config.get('sessionLogLimit'))}, 0 = unlimited)
800
964
  persona <name> - calliope, muse, minimal
801
965
  fancyOutput <bool> - true/false`);
802
966
  break;
@@ -804,12 +968,22 @@ Available keys:
804
968
  try {
805
969
  if (key === 'maxIterations') {
806
970
  const num = parseInt(value, 10);
807
- if (isNaN(num) || num < 1 || num > 10000) {
808
- ctx.addMessage('error', 'maxIterations must be 1-10000');
971
+ if (isNaN(num) || num < 0 || num > 1000000) {
972
+ ctx.addMessage('error', 'maxIterations must be 0-1000000 (0 = unlimited)');
809
973
  break;
810
974
  }
811
975
  config.set('maxIterations', num);
812
- ctx.addMessage('system', `\u2713 maxIterations set to ${num}`);
976
+ ctx.addMessage('system', `\u2713 maxIterations set to ${formatIterationLimit(resolveIterationLimit(num))}`);
977
+ }
978
+ else if (key === 'sessionLogLimit') {
979
+ const num = parseInt(value, 10);
980
+ if (isNaN(num) || num < 0 || num > 100000) {
981
+ ctx.addMessage('error', 'sessionLogLimit must be 0-100000 (0 = unlimited)');
982
+ break;
983
+ }
984
+ config.set('sessionLogLimit', num);
985
+ ctx.ledger?.setRetentionLimit(num);
986
+ ctx.addMessage('system', `\u2713 sessionLogLimit set to ${num === 0 ? 'unlimited (set > 0 to cap)' : num}`);
813
987
  }
814
988
  else if (key === 'persona') {
815
989
  if (!['calliope', 'muse', 'minimal'].includes(value)) {
@@ -947,6 +1121,10 @@ Usage:
947
1121
  }
948
1122
  case '/loop': {
949
1123
  // Parse /loop "<prompt>" [--max-iterations N] [--completion-promise "text"]
1124
+ if (ctx.loopActive) {
1125
+ ctx.addMessage('system', 'Loop already running. Use /breakloop to stop it first.');
1126
+ break;
1127
+ }
950
1128
  const loopArgs = parts.slice(1).join(' ');
951
1129
  const maxIterMatch = loopArgs.match(/--max-iterations\s+(\d+)/);
952
1130
  const completionMatch = loopArgs.match(/--completion-promise\s+"([^"]+)"/);
@@ -963,23 +1141,28 @@ Usage:
963
1141
  Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE"`);
964
1142
  break;
965
1143
  }
1144
+ const defaultMaxIterations = resolveIterationLimit(config.get('maxIterations'));
1145
+ const loopMaxIterations = maxIterMatch
1146
+ ? resolveIterationLimit(parseInt(maxIterMatch[1], 10))
1147
+ : defaultMaxIterations;
966
1148
  // Start the loop
967
1149
  ctx.setLoopActive(true);
968
1150
  ctx.setLoopPrompt(prompt);
969
- ctx.setLoopMaxIterations(maxIterMatch ? parseInt(maxIterMatch[1], 10) : 100);
1151
+ ctx.setLoopMaxIterations(loopMaxIterations);
970
1152
  ctx.setLoopCompletionPromise(completionMatch ? completionMatch[1] : undefined);
971
1153
  ctx.setLoopIteration(0);
972
1154
  ctx.loopCancelledRef.current = false;
973
1155
  ctx.addMessage('system', `\u{1F504} Agent Loop Started
974
1156
  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`);
1157
+ Max iterations: ${formatIterationLimit(loopMaxIterations)}
1158
+ ${completionMatch ? `Completion promise: "${completionMatch[1]}"` : isFiniteIterationLimit(loopMaxIterations) ? 'No completion promise (runs until max iterations)' : 'No completion promise (runs until stopped)'}
1159
+ Use /breakloop to stop`);
978
1160
  // Start the loop execution (non-blocking)
979
- ctx.runLoop(prompt, maxIterMatch ? parseInt(maxIterMatch[1], 10) : 100, completionMatch?.[1]);
1161
+ ctx.runLoop(prompt, loopMaxIterations, completionMatch?.[1]);
980
1162
  break;
981
1163
  }
982
1164
  case '/cancel-loop':
1165
+ case '/breakloop':
983
1166
  case '/stop':
984
1167
  if (ctx.loopActive) {
985
1168
  ctx.loopCancelledRef.current = true;
@@ -1272,7 +1455,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1272
1455
  case '/branch': {
1273
1456
  const branching = await import('../branching.js');
1274
1457
  const subCmd = parts[1];
1275
- const sessionId = ctx.sessionRef.current?.id || `session_${Date.now()}`;
1458
+ const sessionId = ctx.sessionRef.current?.id || storage.createSessionId();
1276
1459
  if (subCmd === 'list' || !subCmd) {
1277
1460
  const tree = branching.getBranchTree(sessionId);
1278
1461
  ctx.addMessage('system', `Branches:\n${tree}`);
@@ -1399,7 +1582,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1399
1582
  const newComp = getCurrentCompanion();
1400
1583
  if (newComp.name === subCmd) {
1401
1584
  config.set('activeCompanion', subCmd);
1402
- ctx.llmMessages.current = [{ role: 'system', content: getSystemPrompt(ctx.persona) }];
1585
+ ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
1403
1586
  ctx.addMessage('system', `Companion set to: ${subCmd} \u2014 "${newComp.greeting}"`);
1404
1587
  }
1405
1588
  else {
@@ -1480,7 +1663,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1480
1663
  : pack.companions.immersive;
1481
1664
  config.set('activeCompanion', companion.name);
1482
1665
  // Reset LLM system prompt to use the companion's persona
1483
- ctx.llmMessages.current = [{ role: 'system', content: getSystemPrompt(ctx.persona) }];
1666
+ ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
1484
1667
  ctx.addMessage('system', `Theme pack: ${subCmd}\n` +
1485
1668
  ` Skin: ${pack.skin.name}, Palette: ${pack.palette.name}, Companion: ${companion.name}\n` +
1486
1669
  ` "${companion.greeting}"`);
@@ -1499,7 +1682,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1499
1682
  const pack = getCurrentPack();
1500
1683
  config.set('companionIntensity', 'professional');
1501
1684
  config.set('activeCompanion', pack.companions.professional.name);
1502
- ctx.llmMessages.current = [{ role: 'system', content: getSystemPrompt(ctx.persona) }];
1685
+ ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
1503
1686
  ctx.addMessage('system', `Switched to professional mode — ${pack.companions.professional.description}`);
1504
1687
  }
1505
1688
  else {
@@ -1512,7 +1695,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1512
1695
  const pack = getCurrentPack();
1513
1696
  config.set('companionIntensity', 'immersive');
1514
1697
  config.set('activeCompanion', pack.companions.immersive.name);
1515
- ctx.llmMessages.current = [{ role: 'system', content: getSystemPrompt(ctx.persona) }];
1698
+ ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
1516
1699
  ctx.addMessage('system', `Switched to immersive mode — ${pack.companions.immersive.description}`);
1517
1700
  }
1518
1701
  else {
@@ -1768,7 +1951,22 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1768
1951
  if (session) {
1769
1952
  const savedMessages = storage.loadMessageHistory();
1770
1953
  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}`);
1954
+ const ledgerTotals = ctx.ledger?.getTotals();
1955
+ const latestRun = ctx.ledger?.getLatestRun();
1956
+ const lines = [
1957
+ `Session: ${session.projectName}`,
1958
+ `ID: ${session.id}`,
1959
+ `Created: ${new Date(session.createdAt).toLocaleString()}`,
1960
+ `Messages: ${session.messageCount}`,
1961
+ `Saved LLM messages: ${savedCount}`,
1962
+ ];
1963
+ if (ledgerTotals) {
1964
+ lines.push(`Iterations logged: ${ledgerTotals.iterations}`, `Failed approaches: ${ctx.ledger?.getFailedApproachCount() ?? 0}`);
1965
+ }
1966
+ if (latestRun) {
1967
+ lines.push(`Latest run: ${formatLedgerRun(latestRun)}`);
1968
+ }
1969
+ ctx.addMessage('system', lines.join('\n'));
1772
1970
  }
1773
1971
  else {
1774
1972
  ctx.addMessage('system', 'No active session.');
@@ -1782,6 +1980,9 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1782
1980
  else {
1783
1981
  // Save current messages before forking
1784
1982
  storage.saveMessageHistory(ctx.llmMessages.current);
1983
+ if (ctx.ledger) {
1984
+ storage.saveIterationLedger(ctx.ledger);
1985
+ }
1785
1986
  const forked = storage.forkSession(session.projectPath);
1786
1987
  if (forked) {
1787
1988
  ctx.sessionRef.current = forked;
@@ -1794,12 +1995,89 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1794
1995
  }
1795
1996
  else if (parts[1] === 'save') {
1796
1997
  storage.saveMessageHistory(ctx.llmMessages.current);
1797
- ctx.addMessage('system', `Saved ${ctx.llmMessages.current.length} LLM messages to session.`);
1998
+ if (ctx.ledger) {
1999
+ storage.saveIterationLedger(ctx.ledger);
2000
+ }
2001
+ ctx.addMessage('system', `Saved ${ctx.llmMessages.current.length} LLM messages and current log state to session.`);
1798
2002
  }
1799
2003
  else {
1800
2004
  ctx.addMessage('system', 'Usage: /session [list|info|fork|save] or just /sessions');
1801
2005
  }
1802
2006
  break;
2007
+ case '/log': {
2008
+ if (!ctx.ledger) {
2009
+ ctx.addMessage('system', 'No session log available.');
2010
+ break;
2011
+ }
2012
+ const subCmd = parts[1] || 'summary';
2013
+ if (subCmd === 'summary') {
2014
+ const totals = ctx.ledger.getTotals();
2015
+ const runs = ctx.ledger.getRuns(5);
2016
+ const allFailures = ctx.ledger.getFailedApproaches();
2017
+ const failures = allFailures.slice(-5);
2018
+ const lines = [
2019
+ 'Session Log',
2020
+ `Iterations: ${totals.iterations}`,
2021
+ `Failed approaches: ${ctx.ledger.getFailedApproachCount()}`,
2022
+ `Tokens: ${totals.totalTokens}`,
2023
+ `Cost: $${totals.totalCost.toFixed(4)}`,
2024
+ `Duration: ${formatLedgerDuration(totals.totalDurationMs)}`,
2025
+ ];
2026
+ if (runs.length > 0) {
2027
+ lines.push('', 'Recent runs:');
2028
+ for (const run of runs) {
2029
+ lines.push(` - ${formatLedgerRun(run)}`);
2030
+ }
2031
+ }
2032
+ if (failures.length > 0) {
2033
+ lines.push('', 'Recent failures:');
2034
+ for (const failure of failures) {
2035
+ lines.push(` - #${failure.iteration} ${failure.description} — ${failure.reason}`);
2036
+ }
2037
+ }
2038
+ ctx.addMessage('system', lines.join('\n'));
2039
+ }
2040
+ else if (subCmd === 'tail') {
2041
+ const limit = parts[2] ? parseInt(parts[2], 10) : 10;
2042
+ if (isNaN(limit) || limit <= 0 || limit > 100) {
2043
+ ctx.addMessage('error', 'Usage: /log tail [1-100]');
2044
+ break;
2045
+ }
2046
+ const entries = ctx.ledger.getEntries().slice(-limit);
2047
+ if (entries.length === 0) {
2048
+ ctx.addMessage('system', 'No logged iterations yet.');
2049
+ break;
2050
+ }
2051
+ const lines = ['Recent iterations:'];
2052
+ for (const entry of entries) {
2053
+ const actions = entry.actions.length > 0
2054
+ ? entry.actions.map(action => `${action.tool}(${action.args})${action.result === 'error' ? ' FAILED' : action.result === 'blocked' ? ' BLOCKED' : ''}`).join(', ')
2055
+ : 'no tool actions';
2056
+ lines.push(` #${entry.iteration} [${entry.outcome}] ${formatLedgerDuration(entry.durationMs)} — ${actions}`);
2057
+ }
2058
+ ctx.addMessage('system', lines.join('\n'));
2059
+ }
2060
+ else if (subCmd === 'failures') {
2061
+ const failures = ctx.ledger.getFailedApproaches();
2062
+ if (failures.length === 0) {
2063
+ ctx.addMessage('system', 'No failed approaches recorded.');
2064
+ break;
2065
+ }
2066
+ const lines = ['Failed approaches:'];
2067
+ for (const failure of failures.slice(-10)) {
2068
+ lines.push(` - #${failure.iteration} ${failure.description} — ${failure.reason}`);
2069
+ }
2070
+ ctx.addMessage('system', lines.join('\n'));
2071
+ }
2072
+ else if (subCmd === 'reset') {
2073
+ ctx.ledger.reset();
2074
+ ctx.addMessage('system', 'Session log reset.');
2075
+ }
2076
+ else {
2077
+ ctx.addMessage('system', 'Usage: /log [summary|tail [N]|failures|reset]');
2078
+ }
2079
+ break;
2080
+ }
1803
2081
  case '/todo': {
1804
2082
  const subCommand = parts[1];
1805
2083
  if (subCommand === 'add' && parts.length > 2) {
@@ -2371,6 +2649,20 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2371
2649
  // Resume a session by loading saved LLM message history
2372
2650
  // Usage: /resume [sessionId] - resume a specific session, or current session if no ID
2373
2651
  const targetSessionId = parts[1];
2652
+ if (targetSessionId) {
2653
+ const resumedSession = storage.setCurrentSessionById(targetSessionId);
2654
+ if (!resumedSession) {
2655
+ ctx.addMessage('system', `Session not found: ${targetSessionId}`);
2656
+ break;
2657
+ }
2658
+ ctx.sessionRef.current = resumedSession;
2659
+ }
2660
+ if (ctx.ledger) {
2661
+ ctx.ledger.loadSnapshot(storage.loadIterationLedger(targetSessionId || ctx.sessionRef.current?.id));
2662
+ if (ctx.sessionRef.current?.id) {
2663
+ storage.saveIterationLedger(ctx.ledger, ctx.sessionRef.current.id);
2664
+ }
2665
+ }
2374
2666
  // Try loading full message history first (preferred - preserves tool calls etc.)
2375
2667
  const savedMessages = storage.loadMessageHistory(targetSessionId);
2376
2668
  if (savedMessages && savedMessages.length > 0) {
@@ -2384,11 +2676,16 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2384
2676
  }
2385
2677
  else {
2386
2678
  // Fall back to chat.log history (legacy format, user/assistant only)
2387
- const history = storage.getChatHistory(20);
2679
+ const history = storage.getChatHistory(20, targetSessionId);
2388
2680
  if (history.length === 0) {
2389
2681
  ctx.addMessage('system', 'No previous messages to resume. Start a conversation first, messages are auto-saved.');
2390
2682
  }
2391
2683
  else {
2684
+ ctx.llmMessages.current.length = 0;
2685
+ ctx.llmMessages.current.push({
2686
+ role: 'system',
2687
+ content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)),
2688
+ });
2392
2689
  for (const msg of history) {
2393
2690
  if (msg.role === 'user' || msg.role === 'assistant') {
2394
2691
  ctx.llmMessages.current.push({
@@ -2442,19 +2739,234 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2442
2739
  }
2443
2740
  else if (subCmd === 'off') {
2444
2741
  config.set('circuitBreakersEnabled', false);
2445
- ctx.addMessage('system', '\u2713 Circuit breakers disabled (will take effect on next agent run)');
2742
+ // Also disable the current circuit breaker instance if it exists
2743
+ if (ctx.circuitBreaker) {
2744
+ ctx.circuitBreaker = undefined;
2745
+ ctx.setBreakerHealth?.('ok');
2746
+ }
2747
+ ctx.addMessage('system', '\u2713 Circuit breakers disabled');
2446
2748
  }
2447
2749
  else if (subCmd === 'on') {
2448
2750
  config.set('circuitBreakersEnabled', true);
2449
2751
  ctx.addMessage('system', '\u2713 Circuit breakers enabled');
2450
2752
  }
2753
+ else if (subCmd === 'adjust') {
2754
+ const breakerTypeString = parts[2];
2755
+ const param = parts[3];
2756
+ const rawValue = parts[4];
2757
+ // Handle special 'list' command to show detailed parameter info
2758
+ if (breakerTypeString === 'list') {
2759
+ ctx.addMessage('system', `Circuit Breaker Configuration Reference:
2760
+
2761
+ 📊 REPEATED-FAILURE (Consecutive Errors)
2762
+ • maxConsecutiveErrors: Number of consecutive errors before tripping (default: 3)
2763
+ Example: /breaker adjust repeated-failure maxConsecutiveErrors 5
2764
+
2765
+ 💰 COST-RUNAWAY (Spending Control)
2766
+ • maxSessionCost: Maximum total cost per session in USD (default: $5.0)
2767
+ • maxCostPerMinute: Maximum spending rate per minute in USD (default: $1.0)
2768
+ • windowSizeMs: Sliding window size for rate calculation in milliseconds (default: 60000)
2769
+ Examples:
2770
+ /breaker adjust cost-runaway maxSessionCost 10.0
2771
+ /breaker adjust cost-runaway maxCostPerMinute 2.0
2772
+
2773
+ 🔄 INFINITE-LOOP (Repetitive Behavior)
2774
+ • maxIdenticalInWindow: Max identical tool calls in window before tripping (default: 3)
2775
+ • windowSize: Number of recent tool calls to analyze (default: 6)
2776
+ • detectOscillation: Detect A-B-A-B oscillation patterns (default: true)
2777
+ Examples:
2778
+ /breaker adjust infinite-loop maxIdenticalInWindow 5
2779
+ /breaker adjust infinite-loop detectOscillation false
2780
+
2781
+ 🔥 TOKEN-BURN (Token Usage Limits)
2782
+ • maxTokensPerIteration: Max tokens per single iteration (default: 200,000)
2783
+ • maxTotalTokens: Max total tokens per session (default: 5,000,000)
2784
+ Examples:
2785
+ /breaker adjust token-burn maxTokensPerIteration 100000
2786
+ /breaker adjust token-burn maxTotalTokens 1000000
2787
+
2788
+ ⏸️ STALL (Progress Detection)
2789
+ • maxIdleIterations: Max iterations with no tool calls/content (default: 5)
2790
+ Example: /breaker adjust stall maxIdleIterations 3
2791
+
2792
+ ⏰ WALL-CLOCK (Time Limits)
2793
+ • maxSessionDurationMs: Max session duration in milliseconds (0 = unlimited, default: 0)
2794
+ • maxIterationDurationMs: Max single iteration duration in milliseconds (default: 600000 = 10 min)
2795
+ Examples:
2796
+ /breaker adjust wall-clock maxSessionDurationMs 3600000 # 1 hour
2797
+ /breaker adjust wall-clock maxIterationDurationMs 300000 # 5 minutes
2798
+
2799
+ Quick Commands:
2800
+ /breaker adjust <type> - Show current settings for that type
2801
+ /breaker adjust - Show types overview
2802
+ /breaker status - Show current breaker states`);
2803
+ break;
2804
+ }
2805
+ // Show basic types overview if no type specified
2806
+ if (!breakerTypeString) {
2807
+ ctx.addMessage('system', `Circuit Breaker Types Available for Configuration:
2808
+
2809
+ repeated-failure - Consecutive errors before tripping
2810
+ cost-runaway - Spending rate and total cost limits
2811
+ infinite-loop - Identical tool calls and oscillation detection
2812
+ token-burn - Token usage per iteration and total limits
2813
+ stall - Idle iterations without progress
2814
+ wall-clock - Time-based session and iteration limits
2815
+
2816
+ Usage: /breaker adjust <type> [param] [value]
2817
+ /breaker adjust list - Show detailed parameter reference
2818
+
2819
+ Examples:
2820
+ /breaker adjust repeated-failure - Show current settings
2821
+ /breaker adjust cost-runaway maxSessionCost 10.0
2822
+ /breaker adjust infinite-loop detectOscillation true`);
2823
+ break;
2824
+ }
2825
+ // Cast to BreakerType and validate
2826
+ const breakerType = breakerTypeString;
2827
+ const validTypes = ['repeated-failure', 'cost-runaway', 'infinite-loop', 'token-burn', 'stall', 'wall-clock'];
2828
+ if (!validTypes.includes(breakerType)) {
2829
+ ctx.addMessage('error', `Invalid breaker type "${breakerType}". Valid types: ${validTypes.join(', ')}`);
2830
+ break;
2831
+ }
2832
+ const currentConfig = ctx.circuitBreaker.getConfig();
2833
+ const breakerConfig = currentConfig.breakers[breakerType];
2834
+ // Show current configuration if no param specified
2835
+ if (!param) {
2836
+ let configDisplay = `${breakerType} Circuit Breaker Settings:\n`;
2837
+ switch (breakerType) {
2838
+ case 'repeated-failure':
2839
+ configDisplay += ` maxConsecutiveErrors: ${breakerConfig.maxConsecutiveErrors} errors\n\nUsage: /breaker adjust repeated-failure <param> <value>\n /breaker adjust repeated-failure maxConsecutiveErrors <number>`;
2840
+ break;
2841
+ case 'cost-runaway':
2842
+ configDisplay += ` maxSessionCost: ${breakerConfig.maxSessionCost} per session\n maxCostPerMinute: ${breakerConfig.maxCostPerMinute} per minute\n windowSizeMs: ${breakerConfig.windowSizeMs}ms\n\nUsage: /breaker adjust cost-runaway <param> <value>\n /breaker adjust cost-runaway maxSessionCost <dollars>\n /breaker adjust cost-runaway maxCostPerMinute <dollars>\n /breaker adjust cost-runaway windowSizeMs <milliseconds>`;
2843
+ break;
2844
+ case 'infinite-loop':
2845
+ configDisplay += ` maxIdenticalInWindow: ${breakerConfig.maxIdenticalInWindow} calls\n windowSize: ${breakerConfig.windowSize} recent calls\n detectOscillation: ${breakerConfig.detectOscillation}\n\nUsage: /breaker adjust infinite-loop <param> <value>\n /breaker adjust infinite-loop maxIdenticalInWindow <number>\n /breaker adjust infinite-loop windowSize <number>\n /breaker adjust infinite-loop detectOscillation <true|false>`;
2846
+ break;
2847
+ case 'token-burn':
2848
+ configDisplay += ` maxTokensPerIteration: ${breakerConfig.maxTokensPerIteration.toLocaleString()} tokens\n maxTotalTokens: ${breakerConfig.maxTotalTokens.toLocaleString()} tokens\n\nUsage: /breaker adjust token-burn <param> <value>\n /breaker adjust token-burn maxTokensPerIteration <number>\n /breaker adjust token-burn maxTotalTokens <number>`;
2849
+ break;
2850
+ case 'stall':
2851
+ configDisplay += ` maxIdleIterations: ${breakerConfig.maxIdleIterations} iterations\n\nUsage: /breaker adjust stall <param> <value>\n /breaker adjust stall maxIdleIterations <number>`;
2852
+ break;
2853
+ case 'wall-clock':
2854
+ const sessionDuration = breakerConfig.maxSessionDurationMs;
2855
+ const iterationDuration = breakerConfig.maxIterationDurationMs;
2856
+ configDisplay += ` maxSessionDurationMs: ${sessionDuration === 0 ? 'unlimited' : sessionDuration + 'ms'}\n maxIterationDurationMs: ${iterationDuration}ms (${Math.round(iterationDuration / 60000)} minutes)\n\nUsage: /breaker adjust wall-clock <param> <value>\n /breaker adjust wall-clock maxSessionDurationMs <milliseconds>\n /breaker adjust wall-clock maxIterationDurationMs <milliseconds>`;
2857
+ break;
2858
+ }
2859
+ ctx.addMessage('system', configDisplay);
2860
+ break;
2861
+ }
2862
+ // Parse and validate the value
2863
+ let parsedValue;
2864
+ // Handle boolean parameters
2865
+ if (param === 'detectOscillation') {
2866
+ if (rawValue === 'true')
2867
+ parsedValue = true;
2868
+ else if (rawValue === 'false')
2869
+ parsedValue = false;
2870
+ else {
2871
+ ctx.addMessage('error', 'detectOscillation must be "true" or "false"');
2872
+ break;
2873
+ }
2874
+ }
2875
+ else {
2876
+ // Handle numeric parameters
2877
+ parsedValue = parseFloat(rawValue);
2878
+ if (isNaN(parsedValue) || parsedValue < 0) {
2879
+ ctx.addMessage('error', 'Value must be a non-negative number');
2880
+ break;
2881
+ }
2882
+ }
2883
+ // Validate parameter names for each breaker type
2884
+ const paramValidations = {
2885
+ 'repeated-failure': {
2886
+ params: ['maxConsecutiveErrors'],
2887
+ validate: (param, value) => value <= 0 ? 'maxConsecutiveErrors must be > 0' : null
2888
+ },
2889
+ 'cost-runaway': {
2890
+ params: ['maxSessionCost', 'maxCostPerMinute', 'windowSizeMs'],
2891
+ validate: (param, value) => value <= 0 ? `${param} must be > 0` : null
2892
+ },
2893
+ 'infinite-loop': {
2894
+ params: ['maxIdenticalInWindow', 'windowSize', 'detectOscillation'],
2895
+ validate: (param, value) => {
2896
+ if (param === 'detectOscillation')
2897
+ return null; // boolean is already validated above
2898
+ return value <= 0 ? `${param} must be > 0` : null;
2899
+ }
2900
+ },
2901
+ 'token-burn': {
2902
+ params: ['maxTokensPerIteration', 'maxTotalTokens'],
2903
+ validate: (param, value) => value <= 0 ? `${param} must be > 0` : null
2904
+ },
2905
+ 'stall': {
2906
+ params: ['maxIdleIterations'],
2907
+ validate: (param, value) => value <= 0 ? 'maxIdleIterations must be > 0' : null
2908
+ },
2909
+ 'wall-clock': {
2910
+ params: ['maxSessionDurationMs', 'maxIterationDurationMs'],
2911
+ validate: (param, value) => {
2912
+ if (param === 'maxSessionDurationMs' && value === 0)
2913
+ return null; // 0 = unlimited is valid
2914
+ return value < 0 ? `${param} cannot be negative` : null;
2915
+ }
2916
+ }
2917
+ };
2918
+ const validation = paramValidations[breakerType];
2919
+ if (!validation.params.includes(param)) {
2920
+ ctx.addMessage('error', `Invalid parameter "${param}" for ${breakerType}. Valid parameters: ${validation.params.join(', ')}`);
2921
+ break;
2922
+ }
2923
+ // Run custom validation if provided
2924
+ if (validation.validate) {
2925
+ const error = validation.validate(param, parsedValue);
2926
+ if (error) {
2927
+ ctx.addMessage('error', error);
2928
+ break;
2929
+ }
2930
+ }
2931
+ // Update the configuration
2932
+ const oldValue = breakerConfig[param];
2933
+ ctx.circuitBreaker.adjust(breakerType, { [param]: parsedValue });
2934
+ // Format the success message based on parameter type
2935
+ let formattedOld, formattedNew;
2936
+ if (param === 'detectOscillation') {
2937
+ formattedOld = String(oldValue);
2938
+ formattedNew = String(parsedValue);
2939
+ }
2940
+ else if (param.includes('Cost')) {
2941
+ formattedOld = `${oldValue}`;
2942
+ formattedNew = `${parsedValue}`;
2943
+ }
2944
+ else if (param.includes('Ms')) {
2945
+ formattedOld = oldValue === 0 ? 'unlimited' : `${oldValue}ms`;
2946
+ formattedNew = parsedValue === 0 ? 'unlimited' : `${parsedValue}ms`;
2947
+ }
2948
+ else if (param.includes('Tokens')) {
2949
+ formattedOld = oldValue.toLocaleString();
2950
+ formattedNew = parsedValue.toLocaleString();
2951
+ }
2952
+ else {
2953
+ formattedOld = String(oldValue);
2954
+ formattedNew = String(parsedValue);
2955
+ }
2956
+ ctx.addMessage('system', `✅ ${breakerType} ${param}: ${formattedOld} → ${formattedNew}`);
2957
+ }
2451
2958
  else {
2452
- ctx.addMessage('system', `Usage: /breaker [status|resume|reset|on|off]
2959
+ ctx.addMessage('system', `Usage: /breaker [status|resume|reset|adjust|on|off]
2453
2960
  /breaker resume [type] - Resume tripped breaker (half-open)
2454
2961
  /breaker reset [type] - Reset breaker to closed
2962
+ /breaker adjust [type] [param] [value] - Configure breaker thresholds
2455
2963
  /breaker on|off - Enable/disable circuit breakers
2456
2964
 
2457
- Breaker types: repeated-failure, cost-runaway, infinite-loop, token-burn, stall`);
2965
+ Breaker types: repeated-failure, cost-runaway, infinite-loop, token-burn, stall, wall-clock
2966
+
2967
+ Quick help:
2968
+ /breaker adjust - Show types overview
2969
+ /breaker adjust list - Show detailed parameter reference`);
2458
2970
  }
2459
2971
  break;
2460
2972
  }
@@ -2523,6 +3035,7 @@ Usage: /smart [on|off|cost <0-1>|test <message>]
2523
3035
  case '/swarm':
2524
3036
  case '/council': {
2525
3037
  const subCmd = parts[1];
3038
+ const swarmCwd = ctx.sessionRef.current?.projectPath ?? process.cwd();
2526
3039
  if (!ctx.agtermEnabled) {
2527
3040
  ctx.addMessage('system', 'Agents mode not enabled. Start with --agents flag to use agent swarms.');
2528
3041
  break;
@@ -2603,7 +3116,7 @@ Usage: /smart [on|off|cost <0-1>|test <message>]
2603
3116
  try {
2604
3117
  let session;
2605
3118
  if (template) {
2606
- session = await councilManager.startFromTemplate(template, cleanPrompt);
3119
+ session = await councilManager.startFromTemplate(template, cleanPrompt, swarmCwd);
2607
3120
  }
2608
3121
  else {
2609
3122
  const { randomUUID } = await import('crypto');
@@ -2612,8 +3125,9 @@ Usage: /smart [on|off|cost <0-1>|test <message>]
2612
3125
  { id: randomUUID(), name: 'Agent B', agent: 'claude', weight: 1.0 },
2613
3126
  { id: randomUUID(), name: 'Agent C', agent: 'claude', weight: 1.0 },
2614
3127
  ];
2615
- session = await councilManager.startCouncil(cleanPrompt, { mode, members });
3128
+ session = await councilManager.startCouncil(cleanPrompt, { mode, members }, swarmCwd);
2616
3129
  }
3130
+ watchAsyncLedgerRun(ctx.ledger, 'council', cleanPrompt, () => councilManager.getSession(session.id));
2617
3131
  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
3132
  }
2619
3133
  catch (err) {
@@ -2674,7 +3188,8 @@ Options:
2674
3188
  cleanPrompt = cleanPrompt.replace(aggMatch[0], '').trim();
2675
3189
  }
2676
3190
  try {
2677
- const session = await swarmManager.startSwarm(cleanPrompt, { decomposition: strategy, aggregation });
3191
+ const session = await swarmManager.startSwarm(cleanPrompt, { decomposition: strategy, aggregation }, swarmCwd);
3192
+ watchAsyncLedgerRun(ctx.ledger, 'swarm', cleanPrompt, () => swarmManager.getSession(session.id));
2678
3193
  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
3194
  }
2680
3195
  catch (err) {
@@ -2858,14 +3373,16 @@ Requires --agents flag.`);
2858
3373
  runJob(bgJob.id, async (prompt, signal) => {
2859
3374
  const { chat } = await import('../providers/index.js');
2860
3375
  const { TOOLS } = await import('../tools.js');
2861
- const { getSystemPrompt: getSysPrompt } = await import('../types.js');
3376
+ const { executeTool: execTool } = await import('../tools.js');
3377
+ const cwd = ctx.sessionRef.current?.projectPath || process.cwd();
3378
+ const maxIterations = resolveIterationLimit(config.get('maxIterations'));
2862
3379
  const bgMessages = [
2863
- { role: 'system', content: getSysPrompt(ctx.persona) },
3380
+ { role: 'system', content: buildFullSystemPrompt(ctx.persona, cwd) },
2864
3381
  { role: 'user', content: prompt },
2865
3382
  ];
2866
3383
  let iterations = 0;
2867
3384
  let lastContent = '';
2868
- while (iterations < 20 && !signal.aborted) {
3385
+ while (!signal.aborted && iterations < maxIterations) {
2869
3386
  iterations++;
2870
3387
  const response = await chat(ctx.provider, bgMessages, TOOLS, ctx.model);
2871
3388
  if (!response.toolCalls?.length) {
@@ -2873,12 +3390,18 @@ Requires --agents flag.`);
2873
3390
  break;
2874
3391
  }
2875
3392
  bgMessages.push({ role: 'assistant', content: response.content, toolCalls: response.toolCalls });
2876
- const { executeTool: execTool } = await import('../tools.js');
2877
3393
  for (const tc of response.toolCalls) {
2878
- const result = await execTool(tc, process.cwd());
3394
+ const result = await execTool(tc, cwd);
2879
3395
  bgMessages.push({ role: 'tool', content: result.result, toolCallId: tc.id });
3396
+ if (signal.aborted)
3397
+ break;
2880
3398
  }
2881
3399
  }
3400
+ if (signal.aborted) {
3401
+ const error = new Error('Background job cancelled');
3402
+ error.name = 'AbortError';
3403
+ throw error;
3404
+ }
2882
3405
  return { result: lastContent, iterations };
2883
3406
  }).then(completed => {
2884
3407
  ctx.addMessage('system', `Background job ${completed.id} ${completed.status}: ${completed.result?.slice(0, 200) || completed.error || 'done'}`);