opencode-swarm 7.23.0 → 7.24.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/dist/cli/index.js CHANGED
@@ -34,7 +34,7 @@ var package_default;
34
34
  var init_package = __esm(() => {
35
35
  package_default = {
36
36
  name: "opencode-swarm",
37
- version: "7.23.0",
37
+ version: "7.24.0",
38
38
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
39
39
  main: "dist/index.js",
40
40
  types: "dist/index.d.ts",
@@ -34292,6 +34292,14 @@ function gitExec(args) {
34292
34292
  }
34293
34293
  return result.stdout;
34294
34294
  }
34295
+ function appendRetentionEvent(directory, event) {
34296
+ try {
34297
+ const eventsPath = path9.join(directory, ".swarm", "events.jsonl");
34298
+ const line = `${JSON.stringify({ ...event, timestamp: new Date().toISOString() })}
34299
+ `;
34300
+ fs6.appendFileSync(eventsPath, line);
34301
+ } catch {}
34302
+ }
34295
34303
  function getCurrentSha() {
34296
34304
  const output = gitExec(["rev-parse", "HEAD"]);
34297
34305
  return output.trim();
@@ -34343,6 +34351,17 @@ function handleSave(label, directory) {
34343
34351
  sha: newSha,
34344
34352
  timestamp
34345
34353
  });
34354
+ if (log2.checkpoints.length > maxCheckpoints) {
34355
+ const evicted = log2.checkpoints.splice(0, log2.checkpoints.length - maxCheckpoints);
34356
+ try {
34357
+ appendRetentionEvent(directory, {
34358
+ event: "checkpoint_retention_applied",
34359
+ evicted_labels: evicted.map((e) => e.label),
34360
+ evicted_count: evicted.length,
34361
+ remaining_count: log2.checkpoints.length
34362
+ });
34363
+ } catch {}
34364
+ }
34346
34365
  writeCheckpointLog(log2, directory);
34347
34366
  return JSON.stringify({
34348
34367
  action: "save",
@@ -43447,7 +43466,7 @@ var init_handoff_service = __esm(() => {
43447
43466
  });
43448
43467
 
43449
43468
  // src/session/snapshot-writer.ts
43450
- import { mkdirSync as mkdirSync10, renameSync as renameSync6 } from "fs";
43469
+ import { closeSync as closeSync3, fsyncSync as fsyncSync2, mkdirSync as mkdirSync10, openSync as openSync3, renameSync as renameSync6 } from "fs";
43451
43470
  import * as path27 from "path";
43452
43471
  function serializeAgentSession(s) {
43453
43472
  const gateLog = {};
@@ -43464,6 +43483,12 @@ function serializeAgentSession(s) {
43464
43483
  const catastrophicPhaseWarnings = Array.from(s.catastrophicPhaseWarnings ?? new Set);
43465
43484
  const phaseAgentsDispatched = Array.from(s.phaseAgentsDispatched ?? new Set);
43466
43485
  const lastCompletedPhaseAgentsDispatched = Array.from(s.lastCompletedPhaseAgentsDispatched ?? new Set);
43486
+ const stageBCompletion = {};
43487
+ if (s.stageBCompletion) {
43488
+ for (const [taskId, agents] of s.stageBCompletion) {
43489
+ stageBCompletion[taskId] = Array.from(agents);
43490
+ }
43491
+ }
43467
43492
  const windows = {};
43468
43493
  const rawWindows = s.windows ?? {};
43469
43494
  for (const [key, win] of Object.entries(rawWindows)) {
@@ -43520,7 +43545,8 @@ function serializeAgentSession(s) {
43520
43545
  fullAutoInteractionCount: s.fullAutoInteractionCount ?? 0,
43521
43546
  fullAutoDeadlockCount: s.fullAutoDeadlockCount ?? 0,
43522
43547
  fullAutoLastQuestionHash: s.fullAutoLastQuestionHash ?? null,
43523
- sessionRehydratedAt: s.sessionRehydratedAt ?? 0
43548
+ sessionRehydratedAt: s.sessionRehydratedAt ?? 0,
43549
+ ...Object.keys(stageBCompletion).length > 0 && { stageBCompletion }
43524
43550
  };
43525
43551
  }
43526
43552
  async function writeSnapshot(directory, state) {
@@ -43542,6 +43568,14 @@ async function writeSnapshot(directory, state) {
43542
43568
  mkdirSync10(dir, { recursive: true });
43543
43569
  const tempPath = `${resolvedPath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;
43544
43570
  await bunWrite(tempPath, content);
43571
+ try {
43572
+ const fd = openSync3(tempPath, "r+");
43573
+ try {
43574
+ fsyncSync2(fd);
43575
+ } finally {
43576
+ closeSync3(fd);
43577
+ }
43578
+ } catch {}
43545
43579
  renameSync6(tempPath, resolvedPath);
43546
43580
  } catch (error93) {
43547
43581
  log("[snapshot-writer] write failed", {
package/dist/index.js CHANGED
@@ -33,7 +33,7 @@ var package_default;
33
33
  var init_package = __esm(() => {
34
34
  package_default = {
35
35
  name: "opencode-swarm",
36
- version: "7.23.0",
36
+ version: "7.24.0",
37
37
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
38
38
  main: "dist/index.js",
39
39
  types: "dist/index.d.ts",
@@ -41417,6 +41417,14 @@ function gitExec(args2) {
41417
41417
  }
41418
41418
  return result.stdout;
41419
41419
  }
41420
+ function appendRetentionEvent(directory, event) {
41421
+ try {
41422
+ const eventsPath = path14.join(directory, ".swarm", "events.jsonl");
41423
+ const line = `${JSON.stringify({ ...event, timestamp: new Date().toISOString() })}
41424
+ `;
41425
+ fs11.appendFileSync(eventsPath, line);
41426
+ } catch {}
41427
+ }
41420
41428
  function getCurrentSha() {
41421
41429
  const output = gitExec(["rev-parse", "HEAD"]);
41422
41430
  return output.trim();
@@ -41468,6 +41476,17 @@ function handleSave(label, directory) {
41468
41476
  sha: newSha,
41469
41477
  timestamp
41470
41478
  });
41479
+ if (log2.checkpoints.length > maxCheckpoints) {
41480
+ const evicted = log2.checkpoints.splice(0, log2.checkpoints.length - maxCheckpoints);
41481
+ try {
41482
+ appendRetentionEvent(directory, {
41483
+ event: "checkpoint_retention_applied",
41484
+ evicted_labels: evicted.map((e) => e.label),
41485
+ evicted_count: evicted.length,
41486
+ remaining_count: log2.checkpoints.length
41487
+ });
41488
+ } catch {}
41489
+ }
41471
41490
  writeCheckpointLog(log2, directory);
41472
41491
  return JSON.stringify({
41473
41492
  action: "save",
@@ -52386,7 +52405,7 @@ var init_handoff_service = __esm(() => {
52386
52405
  });
52387
52406
 
52388
52407
  // src/session/snapshot-writer.ts
52389
- import { mkdirSync as mkdirSync13, renameSync as renameSync9 } from "node:fs";
52408
+ import { closeSync as closeSync3, fsyncSync as fsyncSync2, mkdirSync as mkdirSync13, openSync as openSync3, renameSync as renameSync9 } from "node:fs";
52390
52409
  import * as path33 from "node:path";
52391
52410
  function serializeAgentSession(s) {
52392
52411
  const gateLog = {};
@@ -52403,6 +52422,12 @@ function serializeAgentSession(s) {
52403
52422
  const catastrophicPhaseWarnings = Array.from(s.catastrophicPhaseWarnings ?? new Set);
52404
52423
  const phaseAgentsDispatched = Array.from(s.phaseAgentsDispatched ?? new Set);
52405
52424
  const lastCompletedPhaseAgentsDispatched = Array.from(s.lastCompletedPhaseAgentsDispatched ?? new Set);
52425
+ const stageBCompletion = {};
52426
+ if (s.stageBCompletion) {
52427
+ for (const [taskId, agents] of s.stageBCompletion) {
52428
+ stageBCompletion[taskId] = Array.from(agents);
52429
+ }
52430
+ }
52406
52431
  const windows = {};
52407
52432
  const rawWindows = s.windows ?? {};
52408
52433
  for (const [key, win] of Object.entries(rawWindows)) {
@@ -52459,7 +52484,8 @@ function serializeAgentSession(s) {
52459
52484
  fullAutoInteractionCount: s.fullAutoInteractionCount ?? 0,
52460
52485
  fullAutoDeadlockCount: s.fullAutoDeadlockCount ?? 0,
52461
52486
  fullAutoLastQuestionHash: s.fullAutoLastQuestionHash ?? null,
52462
- sessionRehydratedAt: s.sessionRehydratedAt ?? 0
52487
+ sessionRehydratedAt: s.sessionRehydratedAt ?? 0,
52488
+ ...Object.keys(stageBCompletion).length > 0 && { stageBCompletion }
52463
52489
  };
52464
52490
  }
52465
52491
  async function writeSnapshot(directory, state) {
@@ -52481,6 +52507,14 @@ async function writeSnapshot(directory, state) {
52481
52507
  mkdirSync13(dir, { recursive: true });
52482
52508
  const tempPath = `${resolvedPath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;
52483
52509
  await bunWrite(tempPath, content);
52510
+ try {
52511
+ const fd = openSync3(tempPath, "r+");
52512
+ try {
52513
+ fsyncSync2(fd);
52514
+ } finally {
52515
+ closeSync3(fd);
52516
+ }
52517
+ } catch {}
52484
52518
  renameSync9(tempPath, resolvedPath);
52485
52519
  } catch (error93) {
52486
52520
  log("[snapshot-writer] write failed", {
@@ -62618,7 +62652,17 @@ If the user answered the gate question, immediately follow up with ONE more ques
62618
62652
  - locked: true
62619
62653
  - recorded_at: <ISO timestamp>
62620
62654
  \`\`\`
62621
- If the user accepts the default (1), skip writing this section entirely — serial execution is the default and needs no config.`;
62655
+ If the user accepts the default (1), skip writing this section entirely — serial execution is the default and needs no config.
62656
+
62657
+ After asking the parallelization question (regardless of whether the user chose serial or parallel), immediately follow up with ONE more question: "Commit frequency for completed tasks? (default: phase-level only; optional per-task checkpoint commit after each task completion)".
62658
+
62659
+ If the user chooses per-task commits, write this section to \`.swarm/context.md\`:
62660
+ \`\`\`
62661
+ ## Task Completion Commit Policy
62662
+ - commit_after_each_completed_task: true
62663
+ - recorded_at: <ISO timestamp>
62664
+ \`\`\`
62665
+ If the user keeps the default phase-level behavior, do not write this section.`;
62622
62666
  }
62623
62667
  function buildAvailableToolsList(council) {
62624
62668
  const tools = AGENT_TOOL_MAP.architect ?? [];
@@ -64107,7 +64151,10 @@ save_plan({
64107
64151
  After \`save_plan\` succeeds, read \`.swarm/context.md\`:
64108
64152
  - If a \`## Pending QA Gate Selection\` section exists: parse the gate values, call \`set_qa_gates\` with those flags, confirm with the user ("QA gates applied: <list>"), then remove the section from context.md.
64109
64153
  - If a \`## Pending Parallelization Config\` section also exists: parse the values and call \`save_plan\` again with \`execution_profile\` set to \`{ parallelization_enabled: <parsed>, max_concurrent_tasks: <parsed>, council_parallel: false, locked: true }\`. Then remove the section from context.md. If the plan already had \`execution_profile.locked: true\`, skip this step — the profile is already locked and immutable.
64154
+ - If a \`## Task Completion Commit Policy\` section exists: preserve it in \`.swarm/context.md\` (do NOT remove). This section is execution-time guidance for optional per-task checkpoint commits after \`update_task_status(status="completed")\`.
64110
64155
  - If no pending section exists: {{QA_GATE_DIALOGUE_PLAN}}
64156
+ - If a \`## Task Completion Commit Policy\` section already exists in context.md, honor it as execution-time guidance (do NOT remove).
64157
+ - If no \`## Task Completion Commit Policy\` section exists AND the \`{{QA_GATE_DIALOGUE_PLAN}}\` template was not rendered (pending sections were pre-written), ask the commit-frequency question now. Write the section to context.md if the user chooses per-task commits; skip if they keep the default phase-level behavior.
64111
64158
  <!-- BEHAVIORAL_GUIDANCE_START -->
64112
64159
  INLINE GATE SELECTION — no pending section found in context.md. You MUST ask now.
64113
64160
  ✗ "I'll call set_qa_gates with defaults and move on"
@@ -64392,7 +64439,14 @@ This step supplements (not replaces) the existing regression-sweep and test-drif
64392
64439
  Any blank "value: ___" field = gate was not run = task is NOT complete.
64393
64440
  Filling this checklist from memory ("I think I ran it") is INVALID. Each value must come from actual tool/agent output in this session.
64394
64441
 
64395
- 5o. Call update_task_status with status "completed", proceed to next task.
64442
+ 5p. Call update_task_status with status "completed".
64443
+ 5q. OPTIONAL TASK-COMPLETION COMMIT POLICY: read \`.swarm/context.md\`.
64444
+ - If \`## Task Completion Commit Policy\` contains \`commit_after_each_completed_task: true\`, immediately call:
64445
+ \`checkpoint save task-<task-id>-complete\`
64446
+ - If the section is absent or false, skip this step.
64447
+ - This optional commit policy NEVER bypasses PRE-COMMIT RULE checks above.
64448
+ - If checkpoint save fails with "duplicate label", the task was already checkpointed from a prior completion or retry. Silently skip — the existing checkpoint is valid.
64449
+ 5r. Proceed to next task.
64396
64450
 
64397
64451
  ## ⛔ RETROSPECTIVE GATE
64398
64452
 
@@ -83611,8 +83665,8 @@ ${formattedIndex}
83611
83665
  } else {
83612
83666
  if (existingContent.length > 0 && !existingContent.endsWith(`
83613
83667
  `)) {
83614
- updatedContent = existingContent + `
83615
- ` + newSection;
83668
+ updatedContent = `${existingContent}
83669
+ ${newSection}`;
83616
83670
  } else {
83617
83671
  updatedContent = existingContent + newSection;
83618
83672
  }
@@ -85196,6 +85250,12 @@ function deserializeAgentSession(s) {
85196
85250
  const catastrophicPhaseWarnings = new Set(s.catastrophicPhaseWarnings ?? []);
85197
85251
  const phaseAgentsDispatched = new Set(s.phaseAgentsDispatched ?? []);
85198
85252
  const lastCompletedPhaseAgentsDispatched = new Set(s.lastCompletedPhaseAgentsDispatched ?? []);
85253
+ const stageBCompletion = new Map;
85254
+ if (s.stageBCompletion) {
85255
+ for (const [taskId, agents] of Object.entries(s.stageBCompletion)) {
85256
+ stageBCompletion.set(taskId, new Set(agents));
85257
+ }
85258
+ }
85199
85259
  const windows = {};
85200
85260
  for (const [key, win] of Object.entries(s.windows ?? {})) {
85201
85261
  windows[key] = {
@@ -85250,7 +85310,8 @@ function deserializeAgentSession(s) {
85250
85310
  prmLastPatternDetected: null,
85251
85311
  prmTrajectoryStep: 0,
85252
85312
  prmHardStopPending: false,
85253
- sessionRehydratedAt: s.sessionRehydratedAt ?? 0
85313
+ sessionRehydratedAt: s.sessionRehydratedAt ?? 0,
85314
+ stageBCompletion
85254
85315
  };
85255
85316
  }
85256
85317
  async function readSnapshot(directory) {
@@ -87087,7 +87148,7 @@ ${body2}`);
87087
87148
 
87088
87149
  // src/council/council-evidence-writer.ts
87089
87150
  import {
87090
- appendFileSync as appendFileSync11,
87151
+ appendFileSync as appendFileSync12,
87091
87152
  existsSync as existsSync53,
87092
87153
  mkdirSync as mkdirSync24,
87093
87154
  readFileSync as readFileSync44,
@@ -87171,7 +87232,7 @@ function writeCouncilEvidence(workingDir, synthesis) {
87171
87232
  timestamp: synthesis.timestamp,
87172
87233
  vetoedBy: synthesis.vetoedBy
87173
87234
  });
87174
- appendFileSync11(join83(councilDir, `${synthesis.taskId}.rounds.jsonl`), `${auditLine}
87235
+ appendFileSync12(join83(councilDir, `${synthesis.taskId}.rounds.jsonl`), `${auditLine}
87175
87236
  `);
87176
87237
  } catch (auditError) {
87177
87238
  console.warn(`writeCouncilEvidence: failed to append round-history audit log: ${auditError instanceof Error ? auditError.message : String(auditError)}`);
@@ -103946,7 +104007,27 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
103946
104007
  }
103947
104008
  }
103948
104009
  const currentStateStr = stateEntries.length > 0 ? stateEntries.join(", ") : "no active sessions";
103949
- const finalReason = evidenceIncompleteReason ?? `Task ${taskId} has not passed QA gates. Current state by session: [${currentStateStr}]. Missing required state: tests_run or complete in at least one valid session. Do not write directly to plan files — use update_task_status after running the reviewer and test_engineer agents.`;
104010
+ const chainEntries = [];
104011
+ for (const [sessionId, chain] of swarmState.delegationChains) {
104012
+ const session = swarmState.agentSessions.get(sessionId);
104013
+ if (session && (session.currentTaskId === taskId || session.lastCoderDelegationTaskId === taskId)) {
104014
+ const targets = chain.map((d) => stripKnownSwarmPrefix(d.to));
104015
+ chainEntries.push(`${sessionId}: [${targets.join(", ")}]`);
104016
+ }
104017
+ }
104018
+ const chainSummary = chainEntries.length > 0 ? chainEntries.join("; ") : "no chains for this task";
104019
+ const rehydratedSessionCount = [
104020
+ ...swarmState.agentSessions.values()
104021
+ ].filter((s) => s.sessionRehydratedAt > 0).length;
104022
+ const finalReason = [
104023
+ `Task ${taskId} has not passed QA gates.`,
104024
+ ` Session states: [${currentStateStr}].`,
104025
+ ` Delegation chains: [${chainSummary}].`,
104026
+ ` Evidence: [${evidenceIncompleteReason ?? "no evidence file found"}].`,
104027
+ ` Rehydrated sessions: ${rehydratedSessionCount}.`,
104028
+ ` Missing required state: tests_run or complete.`
104029
+ ].join(`
104030
+ `);
103950
104031
  telemetry.gateFailed("", "qa_gate", taskId, evidenceIncompleteReason ? `Missing gates: evidence incomplete` : `Missing state: tests_run or complete`);
103951
104032
  return {
103952
104033
  blocked: true,
@@ -103968,7 +104049,7 @@ async function checkReviewerGateWithScope(taskId, workingDirectory, sessionID) {
103968
104049
  ${scopeWarning}` : scopeWarning
103969
104050
  };
103970
104051
  }
103971
- function recoverTaskStateFromDelegations(taskId) {
104052
+ function recoverTaskStateFromDelegations(taskId, directory) {
103972
104053
  let hasReviewer = false;
103973
104054
  let hasTestEngineer = false;
103974
104055
  for (const [sessionId, chain] of swarmState.delegationChains) {
@@ -103983,8 +104064,24 @@ function recoverTaskStateFromDelegations(taskId) {
103983
104064
  }
103984
104065
  }
103985
104066
  }
104067
+ if ((!hasReviewer || !hasTestEngineer) && directory) {
104068
+ try {
104069
+ const evidence = readTaskEvidenceRaw(directory, taskId);
104070
+ if (evidence && evidence.gates && Array.isArray(evidence.required_gates)) {
104071
+ if (evidence.gates.reviewer != null)
104072
+ hasReviewer = true;
104073
+ if (evidence.gates.test_engineer != null)
104074
+ hasTestEngineer = true;
104075
+ }
104076
+ } catch {}
104077
+ }
103986
104078
  if (!hasReviewer && !hasTestEngineer)
103987
104079
  return;
104080
+ if (swarmState.agentSessions.size === 0) {
104081
+ try {
104082
+ startAgentSession("recovery-session", "architect");
104083
+ } catch {}
104084
+ }
103988
104085
  for (const [, session] of swarmState.agentSessions) {
103989
104086
  if (!(session.taskWorkflowStates instanceof Map))
103990
104087
  continue;
@@ -104128,7 +104225,7 @@ async function executeUpdateTaskStatus(args2, fallbackDir, ctx) {
104128
104225
  } catch {}
104129
104226
  }
104130
104227
  if (args2.status === "completed") {
104131
- recoverTaskStateFromDelegations(args2.task_id);
104228
+ recoverTaskStateFromDelegations(args2.task_id, directory);
104132
104229
  let phaseRequiresReviewer = true;
104133
104230
  try {
104134
104231
  const planPath = path137.join(directory, ".swarm", "plan.json");
@@ -59,6 +59,8 @@ export interface SerializedAgentSession {
59
59
  fullAutoLastQuestionHash?: string | null;
60
60
  /** Timestamp when session was rehydrated from snapshot (0 if never rehydrated) */
61
61
  sessionRehydratedAt?: number;
62
+ /** Stage B completion tracking: per-task set of completed Stage B agents. Optional for backward compat with old snapshots. */
63
+ stageBCompletion?: Record<string, string[]>;
62
64
  }
63
65
  /**
64
66
  * Minimal interface for serialized InvocationWindow
@@ -71,9 +71,14 @@ export declare function checkReviewerGateWithScope(taskId: string, workingDirect
71
71
  * missing), this function advances the task state so that checkReviewerGate can
72
72
  * make an accurate decision without attributing unrelated delegation activity.
73
73
  *
74
+ * Falls back to reading durable evidence files when delegation chains are empty
75
+ * (e.g., after a crash or session restart without snapshot). This ensures
76
+ * recovery works even when no in-memory delegation history exists.
77
+ *
74
78
  * @param taskId - The task ID to recover state for
79
+ * @param directory - Optional project directory for evidence file fallback
75
80
  */
76
- export declare function recoverTaskStateFromDelegations(taskId: string): void;
81
+ export declare function recoverTaskStateFromDelegations(taskId: string, directory?: string): void;
77
82
  /**
78
83
  * Result of the council-gate check used when transitioning to 'completed'.
79
84
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.23.0",
3
+ "version": "7.24.0",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",