opencode-swarm-plugin 0.31.7 → 0.32.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/.turbo/turbo-build.log +10 -9
- package/.turbo/turbo-test.log +319 -317
- package/CHANGELOG.md +134 -0
- package/README.md +7 -4
- package/bin/swarm.ts +388 -128
- package/dist/compaction-hook.d.ts +1 -1
- package/dist/compaction-hook.d.ts.map +1 -1
- package/dist/hive.d.ts.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +123 -134
- package/dist/memory-tools.d.ts.map +1 -1
- package/dist/memory.d.ts +5 -4
- package/dist/memory.d.ts.map +1 -1
- package/dist/plugin.js +118 -131
- package/dist/swarm-orchestrate.d.ts +29 -5
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +7 -0
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm.d.ts +0 -2
- package/dist/swarm.d.ts.map +1 -1
- package/evals/lib/{data-loader.test.ts → data-loader.evalite-test.ts} +7 -6
- package/evals/lib/data-loader.ts +1 -1
- package/evals/scorers/{outcome-scorers.test.ts → outcome-scorers.evalite-test.ts} +1 -1
- package/examples/plugin-wrapper-template.ts +19 -4
- package/global-skills/swarm-coordination/SKILL.md +118 -8
- package/package.json +2 -2
- package/src/compaction-hook.ts +5 -3
- package/src/hive.integration.test.ts +83 -1
- package/src/hive.ts +37 -12
- package/src/mandate-storage.integration.test.ts +601 -0
- package/src/memory-tools.ts +6 -4
- package/src/memory.integration.test.ts +117 -49
- package/src/memory.test.ts +41 -217
- package/src/memory.ts +12 -8
- package/src/repo-crawl.integration.test.ts +441 -0
- package/src/skills.integration.test.ts +1056 -0
- package/src/structured.integration.test.ts +817 -0
- package/src/swarm-deferred.integration.test.ts +157 -0
- package/src/swarm-deferred.test.ts +38 -0
- package/src/swarm-mail.integration.test.ts +15 -19
- package/src/swarm-orchestrate.integration.test.ts +282 -0
- package/src/swarm-orchestrate.ts +96 -201
- package/src/swarm-prompts.test.ts +92 -0
- package/src/swarm-prompts.ts +69 -0
- package/src/swarm-review.integration.test.ts +290 -0
- package/src/swarm.integration.test.ts +23 -20
- package/src/tool-adapter.integration.test.ts +1221 -0
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - OPENCODE_SESSION_ID: Passed to CLI for session state persistence
|
|
12
12
|
* - OPENCODE_MESSAGE_ID: Passed to CLI for context
|
|
13
13
|
* - OPENCODE_AGENT: Passed to CLI for context
|
|
14
|
+
* - SWARM_PROJECT_DIR: Project directory (critical for database path)
|
|
14
15
|
*/
|
|
15
16
|
import type { Plugin, PluginInput, Hooks } from "@opencode-ai/plugin";
|
|
16
17
|
import { tool } from "@opencode-ai/plugin";
|
|
@@ -18,6 +19,10 @@ import { spawn } from "child_process";
|
|
|
18
19
|
|
|
19
20
|
const SWARM_CLI = "swarm";
|
|
20
21
|
|
|
22
|
+
// Module-level project directory - set during plugin initialization
|
|
23
|
+
// This is CRITICAL: without it, the CLI uses process.cwd() which may be wrong
|
|
24
|
+
let projectDirectory: string = process.cwd();
|
|
25
|
+
|
|
21
26
|
// =============================================================================
|
|
22
27
|
// CLI Execution Helper
|
|
23
28
|
// =============================================================================
|
|
@@ -27,6 +32,8 @@ const SWARM_CLI = "swarm";
|
|
|
27
32
|
*
|
|
28
33
|
* Spawns `swarm tool <name> --json '<args>'` and returns the result.
|
|
29
34
|
* Passes session context via environment variables.
|
|
35
|
+
*
|
|
36
|
+
* IMPORTANT: Runs in projectDirectory (set by OpenCode) not process.cwd()
|
|
30
37
|
*/
|
|
31
38
|
async function execTool(
|
|
32
39
|
name: string,
|
|
@@ -40,12 +47,14 @@ async function execTool(
|
|
|
40
47
|
: ["tool", name];
|
|
41
48
|
|
|
42
49
|
const proc = spawn(SWARM_CLI, cliArgs, {
|
|
50
|
+
cwd: projectDirectory, // Run in project directory, not plugin directory
|
|
43
51
|
stdio: ["ignore", "pipe", "pipe"],
|
|
44
52
|
env: {
|
|
45
53
|
...process.env,
|
|
46
54
|
OPENCODE_SESSION_ID: ctx.sessionID,
|
|
47
55
|
OPENCODE_MESSAGE_ID: ctx.messageID,
|
|
48
56
|
OPENCODE_AGENT: ctx.agent,
|
|
57
|
+
SWARM_PROJECT_DIR: projectDirectory, // Also pass as env var
|
|
49
58
|
},
|
|
50
59
|
});
|
|
51
60
|
|
|
@@ -1058,9 +1067,11 @@ Extract from session context:
|
|
|
1058
1067
|
|
|
1059
1068
|
1. \`swarm_status(epic_id="<epic>", project_key="<path>")\` - Get current state
|
|
1060
1069
|
2. \`swarmmail_inbox(limit=5)\` - Check for agent messages
|
|
1061
|
-
3.
|
|
1062
|
-
4.
|
|
1063
|
-
5. **
|
|
1070
|
+
3. \`swarm_review(project_key, epic_id, task_id, files_touched)\` - Review any completed work
|
|
1071
|
+
4. \`swarm_review_feedback(project_key, task_id, worker_id, status, issues)\` - Approve or request changes
|
|
1072
|
+
5. **Spawn ready subtasks** - Don't wait, fire them off
|
|
1073
|
+
6. **Unblock blocked work** - Resolve dependencies, reassign if needed
|
|
1074
|
+
7. **Collect completed work** - Close done subtasks, verify quality
|
|
1064
1075
|
|
|
1065
1076
|
### Keep the Swarm Cooking
|
|
1066
1077
|
|
|
@@ -1122,8 +1133,12 @@ type ExtendedHooks = Hooks & {
|
|
|
1122
1133
|
};
|
|
1123
1134
|
|
|
1124
1135
|
export const SwarmPlugin: Plugin = async (
|
|
1125
|
-
|
|
1136
|
+
input: PluginInput,
|
|
1126
1137
|
): Promise<ExtendedHooks> => {
|
|
1138
|
+
// CRITICAL: Set project directory from OpenCode input
|
|
1139
|
+
// Without this, CLI uses wrong database path
|
|
1140
|
+
projectDirectory = input.directory;
|
|
1141
|
+
|
|
1127
1142
|
return {
|
|
1128
1143
|
tool: {
|
|
1129
1144
|
// Beads
|
|
@@ -13,6 +13,8 @@ tools:
|
|
|
13
13
|
- swarm_complete
|
|
14
14
|
- swarm_status
|
|
15
15
|
- swarm_progress
|
|
16
|
+
- swarm_review
|
|
17
|
+
- swarm_review_feedback
|
|
16
18
|
- hive_create_epic
|
|
17
19
|
- hive_query
|
|
18
20
|
- swarmmail_init
|
|
@@ -442,19 +444,120 @@ for (const subtask of subtasks) {
|
|
|
442
444
|
}
|
|
443
445
|
```
|
|
444
446
|
|
|
445
|
-
### Phase 6:
|
|
447
|
+
### Phase 6: MANDATORY Review Loop (NON-NEGOTIABLE)
|
|
446
448
|
|
|
447
|
-
|
|
448
|
-
// Check progress
|
|
449
|
-
const status = await swarm_status({ epic_id, project_key });
|
|
449
|
+
**⚠️ AFTER EVERY Worker Returns, You MUST Complete This Checklist:**
|
|
450
450
|
|
|
451
|
-
|
|
452
|
-
const inbox = await swarmmail_inbox({ limit: 5 });
|
|
451
|
+
This is the **quality gate** that prevents shipping broken code. DO NOT skip this.
|
|
453
452
|
|
|
454
|
-
|
|
453
|
+
```typescript
|
|
454
|
+
// ============================================================
|
|
455
|
+
// Step 1: Check Swarm Mail (Worker may have sent messages)
|
|
456
|
+
// ============================================================
|
|
457
|
+
const inbox = await swarmmail_inbox({ limit: 5 });
|
|
455
458
|
const message = await swarmmail_read_message({ message_id: N });
|
|
456
459
|
|
|
457
|
-
//
|
|
460
|
+
// ============================================================
|
|
461
|
+
// Step 2: Review the Work (Generate review prompt with diff)
|
|
462
|
+
// ============================================================
|
|
463
|
+
const reviewPrompt = await swarm_review({
|
|
464
|
+
project_key: "/abs/path/to/project",
|
|
465
|
+
epic_id: "epic-id",
|
|
466
|
+
task_id: "subtask-id",
|
|
467
|
+
files_touched: ["src/auth/service.ts", "src/auth/service.test.ts"]
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// This generates a review prompt that includes:
|
|
471
|
+
// - Epic context (what we're trying to achieve)
|
|
472
|
+
// - Subtask requirements
|
|
473
|
+
// - Git diff of changes
|
|
474
|
+
// - Dependency status (what came before, what comes next)
|
|
475
|
+
|
|
476
|
+
// ============================================================
|
|
477
|
+
// Step 3: Evaluate Against Criteria
|
|
478
|
+
// ============================================================
|
|
479
|
+
// Ask yourself:
|
|
480
|
+
// - Does the work fulfill the subtask requirements?
|
|
481
|
+
// - Does it serve the overall epic goal?
|
|
482
|
+
// - Does it enable downstream tasks?
|
|
483
|
+
// - Type safety, no obvious bugs?
|
|
484
|
+
|
|
485
|
+
// ============================================================
|
|
486
|
+
// Step 4: Send Feedback (Approve or Request Changes)
|
|
487
|
+
// ============================================================
|
|
488
|
+
await swarm_review_feedback({
|
|
489
|
+
project_key: "/abs/path/to/project",
|
|
490
|
+
task_id: "subtask-id",
|
|
491
|
+
worker_id: "WorkerName",
|
|
492
|
+
status: "approved", // or "needs_changes"
|
|
493
|
+
summary: "LGTM - auth service looks solid",
|
|
494
|
+
issues: "[]" // or "[{file, line, issue, suggestion}]"
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// ============================================================
|
|
498
|
+
// Step 5: ONLY THEN Continue
|
|
499
|
+
// ============================================================
|
|
500
|
+
// If approved:
|
|
501
|
+
// - Close the cell
|
|
502
|
+
// - Spawn next worker (if dependencies allow)
|
|
503
|
+
// - Update swarm status
|
|
504
|
+
//
|
|
505
|
+
// If needs_changes:
|
|
506
|
+
// - Worker gets feedback
|
|
507
|
+
// - Worker retries (max 3 attempts)
|
|
508
|
+
// - Review again when worker re-submits
|
|
509
|
+
//
|
|
510
|
+
// If 3 failures:
|
|
511
|
+
// - Mark task blocked
|
|
512
|
+
// - Escalate to human (architectural problem, not "try harder")
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
**❌ Anti-Pattern (Skipping Review):**
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
// Worker completes
|
|
519
|
+
swarm_complete({ ... });
|
|
520
|
+
|
|
521
|
+
// Coordinator immediately spawns next worker
|
|
522
|
+
// ⚠️ WRONG - No quality gate!
|
|
523
|
+
Task({ subagent_type: "swarm/worker", prompt: nextWorkerPrompt });
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
**✅ Correct Pattern (Review Before Proceeding):**
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
// Worker completes
|
|
530
|
+
swarm_complete({ ... });
|
|
531
|
+
|
|
532
|
+
// Coordinator REVIEWS first
|
|
533
|
+
swarm_review({ ... });
|
|
534
|
+
// ... evaluates changes ...
|
|
535
|
+
swarm_review_feedback({ status: "approved" });
|
|
536
|
+
|
|
537
|
+
// ONLY THEN spawn next worker
|
|
538
|
+
Task({ subagent_type: "swarm/worker", prompt: nextWorkerPrompt });
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
**Review Workflow (3-Strike Rule):**
|
|
542
|
+
|
|
543
|
+
1. Worker calls `swarm_complete` → Coordinator notified
|
|
544
|
+
2. Coordinator runs `swarm_review` → Gets diff + epic context
|
|
545
|
+
3. Coordinator evaluates against epic goals
|
|
546
|
+
4. If good: `swarm_review_feedback(status="approved")` → Task closed
|
|
547
|
+
5. If issues: `swarm_review_feedback(status="needs_changes", issues=[...])` → Worker fixes
|
|
548
|
+
6. After 3 rejections → Task marked blocked (architectural problem, not "try harder")
|
|
549
|
+
|
|
550
|
+
**Review Criteria:**
|
|
551
|
+
- Does work fulfill subtask requirements?
|
|
552
|
+
- Does it serve the overall epic goal?
|
|
553
|
+
- Does it enable downstream tasks?
|
|
554
|
+
- Type safety, no obvious bugs?
|
|
555
|
+
|
|
556
|
+
**Monitoring & Intervention:**
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
// Check overall swarm status
|
|
560
|
+
const status = await swarm_status({ epic_id, project_key });
|
|
458
561
|
```
|
|
459
562
|
|
|
460
563
|
### Phase 7: Aggregate & Complete
|
|
@@ -778,6 +881,13 @@ One blocker affects multiple subtasks.
|
|
|
778
881
|
| `swarmmail_ack` | Acknowledge message |
|
|
779
882
|
| `swarmmail_health` | Check database health |
|
|
780
883
|
|
|
884
|
+
## Swarm Review Quick Reference
|
|
885
|
+
|
|
886
|
+
| Tool | Purpose |
|
|
887
|
+
| ------------------------ | ------------------------------------------ |
|
|
888
|
+
| `swarm_review` | Generate review prompt with epic context + diff |
|
|
889
|
+
| `swarm_review_feedback` | Send approval/rejection to worker (3-strike rule) |
|
|
890
|
+
|
|
781
891
|
## Full Swarm Flow
|
|
782
892
|
|
|
783
893
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Multi-agent swarm coordination for OpenCode with learning capabilities, beads integration, and Agent Mail",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"gray-matter": "^4.0.3",
|
|
40
40
|
"ioredis": "^5.4.1",
|
|
41
41
|
"minimatch": "^10.1.1",
|
|
42
|
-
"swarm-mail": "1.
|
|
42
|
+
"swarm-mail": "1.3.0",
|
|
43
43
|
"zod": "4.1.8"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
package/src/compaction-hook.ts
CHANGED
|
@@ -88,9 +88,11 @@ Extract from session context:
|
|
|
88
88
|
|
|
89
89
|
1. \`swarm_status(epic_id="<epic>", project_key="<path>")\` - Get current state
|
|
90
90
|
2. \`swarmmail_inbox(limit=5)\` - Check for agent messages
|
|
91
|
-
3.
|
|
92
|
-
4.
|
|
93
|
-
5. **
|
|
91
|
+
3. \`swarm_review(project_key, epic_id, task_id, files_touched)\` - Review any completed work
|
|
92
|
+
4. \`swarm_review_feedback(project_key, task_id, worker_id, status, issues)\` - Approve or request changes
|
|
93
|
+
5. **Spawn ready subtasks** - Don't wait, fire them off
|
|
94
|
+
6. **Unblock blocked work** - Resolve dependencies, reassign if needed
|
|
95
|
+
7. **Collect completed work** - Close done subtasks, verify quality
|
|
94
96
|
|
|
95
97
|
### Keep the Swarm Cooking
|
|
96
98
|
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* Run with: bun test src/hive.integration.test.ts
|
|
8
8
|
*/
|
|
9
9
|
import { describe, it, expect, beforeAll, beforeEach, afterAll } from "vitest";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
10
12
|
import {
|
|
11
13
|
hive_create,
|
|
12
14
|
hive_create_epic,
|
|
@@ -56,7 +58,7 @@ const createdBeadIds: string[] = [];
|
|
|
56
58
|
/**
|
|
57
59
|
* Test project key - use temp directory to isolate tests
|
|
58
60
|
*/
|
|
59
|
-
const TEST_PROJECT_KEY =
|
|
61
|
+
const TEST_PROJECT_KEY = join(tmpdir(), `beads-integration-test-${Date.now()}`);
|
|
60
62
|
|
|
61
63
|
/**
|
|
62
64
|
* Adapter instance for verification
|
|
@@ -1353,6 +1355,86 @@ describe("beads integration", () => {
|
|
|
1353
1355
|
});
|
|
1354
1356
|
|
|
1355
1357
|
describe("hive_sync", () => {
|
|
1358
|
+
it("succeeds with unstaged changes outside .hive/ (stash-before-pull)", async () => {
|
|
1359
|
+
const { mkdirSync, rmSync, writeFileSync, existsSync } = await import("node:fs");
|
|
1360
|
+
const { join } = await import("node:path");
|
|
1361
|
+
const { tmpdir } = await import("node:os");
|
|
1362
|
+
const { execSync } = await import("node:child_process");
|
|
1363
|
+
|
|
1364
|
+
// Create a temp git repository with a remote (to trigger pull)
|
|
1365
|
+
const tempProject = join(tmpdir(), `hive-sync-stash-test-${Date.now()}`);
|
|
1366
|
+
const remoteProject = join(tmpdir(), `hive-sync-remote-${Date.now()}`);
|
|
1367
|
+
|
|
1368
|
+
// Create "remote" bare repo
|
|
1369
|
+
mkdirSync(remoteProject, { recursive: true });
|
|
1370
|
+
execSync("git init --bare", { cwd: remoteProject });
|
|
1371
|
+
|
|
1372
|
+
// Create local repo
|
|
1373
|
+
mkdirSync(tempProject, { recursive: true });
|
|
1374
|
+
execSync("git init", { cwd: tempProject });
|
|
1375
|
+
execSync('git config user.email "test@example.com"', { cwd: tempProject });
|
|
1376
|
+
execSync('git config user.name "Test User"', { cwd: tempProject });
|
|
1377
|
+
execSync(`git remote add origin ${remoteProject}`, { cwd: tempProject });
|
|
1378
|
+
|
|
1379
|
+
// Create .hive directory and a source file
|
|
1380
|
+
const hiveDir = join(tempProject, ".hive");
|
|
1381
|
+
mkdirSync(hiveDir, { recursive: true });
|
|
1382
|
+
writeFileSync(join(hiveDir, "issues.jsonl"), "");
|
|
1383
|
+
writeFileSync(join(tempProject, "src.ts"), "// initial");
|
|
1384
|
+
|
|
1385
|
+
// Initial commit and push
|
|
1386
|
+
execSync("git add .", { cwd: tempProject });
|
|
1387
|
+
execSync('git commit -m "initial commit"', { cwd: tempProject });
|
|
1388
|
+
execSync("git push -u origin main", { cwd: tempProject });
|
|
1389
|
+
|
|
1390
|
+
// Now create unstaged changes OUTSIDE .hive/
|
|
1391
|
+
writeFileSync(join(tempProject, "src.ts"), "// modified but not staged");
|
|
1392
|
+
|
|
1393
|
+
// Set working directory for hive commands
|
|
1394
|
+
const originalDir = getHiveWorkingDirectory();
|
|
1395
|
+
setHiveWorkingDirectory(tempProject);
|
|
1396
|
+
|
|
1397
|
+
try {
|
|
1398
|
+
// Create a cell (this will mark it dirty and flush will write to JSONL)
|
|
1399
|
+
await hive_create.execute(
|
|
1400
|
+
{ title: "Stash test cell", type: "task" },
|
|
1401
|
+
mockContext,
|
|
1402
|
+
);
|
|
1403
|
+
|
|
1404
|
+
// Sync WITH auto_pull=true (this is where the bug manifests)
|
|
1405
|
+
// Before fix: fails with "cannot pull with rebase: You have unstaged changes"
|
|
1406
|
+
// After fix: stashes, pulls, pops, succeeds
|
|
1407
|
+
const result = await hive_sync.execute(
|
|
1408
|
+
{ auto_pull: true },
|
|
1409
|
+
mockContext,
|
|
1410
|
+
);
|
|
1411
|
+
|
|
1412
|
+
// Should succeed
|
|
1413
|
+
expect(result).toContain("successfully");
|
|
1414
|
+
|
|
1415
|
+
// Verify .hive changes were committed
|
|
1416
|
+
const hiveStatus = execSync("git status --porcelain .hive/", {
|
|
1417
|
+
cwd: tempProject,
|
|
1418
|
+
encoding: "utf-8",
|
|
1419
|
+
});
|
|
1420
|
+
expect(hiveStatus.trim()).toBe("");
|
|
1421
|
+
|
|
1422
|
+
// Verify unstaged changes are still there (stash was popped)
|
|
1423
|
+
const srcStatus = execSync("git status --porcelain src.ts", {
|
|
1424
|
+
cwd: tempProject,
|
|
1425
|
+
encoding: "utf-8",
|
|
1426
|
+
});
|
|
1427
|
+
expect(srcStatus.trim()).toContain("M src.ts");
|
|
1428
|
+
} finally {
|
|
1429
|
+
// Restore original working directory
|
|
1430
|
+
setHiveWorkingDirectory(originalDir);
|
|
1431
|
+
|
|
1432
|
+
// Cleanup
|
|
1433
|
+
rmSync(tempProject, { recursive: true, force: true });
|
|
1434
|
+
rmSync(remoteProject, { recursive: true, force: true });
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1356
1438
|
it("commits .hive changes before pulling (regression test for unstaged changes error)", async () => {
|
|
1357
1439
|
const { mkdirSync, rmSync, writeFileSync, existsSync } = await import("node:fs");
|
|
1358
1440
|
const { join } = await import("node:path");
|
package/src/hive.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
syncMemories,
|
|
23
23
|
type HiveAdapter,
|
|
24
24
|
type Cell as AdapterCell,
|
|
25
|
-
|
|
25
|
+
getSwarmMailLibSQL,
|
|
26
26
|
resolvePartialId,
|
|
27
27
|
} from "swarm-mail";
|
|
28
28
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -508,7 +508,7 @@ export async function getHiveAdapter(projectKey: string): Promise<HiveAdapter> {
|
|
|
508
508
|
return adapterCache.get(projectKey)!;
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
-
const swarmMail = await
|
|
511
|
+
const swarmMail = await getSwarmMailLibSQL(projectKey);
|
|
512
512
|
const db = await swarmMail.getDatabase();
|
|
513
513
|
const adapter = createHiveAdapter(db, projectKey);
|
|
514
514
|
|
|
@@ -1158,7 +1158,7 @@ export const hive_sync = tool({
|
|
|
1158
1158
|
);
|
|
1159
1159
|
|
|
1160
1160
|
// 2b. Sync memories to JSONL
|
|
1161
|
-
const swarmMail = await
|
|
1161
|
+
const swarmMail = await getSwarmMailLibSQL(projectKey);
|
|
1162
1162
|
const db = await swarmMail.getDatabase();
|
|
1163
1163
|
const hivePath = join(projectKey, ".hive");
|
|
1164
1164
|
let memoriesSynced = 0;
|
|
@@ -1217,18 +1217,43 @@ export const hive_sync = tool({
|
|
|
1217
1217
|
const hasRemote = remoteCheckResult.stdout.trim() !== "";
|
|
1218
1218
|
|
|
1219
1219
|
if (hasRemote) {
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1220
|
+
// Check for unstaged changes that would block pull --rebase
|
|
1221
|
+
const statusResult = await runGitCommand(["status", "--porcelain"]);
|
|
1222
|
+
const hasUnstagedChanges = statusResult.stdout.trim() !== "";
|
|
1223
|
+
let didStash = false;
|
|
1224
|
+
|
|
1225
|
+
if (hasUnstagedChanges) {
|
|
1226
|
+
// Stash all changes (including untracked) before pull
|
|
1227
|
+
const stashResult = await runGitCommand(["stash", "push", "-u", "-m", "hive_sync: auto-stash before pull"]);
|
|
1228
|
+
if (stashResult.exitCode === 0) {
|
|
1229
|
+
didStash = true;
|
|
1230
|
+
}
|
|
1231
|
+
// If stash fails (e.g., nothing to stash), continue anyway
|
|
1232
|
+
}
|
|
1225
1233
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1234
|
+
try {
|
|
1235
|
+
const pullResult = await withTimeout(
|
|
1236
|
+
runGitCommand(["pull", "--rebase"]),
|
|
1237
|
+
TIMEOUT_MS,
|
|
1229
1238
|
"git pull --rebase",
|
|
1230
|
-
pullResult.exitCode,
|
|
1231
1239
|
);
|
|
1240
|
+
|
|
1241
|
+
if (pullResult.exitCode !== 0) {
|
|
1242
|
+
throw new HiveError(
|
|
1243
|
+
`Failed to pull: ${pullResult.stderr}`,
|
|
1244
|
+
"git pull --rebase",
|
|
1245
|
+
pullResult.exitCode,
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
} finally {
|
|
1249
|
+
// Pop stash if we stashed
|
|
1250
|
+
if (didStash) {
|
|
1251
|
+
const popResult = await runGitCommand(["stash", "pop"]);
|
|
1252
|
+
if (popResult.exitCode !== 0) {
|
|
1253
|
+
// Stash pop failed - likely a conflict. Log warning but don't fail sync.
|
|
1254
|
+
console.warn(`[hive_sync] Warning: stash pop failed. Your changes are in 'git stash list'. Error: ${popResult.stderr}`);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1232
1257
|
}
|
|
1233
1258
|
}
|
|
1234
1259
|
}
|