helixmind 0.5.26 → 0.6.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 (103) hide show
  1. package/dist/cli/agent/loop.js +3 -3
  2. package/dist/cli/agent/loop.js.map +1 -1
  3. package/dist/cli/agent/permissions.d.ts.map +1 -1
  4. package/dist/cli/agent/permissions.js +27 -9
  5. package/dist/cli/agent/permissions.js.map +1 -1
  6. package/dist/cli/agent/sandbox.d.ts +2 -12
  7. package/dist/cli/agent/sandbox.d.ts.map +1 -1
  8. package/dist/cli/agent/sandbox.js +3 -124
  9. package/dist/cli/agent/sandbox.js.map +1 -1
  10. package/dist/cli/agent/security.d.ts +4 -0
  11. package/dist/cli/agent/security.d.ts.map +1 -0
  12. package/dist/cli/agent/security.js +7 -0
  13. package/dist/cli/agent/security.js.map +1 -0
  14. package/dist/cli/agent/shell/classifier.d.ts +9 -0
  15. package/dist/cli/agent/shell/classifier.d.ts.map +1 -0
  16. package/dist/cli/agent/shell/classifier.js +283 -0
  17. package/dist/cli/agent/shell/classifier.js.map +1 -0
  18. package/dist/cli/agent/shell/summary.d.ts +3 -0
  19. package/dist/cli/agent/shell/summary.d.ts.map +1 -0
  20. package/dist/cli/agent/shell/summary.js +45 -0
  21. package/dist/cli/agent/shell/summary.js.map +1 -0
  22. package/dist/cli/agent/shell/types.d.ts +14 -0
  23. package/dist/cli/agent/shell/types.d.ts.map +1 -0
  24. package/dist/cli/agent/shell/types.js +2 -0
  25. package/dist/cli/agent/shell/types.js.map +1 -0
  26. package/dist/cli/agent/shell/windows.d.ts +5 -0
  27. package/dist/cli/agent/shell/windows.d.ts.map +1 -0
  28. package/dist/cli/agent/shell/windows.js +113 -0
  29. package/dist/cli/agent/shell/windows.js.map +1 -0
  30. package/dist/cli/agent/tools/edit-file.js +1 -1
  31. package/dist/cli/agent/tools/edit-file.js.map +1 -1
  32. package/dist/cli/agent/tools/find.js +1 -1
  33. package/dist/cli/agent/tools/find.js.map +1 -1
  34. package/dist/cli/agent/tools/git-commit.js +7 -7
  35. package/dist/cli/agent/tools/git-commit.js.map +1 -1
  36. package/dist/cli/agent/tools/git-diff.js +3 -3
  37. package/dist/cli/agent/tools/git-diff.js.map +1 -1
  38. package/dist/cli/agent/tools/git-log.js +3 -3
  39. package/dist/cli/agent/tools/git-log.js.map +1 -1
  40. package/dist/cli/agent/tools/git-status.js +6 -6
  41. package/dist/cli/agent/tools/git-status.js.map +1 -1
  42. package/dist/cli/agent/tools/list-dir.js +3 -3
  43. package/dist/cli/agent/tools/list-dir.js.map +1 -1
  44. package/dist/cli/agent/tools/read-file.js +1 -1
  45. package/dist/cli/agent/tools/read-file.js.map +1 -1
  46. package/dist/cli/agent/tools/registry.d.ts +3 -0
  47. package/dist/cli/agent/tools/registry.d.ts.map +1 -1
  48. package/dist/cli/agent/tools/registry.js.map +1 -1
  49. package/dist/cli/agent/tools/run-command.js +18 -17
  50. package/dist/cli/agent/tools/run-command.js.map +1 -1
  51. package/dist/cli/agent/tools/search.js +2 -2
  52. package/dist/cli/agent/tools/search.js.map +1 -1
  53. package/dist/cli/agent/tools/write-file.js +1 -1
  54. package/dist/cli/agent/tools/write-file.js.map +1 -1
  55. package/dist/cli/bench/runner.d.ts.map +1 -1
  56. package/dist/cli/bench/runner.js +1 -0
  57. package/dist/cli/bench/runner.js.map +1 -1
  58. package/dist/cli/brain/web-chat-handler.d.ts.map +1 -1
  59. package/dist/cli/brain/web-chat-handler.js +1 -0
  60. package/dist/cli/brain/web-chat-handler.js.map +1 -1
  61. package/dist/cli/commands/chat.d.ts.map +1 -1
  62. package/dist/cli/commands/chat.js +270 -92
  63. package/dist/cli/commands/chat.js.map +1 -1
  64. package/dist/cli/config/store.d.ts +11 -0
  65. package/dist/cli/config/store.d.ts.map +1 -1
  66. package/dist/cli/config/store.js +13 -0
  67. package/dist/cli/config/store.js.map +1 -1
  68. package/dist/cli/sessions/session.d.ts +5 -0
  69. package/dist/cli/sessions/session.d.ts.map +1 -1
  70. package/dist/cli/sessions/session.js +2 -0
  71. package/dist/cli/sessions/session.js.map +1 -1
  72. package/dist/cli/sessions/tab-view.d.ts.map +1 -1
  73. package/dist/cli/sessions/tab-view.js +6 -0
  74. package/dist/cli/sessions/tab-view.js.map +1 -1
  75. package/dist/cli/ui/statusbar.d.ts +4 -0
  76. package/dist/cli/ui/statusbar.d.ts.map +1 -1
  77. package/dist/cli/ui/statusbar.js +14 -1
  78. package/dist/cli/ui/statusbar.js.map +1 -1
  79. package/dist/cli/worktree/git.d.ts +9 -0
  80. package/dist/cli/worktree/git.d.ts.map +1 -0
  81. package/dist/cli/worktree/git.js +108 -0
  82. package/dist/cli/worktree/git.js.map +1 -0
  83. package/dist/cli/worktree/manager.d.ts +12 -0
  84. package/dist/cli/worktree/manager.d.ts.map +1 -0
  85. package/dist/cli/worktree/manager.js +48 -0
  86. package/dist/cli/worktree/manager.js.map +1 -0
  87. package/dist/cli/worktree/policy.d.ts +5 -0
  88. package/dist/cli/worktree/policy.d.ts.map +1 -0
  89. package/dist/cli/worktree/policy.js +46 -0
  90. package/dist/cli/worktree/policy.js.map +1 -0
  91. package/dist/cli/worktree/runtime.d.ts +11 -0
  92. package/dist/cli/worktree/runtime.d.ts.map +1 -0
  93. package/dist/cli/worktree/runtime.js +27 -0
  94. package/dist/cli/worktree/runtime.js.map +1 -0
  95. package/dist/cli/worktree/session.d.ts +11 -0
  96. package/dist/cli/worktree/session.d.ts.map +1 -0
  97. package/dist/cli/worktree/session.js +22 -0
  98. package/dist/cli/worktree/session.js.map +1 -0
  99. package/dist/cli/worktree/types.d.ts +28 -0
  100. package/dist/cli/worktree/types.d.ts.map +1 -0
  101. package/dist/cli/worktree/types.js +2 -0
  102. package/dist/cli/worktree/types.js.map +1 -0
  103. package/package.json +1 -1
