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 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 archivePath = path8.join(swarmDir2, `plan-ledger.archived-${Date.now()}-${Math.floor(Math.random() * 1e9)}.jsonl`);
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
- renameSync3(oldLedgerPath, archivePath);
16651
- warn(`[savePlan] Ledger identity mismatch (was "${existingEvents[0].plan_id}", now "${planId}") \u2014 archived old ledger to ${archivePath} and reinitializing.`);
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
- try {
16654
- await initLedger(directory, planId, planHashForInit);
16655
- } catch (initErr) {
16656
- if (!(initErr instanceof Error && initErr.message.includes("already initialized"))) {
16657
- throw initErr;
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(path11.join(directory, ".swarm")).then(() => true).catch(() => false);
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: "Phase closed via /swarm close",
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(directory, ".swarm", "close-lessons.md");
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(directory, ".swarm", "context.md");
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
- if (pruneBranches) {
33546
+ let gitAlignResult = "";
33547
+ const isGit = isGitRepo2(directory);
33548
+ if (isGit) {
33366
33549
  try {
33367
- const branchOutput = execFileSync("git", ["branch", "-vv"], {
33368
- cwd: directory,
33369
- encoding: "utf-8",
33370
- stdio: ["pipe", "pipe", "pipe"]
33371
- });
33372
- const goneBranches = branchOutput.split(`
33373
- `).filter((line) => line.includes(": gone]")).map((line) => line.trim().replace(/^[*+]\s+/, "").split(/\s+/)[0]).filter(Boolean);
33374
- for (const branch of goneBranches) {
33375
- try {
33376
- execFileSync("git", ["branch", "-d", branch], {
33377
- cwd: directory,
33378
- encoding: "utf-8",
33379
- stdio: ["pipe", "pipe", "pipe"]
33380
- });
33381
- prunedBranches.push(branch);
33382
- } catch {
33383
- pruneErrors.push(branch);
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
- "- Archived evidence bundles",
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 ? ` Warnings: ${warnings.join("; ")}.` : "";
33683
+ const warningMsg = warnings.length > 0 ? `
33684
+
33685
+ **Warnings:**
33686
+ ${warnings.map((w) => `- ${w}`).join(`
33687
+ `)}` : "";
33435
33688
  if (planAlreadyDone) {
33436
- return `\u2705 Session closed. Plan was already in a terminal state \u2014 cleanup steps applied.${warningMsg}`;
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 closed successfully. ${closedPhases.length} phase(s) closed, ${closedTasks.length} incomplete task(s) marked closed.${warningMsg}`;
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 child_process2 from "child_process";
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(child_process2.execFile);
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 child_process3 from "child_process";
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
- child_process3.execSync("git rev-parse --git-dir", {
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 full-auto from within an OpenCode session, or start a session first.";
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
- feedback = newFullAutoMode ? "Full-Auto Mode enabled" : "Full-Auto Mode disabled";
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 feedback;
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
- **Next Step:** Start a new OpenCode session, switch to your target model, and send: \`continue the previous work\``;
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
@@ -1,6 +1,10 @@
1
1
  /**
2
- * Handles /swarm close command - closes the swarm by archiving evidence,
3
- * writing retrospectives for in-progress phases, and clearing session state.
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,7 @@
1
+ /**
2
+ * Tests for the config-guard behavior in handleFullAutoCommand.
3
+ *
4
+ * When swarmState.fullAutoEnabledInConfig is false, activation must be blocked.
5
+ * When it is true, activation must succeed. Disabling always works regardless.
6
+ */
7
+ export {};
@@ -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, _escalationType: 'phase_completion' | 'question', oversightAgentName: string): void;
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
  *