opencode-swarm 6.53.3 → 6.53.6
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/cli/index.js +376 -50
- package/dist/commands/close.d.ts +6 -2
- package/dist/commands/full-auto-config-guard.test.d.ts +7 -0
- package/dist/commands/full-auto-discoverability.test.d.ts +8 -0
- package/dist/hooks/full-auto-intercept.d.ts +1 -1
- package/dist/index.js +470 -75
- package/dist/services/handoff-service.d.ts +2 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -16387,7 +16387,7 @@ var init_ledger = __esm(() => {
|
|
|
16387
16387
|
});
|
|
16388
16388
|
|
|
16389
16389
|
// src/plan/manager.ts
|
|
16390
|
-
import { existsSync as existsSync5, renameSync as renameSync3, unlinkSync } from "fs";
|
|
16390
|
+
import { copyFileSync, existsSync as existsSync5, renameSync as renameSync3, unlinkSync } from "fs";
|
|
16391
16391
|
import * as path8 from "path";
|
|
16392
16392
|
async function loadPlanJsonOnly(directory) {
|
|
16393
16393
|
const planJsonContent = await readSwarmFileAsync(directory, "plan.json");
|
|
@@ -16645,17 +16645,60 @@ async function savePlan(directory, plan, options) {
|
|
|
16645
16645
|
if (existingEvents.length > 0 && existingEvents[0].plan_id !== planId) {
|
|
16646
16646
|
const swarmDir2 = path8.resolve(directory, ".swarm");
|
|
16647
16647
|
const oldLedgerPath = path8.join(swarmDir2, "plan-ledger.jsonl");
|
|
16648
|
-
const
|
|
16648
|
+
const oldLedgerBackupPath = path8.join(swarmDir2, `plan-ledger.backup-${Date.now()}-${Math.floor(Math.random() * 1e9)}.jsonl`);
|
|
16649
|
+
let backupExists = false;
|
|
16649
16650
|
if (existsSync5(oldLedgerPath)) {
|
|
16650
|
-
|
|
16651
|
-
|
|
16651
|
+
try {
|
|
16652
|
+
renameSync3(oldLedgerPath, oldLedgerBackupPath);
|
|
16653
|
+
backupExists = true;
|
|
16654
|
+
} catch (renameErr) {
|
|
16655
|
+
throw new Error(`[savePlan] Cannot reinitialize ledger: could not move old ledger aside (rename failed: ${renameErr instanceof Error ? renameErr.message : String(renameErr)}). The existing ledger has plan_id="${existingEvents[0].plan_id}" which does not match the current plan="${planId}". To proceed, close any programs that may have the ledger file open, or run /swarm reset-session to clear the ledger.`);
|
|
16656
|
+
}
|
|
16652
16657
|
}
|
|
16653
|
-
|
|
16654
|
-
|
|
16655
|
-
|
|
16656
|
-
|
|
16657
|
-
|
|
16658
|
+
let initSucceeded = false;
|
|
16659
|
+
if (backupExists) {
|
|
16660
|
+
try {
|
|
16661
|
+
await initLedger(directory, planId, planHashForInit);
|
|
16662
|
+
initSucceeded = true;
|
|
16663
|
+
} catch (initErr) {
|
|
16664
|
+
const errorMessage = String(initErr);
|
|
16665
|
+
if (errorMessage.includes("already initialized")) {
|
|
16666
|
+
try {
|
|
16667
|
+
if (existsSync5(oldLedgerBackupPath))
|
|
16668
|
+
unlinkSync(oldLedgerBackupPath);
|
|
16669
|
+
} catch {}
|
|
16670
|
+
} else {
|
|
16671
|
+
if (existsSync5(oldLedgerBackupPath)) {
|
|
16672
|
+
try {
|
|
16673
|
+
renameSync3(oldLedgerBackupPath, oldLedgerPath);
|
|
16674
|
+
} catch {
|
|
16675
|
+
copyFileSync(oldLedgerBackupPath, oldLedgerPath);
|
|
16676
|
+
try {
|
|
16677
|
+
unlinkSync(oldLedgerBackupPath);
|
|
16678
|
+
} catch {}
|
|
16679
|
+
}
|
|
16680
|
+
}
|
|
16681
|
+
throw initErr;
|
|
16682
|
+
}
|
|
16683
|
+
}
|
|
16684
|
+
}
|
|
16685
|
+
if (initSucceeded && backupExists) {
|
|
16686
|
+
const archivePath = path8.join(swarmDir2, `plan-ledger.archived-${Date.now()}-${Math.floor(Math.random() * 1e9)}.jsonl`);
|
|
16687
|
+
try {
|
|
16688
|
+
renameSync3(oldLedgerBackupPath, archivePath);
|
|
16689
|
+
warn(`[savePlan] Ledger identity mismatch (was "${existingEvents[0].plan_id}", now "${planId}") \u2014 archived old ledger to ${archivePath} and reinitializing.`);
|
|
16690
|
+
} catch (renameErr) {
|
|
16691
|
+
warn(`[savePlan] Could not archive old ledger (rename failed: ${renameErr instanceof Error ? renameErr.message : String(renameErr)}). Old ledger may still exist at ${oldLedgerBackupPath}.`);
|
|
16692
|
+
try {
|
|
16693
|
+
if (existsSync5(oldLedgerBackupPath))
|
|
16694
|
+
unlinkSync(oldLedgerBackupPath);
|
|
16695
|
+
} catch {}
|
|
16658
16696
|
}
|
|
16697
|
+
} else if (!initSucceeded && backupExists) {
|
|
16698
|
+
try {
|
|
16699
|
+
if (existsSync5(oldLedgerBackupPath))
|
|
16700
|
+
unlinkSync(oldLedgerBackupPath);
|
|
16701
|
+
} catch {}
|
|
16659
16702
|
}
|
|
16660
16703
|
}
|
|
16661
16704
|
}
|
|
@@ -32107,6 +32150,52 @@ import { promises as fs6 } from "fs";
|
|
|
32107
32150
|
import path11 from "path";
|
|
32108
32151
|
init_manager();
|
|
32109
32152
|
|
|
32153
|
+
// src/git/branch.ts
|
|
32154
|
+
init_logger();
|
|
32155
|
+
import * as child_process2 from "child_process";
|
|
32156
|
+
var GIT_TIMEOUT_MS2 = 30000;
|
|
32157
|
+
function gitExec2(args, cwd) {
|
|
32158
|
+
const result = child_process2.spawnSync("git", args, {
|
|
32159
|
+
cwd,
|
|
32160
|
+
encoding: "utf-8",
|
|
32161
|
+
timeout: GIT_TIMEOUT_MS2,
|
|
32162
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
32163
|
+
});
|
|
32164
|
+
if (result.status !== 0) {
|
|
32165
|
+
throw new Error(result.stderr || `git exited with ${result.status}`);
|
|
32166
|
+
}
|
|
32167
|
+
return result.stdout;
|
|
32168
|
+
}
|
|
32169
|
+
function isGitRepo2(cwd) {
|
|
32170
|
+
try {
|
|
32171
|
+
gitExec2(["rev-parse", "--git-dir"], cwd);
|
|
32172
|
+
return true;
|
|
32173
|
+
} catch {
|
|
32174
|
+
return false;
|
|
32175
|
+
}
|
|
32176
|
+
}
|
|
32177
|
+
function getCurrentBranch(cwd) {
|
|
32178
|
+
const output = gitExec2(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
32179
|
+
return output.trim();
|
|
32180
|
+
}
|
|
32181
|
+
function getDefaultBaseBranch(cwd) {
|
|
32182
|
+
try {
|
|
32183
|
+
gitExec2(["rev-parse", "--verify", "origin/main"], cwd);
|
|
32184
|
+
return "origin/main";
|
|
32185
|
+
} catch {
|
|
32186
|
+
try {
|
|
32187
|
+
gitExec2(["rev-parse", "--verify", "origin/master"], cwd);
|
|
32188
|
+
return "origin/master";
|
|
32189
|
+
} catch {
|
|
32190
|
+
return "origin/main";
|
|
32191
|
+
}
|
|
32192
|
+
}
|
|
32193
|
+
}
|
|
32194
|
+
function hasUncommittedChanges(cwd) {
|
|
32195
|
+
const status = gitExec2(["status", "--porcelain"], cwd);
|
|
32196
|
+
return status.trim().length > 0;
|
|
32197
|
+
}
|
|
32198
|
+
|
|
32110
32199
|
// src/hooks/knowledge-store.ts
|
|
32111
32200
|
var import_proper_lockfile = __toESM(require_proper_lockfile(), 1);
|
|
32112
32201
|
import { existsSync as existsSync3 } from "fs";
|
|
@@ -33220,8 +33309,28 @@ var write_retro = createSwarmTool({
|
|
|
33220
33309
|
});
|
|
33221
33310
|
|
|
33222
33311
|
// src/commands/close.ts
|
|
33312
|
+
var ARCHIVE_ARTIFACTS = [
|
|
33313
|
+
"plan.json",
|
|
33314
|
+
"plan.md",
|
|
33315
|
+
"context.md",
|
|
33316
|
+
"events.jsonl",
|
|
33317
|
+
"handoff.md",
|
|
33318
|
+
"handoff-prompt.md",
|
|
33319
|
+
"handoff-consumed.md",
|
|
33320
|
+
"escalation-report.md",
|
|
33321
|
+
"close-lessons.md"
|
|
33322
|
+
];
|
|
33323
|
+
var ACTIVE_STATE_TO_CLEAN = [
|
|
33324
|
+
"plan.md",
|
|
33325
|
+
"events.jsonl",
|
|
33326
|
+
"handoff.md",
|
|
33327
|
+
"handoff-prompt.md",
|
|
33328
|
+
"handoff-consumed.md",
|
|
33329
|
+
"escalation-report.md"
|
|
33330
|
+
];
|
|
33223
33331
|
async function handleCloseCommand(directory, args) {
|
|
33224
33332
|
const planPath = validateSwarmPath(directory, "plan.json");
|
|
33333
|
+
const swarmDir = path11.join(directory, ".swarm");
|
|
33225
33334
|
let planExists = false;
|
|
33226
33335
|
let planData = {
|
|
33227
33336
|
title: path11.basename(directory) || "Ad-hoc session",
|
|
@@ -33235,13 +33344,14 @@ async function handleCloseCommand(directory, args) {
|
|
|
33235
33344
|
if (error93?.code !== "ENOENT") {
|
|
33236
33345
|
return `\u274C Failed to read plan.json: ${error93 instanceof Error ? error93.message : String(error93)}`;
|
|
33237
33346
|
}
|
|
33238
|
-
const swarmDirExists = await fs6.access(
|
|
33347
|
+
const swarmDirExists = await fs6.access(swarmDir).then(() => true).catch(() => false);
|
|
33239
33348
|
if (!swarmDirExists) {
|
|
33240
33349
|
return `\u274C No .swarm/ directory found in ${directory}. Run /swarm close from the project root, or run /swarm plan first.`;
|
|
33241
33350
|
}
|
|
33242
33351
|
}
|
|
33243
33352
|
const phases = planData.phases ?? [];
|
|
33244
33353
|
const inProgressPhases = phases.filter((p) => p.status === "in_progress");
|
|
33354
|
+
const isForced = args.includes("--force");
|
|
33245
33355
|
let planAlreadyDone = false;
|
|
33246
33356
|
if (planExists) {
|
|
33247
33357
|
planAlreadyDone = phases.length > 0 && phases.every((p) => p.status === "complete" || p.status === "completed" || p.status === "blocked" || p.status === "closed");
|
|
@@ -33258,7 +33368,7 @@ async function handleCloseCommand(directory, args) {
|
|
|
33258
33368
|
try {
|
|
33259
33369
|
retroResult = await executeWriteRetro({
|
|
33260
33370
|
phase: phase.id,
|
|
33261
|
-
summary:
|
|
33371
|
+
summary: isForced ? `Phase force-closed via /swarm close --force` : `Phase closed via /swarm close`,
|
|
33262
33372
|
task_count: Math.max(1, (phase.tasks ?? []).length),
|
|
33263
33373
|
task_complexity: "simple",
|
|
33264
33374
|
total_tool_calls: 0,
|
|
@@ -33286,7 +33396,7 @@ async function handleCloseCommand(directory, args) {
|
|
|
33286
33396
|
}
|
|
33287
33397
|
}
|
|
33288
33398
|
}
|
|
33289
|
-
const lessonsFilePath = path11.join(
|
|
33399
|
+
const lessonsFilePath = path11.join(swarmDir, "close-lessons.md");
|
|
33290
33400
|
let explicitLessons = [];
|
|
33291
33401
|
try {
|
|
33292
33402
|
const lessonsText = await fs6.readFile(lessonsFilePath, "utf-8");
|
|
@@ -33326,13 +33436,83 @@ async function handleCloseCommand(directory, args) {
|
|
|
33326
33436
|
console.warn("[close-command] Failed to write plan.json:", error93);
|
|
33327
33437
|
}
|
|
33328
33438
|
}
|
|
33439
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
33440
|
+
const archiveDir = path11.join(swarmDir, "archive", `swarm-${timestamp}`);
|
|
33441
|
+
let archiveResult = "";
|
|
33442
|
+
let archivedFileCount = 0;
|
|
33443
|
+
const archivedActiveStateFiles = new Set;
|
|
33444
|
+
try {
|
|
33445
|
+
await fs6.mkdir(archiveDir, { recursive: true });
|
|
33446
|
+
for (const artifact of ARCHIVE_ARTIFACTS) {
|
|
33447
|
+
const srcPath = path11.join(swarmDir, artifact);
|
|
33448
|
+
const destPath = path11.join(archiveDir, artifact);
|
|
33449
|
+
try {
|
|
33450
|
+
await fs6.copyFile(srcPath, destPath);
|
|
33451
|
+
archivedFileCount++;
|
|
33452
|
+
if (ACTIVE_STATE_TO_CLEAN.includes(artifact)) {
|
|
33453
|
+
archivedActiveStateFiles.add(artifact);
|
|
33454
|
+
}
|
|
33455
|
+
} catch {}
|
|
33456
|
+
}
|
|
33457
|
+
const evidenceDir = path11.join(swarmDir, "evidence");
|
|
33458
|
+
const archiveEvidenceDir = path11.join(archiveDir, "evidence");
|
|
33459
|
+
try {
|
|
33460
|
+
const evidenceEntries = await fs6.readdir(evidenceDir);
|
|
33461
|
+
if (evidenceEntries.length > 0) {
|
|
33462
|
+
await fs6.mkdir(archiveEvidenceDir, { recursive: true });
|
|
33463
|
+
for (const entry of evidenceEntries) {
|
|
33464
|
+
const srcEntry = path11.join(evidenceDir, entry);
|
|
33465
|
+
const destEntry = path11.join(archiveEvidenceDir, entry);
|
|
33466
|
+
try {
|
|
33467
|
+
const stat = await fs6.stat(srcEntry);
|
|
33468
|
+
if (stat.isDirectory()) {
|
|
33469
|
+
await fs6.mkdir(destEntry, { recursive: true });
|
|
33470
|
+
const subEntries = await fs6.readdir(srcEntry);
|
|
33471
|
+
for (const sub of subEntries) {
|
|
33472
|
+
await fs6.copyFile(path11.join(srcEntry, sub), path11.join(destEntry, sub)).catch(() => {});
|
|
33473
|
+
}
|
|
33474
|
+
} else {
|
|
33475
|
+
await fs6.copyFile(srcEntry, destEntry);
|
|
33476
|
+
}
|
|
33477
|
+
archivedFileCount++;
|
|
33478
|
+
} catch {}
|
|
33479
|
+
}
|
|
33480
|
+
}
|
|
33481
|
+
} catch {}
|
|
33482
|
+
const sessionStatePath = path11.join(swarmDir, "session", "state.json");
|
|
33483
|
+
try {
|
|
33484
|
+
const archiveSessionDir = path11.join(archiveDir, "session");
|
|
33485
|
+
await fs6.mkdir(archiveSessionDir, { recursive: true });
|
|
33486
|
+
await fs6.copyFile(sessionStatePath, path11.join(archiveSessionDir, "state.json"));
|
|
33487
|
+
archivedFileCount++;
|
|
33488
|
+
} catch {}
|
|
33489
|
+
archiveResult = `Archived ${archivedFileCount} artifact(s) to .swarm/archive/swarm-${timestamp}/`;
|
|
33490
|
+
} catch (archiveError) {
|
|
33491
|
+
warnings.push(`Archive creation failed: ${archiveError instanceof Error ? archiveError.message : String(archiveError)}`);
|
|
33492
|
+
archiveResult = "Archive creation failed (see warnings)";
|
|
33493
|
+
}
|
|
33329
33494
|
try {
|
|
33330
33495
|
await archiveEvidence(directory, 30, 10);
|
|
33331
33496
|
} catch (error93) {
|
|
33332
33497
|
console.warn("[close-command] archiveEvidence error:", error93);
|
|
33333
33498
|
}
|
|
33334
|
-
const swarmDir = path11.join(directory, ".swarm");
|
|
33335
33499
|
let configBackupsRemoved = 0;
|
|
33500
|
+
const cleanedFiles = [];
|
|
33501
|
+
if (archivedActiveStateFiles.size > 0) {
|
|
33502
|
+
for (const artifact of ACTIVE_STATE_TO_CLEAN) {
|
|
33503
|
+
if (!archivedActiveStateFiles.has(artifact)) {
|
|
33504
|
+
warnings.push(`Preserved ${artifact} because it was not successfully archived.`);
|
|
33505
|
+
continue;
|
|
33506
|
+
}
|
|
33507
|
+
const filePath = path11.join(swarmDir, artifact);
|
|
33508
|
+
try {
|
|
33509
|
+
await fs6.unlink(filePath);
|
|
33510
|
+
cleanedFiles.push(artifact);
|
|
33511
|
+
} catch {}
|
|
33512
|
+
}
|
|
33513
|
+
} else {
|
|
33514
|
+
warnings.push("Skipped active-state cleanup because no active-state files were archived. Files preserved to prevent data loss.");
|
|
33515
|
+
}
|
|
33336
33516
|
try {
|
|
33337
33517
|
const swarmFiles = await fs6.readdir(swarmDir);
|
|
33338
33518
|
const configBackups = swarmFiles.filter((f) => f.startsWith("config-backup-") && f.endsWith(".json"));
|
|
@@ -33343,13 +33523,14 @@ async function handleCloseCommand(directory, args) {
|
|
|
33343
33523
|
} catch {}
|
|
33344
33524
|
}
|
|
33345
33525
|
} catch {}
|
|
33346
|
-
const contextPath = path11.join(
|
|
33526
|
+
const contextPath = path11.join(swarmDir, "context.md");
|
|
33347
33527
|
const contextContent = [
|
|
33348
33528
|
"# Context",
|
|
33349
33529
|
"",
|
|
33350
33530
|
"## Status",
|
|
33351
33531
|
`Session closed after: ${projectName}`,
|
|
33352
33532
|
`Closed: ${new Date().toISOString()}`,
|
|
33533
|
+
`Finalization: ${isForced ? "forced" : planAlreadyDone ? "plan-already-done" : "normal"}`,
|
|
33353
33534
|
"No active plan. Next session starts fresh.",
|
|
33354
33535
|
""
|
|
33355
33536
|
].join(`
|
|
@@ -33362,46 +33543,112 @@ async function handleCloseCommand(directory, args) {
|
|
|
33362
33543
|
const pruneBranches = args.includes("--prune-branches");
|
|
33363
33544
|
const prunedBranches = [];
|
|
33364
33545
|
const pruneErrors = [];
|
|
33365
|
-
|
|
33546
|
+
let gitAlignResult = "";
|
|
33547
|
+
const isGit = isGitRepo2(directory);
|
|
33548
|
+
if (isGit) {
|
|
33366
33549
|
try {
|
|
33367
|
-
const
|
|
33368
|
-
|
|
33369
|
-
|
|
33370
|
-
|
|
33371
|
-
})
|
|
33372
|
-
|
|
33373
|
-
|
|
33374
|
-
|
|
33375
|
-
|
|
33376
|
-
|
|
33377
|
-
|
|
33378
|
-
|
|
33379
|
-
|
|
33380
|
-
|
|
33381
|
-
|
|
33382
|
-
|
|
33383
|
-
|
|
33550
|
+
const currentBranch = getCurrentBranch(directory);
|
|
33551
|
+
if (currentBranch === "HEAD") {
|
|
33552
|
+
gitAlignResult = "Skipped git alignment: detached HEAD state";
|
|
33553
|
+
warnings.push("Repo is in detached HEAD state. Checkout a branch before starting a new swarm.");
|
|
33554
|
+
} else if (hasUncommittedChanges(directory)) {
|
|
33555
|
+
gitAlignResult = "Skipped git alignment: uncommitted changes in worktree";
|
|
33556
|
+
warnings.push("Uncommitted changes detected. Commit or stash before aligning to main.");
|
|
33557
|
+
} else {
|
|
33558
|
+
const baseBranch = getDefaultBaseBranch(directory);
|
|
33559
|
+
const localBase = baseBranch.replace(/^origin\//, "");
|
|
33560
|
+
if (currentBranch === localBase) {
|
|
33561
|
+
try {
|
|
33562
|
+
execFileSync("git", ["fetch", "origin", localBase], {
|
|
33563
|
+
cwd: directory,
|
|
33564
|
+
encoding: "utf-8",
|
|
33565
|
+
timeout: 30000,
|
|
33566
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
33567
|
+
});
|
|
33568
|
+
const mergeBase = execFileSync("git", ["merge-base", "HEAD", baseBranch], {
|
|
33569
|
+
cwd: directory,
|
|
33570
|
+
encoding: "utf-8",
|
|
33571
|
+
timeout: 1e4,
|
|
33572
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
33573
|
+
}).trim();
|
|
33574
|
+
const headSha = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
33575
|
+
cwd: directory,
|
|
33576
|
+
encoding: "utf-8",
|
|
33577
|
+
timeout: 1e4,
|
|
33578
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
33579
|
+
}).trim();
|
|
33580
|
+
if (mergeBase === headSha) {
|
|
33581
|
+
execFileSync("git", ["merge", "--ff-only", baseBranch], {
|
|
33582
|
+
cwd: directory,
|
|
33583
|
+
encoding: "utf-8",
|
|
33584
|
+
timeout: 30000,
|
|
33585
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
33586
|
+
});
|
|
33587
|
+
gitAlignResult = `Aligned to ${baseBranch} (fast-forward)`;
|
|
33588
|
+
} else {
|
|
33589
|
+
gitAlignResult = `On ${localBase} but cannot fast-forward to ${baseBranch} (diverged)`;
|
|
33590
|
+
warnings.push(`Local ${localBase} has diverged from ${baseBranch}. Manual merge/rebase needed.`);
|
|
33591
|
+
}
|
|
33592
|
+
} catch (fetchErr) {
|
|
33593
|
+
gitAlignResult = `Fetch from origin/${localBase} failed \u2014 remote may be unavailable`;
|
|
33594
|
+
warnings.push(`Git fetch failed: ${fetchErr instanceof Error ? fetchErr.message : String(fetchErr)}`);
|
|
33595
|
+
}
|
|
33596
|
+
} else {
|
|
33597
|
+
gitAlignResult = `On branch ${currentBranch}. Switch to ${localBase} manually when ready for a new swarm.`;
|
|
33384
33598
|
}
|
|
33385
33599
|
}
|
|
33386
|
-
} catch {
|
|
33600
|
+
} catch (gitError) {
|
|
33601
|
+
gitAlignResult = `Git alignment error: ${gitError instanceof Error ? gitError.message : String(gitError)}`;
|
|
33602
|
+
}
|
|
33603
|
+
if (pruneBranches) {
|
|
33604
|
+
try {
|
|
33605
|
+
const branchOutput = execFileSync("git", ["branch", "-vv"], {
|
|
33606
|
+
cwd: directory,
|
|
33607
|
+
encoding: "utf-8",
|
|
33608
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
33609
|
+
});
|
|
33610
|
+
const goneBranches = branchOutput.split(`
|
|
33611
|
+
`).filter((line) => line.includes(": gone]")).map((line) => line.trim().replace(/^[*+]\s+/, "").split(/\s+/)[0]).filter(Boolean);
|
|
33612
|
+
for (const branch of goneBranches) {
|
|
33613
|
+
try {
|
|
33614
|
+
execFileSync("git", ["branch", "-d", branch], {
|
|
33615
|
+
cwd: directory,
|
|
33616
|
+
encoding: "utf-8",
|
|
33617
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
33618
|
+
});
|
|
33619
|
+
prunedBranches.push(branch);
|
|
33620
|
+
} catch {
|
|
33621
|
+
pruneErrors.push(branch);
|
|
33622
|
+
}
|
|
33623
|
+
}
|
|
33624
|
+
} catch {}
|
|
33625
|
+
}
|
|
33626
|
+
} else {
|
|
33627
|
+
gitAlignResult = "Not a git repository \u2014 skipped git alignment";
|
|
33387
33628
|
}
|
|
33388
33629
|
const closeSummaryPath = validateSwarmPath(directory, "close-summary.md");
|
|
33630
|
+
const finalizationType = isForced ? "Forced closure" : planAlreadyDone ? "Plan already terminal \u2014 cleanup only" : "Normal finalization";
|
|
33389
33631
|
const actionsPerformed = [
|
|
33390
33632
|
...!planAlreadyDone && inProgressPhases.length > 0 ? ["- Wrote retrospectives for in-progress phases"] : [],
|
|
33391
|
-
|
|
33633
|
+
`- ${archiveResult}`,
|
|
33634
|
+
...cleanedFiles.length > 0 ? [
|
|
33635
|
+
`- Cleaned ${cleanedFiles.length} active-state file(s): ${cleanedFiles.join(", ")}`
|
|
33636
|
+
] : [],
|
|
33392
33637
|
"- Reset context.md for next session",
|
|
33393
33638
|
...configBackupsRemoved > 0 ? [`- Removed ${configBackupsRemoved} stale config backup file(s)`] : [],
|
|
33394
33639
|
...prunedBranches.length > 0 ? [
|
|
33395
33640
|
`- Pruned ${prunedBranches.length} stale local git branch(es): ${prunedBranches.join(", ")}`
|
|
33396
33641
|
] : [],
|
|
33397
33642
|
"- Cleared agent sessions and delegation chains",
|
|
33398
|
-
...planExists && !planAlreadyDone ? ["- Set non-completed phases/tasks to closed status"] : []
|
|
33643
|
+
...planExists && !planAlreadyDone ? ["- Set non-completed phases/tasks to closed status"] : [],
|
|
33644
|
+
...gitAlignResult ? [`- Git: ${gitAlignResult}`] : []
|
|
33399
33645
|
];
|
|
33400
33646
|
const summaryContent = [
|
|
33401
33647
|
"# Swarm Close Summary",
|
|
33402
33648
|
"",
|
|
33403
33649
|
`**Project:** ${projectName}`,
|
|
33404
33650
|
`**Closed:** ${new Date().toISOString()}`,
|
|
33651
|
+
`**Finalization:** ${finalizationType}`,
|
|
33405
33652
|
"",
|
|
33406
33653
|
`## Phases Closed: ${closedPhases.length}`,
|
|
33407
33654
|
!planExists ? "_No plan \u2014 ad-hoc session_" : closedPhases.length > 0 ? closedPhases.map((id) => `- Phase ${id}`).join(`
|
|
@@ -33412,7 +33659,9 @@ async function handleCloseCommand(directory, args) {
|
|
|
33412
33659
|
`) : "_No incomplete tasks_",
|
|
33413
33660
|
"",
|
|
33414
33661
|
"## Actions Performed",
|
|
33415
|
-
...actionsPerformed
|
|
33662
|
+
...actionsPerformed,
|
|
33663
|
+
"",
|
|
33664
|
+
...warnings.length > 0 ? ["## Warnings", ...warnings.map((w) => `- ${w}`), ""] : []
|
|
33416
33665
|
].join(`
|
|
33417
33666
|
`);
|
|
33418
33667
|
try {
|
|
@@ -33431,11 +33680,21 @@ async function handleCloseCommand(directory, args) {
|
|
|
33431
33680
|
if (pruneErrors.length > 0) {
|
|
33432
33681
|
warnings.push(`Could not prune ${pruneErrors.length} branch(es) (unmerged or checked out): ${pruneErrors.join(", ")}`);
|
|
33433
33682
|
}
|
|
33434
|
-
const warningMsg = warnings.length > 0 ? `
|
|
33683
|
+
const warningMsg = warnings.length > 0 ? `
|
|
33684
|
+
|
|
33685
|
+
**Warnings:**
|
|
33686
|
+
${warnings.map((w) => `- ${w}`).join(`
|
|
33687
|
+
`)}` : "";
|
|
33435
33688
|
if (planAlreadyDone) {
|
|
33436
|
-
return `\u2705 Session
|
|
33689
|
+
return `\u2705 Session finalized. Plan was already in a terminal state \u2014 cleanup and archive applied.
|
|
33690
|
+
|
|
33691
|
+
**Archive:** ${archiveResult}
|
|
33692
|
+
**Git:** ${gitAlignResult}${warningMsg}`;
|
|
33437
33693
|
}
|
|
33438
|
-
return `\u2705 Swarm
|
|
33694
|
+
return `\u2705 Swarm finalized. ${closedPhases.length} phase(s) closed, ${closedTasks.length} incomplete task(s) marked closed.
|
|
33695
|
+
|
|
33696
|
+
**Archive:** ${archiveResult}
|
|
33697
|
+
**Git:** ${gitAlignResult}${warningMsg}`;
|
|
33439
33698
|
}
|
|
33440
33699
|
|
|
33441
33700
|
// src/commands/config.ts
|
|
@@ -33718,13 +33977,13 @@ function formatCurationSummary(summary) {
|
|
|
33718
33977
|
import path14 from "path";
|
|
33719
33978
|
|
|
33720
33979
|
// src/tools/co-change-analyzer.ts
|
|
33721
|
-
import * as
|
|
33980
|
+
import * as child_process3 from "child_process";
|
|
33722
33981
|
import { randomUUID } from "crypto";
|
|
33723
33982
|
import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
33724
33983
|
import * as path13 from "path";
|
|
33725
33984
|
import { promisify } from "util";
|
|
33726
33985
|
function getExecFileAsync() {
|
|
33727
|
-
return promisify(
|
|
33986
|
+
return promisify(child_process3.execFile);
|
|
33728
33987
|
}
|
|
33729
33988
|
async function parseGitLog(directory, maxCommits) {
|
|
33730
33989
|
const commitMap = new Map;
|
|
@@ -34096,7 +34355,7 @@ async function handleDarkMatterCommand(directory, args) {
|
|
|
34096
34355
|
}
|
|
34097
34356
|
|
|
34098
34357
|
// src/services/diagnose-service.ts
|
|
34099
|
-
import * as
|
|
34358
|
+
import * as child_process4 from "child_process";
|
|
34100
34359
|
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
|
|
34101
34360
|
import path15 from "path";
|
|
34102
34361
|
import { fileURLToPath } from "url";
|
|
@@ -34341,7 +34600,7 @@ async function checkGitRepository(directory) {
|
|
|
34341
34600
|
detail: "Invalid directory \u2014 cannot check git status"
|
|
34342
34601
|
};
|
|
34343
34602
|
}
|
|
34344
|
-
|
|
34603
|
+
child_process4.execSync("git rev-parse --git-dir", {
|
|
34345
34604
|
cwd: directory,
|
|
34346
34605
|
stdio: "pipe"
|
|
34347
34606
|
});
|
|
@@ -36580,7 +36839,7 @@ async function handleExportCommand(directory, _args) {
|
|
|
36580
36839
|
// src/commands/full-auto.ts
|
|
36581
36840
|
async function handleFullAutoCommand(_directory, args, sessionID) {
|
|
36582
36841
|
if (!sessionID || sessionID.trim() === "") {
|
|
36583
|
-
return "Error: No active session context. Full-Auto Mode requires an active session. Use /swarm
|
|
36842
|
+
return "Error: No active session context. Full-Auto Mode requires an active session. Use /swarm-full-auto from within an OpenCode session, or start a session first.";
|
|
36584
36843
|
}
|
|
36585
36844
|
const session = getAgentSession(sessionID);
|
|
36586
36845
|
if (!session) {
|
|
@@ -36588,16 +36847,15 @@ async function handleFullAutoCommand(_directory, args, sessionID) {
|
|
|
36588
36847
|
}
|
|
36589
36848
|
const arg = args[0]?.toLowerCase();
|
|
36590
36849
|
let newFullAutoMode;
|
|
36591
|
-
let feedback;
|
|
36592
36850
|
if (arg === "on") {
|
|
36593
36851
|
newFullAutoMode = true;
|
|
36594
|
-
feedback = "Full-Auto Mode enabled";
|
|
36595
36852
|
} else if (arg === "off") {
|
|
36596
36853
|
newFullAutoMode = false;
|
|
36597
|
-
feedback = "Full-Auto Mode disabled";
|
|
36598
36854
|
} else {
|
|
36599
36855
|
newFullAutoMode = !session.fullAutoMode;
|
|
36600
|
-
|
|
36856
|
+
}
|
|
36857
|
+
if (newFullAutoMode && !swarmState.fullAutoEnabledInConfig) {
|
|
36858
|
+
return "Error: Full-Auto Mode cannot be enabled because full_auto.enabled is not set to true in the swarm plugin config. The autonomous oversight hook is inactive without config-level enablement. Set full_auto.enabled = true in your opencode-swarm config and restart.";
|
|
36601
36859
|
}
|
|
36602
36860
|
session.fullAutoMode = newFullAutoMode;
|
|
36603
36861
|
if (!newFullAutoMode) {
|
|
@@ -36605,7 +36863,7 @@ async function handleFullAutoCommand(_directory, args, sessionID) {
|
|
|
36605
36863
|
session.fullAutoDeadlockCount = 0;
|
|
36606
36864
|
session.fullAutoLastQuestionHash = null;
|
|
36607
36865
|
}
|
|
36608
|
-
return
|
|
36866
|
+
return newFullAutoMode ? "Full-Auto Mode enabled" : "Full-Auto Mode disabled";
|
|
36609
36867
|
}
|
|
36610
36868
|
|
|
36611
36869
|
// src/commands/handoff.ts
|
|
@@ -36913,6 +37171,64 @@ function formatHandoffMarkdown(data) {
|
|
|
36913
37171
|
return lines.join(`
|
|
36914
37172
|
`);
|
|
36915
37173
|
}
|
|
37174
|
+
function formatContinuationPrompt(data) {
|
|
37175
|
+
const lines = [];
|
|
37176
|
+
lines.push("## Resume Swarm");
|
|
37177
|
+
lines.push("");
|
|
37178
|
+
if (data.currentPhase) {
|
|
37179
|
+
lines.push(`**Phase**: ${data.currentPhase}`);
|
|
37180
|
+
}
|
|
37181
|
+
if (data.currentTask) {
|
|
37182
|
+
lines.push(`**Current Task**: ${data.currentTask}`);
|
|
37183
|
+
}
|
|
37184
|
+
let nextTask;
|
|
37185
|
+
if (data.incompleteTasks.length > 0) {
|
|
37186
|
+
nextTask = data.incompleteTasks.find((t) => t !== data.currentTask);
|
|
37187
|
+
if (nextTask) {
|
|
37188
|
+
lines.push(`**Next Task**: ${nextTask}`);
|
|
37189
|
+
}
|
|
37190
|
+
}
|
|
37191
|
+
if (data.pendingQA) {
|
|
37192
|
+
lines.push("");
|
|
37193
|
+
lines.push(`**Pending QA Blocker**: ${data.pendingQA.taskId}`);
|
|
37194
|
+
if (data.pendingQA.lastFailure) {
|
|
37195
|
+
lines.push(` - Last failure: ${data.pendingQA.lastFailure}`);
|
|
37196
|
+
}
|
|
37197
|
+
}
|
|
37198
|
+
if (data.recentDecisions.length > 0) {
|
|
37199
|
+
const last3 = data.recentDecisions.slice(-3);
|
|
37200
|
+
lines.push("");
|
|
37201
|
+
lines.push("**Recent Decisions (do not revisit)**:");
|
|
37202
|
+
for (const decision of last3) {
|
|
37203
|
+
lines.push(`- ${decision}`);
|
|
37204
|
+
}
|
|
37205
|
+
}
|
|
37206
|
+
if (data.incompleteTasks.length > 2) {
|
|
37207
|
+
const remaining = data.incompleteTasks.filter((t) => t !== data.currentTask && t !== nextTask);
|
|
37208
|
+
if (remaining.length > 0) {
|
|
37209
|
+
lines.push("");
|
|
37210
|
+
lines.push(`**Remaining Tasks**: ${remaining.slice(0, 8).join(", ")}${remaining.length > 8 ? ` (+${remaining.length - 8} more)` : ""}`);
|
|
37211
|
+
}
|
|
37212
|
+
}
|
|
37213
|
+
lines.push("");
|
|
37214
|
+
lines.push("**To resume**:");
|
|
37215
|
+
lines.push("1. Read `.swarm/handoff.md` for full context");
|
|
37216
|
+
lines.push("2. Use `knowledge_recall` to recall relevant lessons before starting");
|
|
37217
|
+
if (data.pendingQA) {
|
|
37218
|
+
lines.push(`3. Resolve QA blocker on task ${data.pendingQA.taskId} before continuing`);
|
|
37219
|
+
} else if (data.currentTask) {
|
|
37220
|
+
lines.push(`3. Continue work on task ${data.currentTask}`);
|
|
37221
|
+
} else if (nextTask) {
|
|
37222
|
+
lines.push(`3. Begin work on task ${nextTask}`);
|
|
37223
|
+
} else {
|
|
37224
|
+
lines.push("3. Review the plan and pick up the next incomplete task");
|
|
37225
|
+
}
|
|
37226
|
+
lines.push("4. Do not re-implement completed tasks or revisit settled decisions");
|
|
37227
|
+
return `\`\`\`markdown
|
|
37228
|
+
${lines.join(`
|
|
37229
|
+
`)}
|
|
37230
|
+
\`\`\``;
|
|
37231
|
+
}
|
|
36916
37232
|
|
|
36917
37233
|
// src/commands/handoff.ts
|
|
36918
37234
|
async function handleHandoffCommand(directory, _args) {
|
|
@@ -36922,17 +37238,27 @@ async function handleHandoffCommand(directory, _args) {
|
|
|
36922
37238
|
const tempPath = `${resolvedPath}.tmp.${crypto4.randomUUID()}`;
|
|
36923
37239
|
await Bun.write(tempPath, markdown);
|
|
36924
37240
|
renameSync5(tempPath, resolvedPath);
|
|
37241
|
+
const continuationPrompt = formatContinuationPrompt(handoffData);
|
|
37242
|
+
const promptPath = validateSwarmPath(directory, "handoff-prompt.md");
|
|
37243
|
+
const promptTempPath = `${promptPath}.tmp.${crypto4.randomUUID()}`;
|
|
37244
|
+
await Bun.write(promptTempPath, continuationPrompt);
|
|
37245
|
+
renameSync5(promptTempPath, promptPath);
|
|
36925
37246
|
await writeSnapshot(directory, swarmState);
|
|
36926
37247
|
await flushPendingSnapshot(directory);
|
|
36927
37248
|
return `## Handoff Brief Written
|
|
36928
37249
|
|
|
36929
37250
|
Brief written to \`.swarm/handoff.md\`.
|
|
37251
|
+
Continuation prompt written to \`.swarm/handoff-prompt.md\`.
|
|
36930
37252
|
|
|
36931
37253
|
${markdown}
|
|
36932
37254
|
|
|
36933
37255
|
---
|
|
36934
37256
|
|
|
36935
|
-
|
|
37257
|
+
## Continuation Prompt
|
|
37258
|
+
|
|
37259
|
+
Copy and paste the block below into your next session to resume cleanly:
|
|
37260
|
+
|
|
37261
|
+
${continuationPrompt}`;
|
|
36936
37262
|
}
|
|
36937
37263
|
|
|
36938
37264
|
// src/services/history-service.ts
|
package/dist/commands/close.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Handles /swarm close command -
|
|
3
|
-
*
|
|
2
|
+
* Handles /swarm close command - performs full terminal session finalization:
|
|
3
|
+
* 1. Finalize: write retrospectives, produce terminal summary
|
|
4
|
+
* 2. Archive: create timestamped bundle of swarm artifacts
|
|
5
|
+
* 3. Clean: clear active-state files that confuse future swarms
|
|
6
|
+
* 4. Align: safe git alignment to main
|
|
7
|
+
*
|
|
4
8
|
* Must be idempotent - safe to run multiple times.
|
|
5
9
|
*/
|
|
6
10
|
export declare function handleCloseCommand(directory: string, args: string[]): Promise<string>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Full-Auto command discoverability fixes.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that TUI shortcuts, error messages, and registry entries
|
|
5
|
+
* all use the dashed form (/swarm-full-auto) while maintaining backward
|
|
6
|
+
* compatibility with the spaced form (/swarm full-auto).
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -51,7 +51,7 @@ export declare function parseCriticResponse(rawResponse: string): CriticDispatch
|
|
|
51
51
|
* - ESCALATE_TO_HUMAN: triggers escalation (handled separately)
|
|
52
52
|
* - APPROVED / NEEDS_REVISION / REJECTED / BLOCKED / REPHRASE: injects verdict message
|
|
53
53
|
*/
|
|
54
|
-
export declare function injectVerdictIntoMessages(messages: MessageWithParts[], architectIndex: number, criticResult: CriticDispatchResult,
|
|
54
|
+
export declare function injectVerdictIntoMessages(messages: MessageWithParts[], architectIndex: number, criticResult: CriticDispatchResult, escalationType: 'phase_completion' | 'question', oversightAgentName: string): void;
|
|
55
55
|
/**
|
|
56
56
|
* Handles critic dispatch and writes the auto_oversight event after the critic responds.
|
|
57
57
|
*
|