swarm-code 0.1.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.
- package/LICENSE +21 -0
- package/README.md +384 -0
- package/bin/swarm.mjs +45 -0
- package/dist/agents/aider.d.ts +12 -0
- package/dist/agents/aider.js +182 -0
- package/dist/agents/claude-code.d.ts +9 -0
- package/dist/agents/claude-code.js +216 -0
- package/dist/agents/codex.d.ts +14 -0
- package/dist/agents/codex.js +193 -0
- package/dist/agents/direct-llm.d.ts +9 -0
- package/dist/agents/direct-llm.js +78 -0
- package/dist/agents/mock.d.ts +9 -0
- package/dist/agents/mock.js +77 -0
- package/dist/agents/opencode.d.ts +23 -0
- package/dist/agents/opencode.js +571 -0
- package/dist/agents/provider.d.ts +11 -0
- package/dist/agents/provider.js +31 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +285 -0
- package/dist/compression/compressor.d.ts +28 -0
- package/dist/compression/compressor.js +265 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.js +170 -0
- package/dist/core/repl.d.ts +69 -0
- package/dist/core/repl.js +336 -0
- package/dist/core/rlm.d.ts +63 -0
- package/dist/core/rlm.js +409 -0
- package/dist/core/runtime.py +335 -0
- package/dist/core/types.d.ts +131 -0
- package/dist/core/types.js +19 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.js +75 -0
- package/dist/interactive-swarm.d.ts +20 -0
- package/dist/interactive-swarm.js +1041 -0
- package/dist/interactive.d.ts +10 -0
- package/dist/interactive.js +1765 -0
- package/dist/main.d.ts +15 -0
- package/dist/main.js +242 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.js +72 -0
- package/dist/mcp/session.d.ts +73 -0
- package/dist/mcp/session.js +184 -0
- package/dist/mcp/tools.d.ts +15 -0
- package/dist/mcp/tools.js +377 -0
- package/dist/memory/episodic.d.ts +132 -0
- package/dist/memory/episodic.js +390 -0
- package/dist/prompts/orchestrator.d.ts +5 -0
- package/dist/prompts/orchestrator.js +191 -0
- package/dist/routing/model-router.d.ts +130 -0
- package/dist/routing/model-router.js +515 -0
- package/dist/swarm.d.ts +14 -0
- package/dist/swarm.js +557 -0
- package/dist/threads/cache.d.ts +58 -0
- package/dist/threads/cache.js +198 -0
- package/dist/threads/manager.d.ts +85 -0
- package/dist/threads/manager.js +659 -0
- package/dist/ui/banner.d.ts +14 -0
- package/dist/ui/banner.js +42 -0
- package/dist/ui/dashboard.d.ts +33 -0
- package/dist/ui/dashboard.js +151 -0
- package/dist/ui/index.d.ts +10 -0
- package/dist/ui/index.js +11 -0
- package/dist/ui/log.d.ts +39 -0
- package/dist/ui/log.js +126 -0
- package/dist/ui/onboarding.d.ts +14 -0
- package/dist/ui/onboarding.js +518 -0
- package/dist/ui/spinner.d.ts +25 -0
- package/dist/ui/spinner.js +113 -0
- package/dist/ui/summary.d.ts +18 -0
- package/dist/ui/summary.js +113 -0
- package/dist/ui/theme.d.ts +63 -0
- package/dist/ui/theme.js +97 -0
- package/dist/viewer.d.ts +12 -0
- package/dist/viewer.js +1284 -0
- package/dist/worktree/manager.d.ts +45 -0
- package/dist/worktree/manager.js +266 -0
- package/dist/worktree/merge.d.ts +28 -0
- package/dist/worktree/merge.js +138 -0
- package/package.json +69 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git worktree manager — creates isolated worktrees for thread execution.
|
|
3
|
+
*
|
|
4
|
+
* Phase 2 enhancements:
|
|
5
|
+
* - Mutex on create() to prevent branch name races under concurrency
|
|
6
|
+
* - Retry logic for git worktree add (transient lock file contention)
|
|
7
|
+
*
|
|
8
|
+
* Lifecycle:
|
|
9
|
+
* 1. create() — git worktree add -b swarm/<id> <path> HEAD
|
|
10
|
+
* 2. Agent runs in worktree directory
|
|
11
|
+
* 3. getDiff() — capture changes
|
|
12
|
+
* 4. commit() — commit changes in worktree
|
|
13
|
+
* 5. destroy() — git worktree remove + branch cleanup
|
|
14
|
+
*/
|
|
15
|
+
import type { WorktreeInfo } from "../core/types.js";
|
|
16
|
+
export declare class WorktreeManager {
|
|
17
|
+
private repoRoot;
|
|
18
|
+
private baseDir;
|
|
19
|
+
private worktrees;
|
|
20
|
+
private createMutex;
|
|
21
|
+
constructor(repoRoot: string, baseDir?: string);
|
|
22
|
+
/** Ensure we're in a git repo and the base directory exists. */
|
|
23
|
+
init(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Create a new worktree for a thread.
|
|
26
|
+
* Serialized via mutex to prevent branch name races.
|
|
27
|
+
* Retries on transient lock-file contention.
|
|
28
|
+
*/
|
|
29
|
+
create(threadId: string): Promise<WorktreeInfo>;
|
|
30
|
+
private createWorktreeWithRetry;
|
|
31
|
+
/** Get the git diff of uncommitted changes in a worktree. */
|
|
32
|
+
getDiff(threadId: string): Promise<string>;
|
|
33
|
+
/** Get diff stats (short summary). */
|
|
34
|
+
getDiffStats(threadId: string): Promise<string>;
|
|
35
|
+
/** Get list of changed files. */
|
|
36
|
+
getChangedFiles(threadId: string): Promise<string[]>;
|
|
37
|
+
/** Commit all changes in a worktree. */
|
|
38
|
+
commit(threadId: string, message: string): Promise<boolean>;
|
|
39
|
+
/** Destroy a worktree and optionally its branch. */
|
|
40
|
+
destroy(threadId: string, deleteBranch?: boolean): Promise<void>;
|
|
41
|
+
/** Get info for a thread's worktree. */
|
|
42
|
+
getWorktreeInfo(threadId: string): WorktreeInfo | undefined;
|
|
43
|
+
/** Cleanup all worktrees. Resilient — continues past individual failures. */
|
|
44
|
+
destroyAll(): Promise<void>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git worktree manager — creates isolated worktrees for thread execution.
|
|
3
|
+
*
|
|
4
|
+
* Phase 2 enhancements:
|
|
5
|
+
* - Mutex on create() to prevent branch name races under concurrency
|
|
6
|
+
* - Retry logic for git worktree add (transient lock file contention)
|
|
7
|
+
*
|
|
8
|
+
* Lifecycle:
|
|
9
|
+
* 1. create() — git worktree add -b swarm/<id> <path> HEAD
|
|
10
|
+
* 2. Agent runs in worktree directory
|
|
11
|
+
* 3. getDiff() — capture changes
|
|
12
|
+
* 4. commit() — commit changes in worktree
|
|
13
|
+
* 5. destroy() — git worktree remove + branch cleanup
|
|
14
|
+
*/
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
function git(args, cwd) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
execFile("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
21
|
+
if (err) {
|
|
22
|
+
reject(new Error(`git ${args[0]} failed: ${stderr || err.message}`));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
resolve({ stdout, stderr });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Simple async mutex — only one holder at a time. */
|
|
31
|
+
class Mutex {
|
|
32
|
+
locked = false;
|
|
33
|
+
waiters = [];
|
|
34
|
+
async acquire() {
|
|
35
|
+
if (!this.locked) {
|
|
36
|
+
this.locked = true;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
await new Promise((resolve) => this.waiters.push(resolve));
|
|
40
|
+
this.locked = true;
|
|
41
|
+
}
|
|
42
|
+
release() {
|
|
43
|
+
this.locked = false;
|
|
44
|
+
const next = this.waiters.shift();
|
|
45
|
+
if (next)
|
|
46
|
+
next();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const WORKTREE_CREATE_RETRIES = 3;
|
|
50
|
+
const WORKTREE_RETRY_DELAY_MS = 500;
|
|
51
|
+
export class WorktreeManager {
|
|
52
|
+
repoRoot;
|
|
53
|
+
baseDir;
|
|
54
|
+
worktrees = new Map();
|
|
55
|
+
createMutex = new Mutex();
|
|
56
|
+
constructor(repoRoot, baseDir = ".swarm-worktrees") {
|
|
57
|
+
this.repoRoot = repoRoot;
|
|
58
|
+
this.baseDir = path.isAbsolute(baseDir) ? baseDir : path.join(repoRoot, baseDir);
|
|
59
|
+
}
|
|
60
|
+
/** Ensure we're in a git repo and the base directory exists. */
|
|
61
|
+
async init() {
|
|
62
|
+
try {
|
|
63
|
+
await git(["rev-parse", "--git-dir"], this.repoRoot);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error(`Not a git repository: ${this.repoRoot}`);
|
|
67
|
+
}
|
|
68
|
+
if (!existsSync(this.baseDir)) {
|
|
69
|
+
mkdirSync(this.baseDir, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
// Add base dir to .gitignore if not already
|
|
72
|
+
const gitignorePath = path.join(this.repoRoot, ".gitignore");
|
|
73
|
+
const baseDirRelative = path.relative(this.repoRoot, this.baseDir);
|
|
74
|
+
try {
|
|
75
|
+
const { readFileSync, appendFileSync } = await import("node:fs");
|
|
76
|
+
let content = "";
|
|
77
|
+
if (existsSync(gitignorePath)) {
|
|
78
|
+
content = readFileSync(gitignorePath, "utf-8");
|
|
79
|
+
}
|
|
80
|
+
if (!content.includes(baseDirRelative)) {
|
|
81
|
+
appendFileSync(gitignorePath, `\n# Swarm worktrees\n${baseDirRelative}/\n`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Non-fatal — .gitignore might be read-only
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create a new worktree for a thread.
|
|
90
|
+
* Serialized via mutex to prevent branch name races.
|
|
91
|
+
* Retries on transient lock-file contention.
|
|
92
|
+
*/
|
|
93
|
+
async create(threadId) {
|
|
94
|
+
await this.createMutex.acquire();
|
|
95
|
+
try {
|
|
96
|
+
return await this.createWorktreeWithRetry(threadId);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
this.createMutex.release();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async createWorktreeWithRetry(threadId) {
|
|
103
|
+
const branch = `swarm/${threadId}`;
|
|
104
|
+
const wtPath = path.join(this.baseDir, `wt-${threadId}`);
|
|
105
|
+
// Remove stale worktree if it exists
|
|
106
|
+
if (existsSync(wtPath)) {
|
|
107
|
+
try {
|
|
108
|
+
await git(["worktree", "remove", "--force", wtPath], this.repoRoot);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
rmSync(wtPath, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Delete stale branch if exists
|
|
115
|
+
try {
|
|
116
|
+
await git(["branch", "-D", branch], this.repoRoot);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Branch didn't exist — fine
|
|
120
|
+
}
|
|
121
|
+
// Create worktree with retry for lock-file contention
|
|
122
|
+
let lastErr;
|
|
123
|
+
for (let attempt = 1; attempt <= WORKTREE_CREATE_RETRIES; attempt++) {
|
|
124
|
+
try {
|
|
125
|
+
await git(["worktree", "add", "-b", branch, wtPath, "HEAD"], this.repoRoot);
|
|
126
|
+
const info = { id: threadId, path: wtPath, branch };
|
|
127
|
+
this.worktrees.set(threadId, info);
|
|
128
|
+
return info;
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
lastErr = err instanceof Error ? err : new Error(String(err));
|
|
132
|
+
const isLockError = lastErr.message.includes(".lock") ||
|
|
133
|
+
lastErr.message.includes("Unable to create") ||
|
|
134
|
+
lastErr.message.includes("index.lock");
|
|
135
|
+
if (isLockError && attempt < WORKTREE_CREATE_RETRIES) {
|
|
136
|
+
// Wait with jitter before retrying
|
|
137
|
+
const delay = WORKTREE_RETRY_DELAY_MS * attempt + Math.random() * 200;
|
|
138
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
throw lastErr;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw lastErr || new Error("Failed to create worktree");
|
|
145
|
+
}
|
|
146
|
+
/** Get the git diff of uncommitted changes in a worktree. */
|
|
147
|
+
async getDiff(threadId) {
|
|
148
|
+
const info = this.worktrees.get(threadId);
|
|
149
|
+
if (!info)
|
|
150
|
+
throw new Error(`No worktree for thread ${threadId}`);
|
|
151
|
+
// Stage all changes first to include new files in diff
|
|
152
|
+
try {
|
|
153
|
+
await git(["add", "-A"], info.path);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Might be empty
|
|
157
|
+
}
|
|
158
|
+
const { stdout: fullDiff } = await git(["diff", "--cached"], info.path);
|
|
159
|
+
return fullDiff || "(no changes)";
|
|
160
|
+
}
|
|
161
|
+
/** Get diff stats (short summary). */
|
|
162
|
+
async getDiffStats(threadId) {
|
|
163
|
+
const info = this.worktrees.get(threadId);
|
|
164
|
+
if (!info)
|
|
165
|
+
throw new Error(`No worktree for thread ${threadId}`);
|
|
166
|
+
try {
|
|
167
|
+
await git(["add", "-A"], info.path);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
/* empty */
|
|
171
|
+
}
|
|
172
|
+
const { stdout } = await git(["diff", "--cached", "--stat"], info.path);
|
|
173
|
+
return stdout.trim() || "(no changes)";
|
|
174
|
+
}
|
|
175
|
+
/** Get list of changed files. */
|
|
176
|
+
async getChangedFiles(threadId) {
|
|
177
|
+
const info = this.worktrees.get(threadId);
|
|
178
|
+
if (!info)
|
|
179
|
+
throw new Error(`No worktree for thread ${threadId}`);
|
|
180
|
+
try {
|
|
181
|
+
await git(["add", "-A"], info.path);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
/* empty */
|
|
185
|
+
}
|
|
186
|
+
const { stdout } = await git(["diff", "--cached", "--name-only"], info.path);
|
|
187
|
+
return stdout.trim().split("\n").filter(Boolean);
|
|
188
|
+
}
|
|
189
|
+
/** Commit all changes in a worktree. */
|
|
190
|
+
async commit(threadId, message) {
|
|
191
|
+
const info = this.worktrees.get(threadId);
|
|
192
|
+
if (!info)
|
|
193
|
+
throw new Error(`No worktree for thread ${threadId}`);
|
|
194
|
+
try {
|
|
195
|
+
await git(["add", "-A"], info.path);
|
|
196
|
+
const { stdout: status } = await git(["status", "--porcelain"], info.path);
|
|
197
|
+
if (!status.trim())
|
|
198
|
+
return false; // Nothing to commit
|
|
199
|
+
await git(["commit", "-m", message], info.path);
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
// Nothing to commit is fine
|
|
204
|
+
if (String(err).includes("nothing to commit"))
|
|
205
|
+
return false;
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Destroy a worktree and optionally its branch. */
|
|
210
|
+
async destroy(threadId, deleteBranch = false) {
|
|
211
|
+
const info = this.worktrees.get(threadId);
|
|
212
|
+
if (!info)
|
|
213
|
+
return;
|
|
214
|
+
try {
|
|
215
|
+
await git(["worktree", "remove", "--force", info.path], this.repoRoot);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// Force remove the directory if git worktree remove fails
|
|
219
|
+
if (existsSync(info.path)) {
|
|
220
|
+
rmSync(info.path, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
// Prune stale worktree entries
|
|
223
|
+
try {
|
|
224
|
+
await git(["worktree", "prune"], this.repoRoot);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
/* non-fatal */
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (deleteBranch) {
|
|
231
|
+
try {
|
|
232
|
+
await git(["branch", "-D", info.branch], this.repoRoot);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Branch already gone
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
this.worktrees.delete(threadId);
|
|
239
|
+
}
|
|
240
|
+
/** Get info for a thread's worktree. */
|
|
241
|
+
getWorktreeInfo(threadId) {
|
|
242
|
+
return this.worktrees.get(threadId);
|
|
243
|
+
}
|
|
244
|
+
/** Cleanup all worktrees. Resilient — continues past individual failures. */
|
|
245
|
+
async destroyAll() {
|
|
246
|
+
for (const [id] of this.worktrees) {
|
|
247
|
+
try {
|
|
248
|
+
await this.destroy(id, true);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// Continue cleaning up remaining worktrees
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Remove base directory if empty
|
|
255
|
+
try {
|
|
256
|
+
const { readdirSync } = await import("node:fs");
|
|
257
|
+
if (existsSync(this.baseDir) && readdirSync(this.baseDir).length === 0) {
|
|
258
|
+
rmSync(this.baseDir, { recursive: true, force: true });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
/* non-fatal */
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=manager.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge thread branches back into the main branch.
|
|
3
|
+
*
|
|
4
|
+
* Phase 2 enhancements:
|
|
5
|
+
* - Partial merge: continues merging non-conflicting branches after a conflict
|
|
6
|
+
* - Conflict hunks: captures the actual diff of conflicted files
|
|
7
|
+
* - Merge ordering: accepts optional order array from orchestrator
|
|
8
|
+
*/
|
|
9
|
+
import type { MergeResult, ThreadState } from "../core/types.js";
|
|
10
|
+
/**
|
|
11
|
+
* Merge a single thread branch into the current branch.
|
|
12
|
+
* On conflict, captures the conflicted file list and diff hunks before aborting.
|
|
13
|
+
*/
|
|
14
|
+
export declare function mergeThreadBranch(repoRoot: string, branchName: string, threadId: string): Promise<MergeResult>;
|
|
15
|
+
export interface MergeAllOptions {
|
|
16
|
+
/** Explicit merge order — thread IDs in desired merge sequence. */
|
|
17
|
+
order?: string[];
|
|
18
|
+
/** If true, continue merging remaining branches after a conflict (default: true). */
|
|
19
|
+
continueOnConflict?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Merge all completed thread branches sequentially.
|
|
23
|
+
*
|
|
24
|
+
* Supports:
|
|
25
|
+
* - Custom merge order via options.order
|
|
26
|
+
* - Partial merge: by default continues past conflicts (skips conflicting branch)
|
|
27
|
+
*/
|
|
28
|
+
export declare function mergeAllThreads(repoRoot: string, threads: ThreadState[], options?: MergeAllOptions): Promise<MergeResult[]>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge thread branches back into the main branch.
|
|
3
|
+
*
|
|
4
|
+
* Phase 2 enhancements:
|
|
5
|
+
* - Partial merge: continues merging non-conflicting branches after a conflict
|
|
6
|
+
* - Conflict hunks: captures the actual diff of conflicted files
|
|
7
|
+
* - Merge ordering: accepts optional order array from orchestrator
|
|
8
|
+
*/
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
function git(args, cwd) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
execFile("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
13
|
+
if (err) {
|
|
14
|
+
// Include stdout in error message — git merge writes CONFLICT info to stdout
|
|
15
|
+
const detail = [stderr, stdout].filter((s) => s?.trim()).join("\n") || err.message;
|
|
16
|
+
reject(new Error(`git ${args[0]} failed: ${detail}`));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
resolve({ stdout, stderr });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Abort a merge safely, falling back to hard reset if --abort fails. */
|
|
25
|
+
async function abortMergeSafe(repoRoot) {
|
|
26
|
+
try {
|
|
27
|
+
await git(["merge", "--abort"], repoRoot);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
try {
|
|
31
|
+
await git(["reset", "--hard", "HEAD"], repoRoot);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* last resort failed — repo may be in bad state */
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Merge a single thread branch into the current branch.
|
|
40
|
+
* On conflict, captures the conflicted file list and diff hunks before aborting.
|
|
41
|
+
*/
|
|
42
|
+
export async function mergeThreadBranch(repoRoot, branchName, threadId) {
|
|
43
|
+
try {
|
|
44
|
+
const { stdout } = await git(["merge", "--no-ff", "-m", `swarm: merge thread ${threadId}`, branchName], repoRoot);
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
branch: branchName,
|
|
48
|
+
conflicts: [],
|
|
49
|
+
conflictDiff: "",
|
|
50
|
+
message: stdout.trim() || `Merged ${branchName}`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
const errMsg = String(err);
|
|
55
|
+
// Check for merge conflicts
|
|
56
|
+
if (errMsg.includes("CONFLICT") || errMsg.includes("Merge conflict")) {
|
|
57
|
+
try {
|
|
58
|
+
// Get list of conflicted files
|
|
59
|
+
const { stdout: conflicted } = await git(["diff", "--name-only", "--diff-filter=U"], repoRoot);
|
|
60
|
+
const conflicts = conflicted.trim().split("\n").filter(Boolean);
|
|
61
|
+
// Capture the conflict diff (shows <<<<<<< markers)
|
|
62
|
+
let conflictDiff = "";
|
|
63
|
+
try {
|
|
64
|
+
const { stdout: diff } = await git(["diff"], repoRoot);
|
|
65
|
+
conflictDiff = diff.slice(0, 5000); // Cap at 5KB
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// diff might fail in weird states
|
|
69
|
+
}
|
|
70
|
+
// Abort the merge to restore clean state
|
|
71
|
+
await abortMergeSafe(repoRoot);
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
branch: branchName,
|
|
75
|
+
conflicts,
|
|
76
|
+
conflictDiff,
|
|
77
|
+
message: `Merge conflicts in: ${conflicts.join(", ")}`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
await abortMergeSafe(repoRoot);
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
branch: branchName,
|
|
85
|
+
conflicts: [],
|
|
86
|
+
conflictDiff: "",
|
|
87
|
+
message: errMsg,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
branch: branchName,
|
|
94
|
+
conflicts: [],
|
|
95
|
+
conflictDiff: "",
|
|
96
|
+
message: errMsg,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Merge all completed thread branches sequentially.
|
|
102
|
+
*
|
|
103
|
+
* Supports:
|
|
104
|
+
* - Custom merge order via options.order
|
|
105
|
+
* - Partial merge: by default continues past conflicts (skips conflicting branch)
|
|
106
|
+
*/
|
|
107
|
+
export async function mergeAllThreads(repoRoot, threads, options = {}) {
|
|
108
|
+
const { order, continueOnConflict = true } = options;
|
|
109
|
+
const results = [];
|
|
110
|
+
// Filter to completed+successful threads with branches
|
|
111
|
+
const eligible = threads.filter((t) => t.status === "completed" && t.branchName && t.result?.success);
|
|
112
|
+
// Apply ordering if specified
|
|
113
|
+
let ordered;
|
|
114
|
+
if (order && order.length > 0) {
|
|
115
|
+
const orderMap = new Map(order.map((id, idx) => [id, idx]));
|
|
116
|
+
ordered = [...eligible].sort((a, b) => {
|
|
117
|
+
const aIdx = orderMap.get(a.id) ?? Infinity;
|
|
118
|
+
const bIdx = orderMap.get(b.id) ?? Infinity;
|
|
119
|
+
return aIdx - bIdx;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// Default: merge in completion order (earliest first)
|
|
124
|
+
ordered = [...eligible].sort((a, b) => (a.completedAt || 0) - (b.completedAt || 0));
|
|
125
|
+
}
|
|
126
|
+
for (const thread of ordered) {
|
|
127
|
+
const result = await mergeThreadBranch(repoRoot, thread.branchName, thread.id);
|
|
128
|
+
results.push(result);
|
|
129
|
+
if (!result.success && !continueOnConflict) {
|
|
130
|
+
// Stop on first conflict (legacy behavior)
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
// If conflict but continueOnConflict is true, we skip this branch
|
|
134
|
+
// and proceed to the next. The merge was already aborted in mergeThreadBranch.
|
|
135
|
+
}
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=merge.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "swarm-code",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Open-source swarm-native coding agent orchestrator — spawns parallel coding agents in isolated git worktrees, built on RLM (arXiv:2512.24601)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"swarm-code": "./bin/swarm.mjs",
|
|
8
|
+
"swarm": "./bin/swarm.mjs"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/main.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/**/*.js",
|
|
13
|
+
"dist/**/*.d.ts",
|
|
14
|
+
"dist/core/runtime.py",
|
|
15
|
+
"bin"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"dev": "tsx src/main.ts",
|
|
19
|
+
"build": "tsc && cp src/core/runtime.py dist/core/",
|
|
20
|
+
"start": "node dist/main.js",
|
|
21
|
+
"cli": "tsx src/cli.ts",
|
|
22
|
+
"swarm": "tsx src/main.ts",
|
|
23
|
+
"bench:oolong": "tsx benchmarks/oolong_synth.ts",
|
|
24
|
+
"bench:longbench": "tsx benchmarks/longbench_narrativeqa.ts",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest",
|
|
27
|
+
"lint": "biome check src/ tests/",
|
|
28
|
+
"lint:fix": "biome check --fix src/ tests/",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"swarm",
|
|
33
|
+
"coding-agent",
|
|
34
|
+
"orchestrator",
|
|
35
|
+
"rlm",
|
|
36
|
+
"recursive-language-model",
|
|
37
|
+
"llm",
|
|
38
|
+
"cli",
|
|
39
|
+
"agent",
|
|
40
|
+
"opencode",
|
|
41
|
+
"worktree",
|
|
42
|
+
"parallel",
|
|
43
|
+
"ai"
|
|
44
|
+
],
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"homepage": "https://github.com/kingjulio8238/swarm-code#readme",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/kingjulio8238/swarm-code.git"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@mariozechner/pi-ai": "^0.55.1",
|
|
53
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
54
|
+
"zod": "^4.3.6"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@biomejs/biome": "^2.4.7",
|
|
58
|
+
"esbuild": "^0.27.3",
|
|
59
|
+
"tsx": "^4.19.0",
|
|
60
|
+
"typescript": "^5.7.0",
|
|
61
|
+
"vitest": "^4.1.0"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=20"
|
|
65
|
+
},
|
|
66
|
+
"overrides": {
|
|
67
|
+
"minimatch": ">=10.2.1"
|
|
68
|
+
}
|
|
69
|
+
}
|