swarm-code 0.1.17 → 0.1.19

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
@@ -373,6 +373,15 @@ export async function runRlmLoop(options) {
373
373
  stderr: execResult.stderr,
374
374
  });
375
375
  if (execResult.hasFinal && execResult.finalValue !== null) {
376
+ // Auto-merge any unmerged thread branches before returning
377
+ if (mergeHandler) {
378
+ try {
379
+ await mergeHandler();
380
+ }
381
+ catch {
382
+ // Non-fatal — merge may have already been done by the LLM
383
+ }
384
+ }
376
385
  return {
377
386
  answer: execResult.finalValue,
378
387
  iterations: iteration,
@@ -392,13 +401,35 @@ export async function runRlmLoop(options) {
392
401
  parts.push("(No output produced. The code ran without printing anything.)");
393
402
  }
394
403
  parts.push(`\nIteration ${iteration}/${config.max_iterations}. Sub-queries used: ${totalSubQueries}/${config.max_sub_queries}.`);
395
- parts.push("Continue processing or call FINAL() when you have the answer.");
404
+ const remaining = config.max_iterations - iteration;
405
+ if (remaining <= 2) {
406
+ parts.push("⚠️ CRITICAL: Only " +
407
+ remaining +
408
+ " iteration(s) remaining! Call merge_threads() then FINAL() NOW with your best result. Any unmerged work will be lost.");
409
+ }
410
+ else if (remaining <= Math.ceil(config.max_iterations * 0.25)) {
411
+ parts.push("⚠️ WARNING: " +
412
+ remaining +
413
+ " iterations remaining. Wrap up: merge_threads() → FINAL(). Don't start new threads.");
414
+ }
415
+ else {
416
+ parts.push("Continue processing or call FINAL() when you have the answer.");
417
+ }
396
418
  conversationHistory.push({
397
419
  role: "user",
398
420
  content: parts.join("\n\n"),
399
421
  timestamp: Date.now(),
400
422
  });
401
423
  }
424
+ // Auto-merge any remaining thread branches even though FINAL was never called
425
+ if (mergeHandler) {
426
+ try {
427
+ await mergeHandler();
428
+ }
429
+ catch {
430
+ // Non-fatal
431
+ }
432
+ }
402
433
  return {
403
434
  answer: "[Maximum iterations reached without calling FINAL]",
404
435
  iterations: config.max_iterations,
@@ -77,9 +77,10 @@ ${agentDescriptions}
77
77
  4. Use \`print()\` for intermediate output visible in the next iteration
78
78
  5. Max ${config.max_threads} concurrent threads, ${config.max_total_threads} total per session
79
79
  6. Thread timeout: ${config.thread_timeout_ms / 1000}s per thread
80
- 7. Don't call FINAL prematurely verify thread results first. Always run verification after merge.
80
+ 7. After merging, try to run a quick verification thread if iterations allow. But don't endlessly loop on verification one attempt is enough.
81
81
  8. Prefer cheap models for sub-agent threads (haiku, gpt-4o-mini) — save premium models for complex work
82
- 8. The REPL persists state — variables survive across iterations
82
+ 9. The REPL persists state — variables survive across iterations
83
+ 10. **Watch your iteration count.** If you're past 75% of max iterations, call \`merge_threads()\` then \`FINAL()\` with your best result. Don't waste iterations on repeated verification cycles.
83
84
 
84
85
  ## Examples
85
86
 
@@ -1,25 +1,19 @@
1
1
  /**
2
- * Text input for the interactive REPL with multi-line paste support.
2
+ * Bordered text input for the interactive REPL.
3
3
  *
4
- * Uses raw stdin to detect paste vs keypress:
5
- * - Pasted text arrives as a single data chunk containing newlines preserved as multi-line
6
- * - Typed Enter alone → submits the input
7
- * - Escape → signals exit
8
- * - Ctrl+D → submits current input
9
- * - Ctrl+C → signals exit
10
- * - Standard editing: backspace, left/right arrows, home/end
4
+ * Visual style inspired by OpenCode full-width bordered text area with
5
+ * a darker background row, accent-colored bottom border, and multi-line
6
+ * paste support.
7
+ *
8
+ * Keys:
9
+ * - Enter: submit
10
+ * - Escape / Ctrl+C: exit
11
+ * - Ctrl+D: submit
12
+ * - Backspace, left/right, home/end: editing
13
+ * - Paste (multi-char with newlines): captured as multi-line
11
14
  */
12
15
  export interface TextInputResult {
13
16
  text: string;
14
17
  action: "submit" | "escape";
15
18
  }
16
- /**
17
- * Read user input with multi-line paste support and escape-to-exit.
18
- *
19
- * Behavior:
20
- * - Single Enter: submits current line(s)
21
- * - Pasted text with newlines: captured as multi-line, then Enter submits all
22
- * - Escape: returns action "escape" to signal exit
23
- * - Ctrl+D: submits whatever is in the buffer
24
- */
25
- export declare function readTextInput(prompt: string): Promise<TextInputResult>;
19
+ export declare function readTextInput(_prompt: string): Promise<TextInputResult>;
@@ -1,25 +1,26 @@
1
1
  /**
2
- * Text input for the interactive REPL with multi-line paste support.
2
+ * Bordered text input for the interactive REPL.
3
3
  *
4
- * Uses raw stdin to detect paste vs keypress:
5
- * - Pasted text arrives as a single data chunk containing newlines preserved as multi-line
6
- * - Typed Enter alone → submits the input
7
- * - Escape → signals exit
8
- * - Ctrl+D → submits current input
9
- * - Ctrl+C → signals exit
10
- * - Standard editing: backspace, left/right arrows, home/end
11
- */
12
- import { dim, isTTY, stripAnsi } from "./theme.js";
13
- /**
14
- * Read user input with multi-line paste support and escape-to-exit.
4
+ * Visual style inspired by OpenCode full-width bordered text area with
5
+ * a darker background row, accent-colored bottom border, and multi-line
6
+ * paste support.
15
7
  *
16
- * Behavior:
17
- * - Single Enter: submits current line(s)
18
- * - Pasted text with newlines: captured as multi-line, then Enter submits all
19
- * - Escape: returns action "escape" to signal exit
20
- * - Ctrl+D: submits whatever is in the buffer
8
+ * Keys:
9
+ * - Enter: submit
10
+ * - Escape / Ctrl+C: exit
11
+ * - Ctrl+D: submit
12
+ * - Backspace, left/right, home/end: editing
13
+ * - Paste (multi-char with newlines): captured as multi-line
21
14
  */
22
- export function readTextInput(prompt) {
15
+ import { coral, dim, isTTY, termWidth } from "./theme.js";
16
+ // ── ANSI helpers ─────────────────────────────────────────────────────────────
17
+ const BG_DARK = "\x1b[48;2;30;33;39m"; // dark background for input row
18
+ const RESET = "\x1b[0m";
19
+ const HIDE_CURSOR = "\x1b[?25l";
20
+ const SHOW_CURSOR = "\x1b[?25h";
21
+ const BORDER_COLOR = "\x1b[38;2;60;63;68m"; // subtle gray for top border
22
+ const ACCENT_COLOR = "\x1b[38;2;215;119;87m"; // coral for bottom border
23
+ export function readTextInput(_prompt) {
23
24
  if (!isTTY) {
24
25
  return new Promise((resolve) => {
25
26
  let data = "";
@@ -37,124 +38,138 @@ export function readTextInput(prompt) {
37
38
  }
38
39
  return new Promise((resolve) => {
39
40
  const linesBuf = [""];
40
- let cursorPos = 0; // position within current (last) line
41
+ let cursorPos = 0;
41
42
  const origRawMode = process.stdin.isRaw;
43
+ const w = termWidth();
42
44
  process.stdin.setRawMode(true);
43
45
  process.stdin.resume();
44
46
  process.stdin.setEncoding("utf-8");
45
- const promptVisible = stripAnsi(prompt);
46
- function redrawCurrentLine() {
47
- const currentLine = linesBuf[linesBuf.length - 1];
48
- process.stderr.write("\x1b[2K\x1b[0G"); // clear line
49
- if (linesBuf.length === 1) {
50
- // First line show prompt
51
- process.stderr.write(` ${prompt}${currentLine}`);
52
- const col = 3 + promptVisible.length + cursorPos;
53
- process.stderr.write(`\x1b[${col}G`);
47
+ // Track how many terminal rows we've drawn so we can clear them
48
+ let drawnRows = 0;
49
+ function drawBox() {
50
+ const out = process.stderr;
51
+ // Clear previously drawn rows
52
+ if (drawnRows > 0) {
53
+ for (let i = 0; i < drawnRows; i++) {
54
+ out.write("\x1b[1A\x1b[2K");
55
+ }
54
56
  }
55
- else {
56
- // Continuation line — indent to match
57
- const pad = " ".repeat(promptVisible.length);
58
- process.stderr.write(` ${dim(".")} ${pad.slice(2)}${currentLine}`);
59
- const col = 3 + promptVisible.length + cursorPos;
60
- process.stderr.write(`\x1b[${col}G`);
57
+ // Top border — thin dim line
58
+ const topLine = `${BORDER_COLOR}${"─".repeat(w)}${RESET}`;
59
+ out.write(`${topLine}\n`);
60
+ // Content rows — dark background, full width
61
+ const promptChar = `${ACCENT_COLOR}❯${RESET} `;
62
+ const promptVisibleLen = 2; // "❯ "
63
+ for (let i = 0; i < linesBuf.length; i++) {
64
+ const lineText = linesBuf[i];
65
+ const prefix = i === 0 ? promptChar : `${dim("·")} `;
66
+ const prefixVisibleLen = promptVisibleLen;
67
+ // How much space for text content
68
+ const contentWidth = w - prefixVisibleLen;
69
+ // Truncate display if line is too long
70
+ const displayText = lineText.length > contentWidth ? lineText.slice(0, contentWidth - 1) + "…" : lineText;
71
+ const padding = Math.max(0, contentWidth - displayText.length);
72
+ out.write(`${BG_DARK}${prefix}${displayText}${" ".repeat(padding)}${RESET}\n`);
61
73
  }
74
+ // Bottom border — accent colored
75
+ const bottomLine = `${ACCENT_COLOR}${"─".repeat(w)}${RESET}`;
76
+ out.write(`${bottomLine}\n`);
77
+ // Hints
78
+ out.write(`${dim(" enter submit esc exit")}\n`);
79
+ drawnRows = linesBuf.length + 3; // top border + content lines + bottom border + hints
80
+ // Position cursor inside the text area
81
+ // We're at the bottom (after hints). Move up to the correct content row.
82
+ const currentLineIdx = linesBuf.length - 1; // cursor is always on last line
83
+ const rowsFromBottom = 2 + (linesBuf.length - 1 - currentLineIdx); // hints + bottom border + lines below cursor
84
+ out.write(`\x1b[${rowsFromBottom}A`);
85
+ // Move to correct column: prefix width + cursor position
86
+ const col = promptVisibleLen + cursorPos + 1;
87
+ out.write(`\x1b[${col}G`);
62
88
  }
63
- // Show initial prompt
64
- process.stderr.write(` ${prompt}`);
89
+ // Initial draw
90
+ process.stderr.write(HIDE_CURSOR);
91
+ drawBox();
92
+ process.stderr.write(SHOW_CURSOR);
65
93
  const onData = (data) => {
66
- // Check if this looks like a paste (multiple chars with newlines)
67
94
  const hasNewlines = data.includes("\n") || data.includes("\r");
68
95
  const isMultiChar = data.length > 1;
69
96
  const isPaste = hasNewlines && isMultiChar;
70
97
  if (isPaste) {
71
- // Paste mode — split on newlines, add all to buffer
72
98
  const pastedLines = data.split(/\r\n|\r|\n/);
73
- // Append first fragment to current line at cursor
74
99
  const currentLine = linesBuf[linesBuf.length - 1];
75
100
  linesBuf[linesBuf.length - 1] = currentLine.slice(0, cursorPos) + pastedLines[0] + currentLine.slice(cursorPos);
76
- // Redraw current line with pasted content
77
101
  cursorPos = (currentLine.slice(0, cursorPos) + pastedLines[0]).length;
78
- redrawCurrentLine();
79
- // Add remaining lines
80
102
  for (let i = 1; i < pastedLines.length; i++) {
81
103
  const line = pastedLines[i];
82
- if (i === pastedLines.length - 1 && line === "") {
83
- // Trailing newline — don't add empty line
104
+ if (i === pastedLines.length - 1 && line === "")
84
105
  break;
85
- }
86
- process.stderr.write("\n");
87
106
  linesBuf.push(line);
88
107
  cursorPos = line.length;
89
- redrawCurrentLine();
90
108
  }
109
+ // Move cursor back to top of box before redraw
110
+ moveCursorToBoxTop();
111
+ drawBox();
91
112
  return;
92
113
  }
93
- // Character-by-character processing
94
114
  for (let i = 0; i < data.length; i++) {
95
115
  const ch = data[i];
96
116
  // Escape sequences
97
117
  if (ch === "\x1b") {
98
118
  if (data[i + 1] === "[") {
99
119
  const code = data[i + 2];
100
- if (code === "C") {
101
- // Right arrow
102
- if (cursorPos < linesBuf[linesBuf.length - 1].length) {
103
- cursorPos++;
104
- redrawCurrentLine();
105
- }
120
+ if (code === "C" && cursorPos < linesBuf[linesBuf.length - 1].length) {
121
+ cursorPos++;
106
122
  i += 2;
123
+ moveCursorToBoxTop();
124
+ drawBox();
107
125
  continue;
108
126
  }
109
- if (code === "D") {
110
- // Left arrow
111
- if (cursorPos > 0) {
112
- cursorPos--;
113
- redrawCurrentLine();
114
- }
127
+ if (code === "D" && cursorPos > 0) {
128
+ cursorPos--;
115
129
  i += 2;
130
+ moveCursorToBoxTop();
131
+ drawBox();
116
132
  continue;
117
133
  }
118
134
  if (code === "H") {
119
- // Home
120
135
  cursorPos = 0;
121
- redrawCurrentLine();
122
136
  i += 2;
137
+ moveCursorToBoxTop();
138
+ drawBox();
123
139
  continue;
124
140
  }
125
141
  if (code === "F") {
126
- // End
127
142
  cursorPos = linesBuf[linesBuf.length - 1].length;
128
- redrawCurrentLine();
129
143
  i += 2;
144
+ moveCursorToBoxTop();
145
+ drawBox();
130
146
  continue;
131
147
  }
132
- // Skip other escape sequences
133
148
  i += 2;
134
149
  continue;
135
150
  }
136
- // Bare Escape key — exit
137
- finish();
151
+ // Bare Escape — exit
152
+ finishAndClear();
138
153
  resolve({ text: "", action: "escape" });
139
154
  return;
140
155
  }
141
156
  // Ctrl+D — submit
142
157
  if (ch === "\x04") {
143
158
  const text = linesBuf.join("\n").trim();
144
- finish();
159
+ finishAndClear();
145
160
  resolve({ text, action: "submit" });
146
161
  return;
147
162
  }
148
163
  // Ctrl+C — exit
149
164
  if (ch === "\x03") {
150
- finish();
165
+ finishAndClear();
151
166
  resolve({ text: "", action: "escape" });
152
167
  return;
153
168
  }
154
169
  // Enter — submit
155
170
  if (ch === "\r" || ch === "\n") {
156
171
  const text = linesBuf.join("\n").trim();
157
- finish();
172
+ finishAndClear();
158
173
  resolve({ text, action: "submit" });
159
174
  return;
160
175
  }
@@ -164,7 +179,8 @@ export function readTextInput(prompt) {
164
179
  const line = linesBuf[linesBuf.length - 1];
165
180
  linesBuf[linesBuf.length - 1] = line.slice(0, cursorPos - 1) + line.slice(cursorPos);
166
181
  cursorPos--;
167
- redrawCurrentLine();
182
+ moveCursorToBoxTop();
183
+ drawBox();
168
184
  }
169
185
  continue;
170
186
  }
@@ -173,7 +189,8 @@ export function readTextInput(prompt) {
173
189
  const line = linesBuf[linesBuf.length - 1];
174
190
  linesBuf[linesBuf.length - 1] = line.slice(0, cursorPos) + " " + line.slice(cursorPos);
175
191
  cursorPos += 2;
176
- redrawCurrentLine();
192
+ moveCursorToBoxTop();
193
+ drawBox();
177
194
  continue;
178
195
  }
179
196
  // Regular printable character
@@ -181,16 +198,41 @@ export function readTextInput(prompt) {
181
198
  const line = linesBuf[linesBuf.length - 1];
182
199
  linesBuf[linesBuf.length - 1] = line.slice(0, cursorPos) + ch + line.slice(cursorPos);
183
200
  cursorPos++;
184
- redrawCurrentLine();
201
+ moveCursorToBoxTop();
202
+ drawBox();
185
203
  }
186
204
  }
187
205
  };
188
- function finish() {
206
+ function moveCursorToBoxTop() {
207
+ // From current cursor position (inside the text area), move to the line
208
+ // before the top border so drawBox() can clear and redraw from there.
209
+ // Current cursor is at content row (linesBuf.length - 1 from top border)
210
+ // We need to go up past: content rows above cursor + top border
211
+ // But drawBox handles clearing with drawnRows, so just go up to start
212
+ const currentLineIdx = linesBuf.length - 1;
213
+ const rowsUp = currentLineIdx + 1; // content lines above + top border
214
+ if (rowsUp > 0) {
215
+ process.stderr.write(`\x1b[${rowsUp}A`);
216
+ }
217
+ process.stderr.write("\x1b[0G");
218
+ }
219
+ function finishAndClear() {
189
220
  process.stdin.removeListener("data", onData);
190
221
  if (origRawMode !== undefined) {
191
222
  process.stdin.setRawMode(origRawMode);
192
223
  }
193
- process.stderr.write("\n");
224
+ // Move cursor to top of box and clear everything
225
+ moveCursorToBoxTop();
226
+ process.stderr.write("\x1b[J"); // erase to end of screen
227
+ // Write the submitted text as a clean line (so it's visible in scrollback)
228
+ const fullText = linesBuf.join("\n").trim();
229
+ if (fullText) {
230
+ const displayLines = fullText.split("\n");
231
+ for (let i = 0; i < displayLines.length; i++) {
232
+ const prefix = i === 0 ? ` ${coral("swarm")}${dim(">")} ` : ` ${dim(".")} `;
233
+ process.stderr.write(`${prefix}${displayLines[i]}\n`);
234
+ }
235
+ }
194
236
  }
195
237
  process.stdin.on("data", onData);
196
238
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarm-code",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
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": {