@@ -70,6 +70,8 @@ import { AGENT_IDENTITIES } from '../agent/plan-types.js';
70
70
  import { SwarmController } from '../agent/swarm.js';
71
71
  import { TaskOrchestrator } from '../jarvis/orchestrator.js';
72
72
  import { ParallelExecutor } from '../jarvis/parallel.js';
73
+ import { WorktreeManager } from '../worktree/manager.js';
74
+ import { prepareExecutionRuntime } from '../worktree/runtime.js';
73
75
  import chalk from 'chalk';
74
76
  const HELP_CATEGORIES = [
75
77
  {
@@ -171,6 +173,8 @@ const HELP_CATEGORIES = [
171
173
  { cmd: '/undo', label: '/undo', description: 'Undo file changes' },
172
174
  { cmd: '/diff', label: '/diff', description: 'Show uncommitted git changes' },
173
175
  { cmd: '/git', label: '/git', description: 'Show git branch & status' },
176
+ { cmd: '/worktree', label: '/worktree', description: 'Show isolated worktree status' },
177
+ { cmd: '/worktree auto', label: '/worktree auto', description: 'Enable isolated worktrees for risky runs' },
174
178
  { cmd: '/project', label: '/project', description: 'Show project info' },
175
179
  { cmd: '/export', label: '/export', description: 'Export spiral as ZIP' },
176
180
  ],
@@ -257,12 +261,14 @@ ${chalk.hex('#00cc66').bold(' Validation Matrix')}
257
261
  ${theme.primary('/validation strict'.padEnd(22))} ${theme.dim('Treat warnings as errors')}
258
262
  ${theme.primary('/validation stats'.padEnd(22))} ${theme.dim('Show validation statistics')}
259
263
 
260
- ${chalk.hex('#8a2be2').bold(' Code & Git')}
261
- ${theme.primary('/undo [n|list]'.padEnd(22))} ${theme.dim('Undo last n file changes (or list history)')}
262
- ${theme.primary('/diff'.padEnd(22))} ${theme.dim('Show all uncommitted git changes')}
263
- ${theme.primary('/git'.padEnd(22))} ${theme.dim('Show git branch & status')}
264
- ${theme.primary('/project'.padEnd(22))} ${theme.dim('Show detected project info')}
265
- ${theme.primary('/export [dir]'.padEnd(22))} ${theme.dim('Export spiral as ZIP archive')}
264
+ ${chalk.hex('#8a2be2').bold(' Code & Git')}
265
+ ${theme.primary('/undo [n|list]'.padEnd(22))} ${theme.dim('Undo last n file changes (or list history)')}
266
+ ${theme.primary('/diff'.padEnd(22))} ${theme.dim('Show all uncommitted git changes')}
267
+ ${theme.primary('/git'.padEnd(22))} ${theme.dim('Show git branch & status')}
268
+ ${theme.primary('/worktree'.padEnd(22))} ${theme.dim('Show isolated worktree status')}
269
+ ${theme.primary('/worktree auto'.padEnd(22))} ${theme.dim('Enable worktrees for risky agent runs')}
270
+ ${theme.primary('/project'.padEnd(22))} ${theme.dim('Show detected project info')}
271
+ ${theme.primary('/export [dir]'.padEnd(22))} ${theme.dim('Export spiral as ZIP archive')}
266
272
 
267
273
  ${chalk.hex('#6c757d').bold(' Navigation')}
268
274
  ${theme.primary('/exit /quit'.padEnd(22))} ${theme.dim('Exit HelixMind')}
@@ -274,6 +280,26 @@ export async function chatCommand(options) {
274
280
  const configDir = join(homedir(), '.helixmind');
275
281
  const store = new ConfigStore(configDir);
276
282
  let config = store.getAll();
283
+ const projectRoot = process.cwd();
284
+ const createWorktreeManager = () => new WorktreeManager(config.worktree);
285
+ const activeWorktreeRuntimes = new Map();
286
+ const prepareSessionRuntime = async (reason, sessionId, onStatus) => {
287
+ const runtime = await prepareExecutionRuntime(createWorktreeManager(), { reason, sessionId, projectRoot }, onStatus);
288
+ if (runtime.isolated) {
289
+ activeWorktreeRuntimes.set(sessionId, runtime);
290
+ }
291
+ let released = false;
292
+ return {
293
+ ...runtime,
294
+ release: async () => {
295
+ if (released)
296
+ return;
297
+ released = true;
298
+ activeWorktreeRuntimes.delete(sessionId);
299
+ await runtime.release();
300
+ },
301
+ };
302
+ };
277
303
  // Collect startup banner — written into scroll region AFTER screen activation
278
304
  // so content sits directly above the input frame (no gap).
279
305
  const startupBannerParts = [renderLogo(), renderVersion(VERSION) + '\n\n'];
@@ -1017,13 +1043,22 @@ export async function chatCommand(options) {
1017
1043
  autonomousMode = true;
1018
1044
  (async () => {
1019
1045
  const completed = [];
1046
+ const runtime = await prepareSessionRuntime('autonomous', bgSession.id, (message) => {
1047
+ bgSession.capture(message);
1048
+ });
1049
+ if (runtime.worktree) {
1050
+ bgSession.worktree = {
1051
+ root: runtime.executionRoot,
1052
+ reason: runtime.worktree.reason,
1053
+ };
1054
+ }
1020
1055
  try {
1021
1056
  await runAutonomousLoop({
1022
1057
  sendMessage: async (prompt) => {
1023
1058
  bgSession.controller.reset();
1024
1059
  const resultTextHolder = { text: '' };
1025
1060
  bgSession.buffer.onSummary = (t) => { resultTextHolder.text = t; };
1026
- await sendAgentMessage(prompt, bgSession.history, provider, project, spiralEngine, config, permissions, bgSession.undoStack, checkpointStore, bgSession.controller, new ActivityIndicator(), bgSession.buffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, undefined, { enabled: false, verbose: false, strict: false });
1061
+ await sendAgentMessage(prompt, bgSession.history, provider, project, spiralEngine, config, permissions, bgSession.undoStack, checkpointStore, bgSession.controller, new ActivityIndicator(), bgSession.buffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, undefined, { enabled: false, verbose: false, strict: false }, undefined, undefined, undefined, undefined, undefined, undefined, undefined, runtime);
1027
1062
  bgSession.buffer.onSummary = undefined;
1028
1063
  return resultTextHolder.text;
1029
1064
  },
@@ -1045,6 +1080,9 @@ export async function chatCommand(options) {
1045
1080
  bgSession.capture(`Error: ${err}`);
1046
1081
  }
1047
1082
  }
1083
+ finally {
1084
+ await runtime.release();
1085
+ }
1048
1086
  autonomousMode = false;
1049
1087
  sessionMgr.complete(bgSession.id, {
1050
1088
  text: completed.join('\n'),
@@ -1572,9 +1610,23 @@ export async function chatCommand(options) {
1572
1610
  onStatusChange: () => { updateStatusBar(); },
1573
1611
  runWorkerSession: async (session, prompt, agentIdentity) => {
1574
1612
  const bgActivity = new ActivityIndicator();
1613
+ const runtime = await prepareSessionRuntime('swarm_worker', session.id, (message) => {
1614
+ session.capture(message);
1615
+ });
1616
+ if (runtime.worktree) {
1617
+ session.worktree = {
1618
+ root: runtime.executionRoot,
1619
+ reason: runtime.worktree.reason,
1620
+ };
1621
+ }
1575
1622
  session.onCapture = (line, index) => { pushOutputLine(session.id, line, index); };
1576
1623
  pushSessionCreated(serializeSession(session));
1577
- await sendAgentMessage(prompt, session.history, provider, project, spiralEngine, config, permissions, session.undoStack, checkpointStore, session.controller, bgActivity, session.buffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; roundToolCalls++; }, undefined, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict });
1624
+ try {
1625
+ await sendAgentMessage(prompt, session.history, provider, project, spiralEngine, config, permissions, session.undoStack, checkpointStore, session.controller, bgActivity, session.buffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; roundToolCalls++; }, undefined, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, undefined, undefined, undefined, undefined, undefined, undefined, undefined, runtime);
1626
+ }
1627
+ finally {
1628
+ await runtime.release();
1629
+ }
1578
1630
  const text = session.buffer.buildContext();
1579
1631
  const errors = session.buffer.getRecentErrors().map(e => e.summary);
1580
1632
  return { text, errors };
@@ -1910,6 +1962,10 @@ export async function chatCommand(options) {
1910
1962
  activeMode: jarvisDaemonSession && jarvisDaemonSession.status === 'running'
1911
1963
  ? 'jarvis'
1912
1964
  : autonomousMode ? 'monitor' : 'cli',
1965
+ worktree: {
1966
+ mode: config.worktree.mode,
1967
+ active: activeWorktreeRuntimes.size,
1968
+ },
1913
1969
  jarvisName: jarvisDaemonSession && jarvisDaemonSession.status === 'running'
1914
1970
  ? jarvisIdentity.getIdentity().name
1915
1971
  : undefined,
@@ -2246,54 +2302,22 @@ export async function chatCommand(options) {
2246
2302
  }
2247
2303
  // Command suggestions are now handled by InputManager's built-in _interceptTtyWrite.
2248
2304
  // The panel opens/updates/closes automatically as the user types slash commands.
2249
- // === ESC detection ===
2305
+ // === ESC detection (single ESC = stop for normal agents) ===
2250
2306
  // Double-ESC (rewind browser) is handled by the raw data listener below.
2251
2307
  // Skip if a full-screen browser (Rewind/Plan) is open — it handles ESC itself.
2252
2308
  // Skip if ESC was used to close the suggestion panel (panelJustClosed flag)
2253
2309
  //
2254
- // Special modes (Jarvis daemon, autonomous) require DELIBERATE double-ESC
2255
- // with >1s gap to stop prevents accidental kills. Quick double-ESC (<800ms)
2256
- // opens Rewind instead. Single ESC is ignored for special modes.
2310
+ // Special modes (Jarvis, autonomous): ESC is handled ENTIRELY by the raw
2311
+ // data listener below the keypress handler does NOTHING for them.
2312
+ // This prevents conflicts between the two handlers and avoids cursor jumps
2313
+ // from hint messages. See raw data listener for: quick double-ESC → Rewind,
2314
+ // deliberate double-ESC (>1s gap) → stop special mode.
2257
2315
  if (key.name === 'escape' && !fullScreenBrowserOpen && !panelJustClosed) {
2258
2316
  const jarvisRunning = jarvisDaemonSession && jarvisDaemonSession.status === 'running';
2259
2317
  const specialMode = jarvisRunning || autonomousMode;
2260
2318
  const normalRunning = agentRunning || sessionMgr.hasBackgroundTasks;
2261
- if (specialMode) {
2262
- // Special modes: single ESC is ignored.
2263
- // Stopping requires deliberate double-ESC (>1s gap) — handled below.
2264
- const now = Date.now();
2265
- if (lastSpecialEscTime > 0 && (now - lastSpecialEscTime) >= 1000) {
2266
- // Deliberate double-ESC (>1s gap) → STOP special mode
2267
- lastSpecialEscTime = 0;
2268
- closeSuggestionPanel();
2269
- activity.stop('Stopped');
2270
- agentController.abort();
2271
- sessionMgr.abortAll();
2272
- if (activeSwarm) {
2273
- activeSwarm.abort();
2274
- activeSwarm = null;
2275
- }
2276
- autonomousMode = false;
2277
- if (jarvisRunning) {
2278
- jarvisDaemonSession.abort();
2279
- jarvisDaemonSession = null;
2280
- jarvisQueue.setDaemonState('stopped');
2281
- jarvisPaused = false;
2282
- releaseJarvisSlot();
2283
- }
2284
- typeAheadBuffer.length = 0;
2285
- agentRunning = false;
2286
- process.stdout.write('\n');
2287
- renderInfo(chalk.red('\u23F9 STOPPED') + chalk.dim(' \u2014 Special mode exited (deliberate double-ESC).'));
2288
- showPrompt();
2289
- }
2290
- else {
2291
- // First ESC or too fast — record time, show hint
2292
- lastSpecialEscTime = now;
2293
- screen.writeOutput(chalk.dim(' \u23F8 ESC registered \u2014 press ESC again after 1s to stop, or quick double-ESC for Rewind\n'));
2294
- }
2295
- }
2296
- else if (normalRunning) {
2319
+ // Special modes: skip entirely — raw data handler manages ESC
2320
+ if (!specialMode && normalRunning) {
2297
2321
  // Normal agent work: single ESC = immediate stop (unchanged)
2298
2322
  closeSuggestionPanel();
2299
2323
  activity.stop('Stopped');
@@ -2381,21 +2405,58 @@ export async function chatCommand(options) {
2381
2405
  // Bare ESC = exactly 1 byte: \x1b
2382
2406
  // Double ESC = exactly 2 bytes: \x1b\x1b
2383
2407
  // Anything else starting with \x1b is an ANSI escape sequence → ignore
2408
+ // Detect special mode (Jarvis/autonomous) — ESC handling is done entirely here
2409
+ const isSpecialMode = !!(jarvisDaemonSession && jarvisDaemonSession.status === 'running') || autonomousMode;
2384
2410
  if (bytes.length === 2 && bytes[0] === 0x1b && bytes[1] === 0x1b) {
2385
- // Double-ESC in one chunk → open rewind immediately
2411
+ // Double-ESC in one chunk → open rewind immediately (all modes)
2386
2412
  lastRawEscTime = 0;
2413
+ lastSpecialEscTime = 0;
2387
2414
  await openRewindBrowser();
2388
2415
  return;
2389
2416
  }
2390
2417
  if (bytes.length === 1 && bytes[0] === 0x1b) {
2391
- // Single bare ESC — check timing for double-ESC
2392
2418
  const now = Date.now();
2419
+ // Quick double-ESC detection (<800ms) → Rewind (all modes)
2393
2420
  if (now - lastRawEscTime < RAW_ESC_THRESHOLD && lastRawEscTime > 0) {
2394
2421
  lastRawEscTime = 0;
2422
+ lastSpecialEscTime = 0;
2395
2423
  await openRewindBrowser();
2424
+ return;
2396
2425
  }
2397
- else {
2398
- lastRawEscTime = now;
2426
+ // Special mode: deliberate double-ESC (>1s gap) → stop
2427
+ if (isSpecialMode && lastSpecialEscTime > 0 && (now - lastSpecialEscTime) >= 1000) {
2428
+ lastSpecialEscTime = 0;
2429
+ lastRawEscTime = 0;
2430
+ activity.stop('Stopped');
2431
+ agentController.abort();
2432
+ sessionMgr.abortAll();
2433
+ if (activeSwarm) {
2434
+ activeSwarm.abort();
2435
+ activeSwarm = null;
2436
+ }
2437
+ autonomousMode = false;
2438
+ const jarvisRunning = jarvisDaemonSession && jarvisDaemonSession.status === 'running';
2439
+ if (jarvisRunning) {
2440
+ jarvisDaemonSession.abort();
2441
+ jarvisDaemonSession = null;
2442
+ jarvisQueue.setDaemonState('stopped');
2443
+ jarvisPaused = false;
2444
+ releaseJarvisSlot();
2445
+ }
2446
+ typeAheadBuffer.length = 0;
2447
+ agentRunning = false;
2448
+ renderInfo(chalk.red('\u23F9 STOPPED') + chalk.dim(' \u2014 Special mode exited.'));
2449
+ showPrompt();
2450
+ return;
2451
+ }
2452
+ // Record ESC time for next detection
2453
+ lastRawEscTime = now;
2454
+ if (isSpecialMode) {
2455
+ lastSpecialEscTime = now;
2456
+ // Brief hint via chrome row (no scroll region write = no cursor jump)
2457
+ chrome.setRow(0, chalk.dim('\u23F8 ESC \u2014 double-ESC: Rewind | wait 1s + ESC: stop'));
2458
+ // Auto-restore chrome row 0 after 2 seconds
2459
+ setTimeout(() => { chrome.drawFrameBottom(); }, 2000);
2399
2460
  }
2400
2461
  return;
2401
2462
  }
@@ -2925,6 +2986,15 @@ export async function chatCommand(options) {
2925
2986
  // Run in background — user keeps their prompt
2926
2987
  (async () => {
2927
2988
  const completed = [];
2989
+ const runtime = await prepareSessionRuntime('autonomous', bgSession.id, (message) => {
2990
+ bgSession.capture(message);
2991
+ });
2992
+ if (runtime.worktree) {
2993
+ bgSession.worktree = {
2994
+ root: runtime.executionRoot,
2995
+ reason: runtime.worktree.reason,
2996
+ };
2997
+ }
2928
2998
  try {
2929
2999
  await runAutonomousLoop({
2930
3000
  sendMessage: async (prompt) => {
@@ -2953,6 +3023,9 @@ export async function chatCommand(options) {
2953
3023
  bgSession.capture(`Error: ${err}`);
2954
3024
  }
2955
3025
  }
3026
+ finally {
3027
+ await runtime.release();
3028
+ }
2956
3029
  autonomousMode = false;
2957
3030
  sessionMgr.complete(bgSession.id, {
2958
3031
  text: completed.join('\n'),
@@ -3140,6 +3213,12 @@ export async function chatCommand(options) {
3140
3213
  engine: planEngine,
3141
3214
  getState: () => planModeState,
3142
3215
  setState: (s) => { planModeState = s; },
3216
+ }, {
3217
+ listActive: () => Array.from(activeWorktreeRuntimes.entries()).map(([sessionId, runtime]) => ({
3218
+ sessionId,
3219
+ reason: runtime.worktree?.reason ?? 'unknown',
3220
+ root: runtime.executionRoot,
3221
+ })),
3143
3222
  });
3144
3223
  if (handled === 'exit') {
3145
3224
  spiralEngine?.close();
@@ -3245,43 +3324,61 @@ export async function chatCommand(options) {
3245
3324
  planEngine.approve(plan.id);
3246
3325
  renderInfo(chalk.green('\u2713 Plan approved — executing...'));
3247
3326
  updateStatusBar();
3248
- await executePlan({
3249
- plan,
3250
- planEngine,
3251
- agentLoopOptions: {
3252
- provider,
3253
- systemPrompt: assembleSystemPrompt(project.name !== 'unknown' ? project : null, { level_1: [], level_2: [], level_3: [], level_4: [], level_5: [], total_tokens: 0, node_count: 0 }, sessionBuffer.buildContext() || undefined, { provider: provider.name, model: provider.model }),
3254
- permissions,
3255
- toolContext: {
3256
- projectRoot: process.cwd(),
3257
- undoStack,
3258
- spiralEngine,
3259
- bugJournal,
3260
- browserController,
3261
- visionProcessor,
3262
- learningJournal: jarvisLearning,
3263
- },
3264
- checkpointStore,
3265
- sessionBuffer,
3266
- agentIdentity: AGENT_IDENTITIES.main,
3267
- },
3268
- controller: agentController,
3269
- conversationHistory: agentHistory,
3270
- callbacks: {
3271
- onPlanStepStart: (step, idx, total) => {
3272
- renderPlanStepStart(step, idx, total, 'main');
3273
- activity.setPlanProgress(idx, total, step.title);
3274
- },
3275
- onPlanStepEnd: (step, status) => {
3276
- const idx = plan.steps.indexOf(step) + 1;
3277
- renderPlanStepEnd(step, idx, plan.steps.length, status);
3278
- activity.clearPlanProgress();
3327
+ const runtime = await prepareSessionRuntime('plan_execution', plan.id, (message) => {
3328
+ renderInfo(chalk.dim(message));
3329
+ });
3330
+ const mainSession = sessionMgr.main;
3331
+ if (runtime.worktree) {
3332
+ mainSession.worktree = {
3333
+ root: runtime.executionRoot,
3334
+ reason: runtime.worktree.reason,
3335
+ };
3336
+ }
3337
+ try {
3338
+ await executePlan({
3339
+ plan,
3340
+ planEngine,
3341
+ agentLoopOptions: {
3342
+ provider,
3343
+ systemPrompt: assembleSystemPrompt(project.name !== 'unknown' ? project : null, { level_1: [], level_2: [], level_3: [], level_4: [], level_5: [], total_tokens: 0, node_count: 0 }, sessionBuffer.buildContext() || undefined, { provider: provider.name, model: provider.model }),
3344
+ permissions,
3345
+ toolContext: {
3346
+ projectRoot,
3347
+ executionRoot: runtime.executionRoot,
3348
+ undoStack,
3349
+ spiralEngine,
3350
+ bugJournal,
3351
+ browserController,
3352
+ visionProcessor,
3353
+ learningJournal: jarvisLearning,
3354
+ worktree: runtime.worktree,
3355
+ },
3356
+ checkpointStore,
3357
+ sessionBuffer,
3358
+ agentIdentity: AGENT_IDENTITIES.main,
3279
3359
  },
3280
- onPlanComplete: (completedPlan) => {
3281
- renderPlanComplete(completedPlan);
3360
+ controller: agentController,
3361
+ conversationHistory: agentHistory,
3362
+ callbacks: {
3363
+ onPlanStepStart: (step, idx, total) => {
3364
+ renderPlanStepStart(step, idx, total, 'main');
3365
+ activity.setPlanProgress(idx, total, step.title);
3366
+ },
3367
+ onPlanStepEnd: (step, status) => {
3368
+ const idx = plan.steps.indexOf(step) + 1;
3369
+ renderPlanStepEnd(step, idx, plan.steps.length, status);
3370
+ activity.clearPlanProgress();
3371
+ },
3372
+ onPlanComplete: (completedPlan) => {
3373
+ renderPlanComplete(completedPlan);
3374
+ },
3282
3375
  },
3283
- },
3284
- });
3376
+ });
3377
+ }
3378
+ finally {
3379
+ delete mainSession.worktree;
3380
+ await runtime.release();
3381
+ }
3285
3382
  if (choice === 'approve_auto')
3286
3383
  permissions.setSkipPermissions(false);
3287
3384
  }
@@ -3322,12 +3419,26 @@ export async function chatCommand(options) {
3322
3419
  onStatusChange: (_active, _total) => { updateStatusBar(); },
3323
3420
  runWorkerSession: async (session, prompt, agentIdentity) => {
3324
3421
  const bgActivity = new ActivityIndicator();
3422
+ const runtime = await prepareSessionRuntime('swarm_worker', session.id, (message) => {
3423
+ session.capture(message);
3424
+ });
3425
+ if (runtime.worktree) {
3426
+ session.worktree = {
3427
+ root: runtime.executionRoot,
3428
+ reason: runtime.worktree.reason,
3429
+ };
3430
+ }
3325
3431
  // Wire output streaming to brain
3326
3432
  session.onCapture = (line, index) => {
3327
3433
  _pushOutputLine(session.id, line, index);
3328
3434
  };
3329
3435
  _pushSessionCreated(_serializeSession(session));
3330
- await sendAgentMessage(prompt, session.history, provider, project, spiralEngine, config, permissions, session.undoStack, checkpointStore, session.controller, bgActivity, session.buffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; roundToolCalls++; }, undefined, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, bugJournal, undefined, undefined, undefined, null, undefined, undefined);
3436
+ try {
3437
+ await sendAgentMessage(prompt, session.history, provider, project, spiralEngine, config, permissions, session.undoStack, checkpointStore, session.controller, bgActivity, session.buffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; roundToolCalls++; }, undefined, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, bugJournal, undefined, undefined, undefined, null, undefined, undefined, runtime);
3438
+ }
3439
+ finally {
3440
+ await runtime.release();
3441
+ }
3331
3442
  const text = session.buffer.buildContext();
3332
3443
  const errors = session.buffer.getRecentErrors().map(e => e.summary);
3333
3444
  return { text, errors };
@@ -3601,13 +3712,14 @@ async function runBackgroundSession(session, prompt, provider, project, spiralEn
3601
3712
  durationMs: session.elapsed,
3602
3713
  };
3603
3714
  }
3604
- async function sendAgentMessage(input, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, controller, activity, sessionBuffer, onTokens, onToolCall, onAgentStart, validationOpts, bugJournal, browserController, visionProcessor, onBrowserScreenshot, jarvisContext, onStepInfo, learningJournal) {
3715
+ async function sendAgentMessage(input, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, controller, activity, sessionBuffer, onTokens, onToolCall, onAgentStart, validationOpts, bugJournal, browserController, visionProcessor, onBrowserScreenshot, jarvisContext, onStepInfo, learningJournal, runtime) {
3605
3716
  // User message was rendered by renderUserMessage() in the caller before entering here.
3717
+ const executionRoot = runtime?.executionRoot ?? process.cwd();
3606
3718
  // Intent Detection: Check if user wants to feed the codebase
3607
3719
  const feedIntent = detectFeedIntent(input);
3608
3720
  if (feedIntent.detected && feedIntent.confidence > 0.7 && spiralEngine) {
3609
3721
  renderInfo('\u{1F300} Analyzing project in the background...\n');
3610
- const rootDir = process.cwd();
3722
+ const rootDir = executionRoot;
3611
3723
  // Background feed runs silently (no progress output) to avoid colliding
3612
3724
  // with the activity indicator which also writes \r\x1b[K on the same line.
3613
3725
  runFeedPipeline(rootDir, spiralEngine, {
@@ -3691,6 +3803,7 @@ async function sendAgentMessage(input, agentHistory, provider, project, spiralEn
3691
3803
  permissions,
3692
3804
  toolContext: {
3693
3805
  projectRoot: process.cwd(),
3806
+ executionRoot,
3694
3807
  undoStack,
3695
3808
  spiralEngine,
3696
3809
  bugJournal,
@@ -3698,6 +3811,7 @@ async function sendAgentMessage(input, agentHistory, provider, project, spiralEn
3698
3811
  visionProcessor,
3699
3812
  onBrowserScreenshot: onBrowserScreenshot ?? undefined,
3700
3813
  learningJournal,
3814
+ worktree: runtime?.worktree,
3701
3815
  },
3702
3816
  checkpointStore,
3703
3817
  sessionBuffer,
@@ -3894,7 +4008,7 @@ function showFullAutonomousWarning() {
3894
4008
  process.stdout.write(d('\u2502 ') + d('ESC = stop agent if needed.') + d(' \u2502') + '\n');
3895
4009
  process.stdout.write(d('\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F') + '\n\n');
3896
4010
  }
3897
- async function handleSlashCommand(input, messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, sessionTokens, sessionToolCalls, onProviderSwitch, onBrainSwitch, currentBrainScope, onAutonomous, onValidation, sessionManager, onRegisterBrainHandlers, onSubPrompt, bugJournal, jarvisCtx, onModeChange, chrome, planCtx) {
4011
+ async function handleSlashCommand(input, messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, sessionTokens, sessionToolCalls, onProviderSwitch, onBrainSwitch, currentBrainScope, onAutonomous, onValidation, sessionManager, onRegisterBrainHandlers, onSubPrompt, bugJournal, jarvisCtx, onModeChange, chrome, planCtx, worktreeCtx) {
3898
4012
  const parts = input.split(/\s+/);
3899
4013
  let cmd = parts[0].toLowerCase();
3900
4014
  // ─── Chrome-aware selectMenu ───────────────────────────
@@ -3998,7 +4112,7 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
3998
4112
  rl.resume();
3999
4113
  if (helpIdx >= 0 && helpCmds[helpIdx]) {
4000
4114
  // Execute the selected command
4001
- return handleSlashCommand(helpCmds[helpIdx], messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, sessionTokens, sessionToolCalls, onProviderSwitch, onBrainSwitch, currentBrainScope, onAutonomous, onValidation, sessionManager, onRegisterBrainHandlers, onSubPrompt, bugJournal, jarvisCtx, onModeChange, chrome, planCtx);
4115
+ return handleSlashCommand(helpCmds[helpIdx], messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, sessionTokens, sessionToolCalls, onProviderSwitch, onBrainSwitch, currentBrainScope, onAutonomous, onValidation, sessionManager, onRegisterBrainHandlers, onSubPrompt, bugJournal, jarvisCtx, onModeChange, chrome, planCtx, worktreeCtx);
4002
4116
  }
4003
4117
  break;
4004
4118
  }
@@ -4559,6 +4673,70 @@ async function handleSlashCommand(input, messages, agentHistory, config, spiralE
4559
4673
  renderInfo('Not a git repository.');
4560
4674
  }
4561
4675
  break;
4676
+ case '/worktree': {
4677
+ const arg = parts[1]?.toLowerCase();
4678
+ const cleanupArg = parts[2]?.toLowerCase();
4679
+ const current = store.getAll().worktree;
4680
+ const active = worktreeCtx?.listActive() ?? [];
4681
+ const renderWorktreeStatus = () => {
4682
+ const modeColor = current.mode === 'force'
4683
+ ? chalk.hex('#FF6600')
4684
+ : current.mode === 'auto'
4685
+ ? chalk.cyan
4686
+ : chalk.dim;
4687
+ renderInfo(`Worktree mode: ${modeColor(current.mode)} | cleanup: ${current.cleanup} | branch prefix: ${current.branchPrefix} | max age: ${current.maxAgeHours}h`);
4688
+ if (active.length === 0) {
4689
+ renderInfo(chalk.dim('No active isolated runs.'));
4690
+ }
4691
+ else {
4692
+ renderInfo(`Active isolated runs: ${active.length}`);
4693
+ for (const runtime of active.slice(0, 8)) {
4694
+ renderInfo(` ${runtime.reason} [${runtime.sessionId}] -> ${runtime.root}`);
4695
+ }
4696
+ }
4697
+ renderInfo(chalk.dim('Usage: /worktree [status|auto|force|off|cleanup keep|cleanup remove|list]'));
4698
+ };
4699
+ if (!arg || arg === 'status' || arg === 'show') {
4700
+ renderWorktreeStatus();
4701
+ break;
4702
+ }
4703
+ if (arg === 'list') {
4704
+ if (active.length === 0) {
4705
+ renderInfo(chalk.dim('No active isolated runs.'));
4706
+ }
4707
+ else {
4708
+ renderInfo(`Active isolated runs: ${active.length}`);
4709
+ for (const runtime of active) {
4710
+ renderInfo(` ${runtime.reason} [${runtime.sessionId}] -> ${runtime.root}`);
4711
+ }
4712
+ }
4713
+ break;
4714
+ }
4715
+ if (arg === 'on') {
4716
+ store.set('worktree.mode', 'auto');
4717
+ renderInfo(chalk.cyan('Worktree mode set to auto') + chalk.dim(' — future /auto, /plan and /swarm runs use isolated worktrees.'));
4718
+ onModeChange?.();
4719
+ break;
4720
+ }
4721
+ if (arg === 'off' || arg === 'auto' || arg === 'force') {
4722
+ store.set('worktree.mode', arg);
4723
+ renderInfo(`Worktree mode set to ${arg}.`);
4724
+ onModeChange?.();
4725
+ break;
4726
+ }
4727
+ if (arg === 'cleanup') {
4728
+ if (cleanupArg === 'keep' || cleanupArg === 'remove') {
4729
+ store.set('worktree.cleanup', cleanupArg);
4730
+ renderInfo(`Worktree cleanup set to ${cleanupArg}.`);
4731
+ }
4732
+ else {
4733
+ renderInfo('Usage: /worktree cleanup [keep|remove]');
4734
+ }
4735
+ break;
4736
+ }
4737
+ renderWorktreeStatus();
4738
+ break;
4739
+ }
4562
4740
  case '/export': {
4563
4741
  const outputDir = parts[1] || process.cwd();
4564
4742
  if (spiralEngine) {