swarm-code 0.1.21 → 0.1.23

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.
package/dist/core/rlm.js CHANGED
@@ -171,6 +171,7 @@ export async function runRlmLoop(options) {
171
171
  const { context, query, model, repl, signal, onProgress, onSubQueryStart, onSubQuery, systemPrompt, threadHandler, mergeHandler, } = options;
172
172
  let totalSubQueries = 0;
173
173
  let iterationSubQueries = 0;
174
+ let mergedAlready = false;
174
175
  const llmQueryHandler = async (subContext, instruction) => {
175
176
  if (signal?.aborted)
176
177
  throw new Error("Aborted");
@@ -217,7 +218,10 @@ export async function runRlmLoop(options) {
217
218
  repl.setThreadHandler(threadHandler);
218
219
  }
219
220
  if (mergeHandler) {
220
- repl.setMergeHandler(mergeHandler);
221
+ repl.setMergeHandler(async () => {
222
+ mergedAlready = true;
223
+ return mergeHandler();
224
+ });
221
225
  }
222
226
  }
223
227
  await initRepl();
@@ -374,7 +378,7 @@ export async function runRlmLoop(options) {
374
378
  });
375
379
  if (execResult.hasFinal && execResult.finalValue !== null) {
376
380
  // Auto-merge any unmerged thread branches before returning
377
- if (mergeHandler) {
381
+ if (mergeHandler && !mergedAlready) {
378
382
  try {
379
383
  await mergeHandler();
380
384
  }
@@ -422,7 +426,7 @@ export async function runRlmLoop(options) {
422
426
  });
423
427
  }
424
428
  // Auto-merge any remaining thread branches even though FINAL was never called
425
- if (mergeHandler) {
429
+ if (mergeHandler && !mergedAlready) {
426
430
  try {
427
431
  await mergeHandler();
428
432
  }
@@ -757,8 +757,11 @@ export async function runInteractiveSwarm(rawArgs) {
757
757
  logError(`Directory "${args.dir}" does not exist`);
758
758
  process.exit(1);
759
759
  }
760
- // First-run onboarding
760
+ // First-run onboarding (may create ~/.swarm/config.yaml with user's chosen model)
761
761
  await runOnboarding();
762
+ // Reload config to pick up any changes from onboarding
763
+ // (onboarding writes ~/.swarm/config.yaml but loadConfig() already ran before it)
764
+ Object.assign(config, loadConfig());
762
765
  // Override config with CLI args
763
766
  if (args.agent)
764
767
  config.default_agent = args.agent;
@@ -960,6 +963,22 @@ export async function runInteractiveSwarm(rawArgs) {
960
963
  else if (merged > 0) {
961
964
  logSuccess(`Merged ${merged} branches`);
962
965
  }
966
+ // Clean up merged worktrees
967
+ if (config.auto_cleanup_worktrees) {
968
+ for (const r of results) {
969
+ if (r.success) {
970
+ const thread = threads.find((t) => t.branchName === r.branch);
971
+ if (thread) {
972
+ try {
973
+ await threadManager.destroyWorktree(thread.id);
974
+ }
975
+ catch {
976
+ // Non-fatal
977
+ }
978
+ }
979
+ }
980
+ }
981
+ }
963
982
  const summary = results
964
983
  .map((r) => (r.success ? `Merged ${r.branch}: ${r.message}` : `FAILED ${r.branch}: ${r.message}`))
965
984
  .join("\n");
@@ -80,6 +80,7 @@ export declare class ThreadManager {
80
80
  };
81
81
  /** Cleanup all worktrees. */
82
82
  cleanup(): Promise<void>;
83
- private cleanupWorktree;
83
+ /** Destroy a specific thread's worktree and branch. */
84
+ destroyWorktree(threadId: string): Promise<void>;
84
85
  private failResult;
85
86
  }
@@ -486,7 +486,7 @@ export class ThreadManager {
486
486
  state.status = "cancelled";
487
487
  state.phase = "cancelled";
488
488
  state.completedAt = Date.now();
489
- await this.cleanupWorktree(threadId);
489
+ await this.destroyWorktree(threadId);
490
490
  return this.failResult(state, "Thread cancelled during execution");
491
491
  }
492
492
  // Capture diff
@@ -570,7 +570,7 @@ export class ThreadManager {
570
570
  const { cost } = this.budget.recordCost(threadId, errModel);
571
571
  state.estimatedCostUsd = cost;
572
572
  // Cleanup worktree on failure
573
- await this.cleanupWorktree(threadId);
573
+ await this.destroyWorktree(threadId);
574
574
  return this.failResult(state, errorMsg);
575
575
  }
576
576
  finally {
@@ -637,7 +637,8 @@ export class ThreadManager {
637
637
  await this.worktreeManager.destroyAll();
638
638
  }
639
639
  }
640
- async cleanupWorktree(threadId) {
640
+ /** Destroy a specific thread's worktree and branch. */
641
+ async destroyWorktree(threadId) {
641
642
  try {
642
643
  await this.worktreeManager.destroy(threadId, true);
643
644
  }
@@ -44,51 +44,93 @@ export function readTextInput(_prompt) {
44
44
  process.stdin.setRawMode(true);
45
45
  process.stdin.resume();
46
46
  process.stdin.setEncoding("utf-8");
47
- // How many rows below the top border the cursor sits after drawBox()
47
+ // Tracks cursor position relative to top border after each drawBox()
48
48
  let cursorRowFromTop = 0;
49
- let hasDrawn = false;
50
- function drawBox() {
51
- const out = process.stderr;
52
- const promptVisibleLen = 2; // "❯ "
53
- if (hasDrawn) {
54
- // Move cursor from its current position back to the top border row
55
- if (cursorRowFromTop > 0) {
56
- out.write(`\x1b[${cursorRowFromTop}A`);
57
- }
58
- out.write("\x1b[0G"); // go to column 0
59
- out.write("\x1b[J"); // erase from cursor to end of screen
60
- }
61
- hasDrawn = true;
62
- // Top border — thin dim line
63
- const topLine = `${BORDER_COLOR}${"─".repeat(w)}${RESET}`;
64
- out.write(`${topLine}\n`);
65
- // Content rows — dark background, full width
49
+ let prevTotalRows = 0;
50
+ function buildRows() {
51
+ const promptVisibleLen = 2;
52
+ const rows = [];
53
+ // Top border
54
+ rows.push(`${BORDER_COLOR}${"─".repeat(w)}${RESET}`);
55
+ // Content rows
66
56
  const promptChar = `${ACCENT_COLOR}❯${RESET} `;
67
57
  for (let i = 0; i < linesBuf.length; i++) {
68
58
  const lineText = linesBuf[i];
69
59
  const prefix = i === 0 ? promptChar : `${dim("·")} `;
70
- // How much space for text content
71
60
  const contentWidth = w - promptVisibleLen;
72
- // Truncate display if line is too long
73
61
  const displayText = lineText.length > contentWidth ? lineText.slice(0, contentWidth - 1) + "…" : lineText;
74
62
  const padding = Math.max(0, contentWidth - displayText.length);
75
- out.write(`${BG_DARK}${prefix}${displayText}${" ".repeat(padding)}${RESET}\n`);
63
+ rows.push(`${BG_DARK}${prefix}${displayText}${" ".repeat(padding)}${RESET}`);
64
+ }
65
+ // Bottom border
66
+ rows.push(`${ACCENT_COLOR}${"─".repeat(w)}${RESET}`);
67
+ // Hints — pad to full width so it fully overwrites old content
68
+ const hintsText = " enter submit esc exit";
69
+ const hintsPad = Math.max(0, w - hintsText.length);
70
+ rows.push(`${dim(hintsText)}${" ".repeat(hintsPad)}`);
71
+ return rows;
72
+ }
73
+ function drawBox() {
74
+ const out = process.stderr;
75
+ const promptVisibleLen = 2;
76
+ const rows = buildRows();
77
+ const totalRows = rows.length;
78
+ if (prevTotalRows > 0) {
79
+ // ── Redraw: overwrite rows in place (no \n, no scrolling) ──
80
+ // Move cursor to top border row
81
+ if (cursorRowFromTop > 0) {
82
+ out.write(`\x1b[${cursorRowFromTop}A`);
83
+ }
84
+ out.write("\r");
85
+ // Overwrite each row in place
86
+ const commonRows = Math.min(totalRows, prevTotalRows);
87
+ for (let i = 0; i < commonRows; i++) {
88
+ out.write(`\x1b[2K${rows[i]}`);
89
+ if (i < commonRows - 1) {
90
+ out.write("\x1b[1B\r"); // cursor down 1 (no scroll), start of line
91
+ }
92
+ }
93
+ if (totalRows > prevTotalRows) {
94
+ // More rows than before (multi-line paste) — append with \n
95
+ for (let i = commonRows; i < totalRows; i++) {
96
+ out.write(`\n\x1b[2K${rows[i]}`);
97
+ }
98
+ }
99
+ else if (prevTotalRows > totalRows) {
100
+ // Fewer rows than before — erase leftover old rows
101
+ for (let i = totalRows; i < prevTotalRows; i++) {
102
+ out.write("\x1b[1B\r\x1b[2K");
103
+ }
104
+ // Move back to last new row
105
+ const extra = prevTotalRows - totalRows;
106
+ if (extra > 0)
107
+ out.write(`\x1b[${extra}A`);
108
+ }
109
+ // Cursor is now on the last row (hints).
110
+ }
111
+ else {
112
+ // ── Initial draw: use \n between rows, no trailing \n ──
113
+ for (let i = 0; i < totalRows; i++) {
114
+ if (i > 0)
115
+ out.write("\n");
116
+ out.write(rows[i]);
117
+ }
118
+ // Cursor is on the hints line (last row).
76
119
  }
77
- // Bottom border — accent colored
78
- const bottomLine = `${ACCENT_COLOR}${"─".repeat(w)}${RESET}`;
79
- out.write(`${bottomLine}\n`);
80
- // Hints
81
- out.write(`${dim(" enter submit esc exit")}\n`);
82
- // Position cursor inside the text area
83
- // We're at the bottom (after hints). Move up to the correct content row.
84
- const currentLineIdx = linesBuf.length - 1; // cursor is always on last line
85
- const rowsFromBottom = 2 + (linesBuf.length - 1 - currentLineIdx); // hints + bottom border + lines below cursor
86
- out.write(`\x1b[${rowsFromBottom}A`);
87
- // Move to correct column: prefix width + cursor position
120
+ prevTotalRows = totalRows;
121
+ // Position cursor at the active content row
122
+ // Cursor is currently on the hints line (last row = index totalRows-1).
123
+ // Content cursor is on row (1 + currentLineIdx).
124
+ const currentLineIdx = linesBuf.length - 1;
125
+ const targetRow = 1 + currentLineIdx;
126
+ const hintsRow = totalRows - 1;
127
+ const rowsUp = hintsRow - targetRow;
128
+ if (rowsUp > 0)
129
+ out.write(`\x1b[${rowsUp}A`);
130
+ // Set column
88
131
  const col = promptVisibleLen + cursorPos + 1;
89
132
  out.write(`\x1b[${col}G`);
90
- // Track where cursor ended up (row 0 = top border)
91
- cursorRowFromTop = 1 + currentLineIdx;
133
+ cursorRowFromTop = targetRow;
92
134
  }
93
135
  // Initial draw
94
136
  process.stderr.write(HIDE_CURSOR);
@@ -203,12 +245,11 @@ export function readTextInput(_prompt) {
203
245
  if (origRawMode !== undefined) {
204
246
  process.stdin.setRawMode(origRawMode);
205
247
  }
206
- // Move cursor to top of box and clear everything
248
+ // Move cursor to top of box
207
249
  if (cursorRowFromTop > 0) {
208
250
  process.stderr.write(`\x1b[${cursorRowFromTop}A`);
209
251
  }
210
- process.stderr.write("\x1b[0G");
211
- process.stderr.write("\x1b[J"); // erase to end of screen
252
+ process.stderr.write("\r\x1b[J"); // erase from here to end of screen
212
253
  // Write the submitted text as a clean line (so it's visible in scrollback)
213
254
  const fullText = linesBuf.join("\n").trim();
214
255
  if (fullText) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarm-code",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "Open-source swarm-native coding agent orchestrator — spawns parallel coding agents in isolated git worktrees, built on RLM (arXiv:2512.24601)",
5
5
  "type": "module",
6
6
  "bin": {